Install

npm install [email protected] @ai-sdk/[email protected]
npx shadcn@latest add https://agentsandhumans.ai/api/registry/approval-gate.json
npx shadcn@latest add https://agentsandhumans.ai/api/registry/ai-sdk-approval.json

The adapter lands in lib/ai-sdk-approval.ts and brings the Interaction Contract with it. It was written against ai 7.0.127 and @ai-sdk/react 4.0.130 and reads the shapes those versions document. It does not import ai, so it adds no package of its own.

1. Ask for approval on the server

Set the tool to 'user-approval' in toolApproval. The reason you return shows in the gate under "What happens".

// app/api/chat/route.ts
const result = streamText({
  model,
  messages: await convertToModelMessages(messages, { tools }),
  tools: { sendInvoice },
  toolApproval: {
    sendInvoice: () => ({
      type: 'user-approval',
      reason: 'The customer gets this invoice by email as soon as you approve.',
    }),
  },
  // Signs each approval request, so a client cannot invent an approval.
  experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
});

return createUIMessageStreamResponse({
  stream: toUIMessageStream({ stream: result.stream, tools, originalMessages: messages }),
});

2. Feed the gate from useChat

'use client';
import { useChat } from '@ai-sdk/react';
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
import { useAiSdkApproval } from '@/lib/ai-sdk-approval';
import { ApprovalGate } from '@/components/approval-gate';

export default function Chat() {
  const { id, messages, addToolApprovalResponse } = useChat({
    // Send the turn back once every approval in it has an answer.
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  });
  const { gateProps } = useAiSdkApproval({
    messages,
    addToolApprovalResponse,
    runId: id,
    describe: (tool, input) =>
      tool === 'sendInvoice' ? { resources: ['billing'], reversible: false } : undefined,
  });

  return gateProps && (
    <ApprovalGate key={gateProps.request.id} {...gateProps} config={{ expiresInMs: 120_000 }} />
  );
}

gateProps is null until a call waits on the person. Key the gate by the request id so each call gets a fresh gate. describe tells the gate what the AI SDK does not know: what the tool touches and whether it can be undone. Left out, the gate says the call cannot be undone.

Each decision, mapped

The personThe gate recordsThe AI SDK receives
Approves granted, version 1 { approved: true }. The tool runs on the server.
Rejects denied { approved: false, reason }. The tool does not run, and the model reads the reason.
Lets the timer run out expired, actor system { approved: false, reason }. The AI SDK has no expiry of its own, so the gate's timer decides.
Edits, then approves granted, version 2, with editedArgs { approved: false, reason } naming the edited input. If the model then calls the tool with that input, the adapter approves it without a second ask.

An edit declines the first call for a reason: the AI SDK runs an approved call with the input it asked about and no other. So the adapter declines that call and gives the model the input the person approved. Any other input from the model opens a new gate.

A session or standing grant approves later calls to the same tool while the page is open. To make a standing grant outlast a reload, set the tool to 'approved' in toolApproval on the server.

Run the example

Node 22.6 or later. From a clone of the repository:

cd examples/ai-sdk-approval
npm ci
npm test           # approve, deny, expire and edit through the real chat route
npm run dev        # http://localhost:3000

Without ANTHROPIC_API_KEY the example runs a scripted model, so the whole flow works offline. Set the key to talk to Claude instead. Source and tests: examples/ai-sdk-approval. The AI SDK's own guide: Tool Approvals.

Before you ship it

In the useChat pattern the browser sends the whole conversation back each turn, so a changed client could claim an approval the person did not give. Set experimental_toolApprovalSecret on the server, and the AI SDK checks that it issued each approval request it sees. The gate's receipt reads "Recorded on this device" until your server stores the decision and passes back a serverReceiptId.