Human approval gates for tool calls with ToolLoopAgent
Guide Human approval gates for tool calls with ToolLoopAgent: 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. Use this when you hit exactly this in Human. Not for different errors or different tools.
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:
- 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.
- 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.
- Provider-executed tools bypass AI SDK approvals entirely. If a tool must be gated, it must be SDK-executed with an execute function.
- Both approve and deny must resolve the pending request. A dialog that only handles approve leaves denied calls hanging.
- Log every approval decision with toolCallId, verdict, and approver. This is your audit trail.
- 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.
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.