Vercel AI SDK
View as MarkdownThe Vercel AI SDK gives TypeScript one API for many model providers, with typed tools. The Dex adapter runs an AI SDK agent definition inside a durable session. It is available for TypeScript.
When to use it
Use it when your TypeScript application already uses AI SDK models and tools. Keep the model, the tools, and stopWhen, and let Dex own the session.
How a session runs
Every tool reaches the model without its execute, so the AI SDK returns the tool calls instead of running them. Each RunModel Step makes one model call with streamText.
Each tool call runs the tool's original execute in its own Step, in parallel: CallTool for a read or an idempotent write, CallSingleAttemptTool for any other write. A call that needs approval waits in AwaitToolApproval first. The next RunModel adds the recorded results, checks stopWhen, and calls the model again.
A turn that looks up an order runs RunModel, CallTool, ResumeTurn, and RunModel.
Install
Install the adapter with its framework. Each adapter brings in the Dex Agents core.
npm i @superdurable/dex-agents-vercel-ai @superdurable/dex-agents @superdurable/dex ai @ai-sdk/openai zod
The example
The example is the order support agent that every framework page uses.
The Flow type is VercelAiOrderSupportAgent. The full example is under examples/typescript/src/products/agent-harnesses.
Agent and tools
The definition holds the model, the instructions, AI SDK tools, and the tool policies. Dex decides approval from the policies.
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,
);
}
Example: examples/typescript/src/products/agent-harnesses/vercel-ai/vercel-ai-order-support-flow.ts
Worker
One Worker serves the session Flows of every harness in the example. Build the same session Flow in every process that starts sessions or calls their RPCs.
// 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);
Example: examples/typescript/src/registry.ts
Session routes
Every harness serves these same routes. Session routes explains them and the session token.
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));
});
Example: examples/typescript/src/products/agent-harnesses/order-support/order-support-routes.ts
Run it locally
Start Dex and the example
In a first terminal, render the session graph, then start Dex. Dex Web reads the graph when it starts, to show the session and its approval controls.
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
Example: examples/typescript/src/products/agent-harnesses/README.md
dexcli dev stays in the foreground. In a second terminal, start the example:
cd examples/typescript
OPENAI_API_KEY=... npm start
Example: examples/typescript/src/products/agent-harnesses/README.md
The app serves on 127.0.0.1:8080. Dex Web is at http://127.0.0.1:8802.
The example has no demo model. It calls OpenAI through @ai-sdk/openai, so it needs OPENAI_API_KEY.
| Variable | Default | Meaning |
|---|---|---|
| DEX_VERCEL_AI_MODEL | gpt-5-mini | The OpenAI model name. |
| OPENAI_API_KEY | none; required | The OpenAI API key of the Worker. |
| DEX_AGENT_SESSION_KEY | a random key | The key that signs session tokens. |
Call the session
Start a session, ask the agent to cancel order 1002, and approve the call:
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"
Example: examples/typescript/src/products/agent-harnesses/README.md
The loop reads the snapshot until the agent asks to run cancel_order. That call waits for approval, then runs once. Read the snapshot again to see the agent's answer.
Approve in Dex Web
Instead of the last command, you can approve the call in Dex Web. Follow the steps in Approve in Dex Web.
Connect Connectors
The agent can call Connector operations as tools, as Connectors as tools describes. The example adds the Slack tool slack_post_thread_reply, which replies in a Slack thread.
Offer the tool
Install @superdurable/connectors-sdk and the Connector package, such as @superdurable/connector-slack. The install commands are in Connector tools.
The Worker opens the Slack connection support-slack from the project configuration. connectorTool turns the operation into a tool, and ConnectorToolSource holds it. Without the connection, the agent gets no 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,
}),
]),
];
}
Example: examples/typescript/src/shared/slack-connector-tools.ts
The Worker opens the connection once, at startup, and every agent uses it:
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
Example: examples/typescript/src/registry.ts
The agent passes the tool source next to its own tools:
return createVercelAiAgentSessionFlow(
{
// ...
toolSources: createSlackConnectorToolSources(slack),
},
vercelAiOrderSupportFlowType,
);
Example: examples/typescript/src/products/agent-harnesses/vercel-ai/vercel-ai-order-support-flow.ts
How a call runs
Each call of slack_post_thread_reply runs once, in its own CallSingleAttemptTool Step, after a person approves it. The Slack Connector tool explains why.
The adapter offers the Connector tools after the agent's tools, as AI SDK tools without execute. A call never runs inside the model call. AI SDK needsApproval does not apply to it; the source decides approval.
Configure the connection
Connections explains where the Worker finds the support-slack connection and what the Slack app needs.
To try it, start the example with DEX_CONNECTOR_CONFIG_FILE set. Then ask the agent to reply in a Slack thread, with the channel ID and the thread timestamp:
Reply in Slack thread C0123456789 1710000000.000100: Order 1002 ships on Thursday.
The post waits for approval, like cancel_order.
Limits
- A crash in RunModel reruns that model call and any provider-executed tools in it.
- A tool's execute gets toolCallId and abortSignal. Its messages are empty and its context is undefined.
- Approval belongs to Dex. AI SDK toolApproval is not exposed, and a needsApproval function is rejected. Provider-executed tools never wait for approval.
- Every tool runs in Dex, so a tool without execute, such as a browser-side tool, is rejected.
- Tool inputs and outputs must survive JSON.
- prepareStep, activeTools, output, and stream transforms are not exposed.
The adapter README lists every limit. See Limits for long sessions for the server limits.