把 Dex 加入你的 Agent
查看 Markdown把 Agent 快速开始 中的示例 Agent 移到你自己的代码中:安装框架的适配器,复制完整文件,再在 Web 应用中对外提供 Session。
你的应用会有哪些变化
- Agent 定义保持原样。 你把它包装一层,并为每个 tool 指定一个策略。
- 请求处理函数(handler)的形态会变。 它把用户消息发给 Session 后就返回。回复出现在 Session 的事件流上,或者在它的快照中。
- 你要多运行两样东西。 Worker 进程运行 Session 的 Step,Dex Server 存储它们。Dex Server 可以运行在 Dex Cloud 上,也可以运行在你自己的基础设施上;选择 Dex 的运行方式 对比了两者。Dex 处于 Beta 阶段,Dex Server 的版本是 0.1,一个集群由一个活动节点提供服务。
- 每个 turn 都要等待 Dex 提交。 一个带三个并行 Tool Call 的 turn 大约多花 0.4 秒;参见 延迟与成本。
在生产环境运行 Agent 列出了需要准备的事项。性能 给出具体数字,限制 给出许可证说明。
你的代码有哪些变化
示例先按框架本身的写法写出 Agent,再把它包进 Dex Agent Session,并为每个 tool 指定 policy。对这两个 function tool 来说,改动只有这些:
- 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),
},
)
例子: 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",
);
}
例子: examples/typescript/src/products/agent-harnesses/quick-start/claude-agent-sdk-quick-start.ts
Claude Agent SDK 在 options 旁边以列表形式接收 tool,因此由 Dex 自己运行每个 tool。
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),
},
)
例子: 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),
},
)
例子: examples/python/agent-harnesses/agent_harness_examples/quick_start/langchain_quick_start.py
DurableToolCallMiddleware 让每次 Tool Call 作为独立的 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" },
},
});
}
例子: 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),
},
)
例子: 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" },
},
});
}
例子: 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),
},
)
例子: 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",
);
}
例子: examples/typescript/src/products/agent-harnesses/quick-start/vercel-ai-quick-start.ts
由框架自己运行的 tool,例如 MCP server 和 hosted tool,需要按你所用框架的 限制 做相应修改。Session 在 Worker 中运行,由 Client 驱动;把 Dex 加入你的 Agent 展示了这部分宿主代码。
在你自己的 Agent 中使用
把适配器和它对应的框架一起安装。每个适配器都会引入 Dex Agents core。然后复制下面的 完整文件。
- Claude Agent SDK
- Google ADK
- LangChain and LangGraph
- Mastra
- OpenAI Agents SDK
- Pydantic AI
- Vercel AI SDK
pip install dex-agents-claude-agent-sdk
npm i @superdurable/dex-agents-claude-agent-sdk @superdurable/dex-agents @superdurable/dex zod
认证
Worker 用 ANTHROPIC_API_KEY 为 Claude Code CLI 认证。把它设置在 Worker 的环境中。它永远不会进入 Flow 状态。
示例为 CLI 构建的环境中,ANTHROPIC_API_KEY 是唯一的凭据。缺少这个 key 时,示例不会注册 Claude Agent,只提供其他 harness。
pip install dex-agents-google-adk
pip install dex-agents-langgraph langchain-openai
npm i @superdurable/dex-agents-mastra @superdurable/dex-agents @superdurable/dex @mastra/core zod
pip install dex-agents-openai-agents
npm i @superdurable/dex-agents-openai-agents @superdurable/dex-agents @superdurable/dex @openai/agents zod
pip install dex-agents-pydantic-ai "pydantic-ai-slim[openai]"
npm i @superdurable/dex-agents-vercel-ai @superdurable/dex-agents @superdurable/dex ai @ai-sdk/openai zod
完整文件
Agent 快速开始程序每种语言有三个文件。除了示例自带的 order desk 及其 tool,以及入口读取的设置之外,它们只用到 Dex、Dex Agents 和你的框架。在仓库中阅读它们:
| 文件 | Python | TypeScript |
|---|---|---|
| 你的 Agent 模块,包含普通版和持久化版,以及全部 import | claude_agent_sdk · google_adk · langchain · openai_agents · pydantic_ai | claude-agent-sdk · mastra · openai-agents · vercel-ai |
| Runner:Registry、Worker、Client,以及一个带审批的 turn | session.py | quick-start-session.ts |
| 在命令行提示中请求审批的入口 | __main__.py | quick-start-main.ts |
把 Session 接入你的应用
对 Agent 本身的改动很小:就是 Agent 快速开始第 2 步 中的那层包装。其余是每个 Dex 应用都有的宿主代码:一个运行 Step 的 Worker,和一个调用它们的 Client。下面这个函数是最小的宿主。它在你的进程中运行一个 Worker 和一个 Client,启动一个 Session,发送一条消息,并在 turn 结束后返回 Session:
"""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()
例子: 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();
}
}
例子: examples/typescript/src/products/agent-harnesses/quick-start/quick-start-embed.ts
它接收你的模块中 build_durable_agent 或 buildDurableAgent 返回的 Session Flow。需要审批的 turn 会一直等待,直到你的应用处理它;做法参见 在你的应用中审批。
Web 应用把同样的部件分到各个路由中:
- 一个包含 Session Flow 的 Registry、一个运行其 Step 的 Worker,以及一个调用它的 Client,和上面的函数一样。Worker 主动向外连接 Dex,所以不需要开放端口。两者都会打开一个 blob cache,即存放较大值的本地缓存;Registry 与 blob cache 对它做了说明。
- 一个启动 Session 的路由。Session ID 就是 Flow ID。
- 一个发送用户消息的路由,以及一个读取 Session 快照(其中包含 Agent 回复)的路由。要在回复生成的同时流式返回,请读取 AgentEvents Stream,如 下一节 所示。
- 一个批准或拒绝等待中 Tool Call 的路由。参见 在你的应用中审批。
订单客服 Agent 示例为每个框架都提供了这些 HTTP 路由。
Web handler 中由什么代替框架调用
Handler 把消息排入 Session 的队列,然后返回。
- Claude Agent SDK
- Google ADK
- LangChain and LangGraph
- Mastra
- OpenAI Agents SDK
- Pydantic AI
- Vercel AI SDK
它代替了原来等待回复的 query 调用。
它代替了原来等待回复的 Runner.run_async 调用。
它代替了原来等待回复的 agent.invoke 调用。
它代替了原来等待回复的 agent.generate 调用。
它代替了原来等待回复的 Runner.run 调用。
它代替了原来等待回复的 agent.run 调用。
它代替了原来等待回复的 generateText 调用。
回复出现在 Session 的 AgentEvents Stream(英文)上,UI 以 server-sent events 的形式读取它。上面的函数轮询 Session 快照,适合脚本和测试。Chat UI 则读取这个 stream。
这个完整的应用托管 Worker 和 Client,并提供 chat UI 需要的所有路由:启动 Session、发送消息、读取 Session、流式读取它的 event,以及答复审批。完整模块 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] = []
例子: examples/python/agent-harnesses/agent_harness_examples/quick_start/web.py
AI Agent 示例的聊天路由展示了同样的部件。一个聊天路由把消息或审批答复排入队列,并以 Vercel AI SDK 的 UI message 格式流式返回回复。第二个路由在重新连接后恢复 stream。下面是节选:requireBody、authorizeSession、session 以及其他辅助函数定义在 完整的路由文件 中。
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));
});