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:
The mapping is the load-bearing piece. A registry function takes a tool result and returns a React element:
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.