Vercel AI SDK
查看 MarkdownVercel AI SDK 为 TypeScript 提供一套对接多家模型 provider 的 API,并支持有类型的 tool。Dex adapter 把 AI SDK 的 Agent 定义放进 durable session 中运行。它支持 TypeScript。
何时使用
TypeScript 应用已经在使用 AI SDK 的模型和 tool 时,使用它。保留模型、tool 和 stopWhen,由 Dex 管理 session。
Session 如何运行
每个 tool 交给模型时都不带 execute,因此 AI SDK 会返回 tool call,而不是运行它们。每个 RunModel Step 用 streamText 调用一次模型。
每个 tool call 在自己的 Step 中并行运行 tool 原本的 execute:read 或 idempotent write 用 CallTool,其他写操作用 CallSingleAttemptTool。需要审批的调用先在 AwaitToolApproval 中等待。下一个 RunModel 加入记录下来的结果,检查 stopWhen,然后再次调用模型。
查询订单的 turn 依次运行 RunModel、CallTool、ResumeTurn 和 RunModel。
安装
把 adapter 与它的 framework 一起安装。每个 adapter 都会带上 Dex Agents core。
npm i @superdurable/dex-agents-vercel-ai @superdurable/dex-agents @superdurable/dex ai @ai-sdk/openai zod
Example
这个 example 就是每个 framework 页面共用的订单客服 Agent。
Flow type 是 VercelAiOrderSupportAgent(TypeScript)。完整 example 位于 examples/typescript/src/products/agent-harnesses。
Agent 与 tool
定义中包含模型、instructions、AI SDK tool 和 tool policy。Dex 根据 policy 决定是否需要审批。
export function createVercelAiOrderSupportFlow(
orderDesk: OrderDesk,
model: LanguageModel,
slack: Connection | undefined,
): AgentSessionFlow {
return createVercelAiAgentSessionFlow(
{
model,
instructions: orderSupportInstructions,
tools: {
look_up_order: tool({
description: "Look up one order by its order ID and return its items and status.",
inputSchema: z.object({ order_id: z.string() }),
execute: ({ order_id }) => orderDesk.describeOrder(order_id),
}),
cancel_order: tool({
description: "Cancel one order that has not shipped yet.",
inputSchema: z.object({ order_id: z.string(), reason: z.string() }),
execute: ({ order_id, reason }) => orderDesk.cancelOrder(order_id, reason),
}),
},
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
toolSources: createSlackConnectorToolSources(slack),
},
vercelAiOrderSupportFlowType,
);
}
例子: examples/typescript/src/products/agent-harnesses/vercel-ai/vercel-ai-order-support-flow.ts
Worker
一个 Worker 为 example 中所有 harness 的 session Flow 提供服务。每个启动 session 或调用其 RPC 的进程,都要构建同一个 session Flow。
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
// ...
// One order desk for the four agent harness examples; each harness registers its own session Flow type.
const orderDesk = new OrderDesk();
// ...
const vercelAiOrderSupportFlow = createVercelAiOrderSupportFlow(orderDesk, loadVercelAiOrderSupportModel(), slack);
Session route
每个 harness 都提供这同一组 route。这些 route 和 session token 的说明见 Session route。
router.post("/sessions", async (request, response) => {
const initialMessage = optionalString(requireBody(request), "message");
// The server picks the session ID, so a caller cannot name another session.
const sessionId = `order-support-${randomUUID()}`;
await startAgentSession(client, flow, sessionId, { initialMessage }, { timeoutMs: 30 * DAY_MS });
response.json({ session_id: sessionId, session_token: issueSessionToken(sessionId) });
});
router.get("/sessions/:id/snapshot", async (request, response) => {
response.json(convertAgentSnapshotToJson(await client.invokeRPC(flow.getAgentSnapshot, sessionIdOf(request))));
});
router.post("/sessions/:id/messages", async (request, response) => {
const sessionId = sessionIdOf(request);
const message = { text: requireString(requireBody(request), "text") };
sendAccepted(response, await client.invokeRPC(flow.sendUserMessage, sessionId, message));
});
router.post("/sessions/:id/interrupts/:interruptId/approve", async (request, response) => {
const sessionId = sessionIdOf(request);
const approval = { interruptId: String(request.params.interruptId), approved: true, rejectionMessage: null };
sendAccepted(response, await client.invokeRPC(flow.resolveInterrupt, sessionId, approval));
});
例子: examples/typescript/src/products/agent-harnesses/order-support/order-support-routes.ts
在本地运行
启动 Dex 与 example
在第一个终端中,先渲染 session graph,再启动 Dex。Dex Web 启动时读取这个 graph,用它显示 session 和审批控件。
cd examples/typescript
npm install
mkdir -p definitions
npx dex-agents-session-graph --flow-type VercelAiOrderSupportAgent \
--permission order-support.operate --out definitions/vercel-ai-sdk.json
dexcli dev --flow-rendering-dir ./definitions
例子: examples/typescript/src/products/agent-harnesses/README.md
dexcli dev 会一直在前台运行。在第二个终端中启动 example:
cd examples/typescript
OPENAI_API_KEY=... npm start
例子: examples/typescript/src/products/agent-harnesses/README.md
应用监听 127.0.0.1:8080。Dex Web 位于 127.0.0.1:8802。
这个 example 没有 demo model。它通过 @ai-sdk/openai 调用 OpenAI,因此需要 OPENAI_API_KEY。
| 变量 | 默认值 | 含义 |
|---|---|---|
| DEX_VERCEL_AI_MODEL | gpt-5-mini | OpenAI 模型名称。 |
| OPENAI_API_KEY | 无;必填 | Worker 的 OpenAI API key。 |
| DEX_AGENT_SESSION_KEY | 随机 key | 签发 session token 的 key。 |
调用 session
启动一个 session,让 Agent 取消订单 1002,然后批准这次调用:
base=http://127.0.0.1:8080/products/agent-harnesses/vercel-ai
session=$(curl -s -X POST "$base/sessions" -H 'Content-Type: application/json' \
-d '{"message": "Please cancel order 1002"}')
id=$(echo "$session" | jq -r .session_id)
token=$(echo "$session" | jq -r .session_token)
until interrupt=$(curl -s "$base/sessions/$id/snapshot" -H "X-Agent-Session-Token: $token" \
| jq -re '.pending_interrupts[0].interrupt_id'); do sleep 1; done
curl -s -X POST "$base/sessions/$id/interrupts/$interrupt/approve" -H "X-Agent-Session-Token: $token"
例子: examples/typescript/src/products/agent-harnesses/README.md
循环会反复读取 snapshot,直到 Agent 请求运行 cancel_order。这次调用先等待审批,批准后只运行一次。再读一次 snapshot 即可看到 Agent 的回答。
在 Dex Web 中审批
也可以不执行最后一条命令,改在 Dex Web 中审批,步骤见 在 Dex Web 中审批。
连接 Connector
Agent 可以把 Connector 的 operation 当作 tool 调用,见 把 Connector 用作 tool。这个 example 加入了 Slack tool slack_post_thread_reply,用来在 Slack thread 中回复。
提供 tool
安装 @superdurable/connectors-sdk 以及 Connector 包,例如 @superdurable/connector-slack。安装命令见 Connector tools。
Worker 从项目配置中打开 Slack connection support-slack。connectorTool 把 operation 变成 tool,由 ConnectorToolSource 持有。没有这个 connection 时,agent 不会得到 Slack tool。
export async function openSlackConnectionFromProject(
environment: NodeJS.ProcessEnv = process.env,
): Promise<Connection | undefined> {
// ...
const project = await loadFromEnvironment(environment);
// ...
return newProjectConnection(project, slackConnectionName);
}
// ...
export function createSlackConnectorToolSources(slack: Connection | undefined): ToolSource[] {
if (slack === undefined) {
return [];
}
return [
new ConnectorToolSource([
connectorTool(slack.operations.postThreadReply(), slack.reference, {
manifest,
inputValidator: PostThreadReplyInputSchema,
// The manifest does not ask for approval; this agent asks before every post.
requiresApproval: true,
}),
]),
];
}
Worker 在启动时打开一次 connection,每个 agent 都使用它:
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
Agent 把 tool source 和自己的 tool 一起传入:
return createVercelAiAgentSessionFlow(
{
// ...
toolSources: createSlackConnectorToolSources(slack),
},
vercelAiOrderSupportFlowType,
);
例子: examples/typescript/src/products/agent-harnesses/vercel-ai/vercel-ai-order-support-flow.ts
调用如何运行
每次调用 slack_post_thread_reply 都要先经人批准,然后在自己的 CallSingleAttemptTool Step 中只运行一次。原因见 Slack Connector tool。
Adapter 在 agent 的 tool 之后提供 Connector tool,它们是没有 execute 的 AI SDK tool。调用从不在模型调用内运行。AI SDK 的 needsApproval 对它不起作用;审批由 source 决定。
配置 connection
Worker 从哪里找到 connection support-slack、Slack app 需要什么,见 Connection。
试用时,先设置 DEX_CONNECTOR_CONFIG_FILE 再启动 example。然后让 agent 在 Slack thread 中回复,并给出 channel ID 和 thread timestamp:
Reply in Slack thread C0123456789 1710000000.000100: Order 1002 ships on Thursday.
这次发帖和 cancel_order 一样,会先等待审批。
限制
- RunModel 中发生崩溃时,会重跑那次模型调用以及其中由 provider 执行的 tool。
- Tool 的 execute 会收到 toolCallId 和 abortSignal。它的 messages 为空,context 为 undefined。
- 审批由 Dex 负责。不开放 AI SDK 的 toolApproval,needsApproval 函数会被拒绝。由 provider 执行的 tool 永远不会等待审批。
- 每个 tool 都在 Dex 中运行,因此没有 execute 的 tool,例如浏览器端 tool,会被拒绝。
- Tool 的输入和输出必须能经受 JSON 序列化。
- 不开放 prepareStep、activeTools、output 和 stream transform。
Adapter README 列出了全部限制。Server 的限制参见 Limits for long sessions。