字节笔记本
2026年8月28日
Cloudflare Agents SDK 这次更新了什么
Cloudflare 给 Agents SDK 补了一轮文档和运行时接口:间隔调度 scheduleEvery()、自定义路由、callable 超时、状态校验钩子,以及 MCP handler 的 options。已经在用 agents 的项目,先升到最新再按下面几条改,不用整包重写。
npm i agents@latest文档入口在 Cloudflare Agents,源码在 github.com/cloudflare/agents。更新条目也写在 Agents changelog。
间隔调度:cron 做不到的秒级轮询
以前定时任务只有三种:延迟秒数、指定时间、cron。cron 最小粒度是一分钟,每 30 秒拉一次接口、每 90 秒同步一次,只能自己在回调里再 schedule() 下一轮。现在多了 scheduleEvery(),间隔按秒算,写进 SQLite,Durable Object 重启也还在。
import { Agent } from "agents";
export class PollingAgent extends Agent {
async onStart() {
// 幂等:同样的回调名 + 间隔 + payload 不会重复建行
await this.scheduleEvery(30, "poll", { source: "api" });
}
async poll(payload: { source: string }) {
const res = await fetch("https://api.example.com/updates");
if (!res.ok) throw new Error("poll failed");
// 抛错只废这一轮,下一轮还按 30 秒走
}
}几条容易踩的:
- 第一次执行在
intervalSeconds之后,不是立刻跑。 - 上一轮还没结束,下一轮会跳过,日志里能看到
Skipping interval schedule。别把慢接口塞进 10 秒间隔。 - 取消整条间隔用
cancelSchedule(id)。 - cron 还是适合「每天 8 点」这种墙上时钟;只要相对间隔、尤其是亚分钟,用
scheduleEvery()。
完整参数和 queue / Workflows 的分工写在 Schedule tasks。
自定义路由:别死守 /agents/{类名}/{实例}
默认路径是 /agents/{agent}/{name}。前端要干净 URL、或者实例名来自登录态而不是 URL,用客户端的 basePath,服务端自己选实例再 fetch 过去。
import { getAgentByName, routeAgentRequest } from "agents";
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/user/")) {
const session = await getSession(request);
const agent = await getAgentByName(env.UserAgent, session.userId);
return agent.fetch(request);
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};客户端:
const agent = useAgent({
agent: "UserAgent",
basePath: "user",
onIdentity: (name, agentType) => {
console.log(name, agentType);
},
});basePath 设了之后,URL 不再按 agent/name 拼。agent 这个参数还是要传,服务端不靠它选实例。连上之后服务端会把 name 和 agent 类型推过来,onIdentity / onIdentityChange 用来对一下你连的是不是那一个。只改前缀、结构不变,用 routeAgentRequest(request, env, { prefix: "/api/agents" }) 就够。
路由细节:Routing。
Callable 超时:别让 RPC 一直转圈
浏览器、App 走 WebSocket 调 @callable() 方法,以前没超时,对端卡住就一直挂着。现在 call() 可以带毫秒超时,到点直接 reject。断线时未完成的调用也会以 Connection closed 失败。
await agent.call("slowMethod", [], { timeout: 5000 });
await agent.call("generateText", [prompt], {
timeout: 30_000,
stream: {
onChunk: (chunk) => append(chunk),
onDone: (value) => console.log("done", value),
onError: (err) => console.error(err),
},
});流式方法用 stream.error(message) 中途收掉,客户端走 onError。getCallableMethods() 能列出带 @callable() 的方法和 metadata,自己做调试页或文档时用得上。
同一 Worker 里、或者 Agent 调另一个 Agent,走 Durable Object RPC,不必加 @callable()。超时是给外部客户端这条 WebSocket 通道用的。文档:Callable methods。
状态校验:setState 之前拦一把
setState() 现在是同步落盘。落盘前会跑 validateStateChange。钩子必须同步:校验、改字段可以,别在里面 await。
当前文档的写法是抛错拒绝:
export class CartAgent extends Agent<Env, { count: number; updatedAt: number }> {
validateStateChange(oldState: { count: number }, newState: { count: number }) {
if (newState.count < 0) {
throw new Error("count cannot be negative");
}
return { ...newState, updatedAt: Date.now() };
}
}早期 changelog 里写过 return false 拒绝。升到 @latest 之后按现在文档来:抛错拒绝,返回对象可以改一版再写入。落盘并广播之后才会进 onStateChanged,那里抛错不影响已经写下的状态。
客户端乱改 count、或并发把库存改成负数,拦在这一层比事后补救干净。
MCP options:createMcpHandler 的第二参数
MCP 这条线已经走到无会话 handler。createMcpHandler 吃一个工厂函数,每个请求新建一份 server,options 用来收路由、CORS、Host、兼容旧客户端。
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "hello-server", version: "1.0.0" });
server.registerTool(
"hello",
{
description: "Return a greeting",
inputSchema: { name: z.string().optional() },
},
async ({ name }) => ({
content: [{ type: "text", text: "Hello, " + (name ?? "World") + "!" }],
}),
);
return server;
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return createMcpHandler(createServer, {
route: "/mcp",
allowedHostnames: ["mcp.example.com"],
corsOptions: { origin: "https://app.example.com" },
legacy: "stateless",
})(request, env, ctx);
},
};常用 option:
| 字段 | 默认 | 干什么 |
|---|---|---|
route | /mcp | Worker 只处理这条路径 |
allowedHostnames | localhost / workers.dev | 自定义域名时显式写上 |
corsOptions | 通配 CORS | 也可 false 关掉响应头 |
legacy | stateless | 旧客户端普通 tool/prompt/resource 还能打进来;要纯新协议用 reject |
responseMode | auto | 需要纯 JSON 用 json(会丢掉结果前的 notification) |
authContext | Execution context props | 给 getMcpAuthContext() 用 |
工厂函数每次请求都跑一遍,别在模块顶层 new 一个 server 反复用。还依赖协议会话、推送 elicitation、事件回放的,暂时并一条 createLegacyMcpHandler / 旧 McpAgent,McpAgent 已经冻结。options 和迁移步骤:MCP handler APIs。
升完怎么验
- 升级 agents 到 latest,看 lockfile 版本。
- 轮询从回调里再 schedule 改成 onStart 里一条 scheduleEvery,重启 Worker,确认 SQLite 里只有一行。
- 有自定义 URL 的,客户端加 basePath,Worker 里用 getAgentByName 加 agent.fetch。
- 外部 RPC 给一个合理 timeout,断线时看 pending call 会不会自己失败。
- 给 setState 加 validateStateChange,故意写非法值,确认被拒。
- MCP 服务端改工厂加 options;旧客户端先用 legacy stateless,确认 tool 列表还在再考虑 reject。
官方还补了一批带例子的文档,调度、路由、callable、MCP 这几页比以前好抄。先升包,再按自己项目用到的接口改,别一次把 Workflows、Think、Code Mode 全翻一遍。