跳到主要内容

Vercel AI SDK

查看 Markdown

Vercel 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

例子: agents/typescript/vercel-ai/README.md

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 决定是否需要审批。

vercel-ai-order-support-flow.tsTypeScript
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。

registry.tsTypeScript
// 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);

例子: examples/typescript/src/registry.ts

Session route​

每个 harness 都提供这同一组 route。这些 route 和 session token 的说明见 Session route。

order-support-routes.tsTypeScript
  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_MODELgpt-5-miniOpenAI 模型名称。
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。

slack-connector-tools.tsTypeScript
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,
}),
]),
];
}

例子: examples/typescript/src/shared/slack-connector-tools.ts

Worker 在启动时打开一次 connection,每个 agent 都使用它:

registry.tsTypeScript
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();

例子: examples/typescript/src/registry.ts

Agent 把 tool source 和自己的 tool 一起传入:

vercel-ai-order-support-flow.tsTypeScript
  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。