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.

A model proposing a tool call is not the same as a model executing one. Push and cancel actions on Amps are real-world side effects: a battery starts charging, a scheduled action terminates, a device’s mode flips. None of those should fire from a free-form text generation. The pattern is straightforward. Destructive tool calls render a confirmation primitive. The user clicks. The tool fires. The agent stays observable; the user stays in control; the audit trail stays clean.

Which calls are destructive

A canonical guideline:
ToolDestructive?Why
list_battery, get_battery, list_action, get_actionNoRead-only. No side effect. Render the result, no gate.
push_battery, push_ev_charger, push_hvacYesSends a command to a device. Real-world, often irreversible within the session.
cancel_actionYesTerminates a scheduled or in-flight action.
Any future write or mutationYes by defaultTreat new tools as destructive until proven otherwise.
Mark destructive tools at the registry layer, not in the model’s prompt. The chat surface intercepts tool calls flagged destructive: true and routes them through the gate.

The interrupt-and-resume pattern

The Vercel AI SDK and similar agent runtimes expose a tool-call interrupt pattern. The flow:
  1. The model emits a tool call.
  2. The chat shell inspects the call. If the tool is destructive, the shell does not pass the call to the executor. Instead, it renders a confirmation primitive with the proposed inputs.
  3. The user clicks Confirm or Cancel.
  4. On confirm, the shell forwards the call to the executor. The tool fires.
  5. On cancel, the shell injects a synthetic tool result ({ cancelled: true } or similar) so the agent sees a clean response and can adapt its plan.
A sketch in pseudocode:
const tools = await mcpClient.tools();

const wrappedTools = Object.fromEntries(
  Object.entries(tools).map(([name, tool]) => [
    name,
    isDestructive(name)
      ? wrapWithConfirmation(tool)
      : tool,
  ])
);

function wrapWithConfirmation(tool: Tool) {
  return {
    ...tool,
    execute: async (args, context) => {
      const confirmed = await context.requestUserConfirmation({
        toolName: tool.name,
        proposedArgs: args,
      });
      if (!confirmed) return { cancelled: true };
      return tool.execute(args, context);
    },
  };
}
The confirmation primitive itself is a UI element. It can be a simple <ConfirmCard> with the tool name, the proposed arguments, and two buttons. Or it can be a richer <CapabilityControls> that lets the user adjust the parameters before confirming.

The richer pattern: capability-driven confirmation

Hand-editing a JSON-shaped action is not the user experience to ship. A better confirmation surface is <CapabilityControls> (see Dynamic UI rendering), pre-filled with the model’s proposed values:
The model proposes: push auto.balanced to device battery-living-room, scheduled to start at 18:00, end at 22:00. (rendered as a card with the verb selected, the schedule window editable, a Confirm button bottom-right)
The user sees what’s about to happen, can adjust the bounds within the device’s declared min/max, and clicks Confirm. The tool fires with the (possibly adjusted) parameters. The agent gets the response and continues. This is the load-bearing user experience pattern. Editable confirmation, not a binary gate. The model proposes; the user calibrates; the tool fires.

Why the gate matters

Three failure modes the gate prevents:
  1. Accidental cost. A model that misreads “discharge to 20%” as “discharge to 80%” pushes a real action against a real battery. The gate gives the user a chance to catch it.
  2. Auditability. Every destructive call has a clear “user clicked Confirm at 14:32 with these args” record, separate from the model’s reasoning. Compliance and debugging both benefit.
  3. Trust. Users who know the model cannot fire side effects without their click are more willing to give the agent broader latitude on read calls. The gate buys reach.

Cancel-side semantics

The confirmation primitive should always offer a clean cancel path. On cancel, inject a synthetic tool result the model can read and adapt to. Common shapes:
{ "cancelled": true, "reason": "user_declined" }
{ "cancelled": true, "reason": "user_edited", "userArgs": { ... } }
The first lets the model say “okay, what would you like to do instead?”. The second lets the model see the user’s edits and continue with the corrected plan. Both keep the conversation flowing without surfacing a rejection as an error state.

What this is not

Confirmation gates do not replace the API’s own safety surface. The API still validates capabilities, still rejects unsupported parameters with structured errors, still resolves conflicts via onConflict. The gate is a UX layer on top of the API contract, not a substitute for it. A model that proposes a malformed action gets the same 422 from the API as any other client; the gate just gives the user a chance to intervene before the call.

What next

Dynamic UI rendering

The <CapabilityControls> primitive that backs the richer confirmation surface.

Self-describing responses

Why the proposed-args card can show real bounds without device-specific code.

Canonical actions

The action shape the confirmation primitive validates against.

Conflict resolution

What happens after confirm: the API’s own conflict semantics.