Vercel AI SDKAI SDK 7 · TypeScript · Node.js 20+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
17 small wins to finish your pathNext lesson

Tool approval before a refund

toolApproval lets a tool call wait for a person. The call ends with a tool-approval-request; your app sends an approval response and calls again.

Examplerefund.ts
import { tool } from "ai";
import { z } from "zod";

export const refundOrder = tool({
  description: "Refund part or all of an order.",
  inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
  execute: async ({ orderId, amount }) => `refunded ${amount} euros on ${orderId}`,
});
Exampleapproval.ts
import { generateText, isStepCount, type ModelMessage } from "ai";
import { shopModel } from "./shop-model.ts";
import { refundOrder } from "./refund.ts";

const settings = {
  model: shopModel,
  tools: { refundOrder },
  toolApproval: { refundOrder: async ({ amount }) => (amount > 50 ? "user-approval" : undefined) },
  stopWhen: isStepCount(5),
};

const messages: ModelMessage[] = [{ role: "user", content: "Refund 90 euros on A-1002" }];
const first = await generateText({ ...settings, messages });
console.log(first.content.map((part) => part.type));
Example
npx tsx approval.ts

toolApproval maps a tool name to a policy. This one is a function of the parsed input: over 50 euros returns "user-approval", anything else runs normally. The model called refundOrder with 90, and instead of running it the SDK added a tool-approval-request and stopped.

Exampleapproval.ts, continued
const request = first.content.find((part) => part.type === "tool-approval-request");
messages.push(...first.response.messages, {
  role: "tool",
  content: [{ type: "tool-approval-response", approvalId: request.approvalId, approved: true }],
});
const second = await generateText({ ...settings, messages });
console.log(second.text);
Example
npx tsx approval.ts

The second call gets the first call's response messages plus a tool message with a tool-approval-response for that approvalId. Approved, the tool runs and the model answers. approved: false with a reason sends the denial to the model instead, and the tool never runs.

Who approves
If the browser sends the approval, a customer could approve their own refund. Check on the server that the person approving is allowed to, or use the SDK's experimental_toolApprovalSecret to sign approval requests.
Try it yourself
  • Refund 30 euros and print the parts.
  • Deny the request with a reason and print the second answer.
  • Return { type: "denied", reason: "Refunds are closed" } from the policy.

This is what real progress feels like.