Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.amps.ai/llms.txt

Use this file to discover all available pages before exploring further.

The same envelope that lets an agent reason about the API also drives the UI. Components are dumb. Props lift straight from canonical responses. The chat layer (or your component registry) picks a primitive based on the response shape. One renderer covers every device family that follows the canonical model. This page is for integrators building a chat surface, generative UI, or any agent-facing UI on top of the Amps API. The patterns work against the deployed REST API and will work the same way against the Execution MCP when it ships.

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 shapePrimitiveNotes
{ 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 acknowledgedcompleted (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.
The mapping is the load-bearing piece. A registry function takes a tool result and returns a React element:
function pickPrimitive(result: unknown): ReactElement {
  if (looksLikeError(result))           return <ErrorAlert envelope={result} />;
  if (looksLikeBatteryState(result))    return <DeviceStateCard device={result} />;
  if (looksLikeActionLifecycle(result)) return <ActionTimeline action={result} />;
  if (looksLikeListResponse(result))    return <ListView response={result} />;
  return <EnvelopeRenderer response={result} />;
}
The model can also emit a structured render hint as part of its response (a 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:
type QuantityUnit =
  | "percent" | "kw" | "watts" | "kwh"
  | "amps" | "volts" | "hours" | "minutes"
  | "celsius" | "fahrenheit";

type Quantity = { value: number; unit: QuantityUnit };

function Gauge({
  quantity,
  min = 0,
  max,
  label,
  pending = false,
}: {
  quantity: Quantity;
  min?: number;
  max?: number;
  label?: string;
  pending?: boolean;
}) {
  if (pending) return <SkeletonRing label={label} />;

  const ceiling = max ?? (quantity.unit === "percent" ? 100 : quantity.value * 1.5);
  const ratio = (quantity.value - min) / (ceiling - min);
  return (
    <Ring
      ratio={ratio}
      label={label ?? quantity.unit}
      readout={`${quantity.value}${unitSuffix(quantity.unit)}`}
    />
  );
}
The component switches on 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:
  1. The model proposes a tool call. The chat surface renders a placeholder primitive (<DeviceStateCard pending /> or a generic skeleton) keyed off the tool name.
  2. The tool fires. The placeholder stays visible.
  3. The response streams in. The registry picks the real primitive. The placeholder swaps for the real component.
  4. Multiple parallel tool calls render multiple parallel placeholders. Each resolves on its own clock.
This is what makes the agent’s plan visible. The user sees three skeletons appear when the model fires 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:
function CapabilityControls({
  capabilities,
  command,
  onPush,
}: {
  capabilities: BatteryCapabilities;
  command: keyof BatteryCapabilities["commands"];
  onPush: (action: BatteryAction) => Promise<void>;
}) {
  const params = capabilities.commands[command]?.parameters ?? {};
  const [values, setValues] = useState<Record<string, Quantity>>({});

  return (
    <Card>
      <h3>Push <code>{command}</code></h3>
      {Object.entries(params).map(([key, spec]) => (
        <ParamInput
          key={key}
          name={key}
          unit={spec.unit}
          min={spec.min}
          max={spec.max}
          value={values[key]}
          onChange={(q) => setValues({ ...values, [key]: q })}
        />
      ))}
      <Button onClick={() => onPush(buildAction(command, values))}>
        Push action
      </Button>
    </Card>
  );
}
A device that declares 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 Quantity enum gets a typed primitive switch with one edit to <Gauge>.
  • Every error code surfaces in <ErrorAlert> with the right code shown and the details.deviceCapabilities snapshot inviting an immediate retry.
The cost of supporting an agent on top of Amps is the cost of writing the canonical primitives once. Devices, OEMs, and operations slot into the same renderer.

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.