Cloudflare Agents SDK:使用方式、业务场景与运行机制
AI 参与说明(2026-10-23,Agent:Codex):本文由 Codex 使用 Context7 检索 Cloudflare 官方文档,结合固定版本源码、npm 依赖和本地运行结果辅助撰写、校验。模型与 reasoning effort 未取得任务级运行记录。示例基于
agents@0.30.0;源码核对基线为9c79ba12778cbb53b6c50db79282d780466c9897。
假设一个应用收到工单 T-42:“导出报表失败”。用户没有填写报表 ID,系统先请求补充;第二天用户补齐信息,系统启动后台任务查询并重试导出,最后保存结果、通知用户。
这项工作需要处理多次事件,还要在事件之间等待。Cloudflare Agents SDK 为 T-42 提供一个有稳定身份、持久状态和事件处理能力的服务端实例,负责协调这段过程。
读者参考
| English | 中文 | 本文含义 |
|---|---|---|
| Agent instance | Agent 实例 | 负责协调 T-42 处理过程的服务端实体 |
| Durable Object | 持久对象 | Cloudflare 为该实例提供身份、执行位置与独立存储的运行单元 |
| state snapshot | 状态快照 | 当前处理阶段、进度与提醒次数等小型状态 |
| RPC | 远程过程调用 | 客户端或其他服务调用实例方法的通信方式 |
| hibernation | 休眠 | 平台保留持久数据与受管理的连接,同时允许回收内存对象 |
| schedule | 调度 | 保存未来何时调用实例中的某个方法 |
| idempotency key | 幂等键 | 让同一业务操作的重复请求能够被识别的稳定标识 |
| Agent Skills | Agent Skills | 以 SKILL.md 描述、可从 Workspace、R2 或清单加载的技能 |
1. 接收工单,找到负责它的实例
用户在应用页面提交 T-42 后,Worker 验证用户身份与工单访问权限,再把请求交给对应的 TicketAgent。后续补充信息、后台任务回调和提醒,都需要找到这个实例。
%%{init: {"sequence": {"actorMargin": 24, "width": 100, "wrap": true}}}%%
sequenceDiagram
participant U as 应用页面
participant W as Worker
participant A as TicketAgent
participant X as 报表服务
U->>W: 提交工单 T-42
W->>A: 验证后按工单 ID 定位
A->>A: 保存 waiting_input
A-->>U: 请求补充报表 ID
U->>W: 用户补充信息
W->>A: 按同一工单 ID 定位
A->>X: 启动后台任务
X-->>A: 返回运行 ID 与结果
A-->>U: 返回处理结果
TicketAgent 管理这张工单的状态与处理记录:用户提供了什么信息、正在等什么、启动了哪次后台任务、接下来何时检查。工单 ID 是应用自己生成的稳定标识,页面、服务端与后台回调都用它找到同一个实例。
Agents SDK 运行在 Workers 与 Durable Objects 上。Worker 接收外部请求,Durable Object 为命名实例提供身份与独立 SQLite 存储,SDK 再组织 RPC、状态同步和调度。在本文核对的 agents@0.30.0 中,Agent 直接继承 DurableObject,通过 Lifecycle 组合这些能力。
后续事件为什么会找到同一个对象
同一个 namespace 下,相同的实例名定位同一个持久身份。routeAgentRequest() 的默认路径为:
/agents/<Env binding 的 kebab-case>/<实例名>
下面的本地程序把 binding 命名为 TicketAgent,实例名为 ticket-42,对应页面上的 T-42:
/agents/ticket-agent/ticket-42
路由段来自 Env binding 名。同一张工单后续的请求继续使用 ticket-42,不要每次生成新名字。实例名负责寻址,访问权限仍由可信入口与服务端方法检查。
2. 信息不足,先保存进度再等待
T-42 缺少报表 ID,Agent 将处理阶段保存为 waiting_input,向页面返回需要补充的内容。当前请求结束后,用户可以关闭页面,第二天再来补充。等待进度保存在工单记录中,无需让一个函数始终挂着。
保存进度并等待后续事件,用到的是 Agents SDK 的身份、状态和事件机制。检查报表 ID 是否为空,可以直接使用规则;需要从用户的自由描述中提取问题与已有信息时,再调用模型。Agent 读取工单内容、取得分析结果,然后保存当前进度。
这张工单的数据放在哪里
| 位置 | T-42 在这里保存什么 |
|---|---|
this.state / setState() | waiting_input、running、completed 等当前阶段,以及进度计数 |
this.sql | 收到的事件、阶段变更历史、当前运行 ID 和外部任务 ID 等持久记录 |
| 普通类字段、局部变量 | 当前调用中的临时数据与缓存 |
setState() 将快照保存到 SQLite,并通过 SDK 协议同步给连接中的客户端。它整体替换状态,更新字段时需要显式保留其他字段。下面的代码使用 ...this.state,所以修改阶段不会丢掉提醒次数。
写入前由 validateStateChange() 校验;保存和广播之后才异步通知 onStateChanged()。拒绝不合法更新要放在写入前。直接修改自己的 SQL 表不会自动刷新客户端查询,本例用 state 同步当前进度,需要历史时再调用查询方法。
这一步何时需要模型 SDK
如果助手要读懂 T-42 的描述,可以直接通过 Workers AI binding、厂商 SDK 或 HTTP API 调用模型。采用 AI SDK 或 TanStack AI,则是为这次分析选择模型调用、工具和流式处理接口。基础 Agent 的状态、RPC 和调度本身可以独立运行。
Cloudflare Models 文档中的 agents/models/ai-sdk 与 agents/models/pi-ai 是模型接入桥接入口。本文的本地程序先用明确的阶段命令模拟事件,展示实例怎样保存与推进进度,不依赖模型包。模型库与界面消息协议的详细机制见 TanStack AI 与 AI SDK 对比。
3. 第二天收到补充信息,从保存的进度继续
用户重新打开 T-42,填写报表 ID 并提交。新请求再次定位 ticket-42,读取持久状态,确认信息已齐备,再把阶段推进到 running。
这期间,内存中的 JavaScript 对象可能已经被平台回收。Agents SDK 的 WebSocket 使用 Hibernation API;后续事件可以重新唤醒实例,已保存的数据仍在。
flowchart TB
E["T-42 的补充信息、任务回调或提醒"] --> M{"实例仍在内存?"}
M -->|是| H["处理本次事件"]
M -->|否| R["重建实例并运行 onStart"]
R --> S["从 SQLite 读取处理进度"]
S --> H
H --> P["校验当前阶段并保存新进度"]
P --> I["等待 T-42 的后续事件"]
onStart() 每个内存对象生命周期执行一次,重建后会再次运行。因此建表要可重复,不能每次启动都把进度重置为 waiting_input,也不能重复登记同一个提醒。initialState 只在没有已保存状态时提供默认值。
Durable storage survives object recreation. An in-flight JavaScript stack does not. 见 Lifecycle。
继续处理的依据是保存的阶段和任务记录。普通类字段、setTimeout、悬空 Promise 与原来的调用栈不能承担这一职责。
启动后台任务并接住结果
Agent 现在可以请求报表服务查询失败原因并重试导出。它保存本次 runId 和后台任务 ID,接收结果时检查它们是否仍属于当前处理过程。这样,上一轮迟到的回调不会直接覆盖新一轮进度。
报表服务负责实际查询和导出,Agent 负责工单的事件与进度。需要模型判断时,模型调用只是处理过程中的一步;用户输入、任务回调和提醒仍由同一个工单实例协调。
一次外部 await 期间,其他事件可能推进。例如等待分析结果时,用户又修改了描述,返回后就要检查当前版本和阶段,再决定是否采用这份结果,避免用调用前的旧快照覆盖新状态。
4. 在本地跑通这张工单的状态推进
下面用客户端模拟 T-42 收到补充信息和后台任务结果的事件,验证 TicketAgent 的 RPC、持久状态、SQL 记录和延迟提醒。示例不实际调用报表服务或模型;接入真实业务时,再将这些输入交给对应方法。
固定运行环境为 Node.js 24.19.0、agents@0.30.0、wrangler@4.149.0。示例仅用于本地,没有应用鉴权。
配置
在空目录创建 package.json:
{
"name": "agents-sdk-ticket-example",
"private": true,
"type": "module",
"scripts": { "dev": "wrangler dev --local --port 8795 --persist-to .local-state" },
"dependencies": { "agents": "0.30.0", "zod": "4.3.6" },
"devDependencies": { "wrangler": "4.149.0" }
}
创建 tsconfig.json:
{
"extends": "agents/tsconfig"
}
创建 wrangler.jsonc,注册 SQLite-backed Durable Object:
{
"name": "agents-sdk-ticket-example",
"main": "server.ts",
"compatibility_date": "2026-10-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "TicketAgent", "class_name": "TicketAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["TicketAgent"] }]
}
compatibility_date 固定运行时兼容语义,本例选用固定 Wrangler 支持的 2026-10-01。@callable() 使用 standard decorators,TypeScript 配置继承 SDK,不另加 experimentalDecorators: true。
服务端:保存阶段、历史与提醒
创建 server.ts:
import { Agent, callable, routeAgentRequest } from 'agents'
type TicketState = {
phase: 'waiting_input' | 'running' | 'completed'
updates: number
reminders: number
}
type Env = { TicketAgent: DurableObjectNamespace<TicketAgent> }
export class TicketAgent extends Agent<Env, TicketState> {
initialState: TicketState = { phase: 'waiting_input', updates: 0, reminders: 0 }
onStart() {
this.sql`CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT, phase TEXT NOT NULL
)`
}
validateStateChange(_next: TicketState, source: unknown) {
if (source !== 'server') throw new Error('Use the setPhase RPC')
}
@callable()
setPhase(phase: TicketState['phase']) {
if (!['waiting_input', 'running', 'completed'].includes(phase)) {
throw new Error('Invalid phase')
}
this.sql`INSERT INTO events (phase) VALUES (${phase})`
this.setState({ ...this.state, phase, updates: this.state.updates + 1 })
return this.state
}
@callable()
async followUp(seconds = 3) {
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 3600) {
throw new Error('seconds must be an integer from 1 to 3600')
}
return (await this.schedule(seconds, 'remind', {})).id
}
remind() {
this.setState({ ...this.state, reminders: this.state.reminders + 1 })
}
@callable()
snapshot() {
return {
state: this.state,
events: this.sql<{ id: number; phase: string }>`SELECT * FROM events ORDER BY id`,
}
}
onRequest() {
return Response.json(this.snapshot())
}
}
export default {
async fetch(request: Request, env: Env) {
return await routeAgentRequest(request, env) ?? new Response('Not found', { status: 404 })
},
} satisfies ExportedHandler<Env>
setPhase() 是服务端命令:校验输入、保存历史,再基于当前 state 更新进度。validateStateChange() 拒绝客户端直接替换整个业务快照。接入生产业务前,还需为这些操作补上身份与业务权限校验。
followUp() 保存一个未来调用 remind() 的调度记录,返回调度 ID。调度记录位于 SQLite,SDK 通过 Durable Object alarm 唤醒实例。这里的回调只增加提醒计数,使用短延迟方便观察;为 T-42 安排后续检查时采用同一机制。
客户端:模拟事件并观察进度
创建 client.mjs:
import { AgentClient } from 'agents/client'
const client = new AgentClient({
host: '127.0.0.1:8795',
protocol: 'ws',
agent: 'ticket-agent',
name: 'ticket-42', // One stable name per application-owned ticket.
onStateUpdate(state) { console.log('state update:', JSON.stringify(state)) },
})
try {
await client.ready
console.log('T-42 ticket automation:', JSON.stringify(client.state))
console.log('schedule id:', await client.call('followUp', [2]))
await new Promise(resolve => setTimeout(resolve, 2500))
await client.call('setPhase', ['running']) // Simulated: the user supplied the missing report ID.
await client.call('setPhase', ['completed']) // Simulated: the background job finished; show the result.
console.log('snapshot:', JSON.stringify(await client.call('snapshot')))
} finally {
client.close()
}
在第一个终端启动:
npm install
npm run dev
Wrangler 显示 Ready on http://localhost:8795 后,在第二个终端执行:
node client.mjs
首次使用新的本地持久目录运行时,可以看到:
state update: {"phase":"waiting_input","updates":0,"reminders":0}
T-42 ticket automation: {"phase":"waiting_input","updates":0,"reminders":0}
schedule id: <generated id>
state update: {"phase":"waiting_input","updates":0,"reminders":1}
state update: {"phase":"running","updates":1,"reminders":1}
state update: {"phase":"completed","updates":2,"reminders":1}
snapshot: {"state":{"phase":"completed","updates":2,"reminders":1},"events":[{"id":1,"phase":"running"},{"id":2,"phase":"completed"}]}
再次连接同一个 ticket-42 会读到原有进度,计数也会继续累加。真实页面可以用 useAgent 连接这个实例,展示当前阶段并调用允许的 RPC。浏览器经 WebSocket 调用的方法使用 @callable() 暴露;Worker 内部也可以通过 getAgentByName() 获取 stub,使用原生 Durable Object RPC。
本次还用独立的本地黑盒脚本验证了等待阶段的持久化:登记提醒后停止 Wrangler/workerd,再用相同持久目录启动,等待阶段和 SQL 记录仍在,提醒随后触发;两个客户端继续观察同一实例的执行和完成阶段。依赖树确认没有 AI SDK 或 Pi。验证覆盖受控的本地进程重启,不代表生产环境的精确调度时间或外部副作用只执行一次。
5. 处理结束,保存结果并通知用户
报表服务返回导出结果后,Agent 校验运行 ID,保存处理结果和下载位置,再把工单推进到 completed。在线用户可以收到状态更新;稍后重新打开页面的用户,也能从同一实例读到结果与处理历史。
整个处理过程还需要应对重复事件和中断。相同请求或后台回调可能再次到达,处理前应检查事件记录;发起导出或发送通知时,应使用稳定的业务幂等键或已有结果检查。外部服务已经接受操作、当前实例尚未记录完成时,重试仍可能再次提交。
如果通知失败,可以持久保存待通知记录,再安排检查和重试。schedule() 保存的是未来调用哪个方法;回调支持重试,但这不会自动让外部 API 副作用只发生一次。也不要在每次 onStart() 时无条件新增相同延迟任务。
当 T-42 的处理步骤和重试规则需要框架保存时,可以让 Workflows 记录步骤结果与等待状态,Agent 继续接收工单输入和展示进度。当前发布的 Tasks 也提供实例内部的步骤 journal 与重放,但标记为 Experimental。恢复时会根据保存的执行证据重放,复用已完成步骤的结果;未完成步骤仍可能重跑。它不会恢复原来的 JavaScript 调用栈。
Agent Skills 的三种来源
54ae3e8 的 agents/skills 按 Agent Skills 的 SKILL.md 解析技能,加载入口是 SkillSource。三种现成来源是:
workspace()读 Computer Workspace 里的文件。默认根目录是/workspace/.agents/skills,每个<skill-name>/SKILL.md是一项技能,旁边的文件是它的资源。根目录不存在时,列表为空。r2()读 R2。对象键是可选前缀加上同样的<skill-name>/SKILL.md布局。manifest()读内存中的清单。fromManifest是同一个函数。
loadSkillCatalog() 每次都向 source 要完整列表,下一次加载能看到新增、修改和删除。解析不符合规范的技能留在 invalid,不进入可用目录。list() 抛错会被收成 SkillSourceError,其他 source 仍继续;若调用方传入上一次的 catalog,失败的 source 会退回那次的列表。这一节只核对了该提交的导出和注释。上文 T-42 的本地例子仍停在 agents@0.30.0 与 9c79ba1,没有改跑技能目录。SkillSource、workspace 与 R2、manifest、loadSkillCatalog
当前官方推荐是 @cloudflare/think 的 Think,在 getSkills() 里返回 skills.workspace()、skills.r2() 或 skills.manifest()(2026-10-29 changelog)。Agent Skills 仍标为 experimental,页面对应 Agents SDK v0.32.0 与 Think v0.23.0 或更高版本。不用 Think 时,同一页仍用 agents/skills 的 loadSkillCatalog() 合并来源。
一手资料与核验范围
本文使用 Context7 检索官方资料,并以 agents@0.30.0 和固定官方源码核对 SDK 机制。围绕 T-42 描述的完整业务流程是架构设计;本地验证范围以运行例子和下列记录为准。
- Agents SDK 固定源码、Routing、State与 Callable Methods。
- Lifecycle与 Scheduling:唤醒、持久状态与调度回调。
- Workflows Integration与 Tasks(Experimental):执行步骤的持久化与重放;站内另有 Workflows API 与恢复机制。
- Cloudflare Models:模型接入桥接的职责。
历史修订记录
修订说明
资料修订(2026-11-09,Agent:Cursor):保留
54ae3e8对agents/skills与loadSkillCatalog()的结论,并补上当前官方推荐:@cloudflare/think的Think与getSkills()(2026-10-29 changelog)。Agent Skills 仍为 experimental。模型grok-4.7-high-fast,提供方 xAI,执行入口 Cursor Cloud。reasoning effort 未作为独立字段取得运行记录。
资料修订(2026-11-01,Agent:Cursor):按
54ae3e8补上 Agent Skills 可从 Workspace、R2 或 manifest 加载。T-42 示例的版本基线未改。模型grok-4.7-high-fast,提供方 xAI,执行入口 Cursor Cloud。reasoning effort 未作为独立字段取得运行记录。
场景补充(2026-10-23,Agent:Codex):通过 Context7 与 Linear 官方文档补充 Issue 自动化场景,区分 Issue 事实、自动化运行状态与 AgentSession。执行入口 Codex Desktop;模型与 reasoning effort 未取得任务级运行记录。
叙事修订(2026-10-23,Agent:Codex):围绕 Linear 中的 ENG-42 重排正文、图解与本地运行例子,沿同一条 Issue 说明状态、事件、休眠、调度和模型接入。执行入口 Codex Desktop;模型与 reasoning effort 未取得任务级运行记录。
原生场景修订(2026-10-23,Agent:Codex):以应用自己管理的工单 T-42 贯穿正文、图解和代码,沿补充信息、后台处理与结果通知说明机制。运行例子、中文与页面重新校验。执行入口 Codex Desktop;模型与 reasoning effort 未取得任务级运行记录。