Appearance
对于发送邮件、支付、删除记录等高风险操作,OpenRouter SDK 提供 requireApproval 审批机制和 StateAccessor 状态持久化,让 agent 在执行敏感 tool 前暂停等待用户确认,即使跨多个 HTTP 请求也能保持完整对话历史。本页包含完整示例和多轮对话实现方案。
为什么需要审批机制?
有些 tool 不应该自动执行——发邮件、付款、删记录等操作需要人工确认。SDK 提供两个机制:
requireApproval— 模型调用敏感 tool 时暂停执行,让用户审批或拒绝StateAccessor— 在多次callModel调用之间持久化对话状态,审批决定、消息历史、tool 结果都能跨请求保留
两者配合,实现 human-in-the-loop 工作流——用户在 tool 执行前审批每次调用,即使跨 HTTP 请求也没问题。
Tool 级别审批
在 tool 定义上直接设置 requireApproval,接受布尔值或函数:
始终需要审批
typescript
import { tool } from '@openrouter/agent';
import { z } from 'zod';
const sendEmailTool = tool({
name: 'send_email',
description: 'Send an email to a recipient',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
outputSchema: z.object({ sent: z.boolean() }),
requireApproval: true,
execute: async (params) => {
await sendEmail(params);
return { sent: true };
},
});条件审批
用函数按情况决定是否需要审批:
typescript
const deleteRecordTool = tool({
name: 'delete_record',
description: 'Delete a record from the database',
inputSchema: z.object({
id: z.string(),
environment: z.enum(['staging', 'production']),
}),
outputSchema: z.object({ deleted: z.boolean() }),
requireApproval: (params, context) => {
// 只有生产环境的删除操作才需要审批
return params.environment === 'production';
},
execute: async (params) => {
await deleteRecord(params.id);
return { deleted: true };
},
});Call 级别审批
在 callModel 上设置 requireApproval 回调,优先级高于 tool 级别设置:
typescript
const result = openrouter.callModel({
model: 'openai/gpt-4o',
input: 'Send an email and search for documents',
tools: [sendEmailTool, searchTool],
state: myStateAccessor,
requireApproval: (toolCall, context) => {
return toolCall.name === 'send_email' || toolCall.name === 'delete_record';
},
});审批流程
当带审批门控的 tool 被模型调用时:
- 模型生成 tool 调用 — 模型决定调用哪些 tool
- SDK 分组 tool 调用 — 按
requireApproval分成需要审批和可自动执行两组 - 自动执行 tool 立即运行 — 不需要审批的 tool 并行执行
- 保存带 pending 审批的状态 — 对话状态更新为
status: 'awaiting_approval' - 控制权返回调用方 — 检查
result.requiresApproval(),通过result.getPendingToolCalls()查看待审批的调用 - 传入决定后恢复 — 携带相同
state再次调用callModel,传入approveToolCalls和/或rejectToolCalls数组 - 已批准的 tool 执行 — SDK 运行已批准的 tool,向模型发送结果
- 对话继续 — 模型处理 tool 结果,生成下一个响应
StateAccessor 接口
StateAccessor 支持任意存储后端:
typescript
import type { StateAccessor, ConversationState } from '@openrouter/agent';
interface StateAccessor<TTools> {
/** 加载当前对话状态,不存在时返回 null */
load: () => Promise<ConversationState<TTools> | null>;
/** 保存对话状态 */
save: (state: ConversationState<TTools>) => Promise<void>;
}内存实现(示例)
typescript
const conversations = new Map<string, ConversationState>();
function createStateAccessor(conversationId: string): StateAccessor {
return {
load: async () => conversations.get(conversationId) ?? null,
save: async (state) => {
conversations.set(conversationId, state);
},
};
}生产环境请用 Redis、数据库或文件存储等持久化后端,以保证进程重启后状态不丢失。
ConversationState 结构
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 唯一对话标识符 |
messages | OpenResponsesInputUnion | 完整消息历史 |
previousResponseId | string? | 服务端链式调用的前序响应 ID |
pendingToolCalls | ParsedToolCall[]? | 待人工审批的 tool 调用 |
unsentToolResults | UnsentToolResult[]? | 已执行但未发给模型的结果 |
status | ConversationStatus | 当前对话状态 |
createdAt | number | 创建时间戳(Unix ms) |
updatedAt | number | 最后更新时间戳(Unix ms) |
Status 值
| 状态 | 含义 |
|---|---|
'in_progress' | 对话正在处理中 |
'awaiting_approval' | 已暂停,等待 tool 调用审批/拒绝 |
'complete' | 对话正常完成 |
'interrupted' | 对话被中断,可恢复 |
完整示例
typescript
import { OpenRouter, tool } from '@openrouter/agent';
import type { ConversationState, StateAccessor } from '@openrouter/agent';
import { z } from 'zod';
// 1. 定义需要审批的 tool
const sendEmailTool = tool({
name: 'send_email',
description: 'Send an email',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
outputSchema: z.object({ sent: z.boolean(), messageId: z.string() }),
requireApproval: true,
execute: async (params) => {
const result = await sendEmail(params);
return { sent: true, messageId: result.id };
},
});
// 2. 创建 state accessor(这里用内存版本)
const store = new Map<string, ConversationState>();
const conversationId = 'conv-123';
const state: StateAccessor = {
load: async () => store.get(conversationId) ?? null,
save: async (s) => { store.set(conversationId, s); },
};
const openrouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
// 3. 第一次 callModel — 模型尝试调用 tool
const result = openrouter.callModel({
model: 'openai/gpt-4o',
input: 'Send a welcome email to alice@example.com',
tools: [sendEmailTool] as const,
state,
});
// 4. 检查是否需要审批
if (await result.requiresApproval()) {
const pending = await result.getPendingToolCalls();
for (const call of pending) {
console.log(`Tool: ${call.name}`);
console.log(`To: ${call.arguments.to}`);
console.log(`ID: ${call.id}`);
}
// 5. 展示给用户决定
const approved = await askUserForApproval(pending);
const approvedIds = approved.filter(a => a.decision === 'approve').map(a => a.id);
const rejectedIds = approved.filter(a => a.decision === 'reject').map(a => a.id);
// 6. 第二次 callModel — 携带审批决定恢复执行
const resumed = openrouter.callModel({
model: 'openai/gpt-4o',
input: [], // 恢复执行不需要新的用户输入
tools: [sendEmailTool] as const,
state,
approveToolCalls: approvedIds,
rejectToolCalls: rejectedIds,
});
// 7. 获取最终响应
const text = await resumed.getText();
console.log(text);
// "I've sent the welcome email to alice@example.com."
} else {
// 无需审批,tool 已自动执行
const text = await result.getText();
console.log(text);
}恢复模式
从待审批状态恢复
typescript
const loaded = await state.load();
if (loaded?.status === 'awaiting_approval') {
const pending = loaded.pendingToolCalls ?? [];
const result = openrouter.callModel({
model: 'openai/gpt-4o',
input: [],
tools: [sendEmailTool] as const,
state,
approveToolCalls: pending.map(c => c.id),
});
const text = await result.getText();
}多轮对话
使用相同 StateAccessor 的多次 callModel 调用会自动积累消息历史:
typescript
const state: StateAccessor = createStateAccessor('conv-456');
// 第 1 轮
const r1 = openrouter.callModel({
model: 'openai/gpt-4o',
input: 'What is the weather in Tokyo?',
tools: [weatherTool] as const,
state,
});
console.log(await r1.getText());
// "The weather in Tokyo is 22°C and sunny."
// 第 2 轮 — state 包含第 1 轮的完整历史
const r2 = openrouter.callModel({
model: 'openai/gpt-4o',
input: 'And in Paris?',
tools: [weatherTool] as const,
state,
});
console.log(await r2.getText());
// "The weather in Paris is 15°C and cloudy."
// 第 3 轮 — 包含前两轮历史,可以做跨轮推理
const r3 = openrouter.callModel({
model: 'openai/gpt-4o',
input: 'Which city is warmer?',
tools: [weatherTool] as const,
state,
});
console.log(await r3.getText());
// "Tokyo is warmer at 22°C compared to Paris at 15°C."常见问题
Q: requireApproval 的 tool 级别和 call 级别设置冲突时谁优先?
A: Call 级别(callModel 上的 requireApproval 回调)优先级更高,会覆盖 tool 定义上的 requireApproval 设置。
Q: 用户拒绝一个 tool 调用后,模型如何响应?
A: SDK 会向模型发送一条错误消息,说明 tool 调用被拒绝。模型通常会相应地调整回复,例如告知用户操作未执行。
Q: StateAccessor 必须用数据库实现吗?
A: 不强制。StateAccessor 只是一个包含 load 和 save 方法的接口,内存、文件、Redis、数据库都可以作为后端。生产环境建议用持久化存储,保证进程重启后状态不丢失。