Cloudflare 上的 Agent 运行在 Durable Object 中。Agents SDK Lifecycle 在该 Durable Object 内部运行,并启动你添加的功能。Harness(例如构建你的第一个 Durable Agent中的 Pi harness)就是其中一种功能。
本指南不包含 harness,以便你单独了解运行时。你将在 Durable Object 上构建一个计数器,并添加 State 和 WebSockets 两项功能。然后通过 HTTP 调用它,并连接一个通过 WebSockets 保持同步的客户端。这里介绍的内容同样适用于同时运行 harness 的对象。
- 一个 Cloudflare 账户 ↗
- Node.js 22 或更高版本
-
创建一个 Worker 项目:
npm create cloudflare@latest -- my-agent --type=hello-world --lang=tsyarn create cloudflare my-agent --type=hello-world --lang=tspnpm create cloudflare@latest my-agent --type=hello-world --lang=ts -
前往项目目录并安装 Agents SDK:
cd my-agentnpm i agentsyarn add agentspnpm add agentsbun add agents
在 Wrangler 配置文件中添加一个由 SQLite 支持的 Durable Object:
{
"name": "my-agent",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-11-14",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "Counter", "class_name": "Counter" }],
},
"exports": {
"Counter": { "type": "durable-object", "storage": "sqlite" },
},
}name = "my-agent"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-11-14"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
name = "Counter"
class_name = "Counter"
[exports.Counter]
type = "durable-object"
storage = "sqlite"- 绑定中的
name会成为env上的属性,例如env.Counter。 class_name必须与导出的类名一致。
exports使用"storage": "sqlite"声明该类。生命周期及其功能将数据存储在每个对象的 SQLite 数据库中。请参阅 Durable Object 类导出。agents软件包需要nodejs_compat标志。
替换 src/index.ts:
import { DurableObject, RpcTarget } from "cloudflare:workers";
import { routeAgentRequest } from "agents";
import { Lifecycle } from "agents/lifecycle";
import { State } from "agents/state";
import { WebSockets } from "agents/websockets";
export class Counter extends DurableObject {
// Durable state, stored in the object's SQLite database
state = new State({
initialState: { count: 0 },
onChanged: (_state, source) => this.webSockets.broadcastState(source),
});
// WebSocket connections. Each client receives the state, and can
// call the methods on CounterMethods.
webSockets = new WebSockets({
state: this.state,
callables: new CounterMethods(this),
});
// The lifecycle starts each capability inside the Durable Object
lifecycle = Lifecycle.install(this).use(this.state).use(this.webSockets);
increment() {
const count = (this.state.get()?.count ?? 0) + 1;
this.state.set({ count });
return count;
}
onRequest(request) {
if (request.method === "POST") {
return Response.json({ count: this.increment() });
}
return Response.json(this.state.get());
}
}
// The methods that clients can call over a WebSocket
class CounterMethods extends RpcTarget {
#counter;
constructor(counter) {
super();
this.#counter = counter;
}
increment() {
return this.#counter.increment();
}
reset() {
this.#counter.state.set({ count: 0 });
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { DurableObject, RpcTarget } from "cloudflare:workers";
import { routeAgentRequest } from "agents";
import { Lifecycle } from "agents/lifecycle";
import { State } from "agents/state";
import { WebSockets } from "agents/websockets";
export type CounterState = { count: number };
export class Counter extends DurableObject<Env> {
// Durable state, stored in the object's SQLite database
readonly state: State<CounterState> = new State({
initialState: { count: 0 },
onChanged: (_state, source) => this.webSockets.broadcastState(source),
});
// WebSocket connections. Each client receives the state, and can
// call the methods on CounterMethods.
readonly webSockets: WebSockets = new WebSockets({
state: this.state,
callables: new CounterMethods(this),
});
// The lifecycle starts each capability inside the Durable Object
readonly lifecycle = Lifecycle.install(this)
.use(this.state)
.use(this.webSockets);
increment(): number {
const count = (this.state.get()?.count ?? 0) + 1;
this.state.set({ count });
return count;
}
onRequest(request: Request): Response {
if (request.method === "POST") {
return Response.json({ count: this.increment() });
}
return Response.json(this.state.get());
}
}
// The methods that clients can call over a WebSocket
class CounterMethods extends RpcTarget {
readonly #counter: Counter;
constructor(counter: Counter) {
super();
this.#counter = counter;
}
increment(): number {
return this.#counter.increment();
}
reset(): void {
this.#counter.state.set({ count: 0 });
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;各部分的作用如下:
Counter扩展平台的DurableObject。每个有名称的实例就是一个 Agent,并拥有自己的 SQLite 数据库。State会在该数据库中存储一个 JSON 值。get()用于读取,set()用于写入。每次状态更改后都会运行onChanged,此处它会将新值发送给每个已连接的客户端。WebSockets用于接受 WebSocket 连接。启用state后,每个客户端连接时以及状态每次更改时都会收到当前值。callables列出客户端可以调用的方法。Lifecycle.install(this)会接管对象的fetch、alarm和 WebSocket 处理程序,.use()则用于添加各项功能。不要在类中自行定义fetch()或alarm()。请改用onRequest()、onStart()和onAlarm()。onRequest()处理未由任何功能处理的 HTTP 请求。POST会递增计数器,GET会返回状态。routeAgentRequest()会将/agents/counter/:name的请求发送到名为:name的Counter实例。
为 Env 接口生成类型:
npx wrangler types-
启动开发服务器:
npx wrangler dev -
在另一个终端中,递增名为
demo的计数器:curl -X POST http://localhost:8787/agents/counter/demo{ "count": 1 } -
读取另一个 counter 的状态。每个名称对应一个独立的 agent:
curl http://localhost:8787/agents/counter/other{ "count": 0 }
来自 agents/client 的 AgentClient 会建立到 agent 的 WebSocket、保留其状态副本并调用其方法。在项目目录中创建 client.mjs:
import { AgentClient } from "agents/client";
const counter = new AgentClient({
agent: "Counter",
name: "demo",
host: "localhost:8787",
onStateUpdate: (state) => console.log("count is now", state.count),
});
await counter.ready;
await counter.stub.increment();
await counter.stub.increment();
counter.close();在开发服务器运行时,运行该文件:
node client.mjscount is now 1
count is now 2
count is now 3第一行是 client 连接时收到的状态。其他两行是每次 increment() 后的广播。连接到 demo 的每个 client 都会收到相同更新,包括通过 HTTP 进行的更改。
在 React 应用程序中,使用来自 agents/react 的 useAgent hook。当 agent 状态更改时,agent.state 会更新:
import { useAgent } from "agents/react";
import type { CounterState } from "./index";
export function CounterView() {
const agent = useAgent<CounterState>({ agent: "Counter", name: "demo" });
return (
<div>
<p>{agent.state?.count ?? 0}</p>
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.reset()}>Reset</button>
</div>
);
}client 也可以使用 agent.setState() 直接替换状态。要检查或拒绝这些更改,请将 validateStateChange 传递给 State,或使用 WebSockets 上的 readonly 选项将连接标记为只读。
flowchart LR
A["客户端"] <-->|WebSocket| B["计数器<br/>(Durable Object)"]
C["curl"] -->|HTTP| B
B --> D["SQLite<br/>(状态)"]
- 每个名称对应一个 agent。
/agents/counter/demo和/agents/counter/other是两个 Durable Object 实例,各自拥有独立数据库。 - 持久状态。
State会将每次更改写入 SQLite,因此计数可以在重启、部署和内存驱逐后继续存在。 - 实时同步。 连接到同一 agent 的每个 client 都会收到每次更改。
- 休眠。 WebSocket 连接使用 WebSocket Hibernation API,因此空闲 agent 会离开内存,同时 client 保持连接。收到消息时,Lifecycle 会在处理消息之前重新启动。
npx wrangler deploy你的 agent 现在运行在 Cloudflare 全球网络上。要调用它,请将 localhost:8787 替换为你的 workers.dev URL。已部署的 Worker 使用 HTTPS 和 WSS。
- 从 Worker 的主 module 导出 Durable Object 类。
- 检查 binding 中的
class_name和exports中的 key 是否与导出的类名匹配。 - 默认 route 为
/agents/{binding}/{name},其中 binding 名称使用 kebab case。名为Counter的 binding 可通过/agents/counter/...访问,MyAgent则通过/agents/my-agent/...访问。
Lifecycle 需要 Durable Object 的名称。使用 getByName()、idFromName() 或 routeAgentRequest() 寻址 object。使用 newUniqueId() 或 idFromString() 创建的 object 没有名称。
从类中移除所有 fetch()、alarm()、webSocketMessage()、webSocketClose() 或 webSocketError() 方法。Lifecycle 只会安装类尚未定义的 handler。
- 构建你的第一个 Durable Agent:在相同 Lifecycle 上运行 Pi harness。
- Pi harness:向 Pi agent 添加 tool、session 和浏览器 UI。
- 添加到现有项目:将 Lifecycle 添加到已有的 Durable Object。
- Lifecycle:请求路径、作业队列以及如何编写自定义功能。
- Scheduler:稍后、按 interval 或按 cron 计划运行工作。
- 测试你的 Agents:使用 Vitest 和 Workers 测试池编写测试。