The principle: shape matches primitive
Every canonical response carries enough type information for a registry to match it to a UI primitive without device-specific code:| Response shape | Primitive | Notes |
|---|---|---|
{ success: false, error: { code, message, details? } } | <ErrorAlert> | Surfaces the code, the message, and any actionable details. |
Battery state with commands and state.level | <DeviceStateCard> | One card layout works across every battery. |
EV charger state with commands and state.charging | <DeviceStateCard> | Same component, different state fields surfaced. |
Action lifecycle ({ id, status, command, parameters }) | <ActionTimeline> | Renders the lifecycle states acknowledged → completed (or failed / cancelled). |
| Capability declaration plus user intent | <CapabilityControls> | Renders form controls (sliders for power, percent inputs for target) bounded by the device’s declared min/max. |
Numeric Quantity with unit: "percent" | <Gauge variant="percent"> | Ring gauge, 0-100 scale. |
Numeric Quantity with unit: "kw" | <Gauge variant="power"> | Power gauge, scaled to the parameter’s max. |
Generic envelope ({ success: true, data: { ... } }) | <EnvelopeRenderer> | Best-effort fallback: tabulates fields, recurses into nested envelopes. |
render: "DeviceStateCard" field, for example); the registry honours the hint first and falls back to shape-matching. Hints are useful when the response is ambiguous. Shape-matching covers the rest.
Components stay dumb
A primitive takes typed props, renders, and returns. No data fetching. No effects beyond the visual. The agent layer (chat, tool-call handler, registry) carries all state. This is what keeps the renderer reusable across every device. A<Gauge> for a state-of-charge level:
unit at the type layer. Adding a new unit to the canonical enum is one edit; every consumer picks it up via the type.
The streaming pattern
Tool calls take time. The UI does not block. The pattern that works:- The model proposes a tool call. The chat surface renders a placeholder primitive (
<DeviceStateCard pending />or a generic skeleton) keyed off the tool name. - The tool fires. The placeholder stays visible.
- The response streams in. The registry picks the real primitive. The placeholder swaps for the real component.
- Multiple parallel tool calls render multiple parallel placeholders. Each resolves on its own clock.
list_battery, list_ev_charger, list_hvac in parallel. As each returns, a real card swaps in. Failures render <ErrorAlert> inline without breaking the rest of the row.
The Vercel AI SDK’s multi-step tool-call pattern composes cleanly with this approach. Each tool call carries an ID; the chat layer keys placeholders off the ID; the swap happens on tool result.
The capability-driven control surface
The most agent-native primitive is<CapabilityControls>. It takes a device’s capability declaration plus a target command and renders form controls bounded by the device’s declared min/max:
parameters: {} for a command (typical for auto.* modes) renders no inputs, just the verb and a button. A device that declares power with unit: "kw" and max: 5.0 renders a 0-5 kW slider. The same component, no per-device code.
What the agent gets for free
- Every new battery the platform supports gets the same
<DeviceStateCard>rendering with no UI work. - Every command added to the canonical surface gets a
<CapabilityControls>renderer with no UI work. - Every new unit added to the
Quantityenum gets a typed primitive switch with one edit to<Gauge>. - Every error code surfaces in
<ErrorAlert>with the right code shown and thedetails.deviceCapabilitiessnapshot inviting an immediate retry.
What next
Self-describing responses
Where the response shapes come from and why they carry what they carry.
Confirmation gates
The destructive-action pattern that gates push and cancel behind a click.
Capabilities
The capability contract
<CapabilityControls> reads from.Canonical actions
The action shape
<CapabilityControls> builds.