diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 2185b6884b8..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/) | @@ -143,7 +144,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 @@ -179,7 +180,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/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/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..97b2d5c817e --- /dev/null +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -0,0 +1,1200 @@ +--- +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 +- **Data parts** — attach typed JSON (citations, progress, usage) to messages alongside text +- **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 + +Override `onConnect` and `onClose` to add custom logic. Stream resumption and message sync are handled for you: + + + +```ts +export class ChatAgent extends AIChatAgent { + async onConnect(connection, ctx) { + // Your custom logic (e.g., logging, auth checks) + console.log("Client connected:", connection.id); + // Stream resumption and message sync are handled automatically + } + + async onClose(connection, code, reason, wasClean) { + console.log("Client disconnected:", connection.id); + // Connection cleanup is handled automatically + } +} +``` + + + +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() }, + }), +}); +``` + + + +## 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. + +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 it manually. + +### 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..f8fc9a7fc61 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; ``` @@ -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://... @@ -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" }], @@ -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/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/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/mcp-client-api.mdx b/src/content/docs/agents/api-reference/mcp-client-api.mdx index 881020d740c..ad8e94c1a62 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: @@ -669,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/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/protocol-messages.mdx b/src/content/docs/agents/api-reference/protocol-messages.mdx new file mode 100644 index 00000000000..07f2436d6ea --- /dev/null +++ b/src/content/docs/agents/api-reference/protocol-messages.mdx @@ -0,0 +1,202 @@ +--- +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 + +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 | + +## 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/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/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/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx index 043bb7e9ad0..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()`. 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 @@ -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/retries.mdx b/src/content/docs/agents/api-reference/retries.mdx new file mode 100644 index 00000000000..8f45646a147 --- /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()` retries up to three times 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/store-and-sync-state.mdx b/src/content/docs/agents/api-reference/store-and-sync-state.mdx index 447e4335afb..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) @@ -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/api-reference/websockets.mdx b/src/content/docs/agents/api-reference/websockets.mdx index 172cdd46e20..5ff4c325347 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`): @@ -149,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: @@ -191,6 +293,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..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 @@ -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; ``` @@ -210,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. @@ -367,7 +358,7 @@ export default { // ... rest of routing }, -}; +} satisfies ExportedHandler; ``` @@ -409,7 +400,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/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/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..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: () => { @@ -370,7 +370,7 @@ export default { new Response("Not found", { status: 404 }) ); }, -}; +} satisfies ExportedHandler; ``` 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/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/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 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; ``` 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,