Add Dex to Your Agent
View as MarkdownMove the example agent of the Agent Quick Start into your own code: install your framework's adapter, copy the complete files, and serve the session from a web app.
What changes in your app
- Your agent definition stays. You wrap it and give each tool a policy.
- Your request handler changes shape. It sends the user's message to the session and returns. The reply arrives on the session's event stream, or in its snapshot.
- You run two more things. A Worker process runs the session Steps, and Dex Server stores them. Dex Server runs in Dex Cloud or on your own infrastructure; Choose How to Run Dex compares the two. Dex is in Beta, Dex Server is at version 0.1, and one active node serves a cluster.
- Each turn waits on Dex commits. A turn with three parallel tool calls adds about 0.4 seconds; see Latency and cost.
Run Agents in Production lists what to set up. Performance gives the numbers, and Limits gives the license.
What changes in your code
The example writes the agent as your framework alone would, then wraps it in a Dex agent session and gives each tool a policy. For these two function tools, that is the only change:
- Claude Agent SDK
- Google ADK
- LangChain and LangGraph
- Mastra
- OpenAI Agents SDK
- Pydantic AI
- Vercel AI SDK
def build_plain_agent(order_desk: OrderDesk, model: str) -> ClaudeAgentOptions:
"""Returns the agent's options as the Claude Agent SDK alone writes them."""
orders = create_sdk_mcp_server("orders", tools=build_order_tools(order_desk))
return ClaudeAgentOptions(
model=model,
system_prompt=ORDER_SUPPORT_INSTRUCTIONS,
mcp_servers={"orders": orders},
allowed_tools=["mcp__orders__look_up_order", "mcp__orders__cancel_order"],
)
def build_durable_agent(order_desk: OrderDesk, model: str) -> AgentSessionFlow:
"""Returns the same agent as a durable Dex agent session."""
# Dex passes the tools to the CLI itself and decides when each one runs.
def build_options(context: ClaudeAgentOptionsContext) -> ClaudeAgentOptions:
return ClaudeAgentOptions(
model=model,
env=build_claude_cli_environment(os.environ),
system_prompt=ORDER_SUPPORT_INSTRUCTIONS,
tools=[],
setting_sources=[],
)
return claude_agent_session_flow(
build_options,
build_order_tools(order_desk),
flow_type="ClaudeAgentSDKQuickStart",
tool_policies={
"look_up_order": ToolPolicy(ToolEffect.READ),
"cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
},
)
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/claude_agent_sdk_quick_start.py
/** Returns the agent's options as the Claude Agent SDK alone writes them. */
export function buildPlainAgent(orderDesk: OrderDesk, model: string): Options {
const orders = createSdkMcpServer({ name: "orders", tools: buildOrderTools(orderDesk) });
return {
model,
systemPrompt: orderSupportInstructions,
mcpServers: { orders },
allowedTools: ["mcp__orders__look_up_order", "mcp__orders__cancel_order"],
};
}
/** Returns the same agent as a durable Dex agent session. Dex passes the tools to the CLI itself. */
export function buildDurableAgent(
orderDesk: OrderDesk,
model: string,
cliEnvironment: Readonly<Record<string, string>>,
): AgentSessionFlow {
return createClaudeAgentSessionFlow(
{
options: { model, systemPrompt: orderSupportInstructions, tools: [], settingSources: [], env: { ...cliEnvironment } },
customTools: buildOrderTools(orderDesk),
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
},
"ClaudeAgentSdkQuickStart",
);
}
Example: examples/typescript/src/products/agent-harnesses/quick-start/claude-agent-sdk-quick-start.ts
The Claude Agent SDK takes the tools in a list next to the options, so Dex runs each one itself.
def build_plain_agent(order_desk: OrderDesk, model: str | BaseLlm) -> LlmAgent:
"""Returns the agent as Google ADK alone writes it."""
def look_up_order(order_id: str) -> str:
"""Look up one order by its order ID and return its items and status."""
return order_desk.describe_order(order_id)
def cancel_order(order_id: str, reason: str) -> str:
"""Cancel one order that has not shipped yet."""
return order_desk.cancel_order(order_id, reason)
return LlmAgent(
name="order_support",
model=model,
instruction=ORDER_SUPPORT_INSTRUCTIONS,
tools=[look_up_order, cancel_order],
)
def build_durable_agent(order_desk: OrderDesk, model: str | BaseLlm) -> AgentSessionFlow:
"""Returns the same agent as a durable Dex agent session."""
return adk_agent_session_flow(
build_plain_agent(order_desk, model),
flow_type="GoogleADKQuickStart",
tool_policies={
"look_up_order": ToolPolicy(ToolEffect.READ),
"cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
},
)
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/google_adk_quick_start.py
def build_plain_agent(
order_desk: OrderDesk,
model: str | BaseChatModel,
middleware: Sequence[AgentMiddleware[Any, Any]] = (),
) -> Pregel[Any, Any, Any, Any]:
"""Returns the agent as LangChain alone writes it."""
@tool
def look_up_order(order_id: str) -> str:
"""Look up one order by its order ID and return its items and status."""
return order_desk.describe_order(order_id)
@tool
def cancel_order(order_id: str, reason: str) -> str:
"""Cancel one order that has not shipped yet."""
return order_desk.cancel_order(order_id, reason)
return create_agent(
model,
tools=[look_up_order, cancel_order],
system_prompt=ORDER_SUPPORT_INSTRUCTIONS,
middleware=middleware,
)
def build_durable_agent(order_desk: OrderDesk, model: str | BaseChatModel) -> AgentSessionFlow:
"""Returns the same agent as a durable Dex agent session."""
return langchain_agent_session_flow(
# Dex builds the agent on the Worker, with middleware that runs each tool call as a Step.
lambda: build_plain_agent(order_desk, model, [DurableToolCallMiddleware()]),
flow_type="LangChainQuickStart",
tool_policies={
"look_up_order": ToolPolicy(ToolEffect.READ),
"cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
},
)
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/langchain_quick_start.py
DurableToolCallMiddleware runs each tool call as its own Step.
/** Returns the agent as Mastra alone writes it. */
export function buildPlainAgent(orderDesk: OrderDesk, model: MastraModelConfig): Agent {
const lookUpOrder = createTool({
id: "look_up_order",
description: "Look up one order by its order ID and return its items and status.",
inputSchema: z.object({ order_id: z.string() }),
execute: async ({ order_id }) => orderDesk.describeOrder(order_id),
});
const cancelOrder = createTool({
id: "cancel_order",
description: "Cancel one order that has not shipped yet.",
inputSchema: z.object({ order_id: z.string(), reason: z.string() }),
execute: async ({ order_id, reason }) => orderDesk.cancelOrder(order_id, reason),
});
return new Agent({
id: "order-support",
name: "OrderSupport",
instructions: orderSupportInstructions,
model,
tools: { look_up_order: lookUpOrder, cancel_order: cancelOrder },
});
}
/** Returns the same agent as a durable Dex agent session. */
export function buildDurableAgent(orderDesk: OrderDesk, model: MastraModelConfig): AgentSessionFlow {
return createMastraAgentSessionFlow(buildPlainAgent(orderDesk, model), "MastraQuickStart", {
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
});
}
Example: examples/typescript/src/products/agent-harnesses/quick-start/mastra-quick-start.ts
def build_plain_agent(order_desk: OrderDesk, model: str | Model) -> Agent:
"""Returns the agent as the OpenAI Agents SDK alone writes it."""
@function_tool
def look_up_order(order_id: str) -> str:
"""Look up one order by its order ID and return its items and status."""
return order_desk.describe_order(order_id)
@function_tool
def cancel_order(order_id: str, reason: str) -> str:
"""Cancel one order that has not shipped yet."""
return order_desk.cancel_order(order_id, reason)
return Agent(
name="OrderSupport",
instructions=ORDER_SUPPORT_INSTRUCTIONS,
model=model,
tools=[look_up_order, cancel_order],
)
def build_durable_agent(order_desk: OrderDesk, model: str | Model) -> AgentSessionFlow:
"""Returns the same agent as a durable Dex agent session."""
return openai_agents_session_flow(
build_plain_agent(order_desk, model),
flow_type="OpenAIAgentsQuickStart",
tool_policies={
"look_up_order": ToolPolicy(ToolEffect.READ),
"cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
},
)
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/openai_agents_quick_start.py
/** Returns the agent as the OpenAI Agents SDK alone writes it. */
export function buildPlainAgent(orderDesk: OrderDesk, model: string | Model): Agent {
const lookUpOrder = tool({
name: "look_up_order",
description: "Look up one order by its order ID and return its items and status.",
parameters: z.object({ order_id: z.string() }),
errorFunction: null,
execute: ({ order_id }) => orderDesk.describeOrder(order_id),
});
const cancelOrder = tool({
name: "cancel_order",
description: "Cancel one order that has not shipped yet.",
parameters: z.object({ order_id: z.string(), reason: z.string() }),
errorFunction: null,
execute: ({ order_id, reason }) => orderDesk.cancelOrder(order_id, reason),
});
return new Agent({
name: "OrderSupport",
instructions: orderSupportInstructions,
model,
tools: [lookUpOrder, cancelOrder],
});
}
/** Returns the same agent as a durable Dex agent session. */
export function buildDurableAgent(orderDesk: OrderDesk, model: string | Model): AgentSessionFlow {
return createOpenAIAgentsSessionFlow(buildPlainAgent(orderDesk, model), "OpenAIAgentsQuickStart", {
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
});
}
Example: examples/typescript/src/products/agent-harnesses/quick-start/openai-agents-quick-start.ts
def build_plain_agent(order_desk: OrderDesk, model: str | Model) -> Agent[None, str]:
"""Returns the agent as Pydantic AI alone writes it."""
# The model is resolved at the first run, so a Client process needs no provider key.
agent = Agent(model, instructions=ORDER_SUPPORT_INSTRUCTIONS, defer_model_check=True)
@agent.tool_plain
def look_up_order(order_id: str) -> str:
"""Look up one order by its order ID and return its items and status."""
return order_desk.describe_order(order_id)
@agent.tool_plain
def cancel_order(order_id: str, reason: str) -> str:
"""Cancel one order that has not shipped yet."""
return order_desk.cancel_order(order_id, reason)
return agent
def build_durable_agent(order_desk: OrderDesk, model: str | Model) -> AgentSessionFlow:
"""Returns the same agent as a durable Dex agent session."""
return pydantic_ai_session_flow(
build_plain_agent(order_desk, model),
flow_type="PydanticAIQuickStart",
tool_policies={
"look_up_order": ToolPolicy(ToolEffect.READ),
"cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
},
)
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/pydantic_ai_quick_start.py
/** Returns the agent as the Vercel AI SDK alone writes it. */
export function buildPlainAgent(orderDesk: OrderDesk, model: LanguageModel) {
return new ToolLoopAgent({ model, instructions: orderSupportInstructions, tools: buildOrderTools(orderDesk) });
}
/** Returns the same agent as a durable Dex agent session: the same settings, plus tool policies. */
export function buildDurableAgent(orderDesk: OrderDesk, model: LanguageModel): AgentSessionFlow {
return createVercelAiAgentSessionFlow(
{
model,
instructions: orderSupportInstructions,
tools: buildOrderTools(orderDesk),
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
},
"VercelAiQuickStart",
);
}
Example: examples/typescript/src/products/agent-harnesses/quick-start/vercel-ai-quick-start.ts
Tools the framework runs itself, such as MCP servers and hosted tools, need the changes in your framework's Limits. The session runs in a Worker and is driven by a Client; Add Dex to Your Agent shows that host code.
Use it in your own agent
Install the adapter with its framework. Each adapter brings in the Dex Agents core. Then copy the complete files below.
- Claude Agent SDK
- Google ADK
- LangChain and LangGraph
- Mastra
- OpenAI Agents SDK
- Pydantic AI
- Vercel AI SDK
pip install dex-agents-claude-agent-sdk
Example: agents/python/claude-agent-sdk
npm i @superdurable/dex-agents-claude-agent-sdk @superdurable/dex-agents @superdurable/dex zod
Example: agents/typescript/claude-agent-sdk
Authentication
The Worker authenticates the Claude Code CLI with ANTHROPIC_API_KEY. Set it in the Worker's environment. It never enters Flow state.
The example builds the CLI's environment so that ANTHROPIC_API_KEY is its only credential. Without the key, the example does not register the Claude agent. It serves the other harnesses.
pip install dex-agents-google-adk
Example: agents/python/google-adk
pip install dex-agents-langgraph langchain-openai
Example: agents/python/langgraph
npm i @superdurable/dex-agents-mastra @superdurable/dex-agents @superdurable/dex @mastra/core zod
Example: agents/typescript/mastra
pip install dex-agents-openai-agents
Example: agents/python/openai-agents
npm i @superdurable/dex-agents-openai-agents @superdurable/dex-agents @superdurable/dex @openai/agents zod
Example: agents/typescript/openai-agents
pip install dex-agents-pydantic-ai "pydantic-ai-slim[openai]"
Example: agents/python/pydantic-ai
npm i @superdurable/dex-agents-vercel-ai @superdurable/dex-agents @superdurable/dex ai @ai-sdk/openai zod
Example: agents/typescript/vercel-ai
The complete files
The quick start program is three files per language. They use only Dex, Dex Agents, and your framework, apart from the example's order desk with its tools and the settings the entry point reads. Read them in the repository:
| File | Python | TypeScript |
|---|---|---|
| Your agent module, plain and durable, with every import | claude_agent_sdk · google_adk · langchain · openai_agents · pydantic_ai | claude-agent-sdk · mastra · openai-agents · vercel-ai |
| The runner: Registry, Worker, Client, and one turn with approvals | session.py | quick-start-session.ts |
| The entry point that asks for approval at the prompt | __main__.py | quick-start-main.ts |
Wire the session into your app
The change to your agent is small: the wrap in step 2 of the Agent Quick Start. The rest is hosting code that every Dex app has, a Worker that runs Steps and a Client that calls them. This function is the smallest host. It runs one Worker and one Client in your process, starts a session, sends one message, and returns the session after the turn:
"""The smallest host for a durable agent: one Worker, one Client, one turn."""
from __future__ import annotations
import asyncio
from dex import (
AsyncClient,
AsyncWorker,
BlobCacheConfig,
ClientOptions,
Registry,
WorkerOptions,
open_blob_cache,
)
from dex_agents import AgentSessionClient, AgentSessionFlow, AgentSessionStatus, AgentSnapshot
async def ask_agent(
session_flow: AgentSessionFlow,
session_id: str,
message: str,
server_address: str = "127.0.0.1:8801",
blob_cache_dir: str = ".dex-blob-cache",
) -> AgentSnapshot:
"""Starts a session, sends one message, and returns the session after the turn."""
registry = Registry((session_flow,), allow_async_handlers=True)
cache = open_blob_cache(BlobCacheConfig(blob_cache_dir, 1 << 30))
worker = AsyncWorker(registry, cache, WorkerOptions(server_address=server_address))
client = AsyncClient(registry, cache, ClientOptions(server_address=server_address))
worker_task = asyncio.create_task(worker.start())
try:
await worker.wait_pull_registered(timeout=30)
sessions = AgentSessionClient(client, session_flow)
await sessions.start_session(session_id)
await sessions.send_user_message(session_id, message)
while True:
snapshot = await sessions.get_agent_snapshot(session_id)
state = snapshot.state
if state and state.turn_count >= 1:
if state.status == AgentSessionStatus.WAITING_FOR_USER_MESSAGE:
return snapshot
if state and state.status == AgentSessionStatus.FAILED:
raise RuntimeError(f"the session failed: {state.last_turn_error}")
await asyncio.sleep(0.5)
finally:
await client.close()
await worker.close()
await worker_task
cache.close()
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/embed.py
/** Starts a session, sends one message, and returns the session after the turn. */
export async function askAgent(
flow: AgentSessionFlow,
sessionId: string,
message: string,
serverAddress = "127.0.0.1:8801",
blobCacheDir = ".dex-blob-cache",
): Promise<AgentSnapshot> {
const registry = new Registry([flow]);
const blobCache = openBlobCache({ directory: blobCacheDir, maxBytes: 1 << 30 });
const worker = new Worker(registry, blobCache, { serverAddress });
const client = new Client(registry, blobCache, { serverAddress });
try {
await worker.start();
await worker.waitPullRegistered({ timeoutMs: 30_000 });
await startAgentSession(client, flow, sessionId, {});
await sendAgentUserMessage(client, flow, sessionId, { text: message });
for (;;) {
const snapshot = await readAgentSnapshot(client, flow, sessionId);
const state = snapshot.state;
if (state?.status === "failed") {
throw new Error(`the session failed: ${state.lastTurnError ?? "unknown error"}`);
}
if (state !== null && state.turnCount >= 1 && state.status === "waiting_for_user_message") {
return snapshot;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
} finally {
await client.close();
await worker.close();
blobCache.close();
}
}
Example: examples/typescript/src/products/agent-harnesses/quick-start/quick-start-embed.ts
It takes the session Flow that your module's build_durable_agent or buildDurableAgent returns. A turn that needs an approval waits until your app resolves it; Approve from your application shows how.
A web app splits the same pieces across routes:
- A Registry with your session Flow, a Worker that runs its Steps, and a Client that calls it, as in the function above. The Worker connects out to Dex, so it needs no open port. Both open a blob cache, a local cache for large values; Registry and blob cache explains it.
- A route that starts a session. The session ID is the Flow ID.
- A route that sends a user message, and one that reads the session snapshot with the agent's reply. To stream the reply as it is written, read the AgentEvents Stream, as the next section shows.
- A route that approves or rejects a waiting tool call. Approve from your application covers it.
The Order Support Agent example has these routes over HTTP, for every framework.
What replaces the framework call in a web handler
The handler queues the message on the session and returns.
- Claude Agent SDK
- Google ADK
- LangChain and LangGraph
- Mastra
- OpenAI Agents SDK
- Pydantic AI
- Vercel AI SDK
This replaces the query call that waited for the reply.
This replaces the Runner.run_async call that waited for the reply.
This replaces the agent.invoke call that waited for the reply.
This replaces the agent.generate call that waited for the reply.
This replaces the Runner.run call that waited for the reply.
This replaces the agent.run call that waited for the reply.
This replaces the generateText call that waited for the reply.
The reply arrives on the session's AgentEvents Stream, which your UI reads as server-sent events. The function above polls the session snapshot, which suits scripts and tests. A chat UI reads the stream instead.
This complete app hosts the Worker and the Client and serves every route a chat UI needs: start a session, send a message, read the session, stream its events, and answer an approval. The whole module, quick_start/web.py:
"""A complete web app for a durable agent: start, message, read, stream, approve.
It hosts the Worker and the Client in the web process. Add your own
authentication before you expose these routes: anyone who knows a session ID
can read and change that session here.
"""
from __future__ import annotations
import uuid
from pathlib import Path
from typing import Any
from dex_agents import AgentSessionFlow
from dex_agents.ui_stream import (
AGENT_EVENT_STREAM_HEADERS,
agent_snapshot_to_json,
stream_agent_event_frames,
)
from quart import Quart, Response, jsonify, make_response, request
from agent_harness_examples.quick_start.safe_send import send_user_message_once
from agent_harness_examples.quick_start.session import QuickStartApp
def create_web_app(
session_flow: AgentSessionFlow, server_address: str, blob_cache_dir: Path
) -> Quart:
"""Returns a Quart app whose routes run one durable agent's sessions."""
web = Quart(__name__)
apps: list[QuickStartApp] = []
Example: examples/python/agent-harnesses/agent_harness_examples/quick_start/web.py
The AI Agent example's chat routes show the same pieces. One chat route queues the message, or the approval answers, and streams the reply in the Vercel AI SDK UI message format. A second route resumes the stream after a reconnect. This is an excerpt: requireBody, authorizeSession, session, and the other helpers are defined in the full routes file.
router.post("/api/chat", async (request, response) => {
const body = requireBody(request);
const flowId = authorizeSession(request, requireString(body, "id"));
const lastMessage = Array.isArray(body.messages) ? (body.messages.at(-1) as JsonBody | undefined) : undefined;
const signal = abortSignalOf(response);
if (lastMessage?.role === "assistant") {
const approvals = readAgentToolApprovalResponses(lastMessage);
await sendFetchResponse(response, resolveAgentToolApprovalsWithUIMessageStream({ ...session, flowId, approvals, signal }));
return;
}
const parts = (Array.isArray(lastMessage?.parts) ? lastMessage.parts : []) as JsonBody[];
const text = parts.map((part) => (part.type === "text" ? String(part.text) : "")).join("");
await sendFetchResponse(response, sendAgentUserMessageWithUIMessageStream({ ...session, flowId, text, signal }));
});
router.get("/api/chat/:id/stream", async (request, response) => {
const resumeToken = request.header(agentResumeTokenHeaderName);
const options = { ...session, flowId: sessionIdOf(request), resumeToken, signal: abortSignalOf(response) };
await sendFetchResponse(response, createAgentUIMessageStreamResponse(options));
});