cloudflare-agents by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-agents状态 : 生产就绪 ✅ 最后更新 : 2026-01-09 依赖项 : cloudflare-worker-base (推荐) 最新版本 : agents@0.3.3, @modelcontextprotocol/sdk@latest 生产测试 : Cloudflare 自家的 MCP 服务器 (https://github.com/cloudflare/mcp-server-cloudflare)
近期更新 (2025-2026) :
import { context }AIChatAgent 现在支持可恢复流式传输,使客户端能够重新连接并继续接收流式响应而不会丢失数据。这解决了关键的实际场景:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键能力 : 流在页面刷新、连接中断时持续存在,并在打开的标签页和设备间同步。
实现 (在 AIChatAgent 中自动实现):
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages,
onFinish
}).toTextStreamResponse();
// ✅ 流自动可恢复
// - 客户端断开连接?流被保留
// - 页面刷新?流继续
// - 多个标签页?全部保持同步
}
}
无需更改代码 - 只需使用 agents@0.2.24 或更高版本的 AIChatAgent。
Cloudflare Agents SDK 支持构建运行在 Cloudflare Workers + Durable Objects 上的 AI 驱动的自主智能体。智能体可以:
每个智能体实例都是一个全局唯一、有状态的微服务器,可以运行数秒、数分钟或数小时。
停 : 在使用 Agents SDK 之前,先问问自己是否真的需要它。
这涵盖了 80% 的聊天应用。 对于这些情况,直接在 Workers 上使用 Vercel AI SDK - 它更简单,需要的基础设施更少,并且自动处理流式传输。
示例 (不需要 Agents SDK):
// worker.ts - 仅使用 AI SDK 的简单聊天
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse(); // 自动 SSE 流式传输
}
}
// client.tsx - 使用内置钩子的 React
import { useChat } from 'ai/react';
function ChatPage() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
// 完成。不需要 Agents SDK。
}
结果 : 100 行代码而不是 500 行。无需设置 Durable Objects,没有 WebSocket 的复杂性,无需迁移。
这大约是 20% 的应用 - 当你需要 Agents SDK 提供的基础设施时。
Agents SDK 是 :
Agents SDK 不是 :
可以这样理解 :
你可以将它们结合使用(大多数情况推荐),或者直接使用 Workers AI(如果你愿意手动处理 SSE 解析)。
正在构建 AI 应用?
│
├─ 需要 WebSocket 双向通信? ───────┐
│ (客户端发送时服务器流式传输,智能体发起的消息)
│
├─ 需要 Durable Objects 有状态实例? ──────────┤
│ (具有持久内存的全局唯一智能体)
│
├─ 需要多智能体协调? ────────────────────┤
│ (智能体调用/消息传递其他智能体)
│
├─ 需要调度任务或 cron 作业? ────────────────┤
│ (延迟执行、重复任务)
│
├─ 需要人在回路工作流? ─────────────────┤
│ (审批关卡、审查流程)
│
└─ 如果以上**全部**为否 ─────────────────────────────→ 直接使用 AI SDK
(更简单的方法)
如果以上**任何一项**为是 ────────────────────────────→ 使用 Agents SDK + AI SDK
(更多基础设施,更强大)
| 功能 | 仅 AI SDK | Agents SDK + AI SDK |
|---|---|---|
| 设置复杂度 | 🟢 低 (npm install,完成) | 🔴 较高 (Durable Objects、迁移、绑定) |
| 代码量 | 🟢 ~100 行 | 🟡 ~500+ 行 |
| 流式传输 | ✅ 自动 (SSE) | ✅ 自动 (AI SDK) 或手动 (Workers AI) |
| 状态管理 | ⚠️ 手动 (D1/KV) | ✅ 内置 (SQLite) |
| WebSocket | ❌ 手动设置 | ✅ 内置 |
| React 钩子 | ✅ useChat, useCompletion | ⚠️ 需要自定义钩子 |
| 多智能体 | ❌ 不支持 | ✅ 内置 (routeAgentRequest) |
| 调度 | ❌ 外部 (Queue/Workflow) | ✅ 内置 (this.schedule) |
| 使用场景 | 简单聊天、补全 | 复杂的有状态工作流 |
从 AI SDK 开始。 如果你后来发现需要 WebSocket 或 Durable Objects,你随时可以迁移到 Agents SDK。后期添加基础设施比移除它更容易。
对于大多数开发者 : 如果你正在构建聊天界面,并且没有对 WebSocket、多智能体协调或调度任务的特定要求,请直接使用 AI SDK。你将更快地交付产品,且复杂度更低。
仅当你已确定对其基础设施能力有特定需求时,才继续使用 Agents SDK。
npm create cloudflare@latest my-agent -- \
--template=cloudflare/agents-starter \
--ts \
--git \
--deploy false
这会创建什么:
cd my-existing-worker
npm install agents
然后创建一个智能体类:
// src/index.ts
import { Agent, AgentNamespace } from "agents";
export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
return new Response("Hello from Agent!");
}
}
export default MyAgent;
创建或更新 wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-10-21",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "MyAgent", // **必须**与类名匹配
"class_name": "MyAgent" // **必须**与导出的类匹配
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"] // **关键**:启用 SQLite 存储
}
]
}
关键配置规则:
name 和 class_name 必须完全相同new_sqlite_classes 必须在第一次迁移中(以后无法添加)npx wrangler@latest deploy
你的智能体现在运行在:https://my-agent.<subdomain>.workers.dev
理解每个工具的作用可以防止混淆,并帮助你选择正确的组合。
┌─────────────────────────────────────────────────────────┐
│ 你的应用程序 │
│ │
│ ┌────────────────┐ ┌──────────────────────┐ │
│ │ Agents SDK │ │ AI 推理 │ │
│ │ (基础设施层) │ + │ (大脑层) │ │
│ │ │ │ │ │
│ │ • WebSocket │ │ 选择**一个**: │ │
│ │ • Durable Objs │ │ • Vercel AI SDK ✅ │ │
│ │ • 状态 (SQL) │ │ • Workers AI ⚠️ │ │
│ │ • 调度 │ │ • 直接 OpenAI │ │
│ │ • 多智能体 │ │ • 直接 Anthropic │ │
│ └────────────────┘ └──────────────────────┘ │
│ ↓ ↓ │
│ 管理连接和状态 生成响应并处理流式传输 │
└─────────────────────────────────────────────────────────┘
↓
Cloudflare Workers + Durable Objects
目的 : 为有状态、实时的智能体提供基础设施
提供 :
onStart, onConnect, onMessage, onClose)this.schedule() 支持 cron/延迟)routeAgentRequest())useAgent, AgentClient, agentFetch)不提供 :
可以将其视为 : 建筑物和基础设施(房间、门、管道),但不是居民(AI)。
目的 : 具有自动流式传输的 AI 推理
提供 :
useChat, useCompletion, useAssistant)示例 :
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [...]
});
// 返回 SSE 流 - 无需手动解析
return result.toTextStreamResponse();
何时与 Agents SDK 一起使用 :
与 Agents SDK 结合 :
import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
export class MyAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
// Agents SDK 提供:WebSocket、状态、this.messages
// AI SDK 提供:自动流式传输、提供商抽象
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages // 由 Agents SDK 管理
}).toTextStreamResponse();
}
}
目的 : Cloudflare 的平台内 AI 推理
提供 :
不提供 :
需要手动解析 :
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// 返回原始 SSE 格式 - **你必须**解析
for await (const chunk of response) {
const text = new TextDecoder().decode(chunk); // Uint8Array → 字符串
if (text.startsWith('data: ')) { // 检查 SSE 格式
const data = JSON.parse(text.slice(6)); // 解析 JSON
if (data.response) { // 提取 .response 字段
fullResponse += data.response;
}
}
}
何时使用 :
权衡 : 节省金钱,花费时间在手动解析上。
当以下情况时使用 : 你需要 WebSocket/状态并且想要干净的 AI 集成
import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages, // Agents SDK 管理历史记录
onFinish
}).toTextStreamResponse();
}
}
优点 :
缺点 :
当以下情况时使用 : 你需要 WebSocket/状态并且成本是关键因素
import { Agent } from "agents";
export class BudgetAgent extends Agent<Env> {
async onMessage(connection, message) {
const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// 需要手动 SSE 解析(参见上面的 Workers AI 部分)
for await (const chunk of response) {
// ... 手动解析 ...
}
}
}
优点 :
缺点 :
当以下情况时使用 : 你不需要 WebSocket 或 Durable Objects
// worker.ts - 简单的 Workers 路由
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse();
}
}
// client.tsx - 内置的 React 钩子
import { useChat } from 'ai/react';
function Chat() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
return <form onSubmit={handleSubmit}>...</form>;
}
优点 :
缺点 :
最适合 : 80% 的聊天应用
| 你的需求 | 推荐的技术栈 | 复杂度 | 成本 |
|---|---|---|---|
| 简单聊天,无状态 | 仅 AI SDK | 🟢 低 | $$ (AI 提供商) |
| 聊天 + WebSocket + 状态 | Agents SDK + AI SDK | 🟡 中 | $$$ (基础设施 + AI) |
| 聊天 + WebSocket + 预算 | Agents SDK + Workers AI | 🔴 高 | $ (仅基础设施) |
| 多智能体工作流 | Agents SDK + AI SDK | 🔴 高 | $$$ (基础设施 + AI) |
| 带有工具的 MCP 服务器 | Agents SDK (McpAgent) | 🟡 中 | $ (仅基础设施) |
Agents SDK 是基础设施,不是 AI。 你将其与 AI 推理工具结合使用:
本技能的其余部分侧重于 Agents SDK(基础设施层)。有关 AI 推理模式,请参阅 ai-sdk-core 或 cloudflare-workers-ai 技能。
关键必需配置 :
{
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] } // **必须**在第一次迁移中
]
}
常见可选绑定 : ai, vectorize, browser, workflows, d1_databases, r2_buckets
关键迁移规则 :
new_sqlite_classes 必须在标签 "v1" 中(无法向已部署的类添加 SQLite)name 和 class_name 必须完全匹配智能体类基础 - 使用生命周期方法扩展 Agent<Env, State>:
onStart() - 智能体初始化onRequest() - 处理 HTTP 请求onConnect/onMessage/onClose() - WebSocket 处理onStateUpdate() - 响应状态变化关键属性 :
this.env - 环境绑定 (AI、数据库等)this.state - 当前智能体状态(只读)this.setState() - 更新持久化状态this.sql - 内置 SQLite 数据库this.name - 智能体实例标识符this.schedule() - 调度未来任务参见 : 官方智能体 API 文档 https://developers.cloudflare.com/agents/api-reference/agents-api/
智能体支持 WebSocket 进行双向实时通信。在以下情况下使用:
基本模式 :
export class ChatAgent extends Agent<Env, State> {
async onConnect(connection: Connection, ctx: ConnectionContext) {
// 身份验证检查,添加到参与者,发送欢迎消息
}
async onMessage(connection: Connection, message: WSMessage) {
// 处理消息,更新状态,广播响应
}
}
SSE 替代方案 : 对于单向服务器 → 客户端流式传输(更简单,基于 HTTP),使用服务器发送事件而不是 WebSocket。
两种状态机制 :
this.setState(newState) - JSON 可序列化状态(最多 1GB)
this.sql - 内置 SQLite 数据库(最多 1GB)
状态规则 :
SQL 模式 :
await this.sql`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)`
await this.sql`INSERT INTO users (email) VALUES (${userEmail})` // ← 预处理语句
const users = await this.sql`SELECT * FROM users WHERE email = ${email}` // ← 返回数组
关键 : 为状态方法提供类型参数不会验证结果是否与你的类型定义匹配。在 TypeScript 中,不存在或不符合你提供的类型的属性(字段)将被静默丢弃。
interface MyState {
count: number;
name: string;
}
export class MyAgent extends Agent<Env, MyState> {
initialState = { count: 0, name: "default" };
async increment() {
// TypeScript 允许这样做,但运行时可能不同
const currentState = this.state; // 类型是 MyState
// 如果状态在外部被损坏/修改:
// { count: "invalid", otherField: 123 }
// TypeScript 仍然将其显示为 MyState
// count 字段不匹配(字符串 vs 数字)
// otherField 被静默丢弃
}
}
预防 : 为关键状态操作添加运行时验证:
// 在运行时验证状态形状
function validateState(state: unknown): state is MyState {
return (
typeof state === 'object' &&
state !== null &&
'count' in state &&
typeof (state as MyState).count === 'number' &&
'name' in state &&
typeof (state as MyState).name === 'string'
);
}
async increment() {
if (!validateState(this.state)) {
console.error('状态验证失败', this.state);
// 重置为有效状态
await this.setState(this.initialState);
return;
}
// 可以安全使用
const newCount = this.state.count + 1;
await this.setState({ ...this.state, count: newCount });
}
智能体可以使用 this.schedule() 调度未来运行的任务。
export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
// 调度任务在 60 秒后运行
const { id } = await this.schedule(60, "checkStatus", { requestId: "123" });
return Response.json({ scheduledTaskId: id });
}
// 此方法将在 60 秒后被调用
async checkStatus(data: { requestId: string }) {
console.log('检查请求状态:', data.requestId);
// 执行检查、更新状态、发送通知等
}
}
export class MyAgent extends Agent {
async scheduleReminder(reminderDate: string) {
const date = new Date(reminderDate);
const { id } = await this.schedule(date, "sendReminder", {
message: "你的预约时间到了!"
});
return id;
}
async sendReminder(data: { message: string }) {
console.log('发送提醒:', data.message);
// 发送电子邮件、推送通知等
}
}
export class MyAgent extends Agent {
async setupRecurringTasks() {
// 每 10 分钟
await this.schedule("*/10 * * * *", "checkUpdates", {});
// 每天上午 8 点
await this.schedule("0 8 * * *", "dailyReport", {});
// 每周一上午 9 点
await this.schedule("0 9 * * 1", "weeklyReport", {});
// 每小时整点
await this.schedule("0 * * * *", "hourlyCheck", {});
}
async checkUpdates(data: any) {
console.log('检查更新...');
}
async dailyReport(data: any) {
console.log('生成每日报告...');
}
async weeklyReport(data: any) {
console.log('生成每周报告...');
}
async hourlyCheck(data: any) {
console.log('运行每小时检查...');
}
}
export class MyAgent extends Agent {
async manageSchedules() {
// 获取所有调度任务
const allTasks = this.getSchedules();
console.log('总任务数:', allTasks.length);
// 按 ID 获取特定任务
const taskId = "some-task-id";
const task = await this.getSchedule(taskId);
if (task) {
console.log('任务:', task.callback, '于', new Date(task.time));
console.log('负载:', task.payload);
console.log('类型:', task.type); // "scheduled" | "delayed" | "cron"
// 取消任务
const cancelled = await this.cancelSchedule(taskId);
console.log('已取消:', cancelled);
}
// 获取时间范围内的任务
const upcomingTasks = this.getSchedules({
timeRange: {
start: new Date(),
end: new Date(Date.now() + 24 * 60 * 60 * 1000) // 未来 24 小时
}
});
console.log('即将到来的任务:', upcomingTasks.length);
// 按类型筛选
const cronTasks = this.getSchedules({ type: "cron" });
const delayedTasks = this.getSchedules({ type: "delayed" });
}
}
调度约束:
(任务大小 * 数量) + 其他状态 < 1GB关键错误 : 如果回调方法不存在:
// ❌ 错误:方法不存在
await this.schedule(60, "nonExistentMethod", {});
// ✅ 正确:方法存在
await this.schedule(60, "existingMethod", {});
async existingMethod(data: any) {
// 实现
}
智能体可以触发异步的 Cloudflare Workflows。
wrangler.jsonc:
{
"workflows": [
{
"name": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}
]
}
如果工作流在不同的脚本中:
{
"workflows": [
{
"name": "EMAIL_WORKFLOW",
"class_name": "EmailWorkflow",
"script_name": "email-workflows" // 不同的项目
}
]
}
import { Agent } from "agents";
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";
interface Env {
MY_WORKFLOW: Workflow;
MyAgent: AgentNamespace<MyAgent>;
}
export class MyAgent extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
const userId = new URL(request.url).searchParams.get('userId');
// 立即触发工作流
const instance = await this.env.MY_WORKFLOW.create({
id: `user-${userId}`,
params: { userId, action: "process" }
});
// 或者调度延迟的工作流触发
await this.schedule(300, "runWorkflow", { userId });
return Response.json({ workflowId: instance.id });
}
async runWorkflow(data: { userId: string }) {
const instance = await this.env.MY_WORKFLOW.create({
id: `delayed-${data.userId}`,
params: data
});
// 定期监控工作流状态
await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id });
}
async checkWorkflowStatus(data: { id: string }) {
// 检查工作流状态(详见工作流文档)
console.log('检查工作流:', data.id);
}
}
// 工作流定义(可以在相同或不同的文件/项目中)
export class MyWorkflow extends WorkflowEntrypoint<Env> {
async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) {
// 工作流实现
const result = await step.do('process-data', async () => {
return { processed: true };
});
return result;
}
}
| 功能 | 智能体 | 工作流 |
|---|---|---|
| 目的 | 交互式、面向用户 | 后台处理 |
| 持续时间 | 秒到小时 | 分钟到小时 |
| 状态 | SQLite 数据库 | 基于步骤的检查点 |
| 交互 | WebSocket、HTTP | 无直接交互 |
| 重试 | 手动 | 每个步骤自动 |
| 使用场景 | 聊天、实时 UI | ETL、批处理 |
最佳实践 : 使用智能体来协调多个工作流。智能体可以触发、监控工作流结果并做出响应,同时保持用户交互。
智能体可以使用浏览器渲染进行网页抓取和自动化:
绑定 : 在 wrangler.jsonc 中添加 "browser": { "binding": "BROWSER" } 包 : @cloudflare/puppeteer 使用场景 : 网页抓取、截图、智能体工作流内的自动化浏览
参见 : cloudflare-browser-rendering 技能,了解完整的 Puppeteer + Workers 集成指南。
智能体可以使用 Vectorize(向量数据库)+ Workers AI(
Status : Production Ready ✅ Last Updated : 2026-01-09 Dependencies : cloudflare-worker-base (recommended) Latest Versions : agents@0.3.3, @modelcontextprotocol/sdk@latest Production Tested : Cloudflare's own MCP servers (https://github.com/cloudflare/mcp-server-cloudflare)
Recent Updates (2025-2026) :
import { context } from agentsAIChatAgent now supports resumable streaming , enabling clients to reconnect and continue receiving streamed responses without data loss. This solves critical real-world scenarios:
Key capability : Streams persist across page refreshes, broken connections, and sync across open tabs and devices.
Implementation (automatic in AIChatAgent):
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages,
onFinish
}).toTextStreamResponse();
// ✅ Stream automatically resumable
// - Client disconnects? Stream preserved
// - Page refresh? Stream continues
// - Multiple tabs? All stay in sync
}
}
No code changes needed - just use AIChatAgent with agents@0.2.24 or later.
Source : Agents SDK v0.2.24 Changelog
The Cloudflare Agents SDK enables building AI-powered autonomous agents that run on Cloudflare Workers + Durable Objects. Agents can:
Each agent instance is a globally unique, stateful micro-server that can run for seconds, minutes, or hours.
STOP : Before using Agents SDK, ask yourself if you actually need it.
This covers 80% of chat applications. For these cases, use Vercel AI SDK directly on Workers - it's simpler, requires less infrastructure, and handles streaming automatically.
Example (no Agents SDK needed):
// worker.ts - Simple chat with AI SDK only
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse(); // Automatic SSE streaming
}
}
// client.tsx - React with built-in hooks
import { useChat } from 'ai/react';
function ChatPage() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
// Done. No Agents SDK needed.
}
Result : 100 lines of code instead of 500. No Durable Objects setup, no WebSocket complexity, no migrations.
This is ~20% of applications - when you need the infrastructure that Agents SDK provides.
Agents SDK IS :
Agents SDK IS NOT :
Think of it this way :
You can use them together (recommended for most cases), or use Workers AI directly (if you're willing to handle manual SSE parsing).
Building an AI application?
│
├─ Need WebSocket bidirectional communication? ───────┐
│ (Client sends while server streams, agent-initiated messages)
│
├─ Need Durable Objects stateful instances? ──────────┤
│ (Globally unique agents with persistent memory)
│
├─ Need multi-agent coordination? ────────────────────┤
│ (Agents calling/messaging other agents)
│
├─ Need scheduled tasks or cron jobs? ────────────────┤
│ (Delayed execution, recurring tasks)
│
├─ Need human-in-the-loop workflows? ─────────────────┤
│ (Approval gates, review processes)
│
└─ If ALL above are NO ─────────────────────────────→ Use AI SDK directly
(Much simpler approach)
If ANY above are YES ────────────────────────────→ Use Agents SDK + AI SDK
(More infrastructure, more power)
| Feature | AI SDK Only | Agents SDK + AI SDK |
|---|---|---|
| Setup Complexity | 🟢 Low (npm install, done) | 🔴 Higher (Durable Objects, migrations, bindings) |
| Code Volume | 🟢 ~100 lines | 🟡 ~500+ lines |
| Streaming | ✅ Automatic (SSE) | ✅ Automatic (AI SDK) or manual (Workers AI) |
| State Management | ⚠️ Manual (D1/KV) | ✅ Built-in (SQLite) |
| WebSockets | ❌ Manual setup | ✅ Built-in |
| React Hooks | ✅ useChat, useCompletion | ⚠️ Custom hooks needed |
| Multi-agent | ❌ Not supported | ✅ Built-in (routeAgentRequest) |
Start with AI SDK. You can always migrate to Agents SDK later if you discover you need WebSockets or Durable Objects. It's easier to add infrastructure later than to remove it.
For most developers : If you're building a chat interface and don't have specific requirements for WebSockets, multi-agent coordination, or scheduled tasks, use AI SDK directly. You'll ship faster and with less complexity.
Proceed with Agents SDK only if you've identified a specific need for its infrastructure capabilities.
npm create cloudflare@latest my-agent -- \
--template=cloudflare/agents-starter \
--ts \
--git \
--deploy false
What this creates:
cd my-existing-worker
npm install agents
Then create an Agent class:
// src/index.ts
import { Agent, AgentNamespace } from "agents";
export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
return new Response("Hello from Agent!");
}
}
export default MyAgent;
Create or update wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-10-21",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "MyAgent", // MUST match class name
"class_name": "MyAgent" // MUST match exported class
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"] // CRITICAL: Enables SQLite storage
}
]
}
CRITICAL Configuration Rules:
name and class_name MUST be identicalnew_sqlite_classes MUST be in first migration (cannot add later)npx wrangler@latest deploy
Your agent is now running at: https://my-agent.<subdomain>.workers.dev
Understanding what each tool does prevents confusion and helps you choose the right combination.
┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌────────────────┐ ┌──────────────────────┐ │
│ │ Agents SDK │ │ AI Inference │ │
│ │ (Infra Layer) │ + │ (Brain Layer) │ │
│ │ │ │ │ │
│ │ • WebSockets │ │ Choose ONE: │ │
│ │ • Durable Objs │ │ • Vercel AI SDK ✅ │ │
│ │ • State (SQL) │ │ • Workers AI ⚠️ │ │
│ │ • Scheduling │ │ • OpenAI Direct │ │
│ │ • Multi-agent │ │ • Anthropic Direct │ │
│ └────────────────┘ └──────────────────────┘ │
│ ↓ ↓ │
│ Manages connections Generates responses │
│ and state and handles streaming │
└─────────────────────────────────────────────────────────┘
↓
Cloudflare Workers + Durable Objects
Purpose : Infrastructure for stateful, real-time agents
Provides :
onStart, onConnect, onMessage, onClose)this.schedule() with cron/delays)routeAgentRequest())useAgent, AgentClient, agentFetch)Does NOT Provide :
Think of it as : The building and infrastructure (rooms, doors, plumbing) but NOT the residents (AI).
Purpose : AI inference with automatic streaming
Provides :
useChat, useCompletion, useAssistant)Example :
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [...]
});
// Returns SSE stream - no manual parsing needed
return result.toTextStreamResponse();
When to use with Agents SDK :
Combine with Agents SDK :
import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
export class MyAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
// Agents SDK provides: WebSocket, state, this.messages
// AI SDK provides: Automatic streaming, provider abstraction
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages // Managed by Agents SDK
}).toTextStreamResponse();
}
}
Purpose : Cloudflare's on-platform AI inference
Provides :
Does NOT Provide :
Manual parsing required :
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// Returns raw SSE format - YOU must parse
for await (const chunk of response) {
const text = new TextDecoder().decode(chunk); // Uint8Array → string
if (text.startsWith('data: ')) { // Check SSE format
const data = JSON.parse(text.slice(6)); // Parse JSON
if (data.response) { // Extract .response field
fullResponse += data.response;
}
}
}
When to use :
Trade-off : Save money, spend time on manual parsing.
Use when : You need WebSockets/state AND want clean AI integration
import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages, // Agents SDK manages history
onFinish
}).toTextStreamResponse();
}
}
Pros :
Cons :
Use when : You need WebSockets/state AND cost is critical
import { Agent } from "agents";
export class BudgetAgent extends Agent<Env> {
async onMessage(connection, message) {
const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// Manual SSE parsing required (see Workers AI section above)
for await (const chunk of response) {
// ... manual parsing ...
}
}
}
Pros :
Cons :
Use when : You DON'T need WebSockets or Durable Objects
// worker.ts - Simple Workers route
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse();
}
}
// client.tsx - Built-in React hooks
import { useChat } from 'ai/react';
function Chat() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
return <form onSubmit={handleSubmit}>...</form>;
}
Pros :
Cons :
Best for : 80% of chat applications
| Your Needs | Recommended Stack | Complexity | Cost |
|---|---|---|---|
| Simple chat, no state | AI SDK only | 🟢 Low | $$ (AI provider) |
| Chat + WebSockets + state | Agents SDK + AI SDK | 🟡 Medium | $$$ (infra + AI) |
| Chat + WebSockets + budget | Agents SDK + Workers AI | 🔴 High | $ (infra only) |
| Multi-agent workflows | Agents SDK + AI SDK | 🔴 High | $$$ (infra + AI) |
| MCP server with tools | Agents SDK (McpAgent) | 🟡 Medium | $ (infra only) |
Agents SDK is infrastructure, not AI. You combine it with AI inference tools:
The rest of this skill focuses on Agents SDK (the infrastructure layer). For AI inference patterns, see the ai-sdk-core or cloudflare-workers-ai skills.
Critical Required Configuration :
{
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] } // MUST be in first migration
]
}
Common Optional Bindings : ai, vectorize, browser, workflows, d1_databases, r2_buckets
CRITICAL Migration Rules :
new_sqlite_classes MUST be in tag "v1" (cannot add SQLite to existing deployed class)name and class_name MUST match exactlySee : https://developers.cloudflare.com/agents/api-reference/configuration/
Agent Class Basics - Extend Agent<Env, State> with lifecycle methods:
onStart() - Agent initializationonRequest() - Handle HTTP requestsonConnect/onMessage/onClose() - WebSocket handlingonStateUpdate() - React to state changesKey Properties :
this.env - Environment bindings (AI, DB, etc.)this.state - Current agent state (read-only)this.setState() - Update persisted statethis.sql - Built-in SQLite databasethis.name - Agent instance identifierthis.schedule() - Schedule future tasksSee : Official Agent API docs at https://developers.cloudflare.com/agents/api-reference/agents-api/
Agents support WebSockets for bidirectional real-time communication. Use when you need:
Basic Pattern :
export class ChatAgent extends Agent<Env, State> {
async onConnect(connection: Connection, ctx: ConnectionContext) {
// Auth check, add to participants, send welcome
}
async onMessage(connection: Connection, message: WSMessage) {
// Process message, update state, broadcast response
}
}
SSE Alternative : For one-way server → client streaming (simpler, HTTP-based), use Server-Sent Events instead of WebSockets.
See : https://developers.cloudflare.com/agents/api-reference/websockets/
Two State Mechanisms :
this.setState(newState) - JSON-serializable state (up to 1GB)
this.sql - Built-in SQLite database (up to 1GB)
State Rules :
SQL Pattern :
await this.sql`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)`
await this.sql`INSERT INTO users (email) VALUES (${userEmail})` // ← Prepared statement
const users = await this.sql`SELECT * FROM users WHERE email = ${email}` // ← Returns array
CRITICAL : Providing a type parameter to state methods does NOT validate that the result matches your type definition. In TypeScript, properties (fields) that do not exist or conform to the type you provided will be dropped silently.
interface MyState {
count: number;
name: string;
}
export class MyAgent extends Agent<Env, MyState> {
initialState = { count: 0, name: "default" };
async increment() {
// TypeScript allows this, but runtime may differ
const currentState = this.state; // Type is MyState
// If state was corrupted/modified externally:
// { count: "invalid", otherField: 123 }
// TypeScript still shows it as MyState
// count field doesn't match (string vs number)
// otherField is dropped silently
}
}
Prevention : Add runtime validation for critical state operations:
// Validate state shape at runtime
function validateState(state: unknown): state is MyState {
return (
typeof state === 'object' &&
state !== null &&
'count' in state &&
typeof (state as MyState).count === 'number' &&
'name' in state &&
typeof (state as MyState).name === 'string'
);
}
async increment() {
if (!validateState(this.state)) {
console.error('State validation failed', this.state);
// Reset to valid state
await this.setState(this.initialState);
return;
}
// Safe to use
const newCount = this.state.count + 1;
await this.setState({ ...this.state, count: newCount });
}
See : https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/
Agents can schedule tasks to run in the future using this.schedule().
export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
// Schedule task to run in 60 seconds
const { id } = await this.schedule(60, "checkStatus", { requestId: "123" });
return Response.json({ scheduledTaskId: id });
}
// This method will be called in 60 seconds
async checkStatus(data: { requestId: string }) {
console.log('Checking status for request:', data.requestId);
// Perform check, update state, send notification, etc.
}
}
export class MyAgent extends Agent {
async scheduleReminder(reminderDate: string) {
const date = new Date(reminderDate);
const { id } = await this.schedule(date, "sendReminder", {
message: "Time for your appointment!"
});
return id;
}
async sendReminder(data: { message: string }) {
console.log('Sending reminder:', data.message);
// Send email, push notification, etc.
}
}
export class MyAgent extends Agent {
async setupRecurringTasks() {
// Every 10 minutes
await this.schedule("*/10 * * * *", "checkUpdates", {});
// Every day at 8 AM
await this.schedule("0 8 * * *", "dailyReport", {});
// Every Monday at 9 AM
await this.schedule("0 9 * * 1", "weeklyReport", {});
// Every hour on the hour
await this.schedule("0 * * * *", "hourlyCheck", {});
}
async checkUpdates(data: any) {
console.log('Checking for updates...');
}
async dailyReport(data: any) {
console.log('Generating daily report...');
}
async weeklyReport(data: any) {
console.log('Generating weekly report...');
}
async hourlyCheck(data: any) {
console.log('Running hourly check...');
}
}
export class MyAgent extends Agent {
async manageSchedules() {
// Get all scheduled tasks
const allTasks = this.getSchedules();
console.log('Total tasks:', allTasks.length);
// Get specific task by ID
const taskId = "some-task-id";
const task = await this.getSchedule(taskId);
if (task) {
console.log('Task:', task.callback, 'at', new Date(task.time));
console.log('Payload:', task.payload);
console.log('Type:', task.type); // "scheduled" | "delayed" | "cron"
// Cancel the task
const cancelled = await this.cancelSchedule(taskId);
console.log('Cancelled:', cancelled);
}
// Get tasks in time range
const upcomingTasks = this.getSchedules({
timeRange: {
start: new Date(),
end: new Date(Date.now() + 24 * 60 * 60 * 1000) // Next 24 hours
}
});
console.log('Upcoming tasks:', upcomingTasks.length);
// Filter by type
const cronTasks = this.getSchedules({ type: "cron" });
const delayedTasks = this.getSchedules({ type: "delayed" });
}
}
Scheduling Constraints:
(task_size * count) + other_state < 1GBCRITICAL ERROR : If callback method doesn't exist:
// ❌ BAD: Method doesn't exist
await this.schedule(60, "nonExistentMethod", {});
// ✅ GOOD: Method exists
await this.schedule(60, "existingMethod", {});
async existingMethod(data: any) {
// Implementation
}
Agents can trigger asynchronous Cloudflare Workflows.
wrangler.jsonc:
{
"workflows": [
{
"name": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}
]
}
If Workflow is in a different script:
{
"workflows": [
{
"name": "EMAIL_WORKFLOW",
"class_name": "EmailWorkflow",
"script_name": "email-workflows" // Different project
}
]
}
import { Agent } from "agents";
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";
interface Env {
MY_WORKFLOW: Workflow;
MyAgent: AgentNamespace<MyAgent>;
}
export class MyAgent extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
const userId = new URL(request.url).searchParams.get('userId');
// Trigger a workflow immediately
const instance = await this.env.MY_WORKFLOW.create({
id: `user-${userId}`,
params: { userId, action: "process" }
});
// Or schedule a delayed workflow trigger
await this.schedule(300, "runWorkflow", { userId });
return Response.json({ workflowId: instance.id });
}
async runWorkflow(data: { userId: string }) {
const instance = await this.env.MY_WORKFLOW.create({
id: `delayed-${data.userId}`,
params: data
});
// Monitor workflow status periodically
await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id });
}
async checkWorkflowStatus(data: { id: string }) {
// Check workflow status (see Workflows docs for details)
console.log('Checking workflow:', data.id);
}
}
// Workflow definition (can be in same or different file/project)
export class MyWorkflow extends WorkflowEntrypoint<Env> {
async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) {
// Workflow implementation
const result = await step.do('process-data', async () => {
return { processed: true };
});
return result;
}
}
| Feature | Agents | Workflows |
|---|---|---|
| Purpose | Interactive, user-facing | Background processing |
| Duration | Seconds to hours | Minutes to hours |
| State | SQLite database | Step-based checkpoints |
| Interaction | WebSockets, HTTP | No direct interaction |
| Retry | Manual | Automatic per step |
| Use Case | Chat, real-time UI | ETL, batch processing |
Best Practice : Use Agents to coordinate multiple Workflows. Agents can trigger, monitor, and respond to Workflow results while maintaining user interaction.
Agents can use Browser Rendering for web scraping and automation:
Binding : Add "browser": { "binding": "BROWSER" } to wrangler.jsonc Package : @cloudflare/puppeteer Use Case : Web scraping, screenshots, automated browsing within agent workflows
See : cloudflare-browser-rendering skill for complete Puppeteer + Workers integration guide.
Agents can implement RAG using Vectorize (vector database) + Workers AI (embeddings):
Pattern : Ingest docs → generate embeddings → store in Vectorize → query → retrieve context → pass to AI
Bindings :
"ai": { "binding": "AI" } - Workers AI for embeddings"vectorize": { "bindings": [{ "binding": "VECTORIZE", "index_name": "my-vectors" }] } - Vector searchTypical Workflow :
@cf/baai/bge-base-en-v1.5)this.env.VECTORIZE.upsert(vectors))this.env.VECTORIZE.query(queryVector, { topK: 5 }))See : cloudflare-vectorize skill for complete RAG implementation guide.
Agents can call AI models using:
Architecture Note : Agents SDK provides infrastructure (WebSockets, state, scheduling). AI inference is a separate layer - use AI SDK for the "brain".
See :
ai-sdk-core skill for complete AI SDK integration patternscloudflare-workers-ai skill for Workers AI streaming parsingTwo Main Patterns :
routeAgentRequest(request, env) - Auto-route via URL pattern /agents/:agent/:name
/agents/my-agent/user-123 routes to MyAgent instance "user-123"getAgentByName<Env, T>(env.AgentBinding, instanceName) - Custom routing
const agent = getAgentByName(env.MyAgent, 'user-${userId}')Multi-Agent Communication :
export class AgentA extends Agent<Env> {
async processData(data: any) {
const agentB = getAgentByName<Env, AgentB>(this.env.AgentB, 'processor-1');
return await (await agentB).analyze(data);
}
}
CRITICAL Security : Always authenticate in Worker BEFORE creating/accessing agents. Agents should assume the caller is authorized.
See : https://developers.cloudflare.com/agents/api-reference/calling-agents/
Browser/React Integration :
AgentClient (from agents/client) - WebSocket client for browseragentFetch (from agents/client) - HTTP requests to agentsuseAgent (from agents/react) - React hook for WebSocket connections + state syncuseAgentChat (from agents/ai-react) - Pre-built chat UI hookAll client libraries automatically handle : WebSocket connections, state synchronization, reconnection logic.
See : https://developers.cloudflare.com/agents/api-reference/client-apis/
Build MCP servers using the Agents SDK.
npm install @modelcontextprotocol/sdk agents
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() {
// Define a tool
this.server.tool(
"add",
"Add two numbers together",
{
a: z.number().describe("First number"),
b: z.number().describe("Second number")
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
}
}
type State = { counter: number };
export class StatefulMCP extends McpAgent<Env, State> {
server = new McpServer({ name: "Counter", version: "1.0.0" });
initialState: State = { counter: 0 };
async init() {
// Resource
this.server.resource(
"counter",
"mcp://resource/counter",
(uri) => ({
contents: [{ uri: uri.href, text: String(this.state.counter) }]
})
);
// Tool
this.server.tool(
"increment",
"Increment the counter",
{ amount: z.number() },
async ({ amount }) => {
this.setState({
...this.state,
counter: this.state.counter + amount
});
return {
content: [{
type: "text",
text: `Counter is now ${this.state.counter}`
}]
};
}
);
}
}
import { Hono } from 'hono';
const app = new Hono();
// Modern streamable HTTP transport (recommended)
app.mount('/mcp', MyMCP.serve('/mcp').fetch, { replaceRequest: false });
// Legacy SSE transport (deprecated)
app.mount('/sse', MyMCP.serveSSE('/sse').fetch, { replaceRequest: false });
export default app;
Transport Comparison:
The Agents SDK supports multiple MCP protocol versions. As of agents@0.3.x, version validation is permissive to accept newer protocol versions:
2024-11-05, 2025-11-25, and future versionsError: Unsupported MCP protocol version: 2025-11-25This aligns with the MCP community's move to stateless transports. If you encounter protocol version errors, update to agents@0.3.x or later.
Source : GitHub Issue #769
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
export default new OAuthProvider({
apiHandlers: {
'/sse': MyMCP.serveSSE('/sse'),
'/mcp': MyMCP.serve('/mcp')
},
// OAuth configuration
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
// ... other OAuth settings
});
# Run MCP inspector
npx @modelcontextprotocol/inspector@latest
# Connect to: http://localhost:8788/mcp
Cloudflare's MCP Servers : See reference for production examples.
This skill prevents 23 documented issues:
Error : "Cannot gradually deploy migration" Source : https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/ Why : Migrations apply to all instances simultaneously Prevention : Deploy migrations independently of code changes, use npx wrangler versions deploy
Error : "Cannot enable SQLite on existing class" Source : https://developers.cloudflare.com/agents/api-reference/configuration/ Why : SQLite must be enabled in first migration Prevention : Include new_sqlite_classes in tag "v1" migration
Error : "Binding not found" or "Cannot access undefined" Source : https://developers.cloudflare.com/agents/api-reference/agents-api/ Why : Durable Objects require exported class Prevention : export class MyAgent extends Agent (with export keyword)
Error : "Binding 'X' not found" Source : https://developers.cloudflare.com/agents/api-reference/configuration/ Why : Binding name must match class name exactly Prevention : Ensure name and class_name are identical in wrangler.jsonc
Error : Unexpected behavior with agent instances Source : https://developers.cloudflare.com/agents/api-reference/agents-api/ Why : Same name always returns same agent instance globally Prevention : Use unique identifiers (userId, sessionId) for instance names
Error : Connection state lost after disconnect Source : https://developers.cloudflare.com/agents/api-reference/websockets/ Why : WebSocket connections don't persist, but agent state does Prevention : Store important data in agent state via setState(), not connection state
Error : "Method X does not exist on Agent" or "IoContext timed out due to inactivity" or "A call to blockConcurrencyWhile() waited for too long" Source : https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ and GitHub Issue #600 Why : this.schedule() calls method that isn't defined, OR scheduled callbacks failed when AI requests exceeded 30 seconds due to blockConcurrencyWhile wrapper (fixed in agents@0.2.x via PR #653) Prevention : Ensure callback method exists before scheduling. For long-running AI requests, update to agents@0.2.x or later.
Historical issue (before 0.2.x): Scheduled callbacks were wrapped in blockConcurrencyWhile, which enforced a 30-second limit. AI requests exceeding this would fail even though they were valid async operations.
// ✅ Fixed in 0.2.x - schedule callbacks can now run for their full duration
export class MyAgent extends Agent<Env> {
async onRequest(request: Request) {
await this.schedule(60, "processAIRequest", { query: "..." });
}
async processAIRequest(data: { query: string }) {
// This can now take > 30s without timeout
const result = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: data.query }]
});
}
}
Error : "Maximum database size exceeded" Source : https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ Why : Agent state + scheduled tasks exceed 1GB Prevention : Monitor state size, use external storage (D1, R2) for large data
Error : "Task payload exceeds 2MB" Source : https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ Why : Each task maps to database row with 2MB limit Prevention : Keep task payloads minimal, store large data in agent state/SQL
Error : "Cannot read property 'create' of undefined" Source : https://developers.cloudflare.com/agents/api-reference/run-workflows/ Why : Workflow binding not configured in wrangler.jsonc Prevention : Add workflow binding before using this.env.WORKFLOW
Error : "BROWSER binding undefined" Source : https://developers.cloudflare.com/agents/api-reference/browse-the-web/ Why : Browser Rendering requires explicit binding Prevention : Add "browser": { "binding": "BROWSER" } to wrangler.jsonc
Error : "Index does not exist" Source : https://developers.cloudflare.com/agents/api-reference/rag/ Why : Vectorize index must be created before use Prevention : Run wrangler vectorize create before deploying agent
Error : "SSE transport deprecated" Source : https://developers.cloudflare.com/agents/model-context-protocol/transport/ Why : SSE transport is legacy, streamable HTTP is recommended Prevention : Use /mcp endpoint with MyMCP.serve('/mcp'), not /sse
Error : Security vulnerability Source : https://developers.cloudflare.com/agents/api-reference/calling-agents/ Why : Authentication done in Agent instead of Worker Prevention : Always authenticate in Worker before calling getAgentByName()
Error : Cross-user data leakage Source : https://developers.cloudflare.com/agents/api-reference/calling-agents/ Why : Poor instance naming allows access to wrong agent Prevention : Use namespaced names like user-${userId}, validate ownership
Error : "Cannot read property 'response' of undefined" or empty AI responses Source : https://developers.cloudflare.com/workers-ai/platform/streaming/ Why : Workers AI returns streaming responses as Uint8Array in Server-Sent Events (SSE) format, not plain objects Prevention : Use TextDecoder + SSE parsing pattern (see "Workers AI (Alternative for AI)" section above)
The problem - Attempting to access stream chunks directly fails:
const response = await env.AI.run(model, { stream: true });
for await (const chunk of response) {
console.log(chunk.response); // ❌ undefined - chunk is Uint8Array, not object
}
The solution - Parse SSE format manually:
const response = await env.AI.run(model, { stream: true });
for await (const chunk of response) {
const text = new TextDecoder().decode(chunk); // Step 1: Uint8Array → string
if (text.startsWith('data: ')) { // Step 2: Check SSE format
const jsonStr = text.slice(6).trim(); // Step 3: Extract JSON from "data: {...}"
if (jsonStr === '[DONE]') break; // Step 4: Handle termination
const data = JSON.parse(jsonStr); // Step 5: Parse JSON
if (data.response) { // Step 6: Extract .response field
fullResponse += data.response;
}
}
}
Better alternative : Use Vercel AI SDK which handles this automatically:
import { streamText } from 'ai';
import { createCloudflare } from '@ai-sdk/cloudflare';
const cloudflare = createCloudflare();
const result = streamText({
model: cloudflare('@cf/meta/llama-3-8b-instruct', { binding: env.AI }),
messages
});
// No manual parsing needed ✅
When to accept manual parsing :
When to use AI SDK instead :
Error : Error: internal error; reference = [reference ID] Source : GitHub Issue #119 Why : WebSocket connections fail when cumulative message payload exceeds approximately 1 MB. After 5-6 tool calls returning large data (e.g., 200KB+ each), the payload exceeds the limit and the connection crashes with "internal error". Prevention : Prune message history client-side to stay under 950KB
The problem : All messages—including large tool results—are streamed back to the client and LLM for continued conversations, causing cumulative payload growth.
// Workaround: Prune old messages client-side
function pruneMessages(messages: Message[]): Message[] {
let totalSize = 0;
const pruned = [];
// Keep recent messages until we hit size limit
for (let i = messages.length - 1; i >= 0; i--) {
const msgSize = JSON.stringify(messages[i]).length;
if (totalSize + msgSize > 950_000) break; // 950KB limit
pruned.unshift(messages[i]);
totalSize += msgSize;
}
return pruned;
}
// Use before sending to agent
const prunedMessages = pruneMessages(allMessages);
Better solution (proposed): Server-side context management where only message summaries are sent to the client, not full tool results.
Error : Duplicate messages with identical toolCallId, original stuck in input-available state Source : GitHub Issue #790 Why : When using needsApproval: true on tools, the system creates duplicate assistant messages instead of updating the original one. The server-generated message (state input-available) never transitions to approval-responded when client approves. Prevention : Understand this is a known limitation. Track both message IDs in your UI until fixed.
// Current behavior (agents@0.3.3)
export class MyAgent extends AIChatAgent<Env> {
tools = {
sensitiveAction: tool({
needsApproval: true, // ⚠️ Causes duplicate messages
execute: async (args) => {
return { result: "action completed" };
}
})
};
}
// Result: Two messages persist
// 1. Server message: ID "assistant_1768917665170_4mub00d32", state "input-available"
// 2. Client message: ID "oFwQwEpvLd8f1Gwd", state "approval-responded"
Workaround : Handle both messages in your UI or avoid needsApproval until this is resolved.
Error : Duplicate item found with id rs_xxx (OpenAI API rejection) Source : GitHub Issue #728 Fixed In : agents@0.2.31+ Why : When using useAgentChat with client-side tools lacking server-side execute functions, the agents library created duplicate assistant messages sharing identical reasoning IDs, triggering OpenAI API rejection. Prevention : Update to agents@0.2.31 or later
Historical issue (before 0.2.31): Client-side tool execution created new messages instead of updating existing ones, leaving the original stuck in incomplete state and causing duplicate providerMetadata.openai.itemId values.
// ✅ Fixed in 0.2.31+ - no changes needed
// Just ensure you're on agents@0.2.31 or later
Error : 401 Unauthorized errors after token expiration despite cacheTtl setting Source : GitHub Issue #725 Fixed In : Check agents release notes Why : The useAgent hook had a caching problem where the queryPromise was computed once per cacheKey and kept forever, even after TTL expired. The useMemo implementation didn't include time in dependencies, so TTL was never enforced. Prevention : Update to latest agents version
Historical workaround (before fix):
// Force cache invalidation by including token version in queryDeps
const [tokenVersion, setTokenVersion] = useState(0);
const { state } = useAgent({
query: async () => ({ token: await getJWT() }),
queryDeps: [tokenVersion], // ✅ Force new cache key
cacheTtl: 60_000,
});
// Manually refresh token before expiry
useEffect(() => {
const interval = setInterval(() => {
setTokenVersion(v => v + 1);
}, 50_000); // Refresh every 50s
return () => clearInterval(interval);
}, []);
Error : TypeError: validator.getValidator is not a function Source : GitHub Issue #663 Fixed In : Check agents release notes Why : When a Durable Object hibernated and restored, the Agents SDK serialized MCP connection options using JSON.stringify(), converting class instances like CfWorkerJsonSchemaValidator into plain objects without methods. Upon restoration via JSON.parse(), the validator lost its methods. Prevention : Update to latest agents version (validator now built-in)
Historical issue (before fix):
// ❌ Before fix - manual validator caused errors
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/cloudflare-worker';
const mcpAgent = new McpAgent({
client: {
jsonSchemaValidator: new CfWorkerJsonSchemaValidator(), // Got serialized to {}
}
});
Current approach (fixed):
// ✅ Now automatic - no manual validator needed
const mcpAgent = new McpAgent({
// SDK handles validator internally
});
Error : Error: Client does not support form elicitation Source : GitHub Issue #777 Fixed In : agents@0.3.5+ Why : When using WorkerTransport with MCP servers in serverless environments, client capabilities failed to persist across Durable Object hibernation cycles because the TransportState interface only stored sessionId and initialized status, not clientCapabilities. Prevention : Update to agents@0.3.5 or later
Historical issue (before 0.3.5):
// Client advertised elicitation capability during handshake,
// but after hibernation, capability info was lost
await server.elicitInput({ /* form */ }); // ❌ Error: capabilities lost
Solution (fixed in 0.3.5):
// TransportState now includes clientCapabilities
interface TransportState {
sessionId: string;
initialized: boolean;
clientCapabilities?: ClientCapabilities; // ✅ Now persisted
}
Error : State never persists, new agent instance every request Source : Cloudflare blog - Building agents with OpenAI Why : If you use newUniqueId() instead of idFromName(), you'll get a new agent instance each time, and your memory/state will never persist. This is a common early bug that silently kills statefulness. Prevention : Always use idFromName() for user-specific agents, never newUniqueId()
// ❌ WRONG: Creates new agent every time (state never persists)
export default {
async fetch(request: Request, env: Env) {
const id = env.MyAgent.newUniqueId(); // New ID = new instance
const agent = env.MyAgent.get(id);
// State never persists - different instance each time
return agent.fetch(request);
}
}
// ✅ CORRECT: Same user = same agent = persistent state
export default {
async fetch(request: Request, env: Env) {
const userId = getUserId(request);
const id = env.MyAgent.idFromName(userId); // Same ID for same user
const agent = env.MyAgent.get(id);
// State persists across requests for this user
return agent.fetch(request);
}
}
Why It Matters :
newUniqueId(): Generates a random unique ID each call → new agent instanceidFromName(string): Deterministic ID from string → same agent for same inputRule of thumb : Use idFromName() for 99% of cases. Only use newUniqueId() when you genuinely need a one-time, ephemeral agent instance.
agents - Agents SDK (required)@modelcontextprotocol/sdk - For building MCP servers@cloudflare/puppeteer - For web browsingai - AI SDK for model calls@ai-sdk/openai - OpenAI models@ai-sdk/anthropic - Anthropic modelswrangler-agents-config.jsonc - Complete configuration examplebasic-agent.ts - Minimal HTTP agentwebsocket-agent.ts - WebSocket handlersstate-sync-agent.ts - State management patternsscheduled-agent.ts - Task schedulingworkflow-agent.ts - Workflow integrationbrowser-agent.ts - Web browsingrag-agent.ts - RAG implementationchat-agent-streaming.ts - Streaming chatagent-class-api.md - Complete Agent class referenceclient-api-reference.md - Browser client APIsstate-management-guide.md - State and SQL deep divewebsockets-sse.md - WebSocket vs SSE comparisonscheduling-api.md - Task scheduling detailsworkflows-integration.md - Workflows guidebrowser-rendering.md - Web browsing patternsrag-patterns.md - RAG best practicesmcp-server-guide.md - MCP server developmentchat-bot-complete.md - Full chat agentmulti-agent-workflow.md - Agent orchestrationscheduled-reports.md - Recurring tasksbrowser-scraper-agent.md - Web scrapingrag-knowledge-base.md - RAG systemmcp-remote-server.md - Production MCP serverLast Verified : 2026-01-21 Package Versions : agents@0.3.6 Compliance : Cloudflare Agents SDK official documentation Changes : Added 7 new issues from community research (WebSocket payload limits, state type safety, idFromName gotcha), updated to agents@0.3.6
Weekly Installs
326
Repository
GitHub Stars
652
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
claude-code272
gemini-cli223
opencode219
cursor207
antigravity202
codex195
Azure Data Explorer (Kusto) 查询技能:KQL数据分析、日志遥测与时间序列处理
100,500 周安装
Salesforce Apex 代码生成与审查工具 - sf-apex 技能详解
318 周安装
Next.js 14全栈开发模板:TypeScript + TailwindCSS + Supabase最佳实践
318 周安装
LinkedIn个人品牌塑造技能 - 专业资料分析与优化工具,提升职场可见度与互动率
318 周安装
goplaces - Google Places API 命令行工具,支持搜索、解析和详情查询
318 周安装
CTO顾问:技术领导力、团队扩展与工程卓越的战略框架与工具
318 周安装
病毒式生成器构建器指南:打造可分享的测验、名称和图像生成工具
318 周安装
| ❌ External (Queue/Workflow) |
| ✅ Built-in (this.schedule) |
| Use Case | Simple chat, completions | Complex stateful workflows |
calling-agents-worker.ts - Agent routingreact-useagent-client.tsx - React clientmcp-server-basic.ts - MCP serverhitl-agent.ts - Human-in-the-loopmcp-tools-reference.md - MCP tools APIhitl-patterns.md - Human-in-the-loop workflowsbest-practices.md - Production patterns