TL;DR: TL;DR: Tools that modify data, spend money, or touch private systems should not run on model authority alone. toolApproval puts a human in the loop. import { ToolLoopAgent, tool } from 'ai'; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: 'your-model-id', instructions: 'You are a careful assistant.', tools: { runCommand: tool({ inputSchema: z.object({ command: z.string() }), execute: async ({ command }) => runCommand(command), }), }, toolApproval: { runCommand: 'user-approval', }, }); When the model calls runCommand, the agent emits a tool-approval-request instead of executing.

TL;DR: Tools that modify data, spend money, or touch private systems should not run on model authority alone. toolApproval puts a human in the loop. import { ToolLoopAgent, tool } from 'ai'; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: 'your-model-id', instructions: 'You are a careful assistant.', tools: { runCommand: tool({ inputSchema: z.object({ command: z.string() }), execute: async ({ command }) => runCommand(command), }), }, toolApproval: { runCommand: 'user-approval', }, }); When the model calls runCommand, the agent emits a tool-approval-request instead of executing.

Tools that modify data, spend money, or touch private systems should not run on model authority alone. toolApproval puts a human in the loop.

import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';

const agent = new ToolLoopAgent({
  model: 'your-model-id',
  instructions: 'You are a careful assistant.',
  tools: {
    runCommand: tool({
      inputSchema: z.object({ command: z.string() }),
      execute: async ({ command }) => runCommand(command),
    }),
  },
  toolApproval: {
    runCommand: 'user-approval',
  },
});

When the model calls runCommand, the agent emits a tool-approval-request instead of executing. Your UI renders a dialog (show the command, who asked, what it does), and the human approves or denies. The verdict goes back via the approval response path, and the agent continues.

Rules:
1. Approval statuses: 'not-applicable' (run normally), 'approved' (auto-approve with a recorded reason), 'denied' (auto-deny with reason), 'user-approval' (wait for a human). Use the object form { type: 'denied', reason: '...' } when you want the reason recorded.
2. Per-tool functions get the typed input plus toolCallId, messages, toolContext, and runtimeContext. Decide on the input: auto-approve reads, require approval for writes, deny deletes in this workspace.
3. Provider-executed tools bypass AI SDK approvals entirely. If a tool must be gated, it must be SDK-executed with an execute function.
4. Both approve and deny must resolve the pending request. A dialog that only handles approve leaves denied calls hanging.
5. Log every approval decision with toolCallId, verdict, and approver. This is your audit trail.
6. For policy-as-code (OPA rego files, CI-testable rules), use @ai-sdk/policy-opa on top of the same toolApproval callback.

## When to use
You hit exactly this: Human approval gates for tool calls with ToolLoopAgent in Human.

## When not to use
A different error, or the same symptom in a different tool. This page only covers the failure above.

## Compatibility
Human.