From c857d6c25fa8ce778aea202ae3e78dfa4f87fe74 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 16 Feb 2026 19:49:07 +0000 Subject: [PATCH 1/9] Docs: add chat agents & retries; update APIs Add comprehensive Chat agents and Retries reference pages. Update numerous API examples to use TypeScript "} satisfies ExportedHandler;" for exported handlers. Change Wrangler compatibility_date to a placeholder "$today". Add MCP client retry options and a custom OAuth provider example. Update queue API docs to mark several methods as synchronous and adjust signatures, and add a note about built-in queue retries. Miscellaneous related doc fixes and clarifications. --- .../docs/agents/api-reference/agents-api.mdx | 2 +- .../agents/api-reference/callable-methods.mdx | 2 +- .../docs/agents/api-reference/chat-agents.mdx | 1065 +++++++++++++++++ .../agents/api-reference/configuration.mdx | 6 +- .../docs/agents/api-reference/email.mdx | 6 +- .../agents/api-reference/mcp-client-api.mdx | 52 +- .../agents/api-reference/mcp-handler-api.mdx | 10 +- .../docs/agents/api-reference/queue-tasks.mdx | 25 +- .../docs/agents/api-reference/retries.mdx | 496 ++++++++ .../docs/agents/api-reference/routing.mdx | 14 +- .../agents/api-reference/run-workflows.mdx | 2 +- .../agents/api-reference/schedule-tasks.mdx | 27 +- .../docs/agents/api-reference/websockets.mdx | 297 +++++ .../docs/agents/concepts/agent-class.mdx | 2 +- .../add-to-existing-project.mdx | 16 +- .../getting-started/build-a-chat-agent.mdx | 472 +++++--- .../agents/getting-started/quick-start.mdx | 4 +- .../docs/agents/guides/chatgpt-app.mdx | 2 +- .../docs/agents/guides/human-in-the-loop.mdx | 2 +- .../docs/agents/guides/oauth-mcp-client.mdx | 2 +- src/content/docs/agents/guides/webhooks.mdx | 6 +- .../model-context-protocol/transport.mdx | 2 +- 22 files changed, 2266 insertions(+), 246 deletions(-) create mode 100644 src/content/docs/agents/api-reference/chat-agents.mdx create mode 100644 src/content/docs/agents/api-reference/retries.mdx diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 2185b6884b8..344888c9f1c 100644 --- a/src/content/docs/agents/api-reference/agents-api.mdx +++ b/src/content/docs/agents/api-reference/agents-api.mdx @@ -179,7 +179,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` Refer to [Routing](/agents/api-reference/routing/) for custom paths, CORS, and instance naming patterns. diff --git a/src/content/docs/agents/api-reference/callable-methods.mdx b/src/content/docs/agents/api-reference/callable-methods.mdx index f5d75681770..b6fff0613ff 100644 --- a/src/content/docs/agents/api-reference/callable-methods.mdx +++ b/src/content/docs/agents/api-reference/callable-methods.mdx @@ -543,7 +543,7 @@ export default { return Response.json(result); }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/api-reference/chat-agents.mdx b/src/content/docs/agents/api-reference/chat-agents.mdx new file mode 100644 index 00000000000..2dda1460a8a --- /dev/null +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -0,0 +1,1065 @@ +--- +title: Chat agents +pcx_content_type: reference +sidebar: + order: 4 +--- + +import { TypeScriptExample, LinkCard } from "~/components"; + +Build AI-powered chat interfaces with `AIChatAgent` and `useAgentChat`. Messages are automatically persisted to SQLite, streams resume on disconnect, and tool calls work across server and client. + +## Overview + +The `@cloudflare/ai-chat` package provides two main exports: + +| Export | Import | Purpose | +| -------------- | --------------------------- | -------------------------------------------------------------- | +| `AIChatAgent` | `@cloudflare/ai-chat` | Server-side agent class with message persistence and streaming | +| `useAgentChat` | `@cloudflare/ai-chat/react` | React hook for building chat UIs | + +Built on the [AI SDK](https://ai-sdk.dev) and Cloudflare Durable Objects, you get: + +- **Automatic message persistence** — conversations stored in SQLite, survive restarts +- **Resumable streaming** — disconnected clients resume mid-stream without data loss +- **Real-time sync** — messages broadcast to all connected clients via WebSocket +- **Tool support** — server-side, client-side, and human-in-the-loop tool patterns +- **Row size protection** — automatic compaction when messages approach SQLite limits + +## Quick start + +### Install + +```sh +npm install @cloudflare/ai-chat agents ai +``` + +### Server + + + +```ts +import { AIChatAgent } from "@cloudflare/ai-chat"; +import { createWorkersAI } from "workers-ai-provider"; +import { streamText, convertToModelMessages } from "ai"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + // Use any provicer such as workers-ai-provider, openai, anthropic, google, etc. + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +### Client + + + +```ts +import { useAgent } from "agents/react"; +import { useAgentChat } from "@cloudflare/ai-chat/react"; + +function Chat() { + const agent = useAgent({ agent: "ChatAgent" }); + const { messages, sendMessage, status } = useAgentChat({ agent }); + + return ( +
+ {messages.map((msg) => ( +
+ {msg.role}: + {msg.parts.map((part, i) => + part.type === "text" ? {part.text} : null, + )} +
+ ))} + +
{ + e.preventDefault(); + const input = e.currentTarget.elements.namedItem( + "input", + ) as HTMLInputElement; + sendMessage({ text: input.value }); + input.value = ""; + }} + > + + +
+
+ ); +} +``` + +
+ +### Wrangler configuration + +```jsonc +// wrangler.jsonc +{ + "ai": { "binding": "AI" }, + "durable_objects": { + "bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }], +} +``` + +The `new_sqlite_classes` migration is required — `AIChatAgent` uses SQLite for message persistence and stream chunk buffering. + +## How it works + +```mermaid +sequenceDiagram + participant Client as Client (useAgentChat) + participant Agent as AIChatAgent + participant DB as SQLite + + Client->>Agent: CF_AGENT_USE_CHAT_REQUEST (WebSocket) + Agent->>DB: Persist messages + Agent->>Agent: onChatMessage() + loop Streaming response + Agent-->>Client: CF_AGENT_USE_CHAT_RESPONSE (chunks) + Agent->>DB: Buffer chunks + end + Agent->>DB: Persist final message + Agent-->>Client: CF_AGENT_CHAT_MESSAGES (broadcast to all clients) +``` + +1. The client sends a message via WebSocket +2. `AIChatAgent` persists messages to SQLite and calls your `onChatMessage` method +3. Your method returns a streaming `Response` (typically from `streamText`) +4. Chunks stream back over WebSocket in real-time +5. When the stream completes, the final message is persisted and broadcast to all connections + +## Server API + +### `AIChatAgent` + +Extends `Agent` from the `agents` package. Manages conversation state, persistence, and streaming. + + + +```ts +import { AIChatAgent } from "@cloudflare/ai-chat"; + +export class ChatAgent extends AIChatAgent { + // Access current messages + // this.messages: UIMessage[] + + // Limit stored messages (optional) + maxPersistedMessages = 200; + + async onChatMessage(onFinish?, options?) { + // onFinish: optional callback for streamText (cleanup is automatic) + // options.abortSignal: cancel signal + // options.body: custom data from client + // Return a Response (streaming or plain text) + } +} +``` + + + +### `onChatMessage` + +This is the main method you override. It receives the conversation context and should return a `Response`. + +**Streaming response** (most common): + + + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + system: "You are a helpful assistant.", + messages: await convertToModelMessages(this.messages), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +**Plain text response**: + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + return new Response("Hello! I am a simple agent.", { + headers: { "Content-Type": "text/plain" }, + }); + } +} +``` + +**Accessing custom body data**: + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage(_onFinish, options) { + const { timezone, userId } = options?.body ?? {}; + // Use these values in your LLM call or business logic + } +} +``` + +### `this.messages` + +The current conversation history, loaded from SQLite. This is an array of `UIMessage` objects from the AI SDK. Messages are automatically persisted after each interaction. + +### `maxPersistedMessages` + +Cap the number of messages stored in SQLite. When the limit is exceeded, the oldest messages are deleted. This controls storage only — it does not affect what is sent to the LLM. + + + +```ts +export class ChatAgent extends AIChatAgent { + maxPersistedMessages = 200; +} +``` + + + +To control what is sent to the model, use the AI SDK's `pruneMessages()`: + + + +```ts +import { streamText, convertToModelMessages, pruneMessages } from "ai"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: pruneMessages({ + messages: await convertToModelMessages(this.messages), + reasoning: "before-last-message", + toolCalls: "before-last-2-messages", + }), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +### `persistMessages` and `saveMessages` + +For advanced cases, you can manually persist messages: + + + +```ts +// Persist messages without triggering a new response +await this.persistMessages(messages); + +// Persist messages AND trigger onChatMessage (e.g., programmatic messages) +await this.saveMessages(messages); +``` + + + +### Lifecycle hooks + +`AIChatAgent` wraps `onConnect` and `onClose` to manage stream resumption and message loading. If you override these methods, you must call `super`: + + + +```ts +export class ChatAgent extends AIChatAgent { + async onConnect(connection, ctx) { + // Your custom logic (e.g., logging, auth checks) + console.log("Client connected:", connection.id); + + // Required — sets up stream resumption and message sync + await super.onConnect(connection, ctx); + } + + async onClose(connection, code, reason, wasClean) { + console.log("Client disconnected:", connection.id); + + // Required — cleans up connection tracking + await super.onClose(connection, code, reason, wasClean); + } +} +``` + + + +The `destroy()` method cancels any pending chat requests and cleans up stream state. It is called automatically when the Durable Object is evicted, but you can call it manually if needed. + +### Request cancellation + +When a user clicks "stop" in the chat UI, the client sends a `CF_AGENT_CHAT_REQUEST_CANCEL` message. The server propagates this to the `abortSignal` in `options`: + + + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage(_onFinish, options) { + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), + abortSignal: options?.abortSignal, // Pass through for cancellation + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +:::caution +If you do not pass `abortSignal` to `streamText`, the LLM call will continue running in the background even after the user cancels. Always forward it when possible. +::: + +## Client API + +### `useAgentChat` + +React hook that connects to an `AIChatAgent` over WebSocket. Wraps the AI SDK's `useChat` with a native WebSocket transport. + + + +```ts +import { useAgent } from "agents/react"; +import { useAgentChat } from "@cloudflare/ai-chat/react"; + +function Chat() { + const agent = useAgent({ agent: "ChatAgent" }); + const { + messages, + sendMessage, + clearHistory, + addToolOutput, + addToolApprovalResponse, + setMessages, + status, + } = useAgentChat({ agent }); + + // ... +} +``` + + + +### Options + +| Option | Type | Default | Description | +| ----------------------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | +| `agent` | `ReturnType` | Required | Agent connection from `useAgent` | +| `onToolCall` | `({ toolCall, addToolOutput }) => void` | — | Handle client-side tool execution | +| `autoContinueAfterToolResult` | `boolean` | `true` | Auto-continue conversation after client tool results and approvals | +| `resume` | `boolean` | `true` | Enable automatic stream resumption on reconnect | +| `body` | `object \| () => object` | — | Custom data sent with every request | +| `prepareSendMessagesRequest` | `(options) => { body?, headers? }` | — | Advanced per-request customization | +| `getInitialMessages` | `(options) => Promise` or `null` | — | Custom initial message loader. Set to `null` to skip the HTTP fetch entirely (useful when providing `messages` directly) | + +### Return values + +| Property | Type | Description | +| ------------------------- | ---------------------------------- | ---------------------------------------------------- | +| `messages` | `UIMessage[]` | Current conversation messages | +| `sendMessage` | `(message) => void` | Send a message | +| `clearHistory` | `() => void` | Clear conversation (client and server) | +| `addToolOutput` | `({ toolCallId, output }) => void` | Provide output for a client-side tool | +| `addToolApprovalResponse` | `({ id, approved }) => void` | Approve or reject a tool requiring approval | +| `setMessages` | `(messages \| updater) => void` | Set messages directly (syncs to server) | +| `status` | `string` | `"idle"`, `"submitted"`, `"streaming"`, or `"error"` | + +## Tools + +`AIChatAgent` supports three tool patterns, all using the AI SDK's `tool()` function: + +| Pattern | Where it runs | When to use | +| ----------- | ---------------------------- | --------------------------------------------- | +| Server-side | Server (automatic) | API calls, database queries, computations | +| Client-side | Browser (via `onToolCall`) | Geolocation, clipboard, camera, local storage | +| Approval | Server (after user approval) | Payments, deletions, external actions | + +### Server-side tools + +Tools with an `execute` function run automatically on the server: + + + +```ts +import { streamText, convertToModelMessages, tool, stepCountIs } from "ai"; +import { z } from "zod"; +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), + tools: { + getWeather: tool({ + description: "Get weather for a city", + inputSchema: z.object({ city: z.string() }), + execute: async ({ city }) => { + const data = await fetchWeather(city); + return { temperature: data.temp, condition: data.condition }; + }, + }), + }, + stopWhen: stepCountIs(5), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +### Client-side tools + +Define a tool on the server without `execute`, then handle it on the client with `onToolCall`. Use this for tools that need browser APIs. + +**Server:** + + + +```ts +tools: { + getLocation: tool({ + description: "Get the user's location from the browser", + inputSchema: z.object({}), + // No execute — the client handles it + }); +} +``` + + + +**Client:** + + + +```ts +const { messages, sendMessage } = useAgentChat({ + agent, + onToolCall: async ({ toolCall, addToolOutput }) => { + if (toolCall.toolName === "getLocation") { + const pos = await new Promise((resolve, reject) => + navigator.geolocation.getCurrentPosition(resolve, reject), + ); + addToolOutput({ + toolCallId: toolCall.toolCallId, + output: { lat: pos.coords.latitude, lng: pos.coords.longitude }, + }); + } + }, +}); +``` + + + +When the LLM invokes `getLocation`, the stream pauses. The `onToolCall` callback fires, your code provides the output, and the conversation continues. + +### Tool approval (human-in-the-loop) + +Use `needsApproval` for tools that require user confirmation before executing. + +**Server:** + + + +```ts +tools: { + processPayment: tool({ + description: "Process a payment", + inputSchema: z.object({ + amount: z.number(), + recipient: z.string(), + }), + needsApproval: async ({ amount }) => amount > 100, + execute: async ({ amount, recipient }) => charge(amount, recipient), + }); +} +``` + + + +**Client:** + + + +```ts +const { messages, addToolApprovalResponse } = useAgentChat({ agent }); + +// Render pending approvals from message parts +{ + messages.map((msg) => + msg.parts + .filter( + (part) => part.type === "tool" && part.state === "approval-required", + ) + .map((part) => ( +
+

Approve {part.toolName}?

+ + +
+ )), + ); +} +``` + +
+ +For more patterns, refer to [Human-in-the-loop](/agents/concepts/human-in-the-loop/). + +## Custom request data + +Include custom data with every chat request using the `body` option: + + + +```ts +const { messages, sendMessage } = useAgentChat({ + agent, + body: { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + userId: currentUser.id, + }, +}); +``` + + + +For dynamic values, use a function: + + + +```ts +body: () => ({ + token: getAuthToken(), + timestamp: Date.now(), +}); +``` + + + +Access these fields on the server: + + + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage(_onFinish, options) { + const { timezone, userId } = options?.body ?? {}; + // ... + } +} +``` + + + +For advanced per-request customization (custom headers, different body per request), use `prepareSendMessagesRequest`: + + + +```ts +const { messages, sendMessage } = useAgentChat({ + agent, + prepareSendMessagesRequest: async ({ messages, trigger }) => ({ + headers: { Authorization: `Bearer ${await getToken()}` }, + body: { requestedAt: Date.now() }, + }), +}); +``` + + + +## Resumable streaming + +Streams automatically resume when a client disconnects and reconnects. No configuration is needed — it works out of the box. + +When streaming is active: + +1. All chunks are buffered in SQLite as they are generated +2. If the client disconnects, the server continues streaming and buffering +3. When the client reconnects, it receives all buffered chunks and resumes live streaming + +Disable with `resume: false`: + + + +```ts +const { messages } = useAgentChat({ agent, resume: false }); +``` + + + +## Storage management + +### Row size protection + +SQLite rows have a maximum size of 2 MB. When a message approaches this limit (for example, a tool returning a very large output), `AIChatAgent` automatically compacts the message: + +1. **Tool output compaction** — Large tool outputs are replaced with an LLM-friendly summary that instructs the model to suggest re-running the tool +2. **Text truncation** — If the message is still too large after tool compaction, text parts are truncated with a note + +Compacted messages include `metadata.compactedToolOutputs` so clients can detect and display this gracefully. + +### Controlling LLM context vs storage + +Storage (`maxPersistedMessages`) and LLM context are independent: + +| Concern | Control | Scope | +| ------------------------------- | ---------------------- | ----------- | +| How many messages SQLite stores | `maxPersistedMessages` | Persistence | +| What the model sees | `pruneMessages()` | LLM context | +| Row size limits | Automatic compaction | Per-message | + + + +```ts +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: pruneMessages({ + // LLM context limit + messages: await convertToModelMessages(this.messages), + reasoning: "before-last-message", + toolCalls: "before-last-2-messages", + }), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +## Using different AI providers + +`AIChatAgent` works with any AI SDK-compatible provider. The server code determines which model to use — the client does not need to change. + +### Workers AI (Cloudflare) + + + +```ts +import { createWorkersAI } from "workers-ai-provider"; + +const workersai = createWorkersAI({ binding: this.env.AI }); +const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), +}); +``` + + + +### OpenAI + + + +```ts +import { createOpenAI } from "@ai-sdk/openai"; + +const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY }); +const result = streamText({ + model: openai.chat("gpt-4o"), + messages: await convertToModelMessages(this.messages), +}); +``` + + + +### Anthropic + + + +```ts +import { createAnthropic } from "@ai-sdk/anthropic"; + +const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY }); +const result = streamText({ + model: anthropic("claude-sonnet-4-20250514"), + messages: await convertToModelMessages(this.messages), +}); +``` + + + +## Advanced patterns + +Since `onChatMessage` gives you full control over the `streamText` call, you can use any AI SDK feature directly. The patterns below all work out of the box — no special `AIChatAgent` configuration is needed. + +### Dynamic model and tool control + +Use [`prepareStep`](https://ai-sdk.dev/docs/agents/loop-control) to change the model, available tools, or system prompt between steps in a multi-step agent loop: + + + +```ts +import { streamText, convertToModelMessages, tool, stepCountIs } from "ai"; +import { z } from "zod"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const result = streamText({ + model: cheapModel, // Default model for simple steps + messages: await convertToModelMessages(this.messages), + tools: { + search: searchTool, + analyze: analyzeTool, + summarize: summarizeTool, + }, + stopWhen: stepCountIs(10), + prepareStep: async ({ stepNumber, messages }) => { + // Phase 1: Search (steps 0-2) + if (stepNumber <= 2) { + return { + activeTools: ["search"], + toolChoice: "required", // Force tool use + }; + } + + // Phase 2: Analyze with a stronger model (steps 3-5) + if (stepNumber <= 5) { + return { + model: expensiveModel, + activeTools: ["analyze"], + }; + } + + // Phase 3: Summarize + return { activeTools: ["summarize"] }; + }, + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +`prepareStep` runs before each step and can return overrides for `model`, `activeTools`, `toolChoice`, `system`, and `messages`. Use it to: + +- **Switch models** — use a cheap model for simple steps, escalate for reasoning +- **Phase tools** — restrict which tools are available at each step +- **Manage context** — prune or transform messages to stay within token limits +- **Force tool calls** — use `toolChoice: { type: "tool", toolName: "search" }` to require a specific tool + +### Language model middleware + +Use [`wrapLanguageModel`](https://ai-sdk.dev/docs/ai-sdk-core/middleware) to add guardrails, RAG, caching, or logging without modifying your chat logic: + + + +```ts +import { streamText, convertToModelMessages, wrapLanguageModel } from "ai"; +import type { LanguageModelV3Middleware } from "@ai-sdk/provider"; + +const guardrailMiddleware: LanguageModelV3Middleware = { + wrapGenerate: async ({ doGenerate }) => { + const { text, ...rest } = await doGenerate(); + // Filter PII or sensitive content from the response + const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]"); + return { text: cleaned, ...rest }; + }, +}; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const model = wrapLanguageModel({ + model: baseModel, + middleware: [guardrailMiddleware], + }); + + const result = streamText({ + model, + messages: await convertToModelMessages(this.messages), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +The AI SDK includes built-in middlewares: + +- `extractReasoningMiddleware` — surface chain-of-thought from models like DeepSeek R1 +- `defaultSettingsMiddleware` — apply default temperature, max tokens, etc. +- `simulateStreamingMiddleware` — add streaming to non-streaming models + +Multiple middlewares compose in order: `middleware: [first, second]` applies as `first(second(model))`. + +### Structured output + +Use [`generateObject`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) inside tools for structured data extraction: + + + +```ts +import { + streamText, + generateObject, + convertToModelMessages, + tool, + stepCountIs, +} from "ai"; +import { z } from "zod"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const result = streamText({ + model: myModel, + messages: await convertToModelMessages(this.messages), + tools: { + extractContactInfo: tool({ + description: + "Extract structured contact information from the conversation", + inputSchema: z.object({ + text: z.string().describe("The text to extract contact info from"), + }), + execute: async ({ text }) => { + const { object } = await generateObject({ + model: myModel, + schema: z.object({ + name: z.string(), + email: z.string().email(), + phone: z.string().optional(), + }), + prompt: `Extract contact information from: ${text}`, + }); + return object; + }, + }), + }, + stopWhen: stepCountIs(5), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +### Subagent delegation + +Tools can delegate work to focused sub-calls with their own context. Use [`ToolLoopAgent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent) to define a reusable agent, then call it from a tool's `execute`: + + + +```ts +import { + ToolLoopAgent, + streamText, + convertToModelMessages, + tool, + stepCountIs, +} from "ai"; +import { z } from "zod"; + +// Define a reusable research agent with its own tools and instructions +const researchAgent = new ToolLoopAgent({ + model: researchModel, + instructions: "You are a research assistant. Be thorough and cite sources.", + tools: { webSearch: webSearchTool }, + stopWhen: stepCountIs(10), +}); + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const result = streamText({ + model: orchestratorModel, + messages: await convertToModelMessages(this.messages), + tools: { + deepResearch: tool({ + description: "Research a topic in depth", + inputSchema: z.object({ + topic: z.string().describe("The topic to research"), + }), + execute: async ({ topic }) => { + const { text } = await researchAgent.generate({ + prompt: topic, + }); + return { summary: text }; + }, + }), + }, + stopWhen: stepCountIs(5), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +The research agent runs in its own context — its token budget is separate from the orchestrator's. Only the summary goes back to the parent model. + +:::note +`ToolLoopAgent` is best suited for subagents, not as a replacement for `streamText` in `onChatMessage` itself. The main `onChatMessage` benefits from direct access to `this.env`, `this.messages`, and `options.body` — things that a pre-configured `ToolLoopAgent` instance cannot reference. +::: + +#### Streaming progress with preliminary results + +By default, a tool part appears as loading until `execute` returns. Use an async generator (`async function*`) to stream progress updates to the client while the tool is still working: + + + +```ts +deepResearch: tool({ + description: "Research a topic in depth", + inputSchema: z.object({ + topic: z.string().describe("The topic to research"), + }), + async *execute({ topic }) { + // Preliminary result — the client sees "searching" immediately + yield { status: "searching", topic, summary: undefined }; + + const { text } = await researchAgent.generate({ prompt: topic }); + + // Final result — sent to the model for its next step + yield { status: "done", topic, summary: text }; + }, +}); +``` + + + +Each `yield` updates the tool part on the client in real-time (with `preliminary: true`). The last yielded value becomes the final output that the model sees. + +This pattern is useful when: + +- A task requires exploring large amounts of information that would bloat the main context +- You want to show real-time progress for long-running tools +- You want to parallelize independent research (multiple tool calls run concurrently) +- You need different models or system prompts for different subtasks + +For more, refer to the [AI SDK Agents docs](https://ai-sdk.dev/docs/agents/overview), [Subagents](https://ai-sdk.dev/docs/agents/subagents), and [Preliminary Tool Results](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#preliminary-tool-results). + +## Multi-client sync + +When multiple clients connect to the same agent instance, messages are automatically broadcast to all connections. If one client sends a message, all other connected clients receive the updated message list. + +``` +Client A ──── sendMessage("Hello") ────▶ AIChatAgent + │ + persist + stream + │ +Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤ +Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘ +``` + +The originating client receives the streaming response. All other clients receive the final messages via a `CF_AGENT_CHAT_MESSAGES` broadcast. + +## API reference + +### Exports + +| Import path | Exports | +| --------------------------- | --------------------------------------------------- | +| `@cloudflare/ai-chat` | `AIChatAgent`, `createToolsFromClientSchemas` | +| `@cloudflare/ai-chat/react` | `useAgentChat` | +| `@cloudflare/ai-chat/types` | `MessageType`, `OutgoingMessage`, `IncomingMessage` | + +### WebSocket protocol + +The chat protocol uses typed JSON messages over WebSocket: + +| Message | Direction | Purpose | +| -------------------------------- | --------------- | --------------------------- | +| `CF_AGENT_USE_CHAT_REQUEST` | Client → Server | Send a chat message | +| `CF_AGENT_USE_CHAT_RESPONSE` | Server → Client | Stream response chunks | +| `CF_AGENT_CHAT_MESSAGES` | Server → Client | Broadcast updated messages | +| `CF_AGENT_CHAT_CLEAR` | Bidirectional | Clear conversation | +| `CF_AGENT_CHAT_REQUEST_CANCEL` | Client → Server | Cancel active stream | +| `CF_AGENT_TOOL_RESULT` | Client → Server | Provide tool output | +| `CF_AGENT_TOOL_APPROVAL` | Client → Server | Approve or reject a tool | +| `CF_AGENT_MESSAGE_UPDATED` | Server → Client | Notify of message update | +| `CF_AGENT_STREAM_RESUMING` | Server → Client | Notify of stream resumption | +| `CF_AGENT_STREAM_RESUME_REQUEST` | Client → Server | Request stream resume check | + +## Deprecated APIs + +The following APIs are deprecated and will emit a console warning when used. They will be removed in a future release. + +| Deprecated | Replacement | Notes | +| --- | --- | --- | +| `addToolResult({ toolCallId, result })` | `addToolOutput({ toolCallId, output })` | Renamed for consistency with AI SDK terminology | +| `createToolsFromClientSchemas()` | Client tools are now registered automatically | No manual schema conversion needed | +| `extractClientToolSchemas()` | Client tools are now registered automatically | Schemas are sent with tool results | +| `detectToolsRequiringConfirmation()` | Use `needsApproval` on the tool definition | Approval is now per-tool, not a global filter | +| `tools` option on `useAgentChat` | Define tools in `onChatMessage` on the server | All tool definitions belong on the server | +| `toolsRequiringConfirmation` option | Use `needsApproval` on individual tools | Per-tool approval replaces global list | + +If you are upgrading from an earlier version, replace deprecated calls with their replacements. The deprecated APIs still work but will be removed in a future major version. + +## Next steps + + + + + + diff --git a/src/content/docs/agents/api-reference/configuration.mdx b/src/content/docs/agents/api-reference/configuration.mdx index b6aba333a6d..d7433ef3558 100644 --- a/src/content/docs/agents/api-reference/configuration.mdx +++ b/src/content/docs/agents/api-reference/configuration.mdx @@ -45,7 +45,7 @@ The `wrangler.jsonc` file configures your Cloudflare Worker and its bindings. He "$schema": "node_modules/wrangler/config-schema.json", "name": "my-agent-app", "main": "src/server.ts", - "compatibility_date": "2025-01-01", + "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], // Static assets (optional) @@ -217,7 +217,7 @@ export default { // Add your own routing logic here return new Response("Not found", { status: 404 }); }, -}; +} satisfies ExportedHandler; ``` @@ -628,7 +628,7 @@ Define environments in the Wrangler configuration file: "main": "src/server.ts", // Base configuration (shared) - "compatibility_date": "2025-01-01", + "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }], diff --git a/src/content/docs/agents/api-reference/email.mdx b/src/content/docs/agents/api-reference/email.mdx index d5ac3a34297..05be04b214d 100644 --- a/src/content/docs/agents/api-reference/email.mdx +++ b/src/content/docs/agents/api-reference/email.mdx @@ -44,7 +44,7 @@ export default { resolver: createAddressBasedEmailResolver("EmailAgent"), }); }, -}; +} satisfies ExportedHandler; ``` @@ -161,7 +161,7 @@ export default { }, }); }, -}; +} satisfies ExportedHandler; ``` @@ -350,7 +350,7 @@ When your agent sends emails and expects replies, use secure reply routing to pr }, }); }, - }; + } satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/api-reference/mcp-client-api.mdx b/src/content/docs/agents/api-reference/mcp-client-api.mdx index 881020d740c..7ab814ad888 100644 --- a/src/content/docs/agents/api-reference/mcp-client-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-client-api.mdx @@ -240,7 +240,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -465,6 +465,11 @@ async addMcpServer( headers?: HeadersInit; type?: "sse" | "streamable-http" | "auto"; }; + retry?: { + maxAttempts?: number; + baseDelayMs?: number; + maxDelayMs?: number; + }; } ): Promise< | { id: string; state: "authenticating"; authUrl: string } @@ -484,6 +489,7 @@ async addMcpServer( - `transport` — Transport layer configuration: - `headers` — Custom HTTP headers for authentication - `type` — Transport type: `"streamable-http"` (default), `"sse"`, or `"auto"` + - `retry` — Retry options for connection and reconnection attempts. Persisted and used when restoring connections after hibernation or after OAuth completion. Default: 3 attempts, 500ms base delay, 5s max delay. Refer to [Retries](/agents/api-reference/retries/) for details on `RetryOptions` #### Returns @@ -615,6 +621,50 @@ export class MyAgent extends Agent { +## Custom OAuth provider + +Override the default OAuth provider used when connecting to MCP servers by implementing `createMcpOAuthProvider()` on your Agent class. This enables custom authentication strategies such as pre-registered client credentials or mTLS, beyond the built-in dynamic client registration. + + + +```ts +import { Agent } from "agents"; +import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; + +export class MyAgent extends Agent { + createMcpOAuthProvider( + serverUrlHash: string, + callbackUrl: string, + ): OAuthClientProvider { + return { + get redirectUrl() { + return new URL(callbackUrl); + }, + get clientMetadata() { + return { + // Use pre-registered client credentials + client_id: this.env.MCP_CLIENT_ID, + client_secret: this.env.MCP_CLIENT_SECRET, + redirect_uris: [callbackUrl], + }; + }, + clientInformation() { + // Return pre-registered client info + return { + client_id: this.env.MCP_CLIENT_ID, + client_secret: this.env.MCP_CLIENT_SECRET, + }; + }, + // ... implement other OAuthClientProvider methods + }; + } +} +``` + + + +If you do not override this method, the agent uses the default provider which performs [OAuth 2.0 Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591) with the MCP server. + ## Advanced: MCPClientManager For fine-grained control, use `this.mcp` directly: diff --git a/src/content/docs/agents/api-reference/mcp-handler-api.mdx b/src/content/docs/agents/api-reference/mcp-handler-api.mdx index 33be5d656e5..8b170384c32 100644 --- a/src/content/docs/agents/api-reference/mcp-handler-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-handler-api.mdx @@ -165,7 +165,7 @@ export default { const server = createServer(); return createMcpHandler(server)(request, env, ctx); }, -}; +} satisfies ExportedHandler; ``` @@ -238,7 +238,7 @@ export default { // Route the MCP request to the agent return await agent.onMcpRequest(request); }, -}; +} satisfies ExportedHandler; ``` @@ -292,7 +292,7 @@ export default { // This will fail on second request with MCP SDK 1.26.0+ return createMcpHandler(server)(request, env, ctx); }, -}; +} satisfies ExportedHandler; ``` @@ -327,7 +327,7 @@ export default { const server = createServer(); return createMcpHandler(server)(request, env, ctx); }, -}; +} satisfies ExportedHandler; ``` @@ -361,7 +361,7 @@ export default { server.connect(transport); return transport.handleRequest(request); }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/api-reference/queue-tasks.mdx b/src/content/docs/agents/api-reference/queue-tasks.mdx index 03fe9d52fc6..460b094c8d6 100644 --- a/src/content/docs/agents/api-reference/queue-tasks.mdx +++ b/src/content/docs/agents/api-reference/queue-tasks.mdx @@ -68,10 +68,10 @@ class MyAgent extends Agent { ### `dequeue()` -Removes a specific task from the queue by ID. +Removes a specific task from the queue by ID. This method is synchronous. ```ts -async dequeue(id: string): Promise +dequeue(id: string): void ``` **Parameters:** @@ -91,10 +91,10 @@ await agent.dequeue("abc123def"); ### `dequeueAll()` -Removes all tasks from the queue. +Removes all tasks from the queue. This method is synchronous. ```ts -async dequeueAll(): Promise +dequeueAll(): void ``` **Example:** @@ -110,10 +110,10 @@ await agent.dequeueAll(); ### `dequeueAllByCallback()` -Removes all tasks that match a specific callback method. +Removes all tasks that match a specific callback method. This method is synchronous. ```ts -async dequeueAllByCallback(callback: string): Promise +dequeueAllByCallback(callback: string): void ``` **Parameters:** @@ -133,10 +133,10 @@ await agent.dequeueAllByCallback("processEmail"); ### `getQueue()` -Retrieves a specific queued task by ID. +Retrieves a specific queued task by ID. This method is synchronous. ```ts -async getQueue(id: string): Promise | undefined> +getQueue(id: string): QueueItem | undefined ``` **Parameters:** @@ -163,10 +163,10 @@ if (task) { ### `getQueues()` -Retrieves all queued tasks that match a specific key-value pair in their payload. +Retrieves all queued tasks that match a specific key-value pair in their payload. This method is synchronous. ```ts -async getQueues(key: string, value: string): Promise[]> +getQueues(key: string, value: string): QueueItem[] ``` **Parameters:** @@ -340,10 +340,13 @@ The queue system works with other Agent SDK features: ## Limitations - Tasks are processed sequentially, not in parallel. -- No built-in retry mechanism (implement your own). - No priority system (FIFO only). - Queue processing happens during agent execution, not as separate background jobs. +:::note +Queue tasks support built-in retries with exponential backoff. Pass `{ retry: { maxAttempts, baseDelayMs, maxDelayMs } }` as the third argument to `queue()`. Refer to [Retries](/agents/api-reference/retries/) for details. +::: + ## Queue vs Schedule Use **queue** when you want tasks to execute as soon as possible in order. Use [**schedule**](/agents/api-reference/schedule-tasks/) when you need tasks to run at specific times or on a recurring basis. diff --git a/src/content/docs/agents/api-reference/retries.mdx b/src/content/docs/agents/api-reference/retries.mdx new file mode 100644 index 00000000000..773cc74aa8b --- /dev/null +++ b/src/content/docs/agents/api-reference/retries.mdx @@ -0,0 +1,496 @@ +--- +title: Retries +pcx_content_type: reference +sidebar: + order: 11 +--- + +import { TypeScriptExample, LinkCard } from "~/components"; + +Retry failed operations with exponential backoff and jitter. The Agents SDK provides built-in retry support for scheduled tasks, queued tasks, and a general-purpose `this.retry()` method for your own code. + +## Overview + +Transient failures are common when calling external APIs, interacting with other services, or running background tasks. The retry system handles these automatically: + +- **Exponential backoff** — each retry waits longer than the last +- **Jitter** — randomized delays prevent thundering herd problems +- **Configurable** — tune attempts, delays, and caps per call site +- **Built-in** — schedule, queue, and workflow operations retry automatically + +## Quick start + +Use `this.retry()` to retry any async operation: + + + +```ts +import { Agent } from "agents"; + +export class MyAgent extends Agent { + async fetchWithRetry(url: string) { + const response = await this.retry(async () => { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }); + + return response; + } +} +``` + + + +By default, `this.retry()` makes up to 3 attempts with jittered exponential backoff. + +## `this.retry()` + +The `retry()` method is available on every `Agent` instance. It retries the provided function on any thrown error by default. + +```ts +async retry( + fn: (attempt: number) => Promise, + options?: RetryOptions & { + shouldRetry?: (err: unknown, nextAttempt: number) => boolean; + } +): Promise +``` + +**Parameters:** + +- `fn` — the async function to retry. Receives the current attempt number (1-indexed). +- `options` — optional retry configuration (refer to [RetryOptions](#retryoptions) below). Options are validated eagerly — invalid values throw immediately. +- `options.shouldRetry` — optional predicate called with the thrown error and the next attempt number. Return `false` to stop retrying immediately. If not provided, all errors are retried. + +**Returns:** the result of `fn` on success. + +**Throws:** the last error if all attempts fail or `shouldRetry` returns `false`. + +### Examples + +**Basic retry:** + + + +```ts +const data = await this.retry(() => fetch("https://api.example.com/data")); +``` + + + +**Custom retry options:** + + + +```ts +const data = await this.retry( + async () => { + const res = await fetch("https://slow-api.example.com/data"); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }, + { + maxAttempts: 5, + baseDelayMs: 500, + maxDelayMs: 10000, + }, +); +``` + + + +**Using the attempt number:** + + + +```ts +const result = await this.retry(async (attempt) => { + console.log(`Attempt ${attempt}...`); + return await this.callExternalService(); +}); +``` + + + +**Selective retry with `shouldRetry`:** + +Use `shouldRetry` to stop retrying on specific errors. The predicate receives both the error and the next attempt number: + + + +```ts +const data = await this.retry( + async () => { + const res = await fetch("https://api.example.com/data"); + if (!res.ok) throw new HttpError(res.status, await res.text()); + return res.json(); + }, + { + maxAttempts: 5, + shouldRetry: (err, nextAttempt) => { + // Do not retry 4xx client errors — our request is wrong + if ( + err instanceof HttpError && + err.status >= 400 && + err.status < 500 + ) { + return false; + } + return true; // retry everything else (5xx, network errors, etc.) + }, + }, +); +``` + + + +## Retries in schedules + +Pass retry options when creating a schedule: + + + +```ts +// Retry up to 5 times if the callback fails +await this.schedule("processTask", 60, { taskId: "123" }, { + retry: { maxAttempts: 5 }, +}); + +// Retry with custom backoff +await this.schedule( + new Date("2026-03-01T09:00:00Z"), + "sendReport", + {}, + { + retry: { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 30000, + }, + }, +); + +// Cron with retries +await this.schedule("0 8 * * *", "dailyDigest", {}, { + retry: { maxAttempts: 3 }, +}); + +// Interval with retries +await this.scheduleEvery(30, "poll", { source: "api" }, { + retry: { maxAttempts: 5, baseDelayMs: 200 }, +}); +``` + + + +If the callback throws, it is retried according to the retry options. If all attempts fail, the error is logged and routed through `onError()`. The schedule is still removed (for one-time schedules) or rescheduled (for cron/interval) regardless of success or failure. + +## Retries in queues + +Pass retry options when adding a task to the queue: + + + +```ts +await this.queue("sendEmail", { to: "user@example.com" }, { + retry: { maxAttempts: 5 }, +}); + +await this.queue("processWebhook", webhookData, { + retry: { + maxAttempts: 3, + baseDelayMs: 500, + maxDelayMs: 5000, + }, +}); +``` + + + +If the callback throws, it is retried before the task is dequeued. After all attempts are exhausted, the task is dequeued and the error is logged. + +## Validation + +Retry options are validated eagerly when you call `this.retry()`, `queue()`, `schedule()`, or `scheduleEvery()`. Invalid options throw immediately instead of failing later at execution time: + + + +```ts +// Throws immediately: "retry.maxAttempts must be >= 1" +await this.queue("sendEmail", data, { + retry: { maxAttempts: 0 }, +}); + +// Throws immediately: "retry.baseDelayMs must be > 0" +await this.schedule(60, "process", {}, { + retry: { baseDelayMs: -100 }, +}); + +// Throws immediately: "retry.maxAttempts must be an integer" +await this.retry(() => fetch(url), { maxAttempts: 2.5 }); + +// Throws immediately: "retry.baseDelayMs must be <= retry.maxDelayMs" +// because baseDelayMs: 5000 exceeds the default maxDelayMs: 3000 +await this.queue("sendEmail", data, { + retry: { baseDelayMs: 5000 }, +}); +``` + + + +Validation resolves partial options against class-level or built-in defaults before checking cross-field constraints. This means `{ baseDelayMs: 5000 }` is caught immediately when the resolved `maxDelayMs` is 3000, rather than failing later at execution time. + +## Default behavior + +Even without explicit retry options, scheduled and queued callbacks are retried with sensible defaults: + +| Setting | Default | +| ------------- | ------- | +| `maxAttempts` | 3 | +| `baseDelayMs` | 100 | +| `maxDelayMs` | 3000 | + +These defaults apply to `this.retry()`, `queue()`, `schedule()`, and `scheduleEvery()`. Per-call-site options override them. + +### Class-level defaults + +Override the defaults for your entire agent via `static options`: + + + +```ts +class MyAgent extends Agent { + static options = { + retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 5000 }, + }; +} +``` + + + +You only need to specify the fields you want to change — unset fields fall back to the built-in defaults: + + + +```ts +class MyAgent extends Agent { + // Only override maxAttempts; baseDelayMs (100) and maxDelayMs (3000) stay default + static options = { + retry: { maxAttempts: 10 }, + }; +} +``` + + + +Class-level defaults are used as fallbacks when a call site does not specify retry options. Per-call-site options always take priority: + + + +```ts +// Uses class-level defaults (10 attempts) +await this.retry(() => fetch(url)); + +// Overrides to 2 attempts for this specific call +await this.retry(() => fetch(url), { maxAttempts: 2 }); +``` + + + +To disable retries for a specific task, set `maxAttempts: 1`: + + + +```ts +await this.schedule(60, "oneShot", {}, { + retry: { maxAttempts: 1 }, +}); +``` + + + +## RetryOptions + +```ts +interface RetryOptions { + /** Maximum number of attempts (including the first). Must be an integer >= 1. Default: 3 */ + maxAttempts?: number; + /** Base delay in milliseconds for exponential backoff. Must be > 0 and <= maxDelayMs. Default: 100 */ + baseDelayMs?: number; + /** Maximum delay cap in milliseconds. Must be > 0. Default: 3000 */ + maxDelayMs?: number; +} +``` + +The delay between retries uses **full jitter exponential backoff**: + +``` +delay = random(0, min(2^attempt * baseDelayMs, maxDelayMs)) +``` + +This means early retries are fast (often under 200ms), and later retries back off to avoid overwhelming a failing service. The randomization (jitter) prevents multiple agents from retrying at the exact same moment. + +## How it works + +### Backoff strategy + +The retry system uses the "Full Jitter" strategy from the [AWS Architecture Blog](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). Given 3 attempts with default settings: + +| Attempt | Upper Bound | Actual Delay | +| ------- | ----------------------------- | ---------------- | +| 1 | min(2^1 \* 100, 3000) = 200ms | random(0, 200ms) | +| 2 | min(2^2 \* 100, 3000) = 400ms | random(0, 400ms) | +| 3 | (no retry — final attempt) | — | + +With `maxAttempts: 5` and `baseDelayMs: 500`: + +| Attempt | Upper Bound | Actual Delay | +| ------- | ----------------------------- | ----------------- | +| 1 | min(2 \* 500, 3000) = 1000ms | random(0, 1000ms) | +| 2 | min(4 \* 500, 3000) = 2000ms | random(0, 2000ms) | +| 3 | min(8 \* 500, 3000) = 3000ms | random(0, 3000ms) | +| 4 | min(16 \* 500, 3000) = 3000ms | random(0, 3000ms) | +| 5 | (no retry — final attempt) | — | + +### MCP server retries + +When adding an MCP server, you can configure retry options for connection and reconnection attempts: + + + +```ts +await this.addMcpServer("github", "https://mcp.github.com", { + retry: { maxAttempts: 5, baseDelayMs: 1000, maxDelayMs: 10000 }, +}); +``` + + + +These options are persisted and used when: + +- Restoring server connections after hibernation +- Establishing connections after OAuth completion + +Default: 3 attempts, 500ms base delay, 5s max delay. + +## Patterns + +### Retry with logging + + + +```ts +class MyAgent extends Agent { + async resilientTask(payload: { url: string }) { + try { + const result = await this.retry( + async (attempt) => { + if (attempt > 1) { + console.log(`Retrying ${payload.url} (attempt ${attempt})...`); + } + const res = await fetch(payload.url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }, + { maxAttempts: 5 }, + ); + console.log("Success:", result); + } catch (e) { + console.error("All retries failed:", e); + } + } +} +``` + + + +### Retry with fallback + + + +```ts +class MyAgent extends Agent { + async fetchData() { + try { + return await this.retry( + () => fetch("https://primary-api.example.com/data"), + { maxAttempts: 3, baseDelayMs: 200 }, + ); + } catch { + // Primary failed, try fallback + return await this.retry( + () => fetch("https://fallback-api.example.com/data"), + { maxAttempts: 2 }, + ); + } + } +} +``` + + + +### Combining retries with scheduling + +For operations that might take a long time to recover (minutes or hours), combine `this.retry()` for immediate retries with `this.schedule()` for delayed retries: + + + +```ts +class MyAgent extends Agent { + async syncData(payload: { source: string; attempt?: number }) { + const attempt = payload.attempt ?? 1; + + try { + // Immediate retries for transient failures (seconds) + await this.retry(() => this.fetchAndProcess(payload.source), { + maxAttempts: 3, + baseDelayMs: 1000, + }); + } catch (e) { + if (attempt >= 5) { + console.error("Giving up after 5 scheduled attempts"); + return; + } + + // Schedule a retry in 5 minutes for longer outages + const delaySeconds = 300 * attempt; + await this.schedule(delaySeconds, "syncData", { + source: payload.source, + attempt: attempt + 1, + }); + console.log(`Scheduled retry ${attempt + 1} in ${delaySeconds}s`); + } + } +} +``` + + + +## Limitations + +- **No dead-letter queue.** If a queued or scheduled task fails all retry attempts, it is removed. Implement your own persistence if you need to track failed tasks. +- **Retry delays block the agent.** During the backoff delay, the Durable Object is awake but idle. For short delays (under 3 seconds) this is fine. For longer recovery times, use `this.schedule()` instead. +- **Queue retries are head-of-line blocking.** Queue items are processed sequentially. If one item is being retried with long delays, it blocks all subsequent items. If you need independent retry behavior, use `this.retry()` inside the callback rather than per-task retry options on `queue()`. +- **No circuit breaker.** The retry system does not track failure rates across calls. If a service is persistently down, each task will exhaust its retry budget independently. +- **`shouldRetry` is only available on `this.retry()`.** The `shouldRetry` predicate cannot be used with `schedule()` or `queue()` because functions cannot be serialized to the database. For scheduled/queued tasks, handle non-retryable errors inside the callback itself. + +## Next steps + + + + + + diff --git a/src/content/docs/agents/api-reference/routing.mdx b/src/content/docs/agents/api-reference/routing.mdx index 5fa87116e5b..6f348667be7 100644 --- a/src/content/docs/agents/api-reference/routing.mdx +++ b/src/content/docs/agents/api-reference/routing.mdx @@ -65,7 +65,7 @@ export default { // No agent matched - handle other routes return new Response("Not found", { status: 404 }); }, -}; +} satisfies ExportedHandler; ``` @@ -209,7 +209,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -461,7 +461,7 @@ export default { props: { userId: session.userId, role: session.role }, }); }, -}; +} satisfies ExportedHandler; ``` @@ -521,7 +521,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -585,7 +585,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -673,7 +673,7 @@ export default { })) ?? new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -708,7 +708,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/api-reference/run-workflows.mdx b/src/content/docs/agents/api-reference/run-workflows.mdx index 63cde658824..11fb316eccd 100644 --- a/src/content/docs/agents/api-reference/run-workflows.mdx +++ b/src/content/docs/agents/api-reference/run-workflows.mdx @@ -115,7 +115,7 @@ export class MyAgent extends Agent { { "name": "my-app", "main": "src/index.ts", - "compatibility_date": "2025-02-11", + "compatibility_date": "$today", "durable_objects": { "bindings": [{ "name": "MY_AGENT", "class_name": "MyAgent" }], }, diff --git a/src/content/docs/agents/api-reference/schedule-tasks.mdx b/src/content/docs/agents/api-reference/schedule-tasks.mdx index 7f10a79ac40..4da390477e1 100644 --- a/src/content/docs/agents/api-reference/schedule-tasks.mdx +++ b/src/content/docs/agents/api-reference/schedule-tasks.mdx @@ -605,34 +605,37 @@ class SmartScheduler extends Agent { ### `scheduleSchema` -A Zod schema for validating parsed scheduling data: +A Zod schema for validating parsed scheduling data. Uses a discriminated union on `when.type` so each variant only contains the fields it needs: ```ts import { scheduleSchema } from "agents"; -// The schema shape: +// The schema is a discriminated union: // { // description: string, -// when: { -// type: "scheduled" | "delayed" | "cron" | "no-schedule", -// date?: Date, // for "scheduled" -// delayInSeconds?: number, // for "delayed" -// cron?: string // for "cron" -// } +// when: +// | { type: "scheduled", date: string } // ISO 8601 date string +// | { type: "delayed", delayInSeconds: number } +// | { type: "cron", cron: string } +// | { type: "no-schedule" } // } ``` +:::note +Dates are returned as ISO 8601 strings (not `Date` objects) for compatibility with both Zod v3 and v4 JSON schema generation. +::: + ## Scheduling vs Queue vs Workflows | Feature | Queue | Scheduling | Workflows | | ------------------ | ------------------ | ----------------- | ------------------- | | **When** | Immediately (FIFO) | Future time | Future time | | **Execution** | Sequential | At scheduled time | Multi-step | -| **Retries** | Manual | Manual | Automatic | +| **Retries** | Built-in | Built-in | Automatic | | **Persistence** | SQLite | SQLite | Workflow engine | | **Recurring** | No | Yes (cron) | No (use scheduling) | | **Complex logic** | No | No | Yes | @@ -715,10 +718,10 @@ Schedule a task to run repeatedly at a fixed interval. ### `getSchedule()` ```ts -async getSchedule(id: string): Promise | undefined> +getSchedule(id: string): Schedule | undefined ``` -Get a scheduled task by ID. Returns `undefined` if not found. +Get a scheduled task by ID. Returns `undefined` if not found. This method is synchronous. ### `getSchedules()` @@ -730,7 +733,7 @@ getSchedules(criteria?: { }): Schedule[] ``` -Get scheduled tasks matching the criteria. +Get scheduled tasks matching the criteria. This method is synchronous. ### `cancelSchedule()` diff --git a/src/content/docs/agents/api-reference/websockets.mdx b/src/content/docs/agents/api-reference/websockets.mdx index 172cdd46e20..6036042d5f9 100644 --- a/src/content/docs/agents/api-reference/websockets.mdx +++ b/src/content/docs/agents/api-reference/websockets.mdx @@ -9,6 +9,46 @@ import { TypeScriptExample, LinkCard } from "~/components"; Agents support WebSocket connections for real-time, bi-directional communication. This page covers server-side WebSocket handling. For client-side connection, refer to the [Client SDK](/agents/api-reference/client-sdk/). +## Lifecycle hooks + +Agents have several lifecycle hooks that fire at different points: + +| Hook | When called | +| --------------------------------------------- | --------------------------------------------------------- | +| `onStart(props?)` | Once when the agent first starts (before any connections) | +| `onRequest(request)` | When an HTTP request is received (non-WebSocket) | +| `onConnect(connection, ctx)` | When a new WebSocket connection is established | +| `onMessage(connection, message)` | When a WebSocket message is received | +| `onClose(connection, code, reason, wasClean)` | When a WebSocket connection closes | +| `onError(connection, error)` | When a WebSocket error occurs | + +### `onStart` + +`onStart()` is called once when the agent first starts, before any connections are established: + + + +```ts +export class MyAgent extends Agent { + async onStart() { + // Initialize resources + console.log(`Agent ${this.name} starting...`); + + // Load data from storage + const savedData = this.sql`SELECT * FROM cache`; + for (const row of savedData) { + // Rebuild in-memory state from persistent storage + } + } + + onConnect(connection: Connection) { + // By the time connections arrive, onStart has completed + } +} +``` + + + ## Handling connections Define `onConnect` and `onMessage` methods on your Agent to accept WebSocket connections: @@ -122,6 +162,64 @@ export class ChatAgent extends Agent { +### Excluding connections + +Pass an array of connection IDs to exclude from the broadcast: + + + +```ts +// Broadcast to everyone except the sender +this.broadcast( + JSON.stringify({ type: "user-typing", userId: "123" }), + [connection.id], // Do not send to the originator +); +``` + + + +## Connection tags + +Tag connections for easy filtering. Override `getConnectionTags()` to assign tags when a connection is established: + + + +```ts +export class ChatAgent extends Agent { + getConnectionTags( + connection: Connection, + ctx: ConnectionContext, + ): string[] { + const url = new URL(ctx.request.url); + const role = url.searchParams.get("role"); + + const tags: string[] = []; + if (role === "admin") tags.push("admin"); + if (role === "moderator") tags.push("moderator"); + + return tags; // Up to 9 tags, max 256 chars each + } + + // Later, broadcast only to admins + notifyAdmins(message: string) { + for (const conn of this.getConnections("admin")) { + conn.send(message); + } + } +} +``` + + + +### Connection management methods + +| Method | Signature | Description | +| ------------------- | ----------------------------------------- | -------------------------------------- | +| `getConnections` | `(tag?: string) => Iterable` | Get all connections, optionally by tag | +| `getConnection` | `(id: string) => Connection \| undefined` | Get connection by ID | +| `getConnectionTags` | `(connection, ctx) => string[]` | Override to tag connections | +| `broadcast` | `(message, without?: string[]) => void` | Send to all connections | + ## Handling binary data Messages can be strings or binary (`ArrayBuffer` / `ArrayBufferView`): @@ -191,6 +289,205 @@ export class ChatAgent extends Agent { | `ArrayBuffer` | Binary data | | `ArrayBufferView` | Typed array view of binary data | +## Hibernation + +Agents support hibernation — they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections. + +### Enabling hibernation + +Hibernation is enabled by default. To disable: + + + +```ts +export class AlwaysOnAgent extends Agent { + static options = { hibernate: false }; +} +``` + + + +### How hibernation works + +1. Agent is active, handling connections +2. After a period of inactivity with no messages, the agent hibernates (sleeps) +3. WebSocket connections remain open (handled by Cloudflare) +4. When a message arrives, the agent wakes up +5. `onMessage` is called as normal + +### What persists across hibernation + +| Persists | Does not persist | +| -------------------------- | ------------------- | +| `this.state` (agent state) | In-memory variables | +| `connection.state` | Timers/intervals | +| SQLite data (`this.sql`) | Promises in flight | +| Connection metadata | Local caches | + +Store important data in `this.state` or SQLite, not in class properties: + + + +```ts +export class MyAgent extends Agent { + initialState = { counter: 0 }; + + // Do not do this - lost on hibernation + private localCounter = 0; + + onMessage(connection: Connection, message: WSMessage) { + // Persists across hibernation + this.setState({ counter: this.state.counter + 1 }); + + // Lost after hibernation + this.localCounter++; + } +} +``` + + + +## Common patterns + +### Presence tracking + +Track who is online using per-connection state. Connection state is automatically cleaned up when users disconnect: + + + +```ts +type UserState = { + name: string; + joinedAt: number; + lastSeen: number; +}; + +export class PresenceAgent extends Agent { + onConnect(connection: Connection, ctx: ConnectionContext) { + const url = new URL(ctx.request.url); + const name = url.searchParams.get("name") || "Anonymous"; + + connection.setState({ + name, + joinedAt: Date.now(), + lastSeen: Date.now(), + }); + + // Send current presence to new user + connection.send( + JSON.stringify({ + type: "presence", + users: this.getPresence(), + }), + ); + + // Notify others that someone joined + this.broadcastPresence(); + } + + onClose(connection: Connection) { + // No manual cleanup needed - connection state is automatically gone + this.broadcastPresence(); + } + + onMessage(connection: Connection, message: WSMessage) { + if (message === "ping") { + connection.setState((prev) => ({ + ...prev!, + lastSeen: Date.now(), + })); + connection.send("pong"); + } + } + + private getPresence() { + const users: Record = {}; + for (const conn of this.getConnections()) { + if (conn.state) { + users[conn.id] = { + name: conn.state.name, + lastSeen: conn.state.lastSeen, + }; + } + } + return users; + } + + private broadcastPresence() { + this.broadcast( + JSON.stringify({ + type: "presence", + users: this.getPresence(), + }), + ); + } +} +``` + + + +### Chat room with broadcast + + + +```ts +type Message = { + type: "message" | "join" | "leave"; + user: string; + text?: string; + timestamp: number; +}; + +export class ChatRoom extends Agent { + onConnect(connection: Connection, ctx: ConnectionContext) { + const url = new URL(ctx.request.url); + const username = url.searchParams.get("username") || "Anonymous"; + + connection.setState({ username }); + + // Notify others + this.broadcast( + JSON.stringify({ + type: "join", + user: username, + timestamp: Date.now(), + } satisfies Message), + [connection.id], // Do not send to the joining user + ); + } + + onMessage(connection: Connection, message: WSMessage) { + if (typeof message !== "string") return; + + const { username } = connection.state as { username: string }; + + this.broadcast( + JSON.stringify({ + type: "message", + user: username, + text: message, + timestamp: Date.now(), + } satisfies Message), + ); + } + + onClose(connection: Connection) { + const { username } = (connection.state as { username: string }) || {}; + if (username) { + this.broadcast( + JSON.stringify({ + type: "leave", + user: username, + timestamp: Date.now(), + } satisfies Message), + ); + } + } +} +``` + + + ## Connecting from clients For browser connections, use the Agents client SDK: diff --git a/src/content/docs/agents/concepts/agent-class.mdx b/src/content/docs/agents/concepts/agent-class.mdx index f9d2e4b9431..a97fe846a24 100644 --- a/src/content/docs/agents/concepts/agent-class.mdx +++ b/src/content/docs/agents/concepts/agent-class.mdx @@ -384,7 +384,7 @@ export default { resolver: createAddressBasedEmailResolver("my-agent"), }); }, -}; +} satisfies ExportedHandler; ``` ### Context Management diff --git a/src/content/docs/agents/getting-started/add-to-existing-project.mdx b/src/content/docs/agents/getting-started/add-to-existing-project.mdx index 6322b0ec7e6..aaab2b48d5d 100644 --- a/src/content/docs/agents/getting-started/add-to-existing-project.mdx +++ b/src/content/docs/agents/getting-started/add-to-existing-project.mdx @@ -36,7 +36,7 @@ Create a new file for your agent (for example, `src/agents/counter.ts`): ```ts -import { Agent } from "agents"; +import { Agent, callable } from "agents"; export type CounterState = { count: number; @@ -45,11 +45,13 @@ export type CounterState = { export class CounterAgent extends Agent { initialState: CounterState = { count: 0 }; + @callable() increment() { this.setState({ count: this.state.count + 1 }); return this.state.count; } + @callable() decrement() { this.setState({ count: this.state.count - 1 }); return this.state.count; @@ -69,7 +71,7 @@ Add the Durable Object binding and migration: { "name": "my-existing-project", "main": "src/index.ts", - "compatibility_date": "2025-01-01", + "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], "durable_objects": { @@ -112,7 +114,7 @@ export { CounterAgent } from "./agents/counter"; // Your existing exports... export default { // ... -}; +} satisfies ExportedHandler; ``` @@ -143,7 +145,7 @@ export default { return new Response("Not found", { status: 404 }); }, -}; +} satisfies ExportedHandler; ``` @@ -191,7 +193,7 @@ export default { return new Response("Not found", { status: 404 }); }, -}; +} satisfies ExportedHandler; ``` @@ -367,7 +369,7 @@ export default { // ... rest of routing }, -}; +} satisfies ExportedHandler; ``` @@ -409,7 +411,7 @@ export default { } // ... }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/getting-started/build-a-chat-agent.mdx b/src/content/docs/agents/getting-started/build-a-chat-agent.mdx index 23cd3f8b879..8c622a55363 100644 --- a/src/content/docs/agents/getting-started/build-a-chat-agent.mdx +++ b/src/content/docs/agents/getting-started/build-a-chat-agent.mdx @@ -1,255 +1,359 @@ --- -title: Build a Chat Agent +title: Build a chat agent pcx_content_type: tutorial sidebar: order: 4 head: [] -description: Build a streaming AI chat agent with tool integration using Cloudflare Workers and the Agents framework. +description: Build a streaming AI chat agent with tools using Workers AI — no API keys required. --- -import { TypeScriptExample, WranglerConfig } from "~/components"; +import { TypeScriptExample, WranglerConfig, LinkCard } from "~/components"; -A complete guide to building a streaming AI chat agent with tool integration using Cloudflare Workers and the Agents framework. +Build a chat agent that streams AI responses, calls server-side tools, executes client-side tools in the browser, and asks for user approval before sensitive actions. -> **Complete example**: Refer to the full implementation in the [agents-starter repository](https://github.com/cloudflare/agents-starter) on GitHub. +**What you will build:** A chat agent powered by Workers AI with three tool types — automatic, client-side, and approval-gated. -## Prerequisites +**Time:** ~15 minutes -- Node.js 18+ -- npm or yarn -- Cloudflare account -- OpenAI API key - -## Step 1: Set up the project +**Prerequisites:** -```bash -npm create cloudflare@latest chat-agent -- --template=cloudflare/agents-starter +- Node.js 18+ +- A Cloudflare account (free tier works) -cd chat-agent +## 1. Create the project -npm install +```sh +npm create cloudflare@latest chat-agent ``` -## Step 2: Configure your environment - -You have two options for setting up your OpenAI API key: - -**Option A: Using `.dev.vars` (recommended for development)** +Select **"Hello World" Worker** when prompted. Then install the dependencies: -Create a `.dev.vars` file in your project root: - -```txt -OPENAI_API_KEY=your-key-here +```sh +cd chat-agent +npm install agents @cloudflare/ai-chat ai workers-ai-provider zod ``` -**Option B: Using Cloudflare secrets (recommended for production)** - -For production deployments, use Cloudflare secrets instead of putting API keys in configuration files: +## 2. Configure Wrangler -```bash -wrangler secret put OPENAI_API_KEY -``` +Replace your `wrangler.jsonc` with: -## Step 3: Test the agent locally + -```bash -npm start +```jsonc +{ + "name": "chat-agent", + "main": "src/server.ts", + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "ai": { "binding": "AI" }, + "durable_objects": { + "bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }], +} ``` -The server will be available at `http://localhost:5174`. Open your browser and start chatting. + -## Understanding the code +Key settings: -### Project structure +- `ai` binds Workers AI — no API key needed +- `durable_objects` registers your chat agent class +- `new_sqlite_classes` enables SQLite storage for message persistence -```txt -chat-agent/ -├── src/ -│ ├── server.ts # Main agent implementation -│ ├── tools.ts # Tool definitions -│ ├── app.tsx # React frontend -│ └── ... -├── wrangler.jsonc # Cloudflare configuration -└── package.json -``` - -### Key components - -#### Chat Agent (`src/server.ts`) +## 3. Write the server -The main agent class that handles chat interactions: +Create `src/server.ts`. This is where your agent lives: ```ts -import { AIChatAgent } from "agents/ai-chat-agent"; +import { AIChatAgent } from "@cloudflare/ai-chat"; +import { routeAgentRequest } from "agents"; +import { createWorkersAI } from "workers-ai-provider"; +import { + streamText, + convertToModelMessages, + pruneMessages, + tool, + stepCountIs, +} from "ai"; +import { z } from "zod"; -export class Chat extends AIChatAgent { - async onChatMessage(onFinish, options) { - // Handles incoming messages and manages streaming responses - // Processes tool calls and generates AI responses +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"), + system: + "You are a helpful assistant. You can check the weather, " + + "get the user's timezone, and run calculations.", + messages: pruneMessages({ + messages: await convertToModelMessages(this.messages), + toolCalls: "before-last-2-messages", + }), + tools: { + // Server-side tool: runs automatically on the server + getWeather: tool({ + description: "Get the current weather for a city", + inputSchema: z.object({ + city: z.string().describe("City name"), + }), + execute: async ({ city }) => { + // Replace with a real weather API in production + const conditions = ["sunny", "cloudy", "rainy"]; + const temp = Math.floor(Math.random() * 30) + 5; + return { + city, + temperature: temp, + condition: + conditions[Math.floor(Math.random() * conditions.length)], + }; + }, + }), + + // Client-side tool: no execute function — the browser handles it + getUserTimezone: tool({ + description: "Get the user's timezone from their browser", + inputSchema: z.object({}), + }), + + // Approval tool: requires user confirmation before executing + calculate: tool({ + description: "Perform a calculation", + inputSchema: z.object({ + expression: z.string().describe("Math expression to evaluate"), + amount: z + .number() + .optional() + .describe("Dollar amount involved, if any"), + }), + needsApproval: async ({ amount }) => (amount ?? 0) > 100, + execute: async ({ expression }) => { + try { + const result = new Function(`return ${expression}`)(); + return { expression, result: Number(result) }; + } catch { + return { expression, error: "Invalid expression" }; + } + }, + }), + }, + stopWhen: stepCountIs(5), + }); + + return result.toUIMessageStreamResponse(); } } -``` - - - -#### Tools (`src/tools.ts`) - -Define what actions your agent can perform: - - - -```ts -import { tool } from "ai"; -import { z } from "zod"; -// Automatic execution (no confirmation needed) -const getLocalTime = tool({ - description: "get the local time for a specified location", - parameters: z.object({ location: z.string() }), - execute: async ({ location }) => { - return "10am"; +export default { + async fetch(request: Request, env: Env) { + return ( + (await routeAgentRequest(request, env)) || + new Response("Not found", { status: 404 }) + ); }, -}); - -// Requires confirmation (no execute function) -const getWeatherInformation = tool({ - description: "show the weather in a given city to the user", - parameters: z.object({ city: z.string() }), - // Omitting execute function makes this tool require human confirmation -}); +} satisfies ExportedHandler; ``` -## Customization - -### Adding custom tools - -**Simple Tool (Automatic Execution)** - - - -```ts -// In src/tools.ts -const getWeather = tool({ - description: "Get current weather for a location", - parameters: z.object({ - location: z.string().describe("The city name"), - }), - execute: async ({ location }) => { - // Call weather API - return `The weather in ${location} is sunny and 72°F`; - }, -}); +### What each tool type does -// Add to exports -export const tools = { - // ... existing tools - getWeather, -}; -``` +| Tool | `execute`? | `needsApproval`? | Behavior | +| ---------------- | ---------- | ----------------- | ---------------------------------------------------- | +| `getWeather` | Yes | No | Runs on the server automatically | +| `getUserTimezone`| No | No | Sent to the client; browser provides the result | +| `calculate` | Yes | Yes (over $100) | Pauses for user approval, then runs on server | - +## 4. Write the client -**Confirmation-Required Tool** +Create `src/client.tsx`: ```ts -// In src/tools.ts -const sensitiveAction = tool({ - description: "Perform a sensitive action", - parameters: z.object({ - action: z.string().describe("The action to perform"), - reason: z.string().describe("Reason for the action"), - }), - // Omitting execute function makes this tool require human confirmation -}); - -// Add to exports -export const tools = { - // ... existing tools - sensitiveAction, -}; - -// Add execution function -export const executions = { - // ... existing executions - sensitiveAction: async ({ action, reason }) => { - return `Action "${action}" completed successfully. Reason: ${reason}`; - }, -}; +import { useAgent } from "agents/react"; +import { useAgentChat } from "@cloudflare/ai-chat/react"; + +function Chat() { + const agent = useAgent({ agent: "ChatAgent" }); + + const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } = + useAgentChat({ + agent, + // Handle client-side tools (tools with no server execute function) + onToolCall: async ({ toolCall, addToolOutput }) => { + if (toolCall.toolName === "getUserTimezone") { + addToolOutput({ + toolCallId: toolCall.toolCallId, + output: { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + localTime: new Date().toLocaleTimeString(), + }, + }); + } + }, + }); + + return ( +
+
+ {messages.map((msg) => ( +
+ {msg.role}: + {msg.parts.map((part, i) => { + if (part.type === "text") { + return {part.text}; + } + + // Render approval UI for tools that need confirmation + if ( + part.type === "tool" && + part.state === "approval-required" + ) { + return ( +
+

+ Approve {part.toolName}? +

+
{JSON.stringify(part.input, null, 2)}
+ + +
+ ); + } + + // Show completed tool results + if ( + part.type === "tool" && + part.state === "output-available" + ) { + return ( +
+ {part.toolName} result +
{JSON.stringify(part.output, null, 2)}
+
+ ); + } + + return null; + })} +
+ ))} +
+ +
{ + e.preventDefault(); + const input = e.currentTarget.elements.namedItem( + "message", + ) as HTMLInputElement; + sendMessage({ text: input.value }); + input.value = ""; + }} + > + + +
+ + +
+ ); +} + +export default function App() { + return ; +} ```
-### Customizing the system prompt - -Edit the system prompt in `src/server.ts`: - -```ts -system: `You are a helpful assistant specializing in software development. -You can help with coding questions, debugging, and best practices. -Always provide clear, actionable advice. +### Key client concepts -${unstable_getSchedulePrompt({ date: new Date() })} +- **`useAgent`** connects to your `ChatAgent` over WebSocket +- **`useAgentChat`** manages the chat lifecycle (messages, streaming, tools) +- **`onToolCall`** handles client-side tools — when the LLM calls `getUserTimezone`, the browser provides the result and the conversation auto-continues +- **`addToolApprovalResponse`** approves or rejects tools that have `needsApproval` +- Messages, streaming, and resumption are all handled automatically -If the user asks to schedule a task, use the schedule tool to schedule the task. -`, -``` +## 5. Run locally -## Deploy to Cloudflare Workers +Generate types and start the dev server: -```bash -npm run deploy +```sh +npx wrangler types +npm run dev ``` -Your agent will be available at `https://your-project-name.your-subdomain.workers.dev`. +Try these prompts: -### Environment variables in production +- **"What is the weather in Tokyo?"** — calls the server-side `getWeather` tool +- **"What timezone am I in?"** — calls the client-side `getUserTimezone` tool (the browser provides the answer) +- **"Calculate 200 * 3 (amount: $600)"** — triggers the approval UI before executing -For production deployment, set your OpenAI API key as a secret: +## 6. Deploy -```bash -wrangler secret put OPENAI_API_KEY +```sh +npx wrangler deploy ``` -## Features - -- **Streaming Responses**: Real-time AI responses with typing indicators. -- **Tool Integration**: Execute actions with or without user confirmation. -- **Task Scheduling**: Schedule tasks to run at specific times. -- **Modern UI**: Dark/light mode, responsive design. -- **Debug Mode**: View detailed message processing. -- **Type Safety**: Full TypeScript support. - -## Troubleshooting - -### Common issues - -**"OPENAI_API_KEY is not set"** - -- Make sure you have set the API key in `.dev.vars` for local development. -- For production, use `wrangler secret put OPENAI_API_KEY`. +Your agent is now live on Cloudflare's global network. Messages persist in SQLite, streams resume on disconnect, and the agent hibernates when idle to save resources. -**"npm run dev" not found** +## What you built -- Use `npm start` instead (the script is named `start`). +Your chat agent has: -**Server not accessible** +- **Streaming AI responses** via Workers AI (no API keys) +- **Message persistence** in SQLite — conversations survive restarts +- **Server-side tools** that execute automatically +- **Client-side tools** that run in the browser and feed results back to the LLM +- **Human-in-the-loop approval** for sensitive operations +- **Resumable streaming** — if a client disconnects mid-stream, it picks up where it left off -- Check that the server is running on port 5174 (not 8787). -- Look for the correct URL in the terminal output. +## Next steps -**Tool execution errors** + -- Make sure tools are properly exported in the `tools` object. -- Check that confirmation-required tools have execution functions in `executions`. + -### Development tips + -- Use the browser developer tools to view detailed error messages. -- Check the terminal output for server logs. -- The debug mode in the UI shows detailed message processing. + diff --git a/src/content/docs/agents/getting-started/quick-start.mdx b/src/content/docs/agents/getting-started/quick-start.mdx index e36dae22b6f..e058cdfadbe 100644 --- a/src/content/docs/agents/getting-started/quick-start.mdx +++ b/src/content/docs/agents/getting-started/quick-start.mdx @@ -89,7 +89,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -102,7 +102,7 @@ Update `wrangler.jsonc` to register the agent: { "name": "my-agent", "main": "src/server.ts", - "compatibility_date": "2025-01-01", + "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ diff --git a/src/content/docs/agents/guides/chatgpt-app.mdx b/src/content/docs/agents/guides/chatgpt-app.mdx index fdb34a88205..1d5d90c160a 100644 --- a/src/content/docs/agents/guides/chatgpt-app.mdx +++ b/src/content/docs/agents/guides/chatgpt-app.mdx @@ -380,7 +380,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; export { ChessGame } from "./chess"; ``` diff --git a/src/content/docs/agents/guides/human-in-the-loop.mdx b/src/content/docs/agents/guides/human-in-the-loop.mdx index 9eba7e6e6fa..f61318f630b 100644 --- a/src/content/docs/agents/guides/human-in-the-loop.mdx +++ b/src/content/docs/agents/guides/human-in-the-loop.mdx @@ -311,7 +311,7 @@ class ExpenseAgent extends Agent { { "name": "expense-approval", "main": "src/index.ts", - "compatibility_date": "2026-01-20", + "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "EXPENSE_AGENT", "class_name": "ExpenseAgent" }], diff --git a/src/content/docs/agents/guides/oauth-mcp-client.mdx b/src/content/docs/agents/guides/oauth-mcp-client.mdx index d802b2a9201..d6a9109e809 100644 --- a/src/content/docs/agents/guides/oauth-mcp-client.mdx +++ b/src/content/docs/agents/guides/oauth-mcp-client.mdx @@ -370,7 +370,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/guides/webhooks.mdx b/src/content/docs/agents/guides/webhooks.mdx index ddb427fee1a..3d874bb9e8e 100644 --- a/src/content/docs/agents/guides/webhooks.mdx +++ b/src/content/docs/agents/guides/webhooks.mdx @@ -91,7 +91,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` @@ -170,7 +170,7 @@ export default { return agent.fetch(request); } }, -}; +} satisfies ExportedHandler; ``` @@ -557,7 +557,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/agents/model-context-protocol/transport.mdx b/src/content/docs/agents/model-context-protocol/transport.mdx index 663de3641fa..3ab9571878b 100644 --- a/src/content/docs/agents/model-context-protocol/transport.mdx +++ b/src/content/docs/agents/model-context-protocol/transport.mdx @@ -69,7 +69,7 @@ export default { const server = createServer(); return createMcpHandler(server)(request, env, ctx); }, -}; +} satisfies ExportedHandler; ``` From 4ef27d37120363dcfefb5b62ba3453e838ca9748 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 16 Feb 2026 20:09:25 +0000 Subject: [PATCH 2/9] Remove generic from Agent examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify Agent class examples in the agents docs by removing explicit generic type parameters (e.g. , ) from class declarations. Updated multiple MDX files under src/content/docs/agents to match the current Agent API/signature and improve example readability. No runtime or behavioral changes — only documentation/type examples were adjusted. --- .../docs/agents/api-reference/agents-api.mdx | 2 +- .../agents/api-reference/browse-the-web.mdx | 4 ++-- .../docs/agents/api-reference/http-sse.mdx | 8 +++---- src/content/docs/agents/api-reference/rag.mdx | 2 +- .../api-reference/store-and-sync-state.mdx | 6 ++--- .../agents/api-reference/using-ai-models.mdx | 10 ++++---- .../add-to-existing-project.mdx | 23 +++++-------------- .../docs/agents/guides/connect-mcp-client.mdx | 6 ++--- .../docs/agents/guides/oauth-mcp-client.mdx | 10 ++++---- src/content/docs/agents/x402.mdx | 4 ++-- 10 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 344888c9f1c..4f7418ffb81 100644 --- a/src/content/docs/agents/api-reference/agents-api.mdx +++ b/src/content/docs/agents/api-reference/agents-api.mdx @@ -143,7 +143,7 @@ For AI chat applications, extend `AIChatAgent` instead of `Agent`: ```ts import { AIChatAgent } from "agents/ai-chat-agent"; -class ChatAgent extends AIChatAgent { +class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { // this.messages contains the conversation history // Return a streaming response diff --git a/src/content/docs/agents/api-reference/browse-the-web.mdx b/src/content/docs/agents/api-reference/browse-the-web.mdx index bece3d7e587..6dddd4860e4 100644 --- a/src/content/docs/agents/api-reference/browse-the-web.mdx +++ b/src/content/docs/agents/api-reference/browse-the-web.mdx @@ -29,7 +29,7 @@ interface Env { BROWSER: Fetcher; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async browse(browserInstance: Fetcher, urls: string[]) { let responses = []; for (const url of urls) { @@ -116,7 +116,7 @@ interface Env { BROWSERBASE_API_KEY: string; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { constructor(env: Env) { super(env); } diff --git a/src/content/docs/agents/api-reference/http-sse.mdx b/src/content/docs/agents/api-reference/http-sse.mdx index ea3fad02ede..a32cd2ded05 100644 --- a/src/content/docs/agents/api-reference/http-sse.mdx +++ b/src/content/docs/agents/api-reference/http-sse.mdx @@ -18,7 +18,7 @@ Define the `onRequest` method to handle HTTP requests to your agent: ```ts import { Agent } from "agents"; -export class APIAgent extends Agent { +export class APIAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -58,7 +58,7 @@ Create an SSE stream manually using `ReadableStream`: ```ts -export class StreamAgent extends Agent { +export class StreamAgent extends Agent { async onRequest(request: Request): Promise { const encoder = new TextEncoder(); @@ -117,7 +117,7 @@ import { Agent } from "agents"; import { streamText } from "ai"; import { createOpenAI } from "@ai-sdk/openai"; -export class ChatAgent extends Agent { +export class ChatAgent extends Agent { async onRequest(request: Request): Promise { const { prompt } = await request.json<{ prompt: string }>(); @@ -148,7 +148,7 @@ SSE connections can be long-lived. Handle client disconnects gracefully: ```ts -export class ResumeAgent extends Agent { +export class ResumeAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); const lastEventId = request.headers.get("Last-Event-ID"); diff --git a/src/content/docs/agents/api-reference/rag.mdx b/src/content/docs/agents/api-reference/rag.mdx index 66c81164607..85b6510ea09 100644 --- a/src/content/docs/agents/api-reference/rag.mdx +++ b/src/content/docs/agents/api-reference/rag.mdx @@ -39,7 +39,7 @@ interface Env { VECTOR_DB: Vectorize; } -export class RAGAgent extends Agent { +export class RAGAgent extends Agent { // Other methods on our Agent // ... // diff --git a/src/content/docs/agents/api-reference/store-and-sync-state.mdx b/src/content/docs/agents/api-reference/store-and-sync-state.mdx index 447e4335afb..e6ceb8ec6b2 100644 --- a/src/content/docs/agents/api-reference/store-and-sync-state.mdx +++ b/src/content/docs/agents/api-reference/store-and-sync-state.mdx @@ -458,7 +458,7 @@ You can access the SQL API within any method on an Agent via `this.sql`. The SQL ```ts -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { let userId = new URL(request.url).searchParams.get("userId"); @@ -483,7 +483,7 @@ type User = { email: string; }; -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { let userId = new URL(request.url).searchParams.get("userId"); // Supply the type parameter to the query when calling this.sql @@ -646,7 +646,7 @@ For example, you can use an Agent's built-in SQL database to pull history, query ```ts -export class ReasoningAgent extends Agent { +export class ReasoningAgent extends Agent { async callReasoningModel(prompt: Prompt) { let result = this .sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; diff --git a/src/content/docs/agents/api-reference/using-ai-models.mdx b/src/content/docs/agents/api-reference/using-ai-models.mdx index 07013caa8c1..acfa9397c38 100644 --- a/src/content/docs/agents/api-reference/using-ai-models.mdx +++ b/src/content/docs/agents/api-reference/using-ai-models.mdx @@ -49,7 +49,7 @@ Instead of buffering the entire response, or risking the client disconnecting, y import { Agent } from "agents"; import { OpenAI } from "openai"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onConnect(connection: Connection, ctx: ConnectionContext) { // } @@ -110,7 +110,7 @@ interface Env { AI: Ai; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { const response = await env.AI.run( "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", @@ -163,7 +163,7 @@ interface Env { AI: Ai; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { const response = await env.AI.run( "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", @@ -217,7 +217,7 @@ import { Agent } from "agents"; import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const { text } = await generateText({ model: openai("o3-mini"), @@ -243,7 +243,7 @@ Agents can stream responses back over HTTP using Server Sent Events (SSE) from w import { Agent } from "agents"; import { OpenAI } from "openai"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const openai = new OpenAI({ apiKey: this.env.GEMINI_API_KEY, diff --git a/src/content/docs/agents/getting-started/add-to-existing-project.mdx b/src/content/docs/agents/getting-started/add-to-existing-project.mdx index aaab2b48d5d..00d721bbcc5 100644 --- a/src/content/docs/agents/getting-started/add-to-existing-project.mdx +++ b/src/content/docs/agents/getting-started/add-to-existing-project.mdx @@ -212,28 +212,17 @@ Configure assets in the Wrangler configuration file: -## 6. Add TypeScript types +## 6. Generate TypeScript types -You can generate types automatically from your Wrangler configuration file: +Do not hand-write your `Env` interface. Run [`wrangler types`](/workers/wrangler/commands/#types) to generate a type definition file that matches your Wrangler configuration. This catches mismatches between your config and code at compile time instead of at deploy time. + +Re-run `wrangler types` whenever you add or rename a binding. ```sh -npx wrangler types env.d.ts +npx wrangler types ``` -This creates an `env.d.ts` file with all your bindings typed. Alternatively, you can manually update your `Env` type to include the agent namespace: - -```ts -import type { CounterAgent } from "./agents/counter"; - -interface Env { - // Your existing bindings - MY_KV: KVNamespace; - MY_DB: D1Database; - - // Add agent bindings - CounterAgent: DurableObjectNamespace; -} -``` +This creates a type definition file with all your bindings typed, including your agent Durable Object namespaces. The `Agent` class defaults to using the generated `Env` type, so you do not need to pass it as a type parameter — `extends Agent` is sufficient unless you need to pass a second type parameter for state (for example, `Agent`). Refer to [Configuration](/agents/api-reference/configuration/#generating-types) for more details on type generation. diff --git a/src/content/docs/agents/guides/connect-mcp-client.mdx b/src/content/docs/agents/guides/connect-mcp-client.mdx index bd4d111ce55..8d5618ee1e1 100644 --- a/src/content/docs/agents/guides/connect-mcp-client.mdx +++ b/src/content/docs/agents/guides/connect-mcp-client.mdx @@ -59,7 +59,7 @@ An MCP server to connect to (or use the public example in this tutorial). HelloAgent: DurableObjectNamespace; }; - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { return new Response("Hello, Agent!", { status: 200 }); } @@ -87,7 +87,7 @@ An MCP server to connect to (or use the public example in this tutorial). ```ts title="src/index.ts" - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -166,7 +166,7 @@ The `addMcpServer()` method connects to an MCP server. If the server requires OA ```ts title="src/index.ts" - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); diff --git a/src/content/docs/agents/guides/oauth-mcp-client.mdx b/src/content/docs/agents/guides/oauth-mcp-client.mdx index d6a9109e809..a63b8f0628d 100644 --- a/src/content/docs/agents/guides/oauth-mcp-client.mdx +++ b/src/content/docs/agents/guides/oauth-mcp-client.mdx @@ -34,7 +34,7 @@ When connecting to an OAuth-protected server, check if `authUrl` is returned. If ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -79,7 +79,7 @@ Redirect users back to your application after OAuth completes: ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", @@ -102,7 +102,7 @@ If you opened OAuth in a popup, close it automatically when complete: ```ts title="src/index.ts" import { Agent } from "agents"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: () => { @@ -181,7 +181,7 @@ Poll the connection status via an endpoint: ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -305,7 +305,7 @@ type Env = { MyAgent: DurableObjectNamespace; }; -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: () => { diff --git a/src/content/docs/agents/x402.mdx b/src/content/docs/agents/x402.mdx index 5b67ff22da8..5591050f3b6 100644 --- a/src/content/docs/agents/x402.mdx +++ b/src/content/docs/agents/x402.mdx @@ -25,7 +25,7 @@ import { privateKeyToAccount } from "viem/accounts"; // The code below creates an Agent that can fetch the protected route and automatically pay. // The agent's account must not be empty! You can get test credits // for base-sepolia here: https://faucet.circle.com/ -export class PayAgent extends Agent { +export class PayAgent extends Agent { fetchWithPay!: ReturnType; onStart() { @@ -103,7 +103,7 @@ const X402_CONFIG: X402Config = { // To learn more about facilitators https://x402.gitbook.io/x402/core-concepts/facilitator }; -export class PaidMCP extends McpAgent { +export class PaidMCP extends McpAgent { server = withX402( new McpServer({ name: "PaidMCP", version: "1.0.0" }), X402_CONFIG, From b75e54e87456198ccf2acb6a1cbce8875bbfa76d Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 16 Feb 2026 20:12:02 +0000 Subject: [PATCH 3/9] Standardize local secrets filename to .env Replace references to `.dev.vars` with `.env` across Agents documentation and update corresponding command examples (touch/echo/cat) and troubleshooting notes. Files updated: src/content/docs/agents/api-reference/configuration.mdx, src/content/docs/agents/guides/remote-mcp-server.mdx, src/content/docs/agents/guides/slack-agent.mdx, and src/content/docs/agents/index.mdx to standardize local development secrets guidance. --- .../docs/agents/api-reference/configuration.mdx | 10 +++++----- src/content/docs/agents/guides/remote-mcp-server.mdx | 10 +++++----- src/content/docs/agents/guides/slack-agent.mdx | 6 +++--- src/content/docs/agents/index.mdx | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/content/docs/agents/api-reference/configuration.mdx b/src/content/docs/agents/api-reference/configuration.mdx index d7433ef3558..f8fc9a7fc61 100644 --- a/src/content/docs/agents/api-reference/configuration.mdx +++ b/src/content/docs/agents/api-reference/configuration.mdx @@ -339,12 +339,12 @@ Add a script for easy regeneration: ## Environment variables and secrets -### Local development (`.dev.vars`) +### Local development (`.env`) -Create a `.dev.vars` file for local secrets (add to `.gitignore`): +Create a `.env` file for local secrets (add to `.gitignore`): ```sh -# .dev.vars +# .env OPENAI_API_KEY=sk-... GITHUB_WEBHOOK_SECRET=whsec_... DATABASE_URL=postgres://... @@ -825,10 +825,10 @@ npx wrangler types env.d.ts --include-runtime false ### Secrets not loading locally -Check that `.dev.vars` exists and contains the variable: +Check that `.env` exists and contains the variable: ```sh -cat .dev.vars +cat .env # Should show: MY_SECRET=value ``` diff --git a/src/content/docs/agents/guides/remote-mcp-server.mdx b/src/content/docs/agents/guides/remote-mcp-server.mdx index fff20ba1f66..fa18c30b759 100644 --- a/src/content/docs/agents/guides/remote-mcp-server.mdx +++ b/src/content/docs/agents/guides/remote-mcp-server.mdx @@ -199,13 +199,13 @@ You'll need to create two [GitHub OAuth Apps](https://docs.github.com/en/apps/oa - **Homepage URL**: `http://localhost:8788` - **Authorization callback URL**: `http://localhost:8788/callback` -2. For the OAuth app you just created, add the client ID of the OAuth app as `GITHUB_CLIENT_ID` and generate a client secret, adding it as `GITHUB_CLIENT_SECRET` to a `.dev.vars` file in the root of your project, which [will be used to set secrets in local development](/workers/configuration/secrets/). +2. For the OAuth app you just created, add the client ID of the OAuth app as `GITHUB_CLIENT_ID` and generate a client secret, adding it as `GITHUB_CLIENT_SECRET` to a `.env` file in the root of your project, which [will be used to set secrets in local development](/workers/configuration/secrets/). ```sh - touch .dev.vars - echo 'GITHUB_CLIENT_ID="your-client-id"' >> .dev.vars - echo 'GITHUB_CLIENT_SECRET="your-client-secret"' >> .dev.vars - cat .dev.vars + touch .env + echo 'GITHUB_CLIENT_ID="your-client-id"' >> .env + echo 'GITHUB_CLIENT_SECRET="your-client-secret"' >> .env + cat .env ``` 3. Run the following command to start the development server: diff --git a/src/content/docs/agents/guides/slack-agent.mdx b/src/content/docs/agents/guides/slack-agent.mdx index 7ac575d34dc..568eff53b57 100644 --- a/src/content/docs/agents/guides/slack-agent.mdx +++ b/src/content/docs/agents/guides/slack-agent.mdx @@ -101,13 +101,13 @@ npm install agents openai ## 3. Set up your environment variables -1. Create a `.dev.vars` file in your project root for local development secrets: +1. Create a `.env` file in your project root for local development secrets: ```sh -touch .dev.vars +touch .env ``` -2. Add your credentials to `.dev.vars`: +2. Add your credentials to `.env`: ```sh SLACK_CLIENT_ID="your-slack-client-id" diff --git a/src/content/docs/agents/index.mdx b/src/content/docs/agents/index.mdx index de5ad8b7ddc..af359caade8 100644 --- a/src/content/docs/agents/index.mdx +++ b/src/content/docs/agents/index.mdx @@ -51,7 +51,7 @@ npm install 3. Set up your environment: -Create a `.dev.vars` file: +Create a `.env` file: ```txt OPENAI_API_KEY=your_openai_api_key From 7c8bdf86395676cbb9f9c2522798928db28c99f9 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Mon, 16 Feb 2026 21:29:22 +0000 Subject: [PATCH 4/9] Clarify chat agent lifecycle hooks Reword the AIChatAgent lifecycle section to clarify that overriding onConnect/onClose is for adding custom logic while stream resumption, message sync, and connection cleanup are handled automatically. Updated the example comments to remove the implication that calling super is required. Also reformatted the deprecated APIs table for consistent column alignment and readability. --- .../docs/agents/api-reference/chat-agents.mdx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/content/docs/agents/api-reference/chat-agents.mdx b/src/content/docs/agents/api-reference/chat-agents.mdx index 2dda1460a8a..248d07a5582 100644 --- a/src/content/docs/agents/api-reference/chat-agents.mdx +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -286,7 +286,7 @@ await this.saveMessages(messages); ### Lifecycle hooks -`AIChatAgent` wraps `onConnect` and `onClose` to manage stream resumption and message loading. If you override these methods, you must call `super`: +Override `onConnect` and `onClose` to add custom logic. Stream resumption and message sync are handled for you: @@ -295,16 +295,12 @@ export class ChatAgent extends AIChatAgent { async onConnect(connection, ctx) { // Your custom logic (e.g., logging, auth checks) console.log("Client connected:", connection.id); - - // Required — sets up stream resumption and message sync - await super.onConnect(connection, ctx); + // Stream resumption and message sync are handled automatically } async onClose(connection, code, reason, wasClean) { console.log("Client disconnected:", connection.id); - - // Required — cleans up connection tracking - await super.onClose(connection, code, reason, wasClean); + // Connection cleanup is handled automatically } } ``` @@ -1033,14 +1029,14 @@ The chat protocol uses typed JSON messages over WebSocket: The following APIs are deprecated and will emit a console warning when used. They will be removed in a future release. -| Deprecated | Replacement | Notes | -| --- | --- | --- | -| `addToolResult({ toolCallId, result })` | `addToolOutput({ toolCallId, output })` | Renamed for consistency with AI SDK terminology | -| `createToolsFromClientSchemas()` | Client tools are now registered automatically | No manual schema conversion needed | -| `extractClientToolSchemas()` | Client tools are now registered automatically | Schemas are sent with tool results | -| `detectToolsRequiringConfirmation()` | Use `needsApproval` on the tool definition | Approval is now per-tool, not a global filter | -| `tools` option on `useAgentChat` | Define tools in `onChatMessage` on the server | All tool definitions belong on the server | -| `toolsRequiringConfirmation` option | Use `needsApproval` on individual tools | Per-tool approval replaces global list | +| Deprecated | Replacement | Notes | +| --------------------------------------- | --------------------------------------------- | ----------------------------------------------- | +| `addToolResult({ toolCallId, result })` | `addToolOutput({ toolCallId, output })` | Renamed for consistency with AI SDK terminology | +| `createToolsFromClientSchemas()` | Client tools are now registered automatically | No manual schema conversion needed | +| `extractClientToolSchemas()` | Client tools are now registered automatically | Schemas are sent with tool results | +| `detectToolsRequiringConfirmation()` | Use `needsApproval` on the tool definition | Approval is now per-tool, not a global filter | +| `tools` option on `useAgentChat` | Define tools in `onChatMessage` on the server | All tool definitions belong on the server | +| `toolsRequiringConfirmation` option | Use `needsApproval` on individual tools | Per-tool approval replaces global list | If you are upgrading from an earlier version, replace deprecated calls with their replacements. The deprecated APIs still work but will be removed in a future major version. From 5daf0014fbb50f738de80c5fa48b6d477ba0393b Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 17 Feb 2026 07:38:40 +0000 Subject: [PATCH 5/9] docs(agents): add protocol messages documentation Add new page documenting `shouldSendProtocolMessages` and `isConnectionProtocolEnabled` for per-connection control of protocol text frames. Update cross-references in agents-api, websockets, readonly-connections, store-and-sync-state, and mcp-client-api pages. Corresponds to cloudflare/agents#920. Co-authored-by: Cursor --- .../docs/agents/api-reference/agents-api.mdx | 1 + .../agents/api-reference/mcp-client-api.mdx | 4 + .../api-reference/protocol-messages.mdx | 199 ++++++++++++++++++ .../api-reference/readonly-connections.mdx | 3 +- .../api-reference/store-and-sync-state.mdx | 2 +- .../docs/agents/api-reference/websockets.mdx | 4 + 6 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/content/docs/agents/api-reference/protocol-messages.mdx diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 4f7418ffb81..0b1573a66cf 100644 --- a/src/content/docs/agents/api-reference/agents-api.mdx +++ b/src/content/docs/agents/api-reference/agents-api.mdx @@ -87,6 +87,7 @@ flowchart TD | **Workflows** | `runWorkflow()`, `waitForApproval()` | [Run Workflows](/agents/api-reference/run-workflows/) | | **MCP Client** | `addMcpServer()`, `removeMcpServer()`, `getMcpServers()` | [MCP Client API](/agents/api-reference/mcp-client-api/ ) | | **AI Models** | Workers AI, OpenAI, Anthropic bindings | [Using AI models](/agents/api-reference/using-ai-models/) | +| **Protocol messages** | `shouldSendProtocolMessages()`, `isConnectionProtocolEnabled()` | [Protocol messages](/agents/api-reference/protocol-messages/) | | **Context** | `getCurrentAgent()` | [getCurrentAgent()](/agents/api-reference/get-current-agent/) | | **Observability** | `observability.emit()` | [Observability](/agents/api-reference/observability/) | diff --git a/src/content/docs/agents/api-reference/mcp-client-api.mdx b/src/content/docs/agents/api-reference/mcp-client-api.mdx index 7ab814ad888..06b904fb97c 100644 --- a/src/content/docs/agents/api-reference/mcp-client-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-client-api.mdx @@ -719,6 +719,10 @@ this.mcp.onServerStateChanged(() => { +:::note +MCP server list broadcasts (`cf_agent_mcp_servers`) are automatically filtered to exclude connections where [`shouldSendProtocolMessages`](/agents/api-reference/protocol-messages/) returned `false`. +::: + ### Lifecycle methods #### `this.mcp.registerServer()` diff --git a/src/content/docs/agents/api-reference/protocol-messages.mdx b/src/content/docs/agents/api-reference/protocol-messages.mdx new file mode 100644 index 00000000000..1918cfa144b --- /dev/null +++ b/src/content/docs/agents/api-reference/protocol-messages.mdx @@ -0,0 +1,199 @@ +--- +title: Protocol messages +pcx_content_type: concept +sidebar: + order: 9 +--- + +import { TypeScriptExample } from "~/components"; + +When a WebSocket client connects to an Agent, the framework automatically sends several JSON text frames — identity, state, and MCP server lists. You can suppress these per-connection protocol messages for clients that cannot handle them. + +## Overview + +On every new connection, the Agent sends three protocol messages: + +| Message type | Content | +| ------------------------- | ------------------------------------------ | +| `cf_agent_identity` | Agent name and class | +| `cf_agent_state` | Current agent state | +| `cf_agent_mcp_servers` | Connected MCP server list | + +State and MCP messages are also broadcast to all connections whenever they change. + +For most web clients this is fine — the [Client SDK](/agents/api-reference/client-sdk/) and `useAgent` hook consume these messages automatically. However, some clients cannot handle JSON text frames: + +- **Binary-only clients** — MQTT devices, IoT sensors, custom binary protocols +- **Lightweight clients** — Embedded systems with minimal WebSocket stacks +- **Non-browser clients** — Hardware devices connecting via WebSocket + +For these connections, you can suppress protocol messages while keeping everything else (RPC, regular messages, broadcasts via `this.broadcast()`) working normally. + +## Suppressing protocol messages + +Override `shouldSendProtocolMessages` to control which connections receive protocol messages. Return `false` to suppress them. + + + +```ts +import { Agent, type Connection, type ConnectionContext } from "agents"; + +export class IoTAgent extends Agent { + shouldSendProtocolMessages( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + return url.searchParams.get("protocol") !== "false"; + } +} +``` + + + +This hook runs during `onConnect`, before any messages are sent. When it returns `false`: + +- No `cf_agent_identity`, `cf_agent_state`, or `cf_agent_mcp_servers` messages are sent on connect +- The connection is excluded from state and MCP broadcasts going forward +- RPC calls, regular `onMessage` handling, and `this.broadcast()` still work normally + +### Using WebSocket subprotocol + +You can also check the WebSocket subprotocol header, which is the standard way to negotiate protocols over WebSocket: + + + +```ts +export class MqttAgent extends Agent { + shouldSendProtocolMessages( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + // MQTT-over-WebSocket clients negotiate via subprotocol + const subprotocol = ctx.request.headers.get("Sec-WebSocket-Protocol"); + return subprotocol !== "mqtt"; + } +} +``` + + + +## Checking protocol status + +Use `isConnectionProtocolEnabled` to check whether a connection has protocol messages enabled: + + + +```ts +export class MyAgent extends Agent { + @callable() + async getConnectionInfo() { + const { connection } = getCurrentAgent(); + if (!connection) return null; + + return { + protocolEnabled: this.isConnectionProtocolEnabled(connection), + readonly: this.isConnectionReadonly(connection), + }; + } +} +``` + + + +## What is and is not suppressed + +| Action | Works? | +| --------------------------------------------------- | ------ | +| Receive `cf_agent_identity` on connect | **No** | +| Receive `cf_agent_state` on connect and broadcasts | **No** | +| Receive `cf_agent_mcp_servers` on connect and broadcasts | **No** | +| Send and receive regular WebSocket messages | Yes | +| Call `@callable()` RPC methods | Yes | +| Receive `this.broadcast()` messages | Yes | +| Send binary data | Yes | +| Mutate agent state via RPC | Yes | + +## Combining with readonly + +A connection can be both readonly and protocol-suppressed. This is useful for binary devices that should observe but not modify state: + + + +```ts +export class SensorHub extends Agent { + shouldSendProtocolMessages( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + // Binary sensors don't handle JSON protocol frames + return url.searchParams.get("type") !== "sensor"; + } + + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + // Sensors can only report data via RPC, not modify shared state + return url.searchParams.get("type") === "sensor"; + } + + @callable() + async reportReading(sensorId: string, value: number) { + // This RPC still works for readonly+no-protocol connections + // because it writes to SQL, not agent state + this.sql`INSERT INTO readings (sensor_id, value, ts) VALUES (${sensorId}, ${value}, ${Date.now()})`; + } +} +``` + + + +Both flags are stored in the connection's WebSocket attachment and hidden from `connection.state` — they do not interfere with each other or with user-defined connection state. + +## API reference + +### `shouldSendProtocolMessages` + +An overridable hook that determines if a connection should receive protocol messages when it connects. + +| Parameter | Type | Description | +| ------------ | ------------------- | ------------------------------------ | +| `connection` | `Connection` | The connecting client | +| `ctx` | `ConnectionContext` | Contains the upgrade request | +| **Returns** | `boolean` | `false` to suppress protocol messages | + +Default: returns `true` (all connections receive protocol messages). + +This hook is evaluated once on connect. The result is persisted in the connection's WebSocket attachment and survives [hibernation](/agents/api-reference/websockets/#hibernation). + +### `isConnectionProtocolEnabled` + +Check if a connection currently has protocol messages enabled. + +| Parameter | Type | Description | +| ------------ | ------------ | ----------------------- | +| `connection` | `Connection` | The connection to check | +| **Returns** | `boolean` | `true` if protocol messages are enabled | + +Safe to call at any time, including after the agent wakes from hibernation. + +## How it works + +Protocol status is stored as an internal flag in the connection's WebSocket attachment — the same mechanism used by [readonly connections](/agents/api-reference/readonly-connections/). This means: + +- **Survives hibernation** — the flag is serialized and restored when the agent wakes up +- **No cleanup needed** — connection state is automatically discarded when the connection closes +- **Zero overhead** — no database tables or queries, just the connection's built-in attachment +- **Safe from user code** — `connection.state` and `connection.setState()` never expose or overwrite the flag + +Unlike [readonly](/agents/api-reference/readonly-connections/) which can be toggled dynamically with `setConnectionReadonly()`, protocol status is set once on connect and cannot be changed afterward. To change a connection's protocol status, the client must disconnect and reconnect. + +## Related resources + +- [Readonly connections](/agents/api-reference/readonly-connections/) +- [WebSockets](/agents/api-reference/websockets/) +- [Store and sync state](/agents/api-reference/store-and-sync-state/) +- [MCP Client API](/agents/api-reference/mcp-client-api/) diff --git a/src/content/docs/agents/api-reference/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx index 043bb7e9ad0..c2c62740663 100644 --- a/src/content/docs/agents/api-reference/readonly-connections.mdx +++ b/src/content/docs/agents/api-reference/readonly-connections.mdx @@ -452,7 +452,7 @@ function GameComponent() { ## How it works -Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. This means: +Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. The same mechanism is used by [protocol message control](/agents/api-reference/protocol-messages/) — both flags coexist safely in the attachment. This means: - **Survives hibernation** — the flag is serialized and restored when the agent wakes up - **No cleanup needed** — connection state is automatically discarded when the connection closes @@ -625,5 +625,6 @@ export class AuditedAgent extends Agent { ## Related resources - [Store and sync state](/agents/api-reference/store-and-sync-state/) +- [Protocol messages](/agents/api-reference/protocol-messages/) — suppress JSON protocol frames for binary-only clients (can be combined with readonly) - [WebSockets](/agents/api-reference/websockets/) - [Callable methods](/agents/api-reference/callable-methods/) diff --git a/src/content/docs/agents/api-reference/store-and-sync-state.mdx b/src/content/docs/agents/api-reference/store-and-sync-state.mdx index e6ceb8ec6b2..00573ee09b6 100644 --- a/src/content/docs/agents/api-reference/store-and-sync-state.mdx +++ b/src/content/docs/agents/api-reference/store-and-sync-state.mdx @@ -183,7 +183,7 @@ export class MinimalAgent extends Agent { Use `setState()` to update state. This: 1. Saves to SQLite (persistent) -2. Broadcasts to all connected clients +2. Broadcasts to all connected clients (excluding connections where [`shouldSendProtocolMessages`](/agents/api-reference/protocol-messages/) returned `false`) 3. Triggers `onStateChanged()` (after broadcast; best-effort) diff --git a/src/content/docs/agents/api-reference/websockets.mdx b/src/content/docs/agents/api-reference/websockets.mdx index 6036042d5f9..5ff4c325347 100644 --- a/src/content/docs/agents/api-reference/websockets.mdx +++ b/src/content/docs/agents/api-reference/websockets.mdx @@ -247,6 +247,10 @@ export class FileAgent extends Agent { +:::note +Agents automatically send JSON text frames (identity, state, MCP servers) to every connection. If your client only handles binary data and cannot process these frames, use [`shouldSendProtocolMessages`](/agents/api-reference/protocol-messages/) to suppress them. +::: + ## Error and close handling Handle connection errors and disconnections: From ec0fa7f7ada922bfd8807cf83adbe792d6d6c6c3 Mon Sep 17 00:00:00 2001 From: Jun Lee Date: Tue, 17 Feb 2026 09:36:55 +0000 Subject: [PATCH 6/9] PCX review --- src/content/docs/agents/api-reference/mcp-client-api.mdx | 2 +- src/content/docs/agents/api-reference/readonly-connections.mdx | 2 +- src/content/docs/agents/api-reference/retries.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/agents/api-reference/mcp-client-api.mdx b/src/content/docs/agents/api-reference/mcp-client-api.mdx index 06b904fb97c..ad8e94c1a62 100644 --- a/src/content/docs/agents/api-reference/mcp-client-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-client-api.mdx @@ -489,7 +489,7 @@ async addMcpServer( - `transport` — Transport layer configuration: - `headers` — Custom HTTP headers for authentication - `type` — Transport type: `"streamable-http"` (default), `"sse"`, or `"auto"` - - `retry` — Retry options for connection and reconnection attempts. Persisted and used when restoring connections after hibernation or after OAuth completion. Default: 3 attempts, 500ms base delay, 5s max delay. Refer to [Retries](/agents/api-reference/retries/) for details on `RetryOptions` + - `retry` — Retry options for connection and reconnection attempts. Persisted and used when restoring connections after hibernation or after OAuth completion. Default: 3 attempts, 500ms base delay, 5s max delay. Refer to [Retries](/agents/api-reference/retries/) for details on `RetryOptions`. #### Returns diff --git a/src/content/docs/agents/api-reference/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx index c2c62740663..0a11e525baf 100644 --- a/src/content/docs/agents/api-reference/readonly-connections.mdx +++ b/src/content/docs/agents/api-reference/readonly-connections.mdx @@ -452,7 +452,7 @@ function GameComponent() { ## How it works -Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. The same mechanism is used by [protocol message control](/agents/api-reference/protocol-messages/) — both flags coexist safely in the attachment. This means: +Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. The same mechanism is used by [protocol message control](/agents/api-reference/protocol-messages/) — both flag coexist safely in the attachment. This means: - **Survives hibernation** — the flag is serialized and restored when the agent wakes up - **No cleanup needed** — connection state is automatically discarded when the connection closes diff --git a/src/content/docs/agents/api-reference/retries.mdx b/src/content/docs/agents/api-reference/retries.mdx index 773cc74aa8b..8f45646a147 100644 --- a/src/content/docs/agents/api-reference/retries.mdx +++ b/src/content/docs/agents/api-reference/retries.mdx @@ -42,7 +42,7 @@ export class MyAgent extends Agent { -By default, `this.retry()` makes up to 3 attempts with jittered exponential backoff. +By default, `this.retry()` retries up to three times with jittered exponential backoff. ## `this.retry()` From 89c06bd2e14c749eafcba77d6c0d966841567305 Mon Sep 17 00:00:00 2001 From: Jun Lee Date: Tue, 17 Feb 2026 09:37:24 +0000 Subject: [PATCH 7/9] Update src/content/docs/agents/api-reference/chat-agents.mdx --- src/content/docs/agents/api-reference/chat-agents.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/agents/api-reference/chat-agents.mdx b/src/content/docs/agents/api-reference/chat-agents.mdx index 248d07a5582..a2bf114fcbd 100644 --- a/src/content/docs/agents/api-reference/chat-agents.mdx +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -678,7 +678,7 @@ export class ChatAgent extends AIChatAgent { ## Using different AI providers -`AIChatAgent` works with any AI SDK-compatible provider. The server code determines which model to use — the client does not need to change. +`AIChatAgent` works with any AI SDK-compatible provider. The server code determines which model to use — the client does not need to change it manually. ### Workers AI (Cloudflare) From 2416d2a2e5f7900d7b5f32147a4754ae64ac7803 Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Tue, 17 Feb 2026 09:49:15 +0000 Subject: [PATCH 8/9] Add intro sentence before table Co-authored-by: threepointone --- .../api-reference/protocol-messages.mdx | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/content/docs/agents/api-reference/protocol-messages.mdx b/src/content/docs/agents/api-reference/protocol-messages.mdx index 1918cfa144b..07f2436d6ea 100644 --- a/src/content/docs/agents/api-reference/protocol-messages.mdx +++ b/src/content/docs/agents/api-reference/protocol-messages.mdx @@ -13,11 +13,11 @@ When a WebSocket client connects to an Agent, the framework automatically sends On every new connection, the Agent sends three protocol messages: -| Message type | Content | -| ------------------------- | ------------------------------------------ | -| `cf_agent_identity` | Agent name and class | -| `cf_agent_state` | Current agent state | -| `cf_agent_mcp_servers` | Connected MCP server list | +| Message type | Content | +| ---------------------- | ------------------------- | +| `cf_agent_identity` | Agent name and class | +| `cf_agent_state` | Current agent state | +| `cf_agent_mcp_servers` | Connected MCP server list | State and MCP messages are also broadcast to all connections whenever they change. @@ -103,16 +103,18 @@ export class MyAgent extends Agent { ## What is and is not suppressed -| Action | Works? | -| --------------------------------------------------- | ------ | -| Receive `cf_agent_identity` on connect | **No** | -| Receive `cf_agent_state` on connect and broadcasts | **No** | +The following table shows what still works when protocol messages are suppressed for a connection: + +| Action | Works? | +| -------------------------------------------------------- | ------ | +| Receive `cf_agent_identity` on connect | **No** | +| Receive `cf_agent_state` on connect and broadcasts | **No** | | Receive `cf_agent_mcp_servers` on connect and broadcasts | **No** | -| Send and receive regular WebSocket messages | Yes | -| Call `@callable()` RPC methods | Yes | -| Receive `this.broadcast()` messages | Yes | -| Send binary data | Yes | -| Mutate agent state via RPC | Yes | +| Send and receive regular WebSocket messages | Yes | +| Call `@callable()` RPC methods | Yes | +| Receive `this.broadcast()` messages | Yes | +| Send binary data | Yes | +| Mutate agent state via RPC | Yes | ## Combining with readonly @@ -144,7 +146,8 @@ export class SensorHub extends Agent { async reportReading(sensorId: string, value: number) { // This RPC still works for readonly+no-protocol connections // because it writes to SQL, not agent state - this.sql`INSERT INTO readings (sensor_id, value, ts) VALUES (${sensorId}, ${value}, ${Date.now()})`; + this + .sql`INSERT INTO readings (sensor_id, value, ts) VALUES (${sensorId}, ${value}, ${Date.now()})`; } } ``` @@ -159,10 +162,10 @@ Both flags are stored in the connection's WebSocket attachment and hidden from ` An overridable hook that determines if a connection should receive protocol messages when it connects. -| Parameter | Type | Description | -| ------------ | ------------------- | ------------------------------------ | -| `connection` | `Connection` | The connecting client | -| `ctx` | `ConnectionContext` | Contains the upgrade request | +| Parameter | Type | Description | +| ------------ | ------------------- | ------------------------------------- | +| `connection` | `Connection` | The connecting client | +| `ctx` | `ConnectionContext` | Contains the upgrade request | | **Returns** | `boolean` | `false` to suppress protocol messages | Default: returns `true` (all connections receive protocol messages). @@ -173,9 +176,9 @@ This hook is evaluated once on connect. The result is persisted in the connectio Check if a connection currently has protocol messages enabled. -| Parameter | Type | Description | -| ------------ | ------------ | ----------------------- | -| `connection` | `Connection` | The connection to check | +| Parameter | Type | Description | +| ------------ | ------------ | --------------------------------------- | +| `connection` | `Connection` | The connection to check | | **Returns** | `boolean` | `true` if protocol messages are enabled | Safe to call at any time, including after the agent wakes from hibernation. From 008aad790f136a17b60bff746eca5224eb968126 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 17 Feb 2026 11:10:17 +0000 Subject: [PATCH 9/9] docs(agents): add data parts section to chat agents reference Document data-* stream parts: server-side writer.write() usage, three patterns (reconciliation, append, transient), typed client access via UIMessage generics, and onData callback for transient parts. Co-authored-by: Cursor --- .../docs/agents/api-reference/chat-agents.mdx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/content/docs/agents/api-reference/chat-agents.mdx b/src/content/docs/agents/api-reference/chat-agents.mdx index a2bf114fcbd..97b2d5c817e 100644 --- a/src/content/docs/agents/api-reference/chat-agents.mdx +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -24,6 +24,7 @@ Built on the [AI SDK](https://ai-sdk.dev) and Cloudflare Durable Objects, you ge - **Resumable streaming** — disconnected clients resume mid-stream without data loss - **Real-time sync** — messages broadcast to all connected clients via WebSocket - **Tool support** — server-side, client-side, and human-in-the-loop tool patterns +- **Data parts** — attach typed JSON (citations, progress, usage) to messages alongside text - **Row size protection** — automatic compaction when messages approach SQLite limits ## Quick start @@ -613,6 +614,144 @@ const { messages, sendMessage } = useAgentChat({ +## Data parts + +Data parts let you attach typed JSON to messages alongside text — progress indicators, source citations, token usage, or any structured data your UI needs. + +### Writing data parts (server) + +Use `createUIMessageStream` with `writer.write()` to send data parts from the server: + + + +```ts +import { + streamText, + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, +} from "ai"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const stream = createUIMessageStream({ + execute: async ({ writer }) => { + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), + }); + + // Merge the LLM stream + writer.merge(result.toUIMessageStream()); + + // Write a data part — persisted to message.parts + writer.write({ + type: "data-sources", + id: "src-1", + data: { query: "agents", status: "searching", results: [] }, + }); + + // Later: update the same part in-place (same type + id) + writer.write({ + type: "data-sources", + id: "src-1", + data: { + query: "agents", + status: "found", + results: ["Agents SDK docs", "Durable Objects guide"], + }, + }); + }, + }); + + return createUIMessageStreamResponse({ stream }); + } +} +``` + + + +### Three patterns + +| Pattern | How | Persisted? | Use case | +| --- | --- | --- | --- | +| **Reconciliation** | Same `type` + `id` → updates in-place | Yes | Progressive state (searching → found) | +| **Append** | No `id`, or different `id` → appends | Yes | Log entries, multiple citations | +| **Transient** | `transient: true` → not added to `message.parts` | No | Ephemeral status (thinking indicator) | + +Transient parts are broadcast to connected clients in real time but excluded from SQLite persistence and `message.parts`. Use the `onData` callback to consume them. + +### Reading data parts (client) + +Non-transient data parts appear in `message.parts`. Use the `UIMessage` generic to type them: + + + +```ts +import { useAgentChat } from "@cloudflare/ai-chat/react"; +import type { UIMessage } from "ai"; + +type ChatMessage = UIMessage< + unknown, + { + sources: { query: string; status: string; results: string[] }; + usage: { model: string; inputTokens: number; outputTokens: number }; + } +>; + +const { messages } = useAgentChat({ agent }); + +// Typed access — no casts needed +for (const msg of messages) { + for (const part of msg.parts) { + if (part.type === "data-sources") { + console.log(part.data.results); // string[] + } + } +} +``` + + + +### Transient parts with `onData` + +Transient data parts are not in `message.parts`. Use the `onData` callback instead: + + + +```ts +const [thinking, setThinking] = useState(false); + +const { messages } = useAgentChat({ + agent, + onData(part) { + if (part.type === "data-thinking") { + setThinking(true); + } + }, +}); +``` + + + +On the server, write transient parts with `transient: true`: + + + +```ts +writer.write({ + transient: true, + type: "data-thinking", + data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() }, +}); +``` + + + +`onData` fires on all code paths — new messages, stream resumption, and cross-tab broadcasts. + ## Resumable streaming Streams automatically resume when a client disconnects and reconnects. No configuration is needed — it works out of the box.