config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config

gitconfig: name/email from chezmoi data, gh credential helper by PATH
lookup (portable to darwin). claude: no credentials, history, projects,
sessions, or plugin caches. ssh config as private_ (0600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Prad NukalaandClaude Fable 5 committed 2026-08-09 14:53:46 -04:00
1 parent 3c691e8dc5
commit 20891f3910
476 files changed
+59086 -7

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
---
name: agents-sdk
description: Build AI agents on Cloudflare Workers using the Agents SDK. Load when creating stateful agents, durable workflows, real-time WebSocket apps, scheduled tasks, MCP servers, chat applications, voice agents, or browser automation. Covers Agent class, state management, callable RPC, Workflows, durable execution, queues, retries, observability, and React hooks. Biases towards retrieval from Cloudflare docs over pre-trained knowledge.
---
# Cloudflare Agents SDK
Your knowledge of the Agents SDK may be outdated. **Prefer retrieval over pre-training** for any Agents SDK task.
## Retrieval Sources
Cloudflare docs: https://developers.cloudflare.com/agents/
| Topic | Docs URL | Use for |
|-------|----------|---------|
| Getting started | [Quick start](https://developers.cloudflare.com/agents/getting-started/quick-start/) | First agent, project setup |
| Adding to existing project | [Add to existing project](https://developers.cloudflare.com/agents/getting-started/add-to-existing-project/) | Install into existing Workers app |
| Configuration | [Configuration](https://developers.cloudflare.com/agents/api-reference/configuration/) | `wrangler.jsonc`, bindings, assets, deployment |
| Agent class | [Agents API](https://developers.cloudflare.com/agents/api-reference/agents-api/) | Agent lifecycle, patterns, pitfalls |
| State | [Store and sync state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) | `setState`, `validateStateChange`, persistence |
| Routing | [Routing](https://developers.cloudflare.com/agents/api-reference/routing/) | URL patterns, `routeAgentRequest` |
| Callable methods | [Callable methods](https://developers.cloudflare.com/agents/api-reference/callable-methods/) | `@callable`, RPC, streaming, timeouts |
| Scheduling | [Schedule tasks](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) | `schedule()`, `scheduleEvery()`, cron |
| Workflows | [Run workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/) | `AgentWorkflow`, durable multi-step tasks |
| HTTP/WebSockets | [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/) | Lifecycle hooks, hibernation |
| Chat agents | [Chat agents](https://developers.cloudflare.com/agents/api-reference/chat-agents/) | `AIChatAgent`, streaming, tools, persistence |
| Client SDK | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/) | `useAgent`, `useAgentChat`, React hooks |
| Client tools | [Client tools](https://developers.cloudflare.com/agents/api-reference/client-tools/) | Client-side tools, `autoContinueAfterToolResult` |
| Server-driven messages | [Trigger patterns](https://developers.cloudflare.com/agents/api-reference/trigger-patterns/) | `saveMessages`, `waitUntilStable`, server-initiated turns |
| Resumable streaming | [Resumable streaming](https://developers.cloudflare.com/agents/api-reference/resumable-streaming/) | Stream recovery on disconnect |
| Email | [Email](https://developers.cloudflare.com/agents/api-reference/email/) | Email routing, secure reply resolver |
| MCP client | [MCP client](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) | Connecting to MCP servers |
| MCP server | [MCP server](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) | Building MCP servers with `McpAgent` |
| MCP transports | [MCP transports](https://developers.cloudflare.com/agents/api-reference/mcp-transports/) | Streamable HTTP, SSE, RPC transport options |
| Securing MCP servers | [Securing MCP](https://developers.cloudflare.com/agents/api-reference/securing-mcp-servers/) | OAuth, proxy MCP, hardening |
| Human-in-the-loop | [Human-in-the-loop](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) | Approval flows, `needsApproval`, workflows |
| Durable execution | [Durable execution](https://developers.cloudflare.com/agents/api-reference/durable-execution/) | `runFiber()`, `stash()`, surviving DO eviction |
| Queue | [Queue](https://developers.cloudflare.com/agents/api-reference/queue-tasks/) | Built-in FIFO queue, `queue()` |
| Retries | [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) | `this.retry()`, backoff/jitter |
| Observability | [Observability](https://developers.cloudflare.com/agents/api-reference/observability/) | Diagnostics-channel events |
| Push notifications | [Push notifications](https://developers.cloudflare.com/agents/api-reference/push-notifications/) | Web Push + VAPID from agents |
| Webhooks | [Webhooks](https://developers.cloudflare.com/agents/api-reference/webhooks/) | Receiving external webhooks |
| Cross-domain auth | [Cross-domain auth](https://developers.cloudflare.com/agents/api-reference/cross-domain-authentication/) | WebSocket auth, tokens, CORS |
| Readonly connections | [Readonly](https://developers.cloudflare.com/agents/api-reference/readonly-connections/) | `shouldConnectionBeReadonly` |
| Voice | [Voice](https://developers.cloudflare.com/agents/api-reference/voice/) | Experimental STT/TTS, `withVoice` |
| Browse the web | [Browser tools](https://developers.cloudflare.com/agents/api-reference/browse-the-web/) | Experimental CDP browser automation |
| Think | [Think](https://developers.cloudflare.com/agents/api-reference/think/) | Experimental higher-level chat agent class |
| Migrations | [AI SDK v5](https://developers.cloudflare.com/agents/guides/migration-to-ai-sdk-v5/), [AI SDK v6](https://developers.cloudflare.com/agents/guides/migration-to-ai-sdk-v6/) | Upgrading `@cloudflare/ai-chat` |
## Capabilities
The Agents SDK provides:
- **Persistent state** — SQLite-backed, auto-synced to clients via `setState`
- **Callable RPC** — `@callable()` methods invoked over WebSocket
- **Scheduling** — One-time, recurring (`scheduleEvery`), and cron tasks
- **Workflows** — Durable multi-step background processing via `AgentWorkflow`
- **Durable execution** — `runFiber()` / `stash()` for work that survives DO eviction
- **Queue** — Built-in FIFO queue with retries via `queue()`
- **Retries** — `this.retry()` with exponential backoff and jitter
- **MCP integration** — Connect to MCP servers or build your own with `McpAgent`
- **Email handling** — Receive and reply to emails with secure routing
- **Streaming chat** — `AIChatAgent` with resumable streams, message persistence, tools
- **Server-driven messages** — `saveMessages`, `waitUntilStable` for proactive agent turns
- **React hooks** — `useAgent`, `useAgentChat` for client apps
- **Observability** — `diagnostics_channel` events for state, RPC, schedule, lifecycle
- **Push notifications** — Web Push + VAPID delivery from agents
- **Webhooks** — Receive and verify external webhooks
- **Voice** (experimental) — STT/TTS via `@cloudflare/voice`
- **Browser tools** (experimental) — CDP-powered browsing via `agents/browser`
- **Think** (experimental) — Higher-level chat agent via `@cloudflare/think`
## FIRST: Verify Installation
```bash
npm ls agents # Should show agents package
```
If not installed:
```bash
npm install agents
```
For chat agents:
```bash
npm install agents @cloudflare/ai-chat ai @ai-sdk/react
```
## Wrangler Configuration
```jsonc
{
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
}
```
**Gotchas:**
- Do NOT enable `experimentalDecorators` in tsconfig (breaks `@callable`)
- Never edit old migrations — always add new tags
- Each agent class needs its own DO binding + migration entry
- Add `"ai": { "binding": "AI" }` for Workers AI
## Agent Class
```typescript
import { Agent, routeAgentRequest, callable } from "agents";
type State = { count: number };
export class Counter extends Agent<Env, State> {
initialState = { count: 0 };
validateStateChange(nextState: State, source: Connection | "server") {
if (nextState.count < 0) throw new Error("Count cannot be negative");
}
onStateUpdate(state: State, source: Connection | "server") {
console.log("State updated:", state);
}
@callable()
increment() {
this.setState({ count: this.state.count + 1 });
return this.state.count;
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
## Routing
Requests route to `/agents/{agent-name}/{instance-name}`:
| Class | URL |
|-------|-----|
| `Counter` | `/agents/counter/user-123` |
| `ChatRoom` | `/agents/chat-room/lobby` |
Client: `useAgent({ agent: "Counter", name: "user-123" })`
Custom routing: use `getAgentByName(env.MyAgent, "instance-id")` then `agent.fetch(request)`.
## Core APIs
| Task | API |
|------|-----|
| Read state | `this.state.count` |
| Write state | `this.setState({ count: 1 })` |
| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` |
| Schedule (delay) | `await this.schedule(60, "task", payload)` |
| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` |
| Schedule (interval) | `await this.scheduleEvery(30, "poll")` |
| RPC method | `@callable() myMethod() { ... }` |
| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` |
| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` |
| Durable fiber | `await this.runFiber("name", async (ctx) => { ... })` |
| Enqueue work | `this.queue("handler", payload)` |
| Retry with backoff | `await this.retry(fn, { maxAttempts: 5 })` |
| Broadcast to clients | `this.broadcast(message)` |
| Get connections | `this.getConnections(tag?)` |
## React Client
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setLocalState] = useState({ count: 0 });
const agent = useAgent({
agent: "Counter",
name: "my-instance",
onStateUpdate: (newState) => setLocalState(newState),
onIdentity: (name, agentType) => console.log(`Connected to ${name}`)
});
return (
<button onClick={() => agent.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>
);
}
```
## References
### Core
- **[references/state-scheduling.md](references/state-scheduling.md)** — State persistence, scheduling, SQL
- **[references/callable.md](references/callable.md)** — RPC methods, streaming, timeouts
- **[references/routing.md](references/routing.md)** — URL patterns, custom routing, `getAgentByName`
- **[references/configuration.md](references/configuration.md)** — Wrangler config, bindings, Vite setup
### Chat & Streaming
- **[references/streaming-chat.md](references/streaming-chat.md)** — AIChatAgent, resumable streams, tools
- **[references/client-sdk.md](references/client-sdk.md)** — `useAgent`, `useAgentChat`, `AgentClient`
- **[references/server-driven-messages.md](references/server-driven-messages.md)** — Trigger patterns, `saveMessages`
- **[references/human-in-the-loop.md](references/human-in-the-loop.md)** — Approval flows, `needsApproval`
### Background Processing
- **[references/workflows.md](references/workflows.md)** — Durable Workflows integration
- **[references/durable-execution.md](references/durable-execution.md)** — `runFiber`, `stash`, surviving eviction
- **[references/queue-retries.md](references/queue-retries.md)** — Built-in queue, retry with backoff
### Integrations
- **[references/mcp.md](references/mcp.md)** — MCP client and server, transports, securing
- **[references/email.md](references/email.md)** — Email routing and handling
- **[references/webhooks-push.md](references/webhooks-push.md)** — Webhooks, push notifications
- **[references/observability.md](references/observability.md)** — Diagnostics-channel events
### Experimental
- **[references/think.md](references/think.md)** — `@cloudflare/think` higher-level chat agent
- **[references/voice.md](references/voice.md)** — `@cloudflare/voice` STT/TTS
- **[references/codemode.md](references/codemode.md)** — Code Mode for tool orchestration
- **[references/browse-the-web.md](references/browse-the-web.md)** — CDP browser tools
@@ -0,0 +1,63 @@
# Browse the Web (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/browse-the-web/ for complete documentation.
CDP-powered browser tools that let agents scrape, screenshot, and interact with web pages.
## Setup
```jsonc
// wrangler.jsonc
{
"browser": { "binding": "BROWSER" },
"worker_loaders": [{ "binding": "LOADER" }],
"compatibility_flags": ["nodejs_compat"]
}
```
## Usage with AI SDK
```typescript
import { createBrowserTools } from "agents/browser/ai";
export class MyAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const browserTools = createBrowserTools({
browser: this.env.BROWSER,
loader: this.env.LOADER
});
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools: { ...myTools, ...browserTools },
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## Available Tools
| Tool | Purpose |
|------|---------|
| `browser_search` | Search the web and return results |
| `browser_execute` | Navigate to URL, execute JS, return results |
The LLM writes async JavaScript IIFEs that run in a fresh browser session.
## When to Use
- Need a real browser (JS rendering, screenshots, interaction) → browser tools
- Just need HTML/API data → use `fetch()` instead (faster, cheaper)
## Low-Level API
```typescript
import { connectBrowser, CdpSession } from "agents/browser";
const browser = await connectBrowser(this.env.BROWSER);
const cdp = new CdpSession(browser);
await cdp.send("Page.navigate", { url: "https://example.com" });
```
@@ -0,0 +1,92 @@
# Callable Methods
Fetch https://developers.cloudflare.com/agents/api-reference/callable-methods/ for complete documentation.
## Overview
`@callable()` exposes agent methods to clients via WebSocket RPC.
```typescript
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
@callable()
async processData(data: unknown): Promise<Result> {
// Long-running work
return result;
}
}
```
## Client Usage
```typescript
// Basic call
const greeting = await agent.call("greet", ["World"]);
// With timeout
const result = await agent.call("processData", [data], {
timeout: 5000 // 5 second timeout
});
```
## Streaming Responses
```typescript
import { Agent, callable, StreamingResponse } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable({ streaming: true })
async streamResults(stream: StreamingResponse, query: string) {
for await (const item of fetchResults(query)) {
stream.send(JSON.stringify(item));
}
stream.close();
}
@callable({ streaming: true })
async streamWithError(stream: StreamingResponse) {
try {
// ... work
} catch (error) {
stream.error(error.message); // Signal error to client
return;
}
stream.close();
}
}
```
Client with streaming:
```typescript
await agent.call("streamResults", ["search term"], {
stream: {
onChunk: (data) => console.log("Chunk:", data),
onDone: () => console.log("Complete"),
onError: (error) => console.error("Error:", error)
}
});
```
## Introspection
```typescript
// Get list of callable methods on an agent
const methods = await agent.call("getCallableMethods", []);
// Returns: ["greet", "processData", "streamResults", ...]
```
## When to Use
| Scenario | Use |
|----------|-----|
| Browser/mobile calling agent | `@callable()` |
| External service calling agent | `@callable()` |
| Worker calling agent (same codebase) | DO RPC directly |
| Agent calling another agent | `getAgentByName()` + DO RPC |
@@ -0,0 +1,110 @@
# Client SDK
Fetch https://developers.cloudflare.com/agents/api-reference/client-sdk/ for complete documentation.
## React: `useAgent`
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setState] = useState({ count: 0 });
const agent = useAgent({
agent: "Counter",
name: "my-instance",
onStateUpdate: (newState) => setState(newState),
onIdentity: (name, agentType) => console.log(`Connected to ${name}`)
});
return <button onClick={() => agent.setState({ count: state.count + 1 })}>
{state.count}
</button>;
}
```
### Typed RPC via `stub`
```tsx
const agent = useAgent<typeof MyAgent>({
agent: "MyAgent",
name: "default"
});
const result = await agent.stub.myMethod(arg1, arg2);
```
### Auth via Query Params
```tsx
useAgent({
agent: "MyAgent",
name: "default",
query: async () => `token=${await getToken()}`,
queryDeps: [tokenVersion]
});
```
## React: `useAgentChat`
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent", name: "session-1" });
const { messages, input, handleInputChange, handleSubmit, status } =
useAgentChat({ agent });
return (
<div>
{messages.map((m) => <div key={m.id}>{m.role}: {m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}
```
## Vanilla JS: `AgentClient`
```typescript
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "MyAgent",
name: "default",
host: "https://my-worker.workers.dev"
});
client.addEventListener("stateUpdate", (e) => console.log(e.state));
const result = await client.call("myMethod", [arg]);
client.close();
```
## `agentFetch` for HTTP-only
```typescript
import { agentFetch } from "agents/client";
const response = await agentFetch({
agent: "MyAgent",
name: "default",
host: "https://my-worker.workers.dev",
path: "/api/data"
});
```
## Streaming RPC
```typescript
await agent.call("streamResults", ["query"], {
stream: {
onChunk: (data) => console.log(data),
onDone: () => console.log("done"),
onError: (err) => console.error(err)
}
});
```
@@ -0,0 +1,110 @@
# Codemode (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/codemode/ for complete documentation.
Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. The LLM gets a single "write code" tool; generated JavaScript runs in an isolated Worker sandbox.
## When to Use
| Scenario | Use Codemode? |
|----------|---------------|
| Single tool call | No — standard tool calling is simpler |
| Chained tool calls with logic | Yes |
| Conditional logic across tools | Yes |
| MCP multi-server workflows | Yes |
| Simple Q&A chat | No |
## Setup
### Wrangler Config
```jsonc
{
"worker_loaders": [{ "binding": "LOADER" }],
"compatibility_flags": ["nodejs_compat"]
}
```
### Install
```bash
npm install @cloudflare/codemode ai zod
```
## Usage
```typescript
import { createCodeTool } from "@cloudflare/codemode/ai";
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import { streamText, tool, convertToModelMessages } from "ai";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather: ${location} 72°F`
}),
sendEmail: tool({
description: "Send an email",
inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
execute: async ({ to, subject, body }) => `Email sent to ${to}`
})
};
export class MyAgent extends Agent<Env, State> {
async onChatMessage() {
const executor = new DynamicWorkerExecutor({
loader: this.env.LOADER
});
const codemode = createCodeTool({ tools, executor });
const result = streamText({
model,
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
tools: { codemode }
});
return result.toUIMessageStreamResponse();
}
}
```
## With MCP Tools
```typescript
const codemode = createCodeTool({
tools: {
...myTools,
...this.mcp.getAITools()
},
executor
});
```
## How It Works
1. `createCodeTool` generates TypeScript type definitions from your tools
2. The LLM writes an async arrow function calling `codemode.toolName(args)`
3. Code runs in an isolated Worker sandbox via `DynamicWorkerExecutor`
4. Tool calls route back to the host via Workers RPC
5. External `fetch()` is blocked by default — sandbox can only call your tools
## Network Isolation
```typescript
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
globalOutbound: null // default — fully isolated
// globalOutbound: env.MY_SERVICE // route through a Fetcher
});
```
## Limitations
- Experimental — API may change
- `needsApproval` tools execute immediately in sandbox (no approval pause yet)
- JavaScript execution only
- Requires `worker_loaders` binding
@@ -0,0 +1,72 @@
# Configuration
Fetch https://developers.cloudflare.com/agents/api-reference/configuration/ for complete documentation.
## Wrangler Config (`wrangler.jsonc`)
```jsonc
{
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-01-28",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "MyAgent", "class_name": "MyAgent" },
{ "name": "ChatAgent", "class_name": "ChatAgent" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyAgent", "ChatAgent"] }
],
"ai": { "binding": "AI" },
"assets": {
"directory": "./dist/client",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": true
}
}
```
## Key Rules
- Every agent class needs a DO binding AND a `new_sqlite_classes` migration entry
- `nodejs_compat` is required
- Never edit old migrations — add a new tag (e.g. `v2`) for new classes
- Do NOT enable `experimentalDecorators` in tsconfig — it breaks `@callable`
- For Workers AI locally, set `"ai": { "binding": "AI", "remote": true }` in `.dev.vars` or config
- Use `wrangler secret put` for secrets, never hardcode them
## Vite Setup
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";
import { agents } from "agents/vite";
export default defineConfig({
plugins: [react(), cloudflare(), agents()]
});
```
## Type Generation
```bash
npx wrangler types
```
This generates `env.d.ts` with typed bindings. Regenerate after changing `wrangler.jsonc`.
## tsconfig
Extend the agents tsconfig for correct settings:
```jsonc
{
"extends": ["agents/tsconfig"],
"include": ["src/**/*.ts", "src/**/*.tsx"],
"compilerOptions": { "paths": { "~/*": ["./src/*"] } }
}
```
@@ -0,0 +1,51 @@
# Durable Execution
Fetch https://developers.cloudflare.com/agents/api-reference/durable-execution/ for complete documentation.
Fibers let agent work survive Durable Object eviction. Progress is checkpointed to SQLite; on recovery, you decide what to do.
## `runFiber`
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
await this.runFiber("process-data", async (ctx) => {
const step1 = await fetchData();
ctx.stash({ step: 1, data: step1 });
const step2 = await transform(step1);
ctx.stash({ step: 2, result: step2 });
this.setState({ result: step2 });
});
return new Response("Started");
}
async onFiberRecovered(ctx) {
const checkpoint = ctx.stash;
if (checkpoint.step === 1) {
const step2 = await transform(checkpoint.data);
this.setState({ result: step2 });
}
}
}
```
## Key APIs
| API | Purpose |
|-----|---------|
| `this.runFiber(name, fn)` | Start a named fiber |
| `ctx.stash` / `this.stash` | Read latest checkpoint |
| `ctx.stash = data` | Write checkpoint (JSON-serializable) |
| `onFiberRecovered(ctx)` | Called on DO restart if fiber was in-flight |
| `keepAlive()` | Prevent hibernation while fiber runs |
| `keepAliveWhile(fn)` | Keep alive for duration of async function |
## Important
- `stash` replaces the entire checkpoint — not a merge
- The lambda is NOT restored on recovery — only the stash data is. You must re-derive what to do in `onFiberRecovered`
- No auto-retry on throw — handle errors yourself
- For long-running pipelines with automatic retries, use Workflows instead
- Filter concurrent fibers by `ctx.name` in `onFiberRecovered`
@@ -0,0 +1,146 @@
# Email Handling
Fetch https://developers.cloudflare.com/agents/api-reference/email/ for complete documentation.
## Overview
Agents receive and reply to emails via Cloudflare Email Routing.
## Wrangler Configuration
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [
{ "name": "SEB", "destination_address": "reply@yourdomain.com" }
]
}
```
## Basic Email Handler
```typescript
import { Agent } from "agents";
import { type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent<Env, State> {
async onEmail(email: AgentEmail) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log("From:", email.from);
console.log("Subject:", parsed.subject);
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${parsed.subject}`,
body: "Thanks for your email!"
});
}
}
```
## Routing Emails
```typescript
import { routeAgentRequest, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent")
});
},
async fetch(request, env) {
return routeAgentRequest(request, env) ?? new Response("Not found", { status: 404 });
}
};
```
## Resolvers
### Address-Based (Inbound Mail)
Routes based on recipient address:
```typescript
import { createAddressBasedEmailResolver } from "agents/email";
const resolver = createAddressBasedEmailResolver("EmailAgent");
// support@example.com → EmailAgent, instance "support"
// NotificationAgent+user123@example.com → NotificationAgent, instance "user123"
```
### Secure Reply (Reply Flows)
Verifies replies are authentic using HMAC-SHA256 signatures:
```typescript
import { createSecureReplyEmailResolver } from "agents/email";
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
maxAge: 7 * 24 * 60 * 60, // 7 days (default: 30 days)
onInvalidSignature: (email, reason) => {
console.warn(`Invalid signature from ${email.from}: ${reason}`);
}
});
```
Sign outbound emails to enable secure reply routing:
```typescript
await this.replyToEmail(email, {
fromName: "My Agent",
body: "Thanks!",
secret: this.env.EMAIL_SECRET // Signs headers for secure reply routing
});
```
### Catch-All (Single Instance)
Routes all emails to one agent instance:
```typescript
import { createCatchAllEmailResolver } from "agents/email";
const resolver = createCatchAllEmailResolver("EmailAgent", "default");
```
### Combining Resolvers
```typescript
async email(message, env) {
const secureReply = createSecureReplyEmailResolver(env.EMAIL_SECRET);
const addressBased = createAddressBasedEmailResolver("EmailAgent");
await routeAgentEmail(message, env, {
resolver: async (email, env) => {
// Try secure reply first
const result = await secureReply(email, env);
if (result) return result;
// Fall back to address-based
return addressBased(email, env);
}
});
}
```
## Utilities
```typescript
import { isAutoReplyEmail } from "agents/email";
async onEmail(email: AgentEmail) {
if (isAutoReplyEmail(email.headers)) {
// Skip auto-replies (vacation, out-of-office, etc.)
return;
}
// Process email...
}
```
@@ -0,0 +1,67 @@
# Human-in-the-Loop
Fetch https://developers.cloudflare.com/agents/concepts/human-in-the-loop/ for complete documentation.
Multiple patterns for adding human approval to agent actions.
## Decision Guide
| Pattern | Best for |
|---------|----------|
| Workflows `waitForApproval` | Long-running background tasks |
| AI SDK `needsApproval` on tools | Chat tool calls requiring approval |
| Client tools (`onToolCall`) | Tools that execute in the browser |
| MCP `elicitInput` | Gathering structured input from MCP clients |
## Workflow Approvals
```typescript
// In AgentWorkflow:
const approved = await step.waitForEvent<{ approved: boolean }>("approval", {
timeout: "7d"
});
if (!approved.approved) throw new Error("Rejected");
// From agent:
await this.approveWorkflow(workflowId);
await this.rejectWorkflow(workflowId);
```
## Chat Tool Approvals (`needsApproval`)
```typescript
const tools = {
deleteItem: tool({
description: "Delete an item",
parameters: z.object({ id: z.string() }),
execute: async ({ id }) => { /* delete */ },
needsApproval: true // or a function: (toolCall) => boolean
})
};
```
Client handles approval:
```tsx
const { addToolApprovalResponse, addToolOutput } = useAgentChat({
agent,
onToolCall: async ({ toolCall }) => {
if (confirm(`Allow ${toolCall.toolName}?`)) {
return { approve: true };
}
return { approve: false };
}
});
```
To deny with a custom message:
```tsx
addToolOutput(toolCallId, "output-error", "User rejected this action");
```
## Important
- `waitForApproval` may return `undefined` on timeout — handle it
- `addToolOutput` with `output-error` does NOT auto-continue the LLM — you may need `sendMessage` after
- For OpenAI Agents SDK, use `needsApproval` on the tool definition (same pattern)
@@ -0,0 +1,188 @@
# MCP Integration
Fetch https://developers.cloudflare.com/agents/api-reference/mcp-client-api/ and https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/ for complete documentation.
Agents include a multi-server MCP client for connecting to external MCP servers, and `McpAgent` for building MCP servers.
## Add an MCP Server
```typescript
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable()
async addServer(name: string, url: string) {
// Options-based API (recommended)
const result = await this.addMcpServer(name, url, {
callbackHost: "https://my-worker.workers.dev",
transport: { headers: { Authorization: "Bearer ..." } }
});
if (result.state === "authenticating") {
// OAuth required - redirect user to result.authUrl
return { needsAuth: true, authUrl: result.authUrl };
}
return { ready: true, id: result.id };
}
}
```
## Use MCP Tools
```typescript
async onChatMessage() {
// Get AI-compatible tools from all connected MCP servers
const mcpTools = this.mcp.getAITools();
const allTools = {
...localTools,
...mcpTools
};
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools: allTools
});
return result.toUIMessageStreamResponse();
}
```
## List MCP Resources
```typescript
// List all registered servers
const servers = this.mcp.listServers();
// List tools from all servers
const tools = this.mcp.listTools();
// List resources
const resources = this.mcp.listResources();
// List prompts
const prompts = this.mcp.listPrompts();
```
## Remove Server
```typescript
await this.removeMcpServer(serverId);
```
## Building an MCP Server
Use `McpAgent` from the SDK to create an MCP server.
**Install dependencies:**
```bash
npm install @modelcontextprotocol/sdk zod
```
**Wrangler config:**
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MyMCP", "class_name": "MyMCP" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyMCP"] }]
}
```
**Server implementation:**
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "MyMCPServer",
version: "1.0.0"
});
initialState = { counter: 0 };
async init() {
// Register a resource
this.server.resource("counter", "mcp://resource/counter", (uri) => ({
contents: [{ text: String(this.state.counter), uri: uri.href }]
}));
// Register a tool
this.server.registerTool(
"increment",
{
description: "Increment the counter",
inputSchema: { amount: z.number().default(1) }
},
async ({ amount }) => {
this.setState({ counter: this.state.counter + amount });
return {
content: [{ text: `Counter: ${this.state.counter}`, type: "text" }]
};
}
);
}
}
```
## Serve MCP Server
```typescript
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
// Streamable HTTP transport (recommended)
if (url.pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp", { binding: "MyMCP" }).fetch(request, env, ctx);
}
// SSE transport (legacy, deprecated)
if (url.pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse", { binding: "MyMCP" }).fetch(request, env, ctx);
}
return new Response("Not found", { status: 404 });
}
};
```
## Transports
Fetch https://developers.cloudflare.com/agents/api-reference/mcp-transports/ for complete documentation.
| Transport | Use for |
|-----------|---------|
| Streamable HTTP (`serve`) | External/public clients (recommended) |
| SSE (`serveSSE`) | Legacy clients only (deprecated) |
| RPC (`addMcpServer(name, env.Binding)`) | Same-Worker internal calls (fastest) |
### RPC Transport (Same Worker)
```typescript
async onStart() {
await this.addMcpServer("internal-tools", this.env.MyMCPBinding, {
props: { userId: this.name }
});
}
```
## Retry on MCP Connections
```typescript
await this.addMcpServer("tools", url, {
retry: { maxAttempts: 3, baseDelayMs: 500 }
});
```
## Securing MCP Servers
Fetch https://developers.cloudflare.com/agents/api-reference/securing-mcp-servers/ for complete documentation.
Use `@cloudflare/workers-oauth-provider` to add OAuth in front of your MCP server. See the securing docs for proxy patterns and `redirect_uri` validation.
@@ -0,0 +1,44 @@
# Observability
Fetch https://developers.cloudflare.com/agents/api-reference/observability/ for complete documentation.
Agents emit structured events via Node.js `diagnostics_channel`. Subscribe in development or forward via Tail Workers in production.
## Subscribe to Events
```typescript
import { subscribe } from "agents/observability";
subscribe("agents:rpc", (event) => {
console.log(`RPC call: ${event.payload.method}`);
});
subscribe("agents:state", (event) => {
console.log(`State change on ${event.agent}`);
});
```
## Available Channels
| Channel | Events |
|---------|--------|
| `agents:state` | State changes |
| `agents:rpc` | `@callable` invocations |
| `agents:message` | WebSocket messages |
| `agents:schedule` | Schedule triggers |
| `agents:lifecycle` | Agent start, connect, disconnect |
| `agents:workflow` | Workflow progress, completion, errors |
| `agents:mcp` | MCP server connections, tool calls |
| `agents:email` | Email received |
## Per-Agent Override
```typescript
export class MyAgent extends Agent<Env, State> {
observability = undefined; // disable for this agent
}
```
## Production: Tail Workers
In production, events appear as `diagnosticsChannelEvents` on the Tail Worker `event` object. Attach a Tail Worker to your agent's Worker to forward events to your observability platform.
@@ -0,0 +1,79 @@
# Queue & Retries
Fetch https://developers.cloudflare.com/agents/api-reference/queue-tasks/ and https://developers.cloudflare.com/agents/api-reference/retries/ for complete documentation.
## Built-in Queue
FIFO queue persisted in SQLite. Sequential processing, one item at a time.
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
this.queue("processItem", { id: "abc", data: "..." });
this.queue("processItem", { id: "def", data: "..." }, { retry: { maxAttempts: 5 } });
return new Response("Queued");
}
async processItem(payload: { id: string; data: string }, queueItem: QueueItem) {
await doWork(payload);
}
}
```
### Queue Management
```typescript
const items = this.getQueue();
const byCallback = this.getQueues("processItem");
this.dequeue(itemId);
this.dequeueAll();
this.dequeueAllByCallback("processItem");
```
## Retries
Exponential backoff with full jitter. Defaults: 3 attempts, 100ms base, 3000ms max.
```typescript
const result = await this.retry(
async () => {
const res = await fetch("https://api.example.com/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{
maxAttempts: 5,
baseDelayMs: 200,
maxDelayMs: 5000,
shouldRetry: (err, nextAttempt) => {
if (err.message.includes("429")) return true;
if (err.message.includes("401")) return false;
return nextAttempt <= 3;
}
}
);
```
### Retry on Schedules and Queue
```typescript
await this.schedule(60, "task", payload, { retry: { maxAttempts: 3 } });
await this.scheduleEvery(30, "poll", undefined, { retry: { maxAttempts: 2 } });
this.queue("handler", payload, { retry: { maxAttempts: 5 } });
```
### Class-level Defaults
```typescript
export class MyAgent extends Agent<Env, State> {
static options = {
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10000 }
};
}
```
## Important
- `shouldRetry` only works on `this.retry()` — not on schedule/queue (callbacks aren't serializable)
- Queue retries block head-of-line; long delays keep the DO awake — use `schedule` for long waits instead
- No dead-letter queue — failed items are removed after retries exhausted
@@ -0,0 +1,75 @@
# Routing
Fetch https://developers.cloudflare.com/agents/api-reference/routing/ for complete documentation.
## Default URL Pattern
`/agents/{kebab-class-name}/{instance-name}`
```typescript
import { routeAgentRequest } from "agents";
export default {
fetch: (req, env) =>
routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
| Class | URL |
|-------|-----|
| `Counter` | `/agents/counter/user-123` |
| `ChatRoom` | `/agents/chat-room/lobby` |
| `MyAgent` | `/agents/my-agent/default` |
Subpaths after the instance name (e.g. `/agents/my-agent/default/api/data`) route to `onRequest`.
## Custom Routing with `getAgentByName`
```typescript
import { getAgentByName } from "agents";
export default {
async fetch(req, env) {
const url = new URL(req.url);
if (url.pathname.startsWith("/api/")) {
const agent = getAgentByName(env.MyAgent, "singleton");
return agent.fetch(req);
}
return routeAgentRequest(req, env);
}
};
```
## Options
```typescript
routeAgentRequest(req, env, {
cors: true,
prefix: "/api/agents",
locationHint: "enam",
jurisdiction: "eu",
props: { userId: "123" },
onBeforeConnect: async (req) => { /* auth check */ },
onBeforeRequest: async (req) => { /* auth check */ }
});
```
`props` are delivered to `onStart(props)` on first access.
## Client Side
```tsx
useAgent({
agent: "MyAgent",
name: "instance-1",
host: "https://my-worker.workers.dev",
basePath: "/api/agents",
path: "/custom-subpath"
});
```
## Common Mistakes
- Class name `MyAgent` becomes kebab `my-agent` in URLs — match exactly
- "Namespace not found" error = the `class_name` in wrangler doesn't match your exported class
- If `sendIdentityOnConnect: false`, the `ready` promise on the client may never resolve — use state sync instead
@@ -0,0 +1,63 @@
# Server-Driven Messages (Trigger Patterns)
Fetch https://developers.cloudflare.com/agents/api-reference/trigger-patterns/ for complete documentation.
Patterns for server-initiated LLM turns in `AIChatAgent` — from schedules, webhooks, email, or other agents.
## `saveMessages` — Trigger an LLM Turn
```typescript
await this.saveMessages((existingMessages) => [
...existingMessages,
{ role: "user", content: "Check for new notifications" }
]);
```
`saveMessages` persists the messages AND triggers `onChatMessage`.
## `persistMessages` — Save Without Triggering
```typescript
await this.persistMessages([
...this.messages,
{ role: "assistant", content: "System note: checked at " + new Date() }
]);
```
## `waitUntilStable`
**Always call before `saveMessages` from non-chat contexts** (schedules, webhooks, email):
```typescript
async checkNotifications(payload: unknown, schedule: Schedule) {
await this.waitUntilStable({ timeout: 30_000 });
await this.saveMessages((msgs) => [
...msgs,
{ role: "user", content: "Run scheduled notification check" }
]);
}
```
## `onChatResponse`
Runs after each LLM turn completes. Use for chaining:
```typescript
async onChatResponse(result: ChatResponseResult) {
if (result.type === "finish" && needsFollowUp(result)) {
await this.saveMessages((msgs) => [
...msgs,
{ role: "user", content: "Continue with next step" }
]);
}
}
```
## Client Status
```tsx
const { isStreaming, isServerStreaming } = useAgentChat({ agent });
```
- `isStreaming` — true during any streaming (user-initiated or server-initiated)
- `isServerStreaming` — true only during server-initiated streams
@@ -0,0 +1,171 @@
# State & Scheduling
Fetch https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ and https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ for complete documentation.
## State Management
State persists to SQLite and broadcasts to connected clients automatically.
### Define Typed State
```typescript
type State = {
count: number;
items: string[];
};
export class MyAgent extends Agent<Env, State> {
initialState: State = { count: 0, items: [] };
}
```
### Read and Update
```typescript
// Read (lazy-loaded from SQLite)
const count = this.state.count;
// Write (sync, persists, broadcasts)
this.setState({ count: this.state.count + 1 });
```
### Validation Hook
`validateStateChange()` runs synchronously before state persists. Throw to reject the update.
```typescript
validateStateChange(nextState: State, source: Connection | "server") {
if (nextState.count < 0) {
throw new Error("Count cannot be negative");
}
}
```
### Execution Order
1. `validateStateChange(nextState, source)` - sync, gating
2. State persisted to SQLite
3. State broadcast to connected clients
4. `onStateUpdate(nextState, source)` - async via `ctx.waitUntil`, non-gating
### Client-Side Sync (React)
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setLocalState] = useState<State>({ count: 0 });
const agent = useAgent<State>({
agent: "MyAgent",
name: "instance-1",
onStateUpdate: (newState) => setLocalState(newState)
});
return <button onClick={() => agent.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>;
}
```
## SQL API
Direct SQLite access for custom queries:
```typescript
// Create table
this.sql`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`;
// Insert
this.sql`INSERT INTO items (id, name) VALUES (${id}, ${name})`;
// Query with types
const items = this.sql<{ id: string; name: string }>`
SELECT * FROM items WHERE name LIKE ${`%${search}%`}
`;
```
## Scheduling
### Schedule Types
| Mode | Syntax | Use Case |
|------|--------|----------|
| Delay | `this.schedule(60, ...)` | Run in 60 seconds |
| Date | `this.schedule(new Date(...), ...)` | Run at specific time |
| Cron | `this.schedule("0 8 * * *", ...)` | Recurring schedule |
| Interval | `this.scheduleEvery(30, ...)` | Fixed interval (every 30s) |
### Examples
```typescript
// Delay (seconds)
await this.schedule(60, "checkStatus", { id: "abc123" });
// Specific date
await this.schedule(new Date("2025-12-25T00:00:00Z"), "sendGreeting", { to: "user" });
// Cron (recurring)
await this.schedule("0 9 * * 1-5", "weekdayReport", {});
// Fixed interval (every 30 seconds, overlap prevention built-in)
await this.scheduleEvery(30, "pollUpdates");
await this.scheduleEvery(300, "syncData", { source: "api" });
```
### Handler
```typescript
async sendGreeting(payload: { to: string }, schedule: Schedule) {
console.log(`Sending greeting to ${payload.to}`);
// Cron schedules auto-reschedule; one-time schedules are deleted
}
```
### Manage Schedules
```typescript
const schedules = this.getSchedules();
const crons = this.getSchedules({ type: "cron" });
await this.cancelSchedule(schedule.id);
```
### Retry on Schedules
```typescript
await this.schedule(60, "task", payload, { retry: { maxAttempts: 3 } });
await this.scheduleEvery(30, "poll", undefined, { retry: { maxAttempts: 2 } });
```
## Lifecycle Callbacks
```typescript
export class MyAgent extends Agent<Env, State> {
async onStart() {
// Agent started or woke from hibernation
}
onConnect(conn: Connection, ctx: ConnectionContext) {
// WebSocket connected
}
onMessage(conn: Connection, message: WSMessage) {
// WebSocket message (non-RPC)
}
onStateUpdate(state: State, source: Connection | "server") {
// State changed (async, non-blocking)
}
onError(error: unknown) {
// Error handler
throw error; // Re-throw to propagate
}
}
```
@@ -0,0 +1,198 @@
# Streaming Chat with AIChatAgent
Fetch https://developers.cloudflare.com/agents/api-reference/chat-agents/ for complete documentation.
`AIChatAgent` from `@cloudflare/ai-chat` provides streaming chat with automatic message persistence and resumable streams.
## Basic Chat Agent
```typescript
import { AIChatAgent } from "@cloudflare/ai-chat";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
**Important:** Always pass `abortSignal` and `onFinish` — they enable proper cleanup and message persistence.
## With Tools
```typescript
import { tool } from "ai";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`
})
};
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools,
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## With Workers AI (no API keys)
```typescript
import { createWorkersAI } from "workers-ai-provider";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## Custom UI Message Stream
For more control, use `createUIMessageStream`:
```typescript
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
onFinish
});
writer.merge(result.toUIMessageStream());
}
});
return createUIMessageStreamResponse({ stream });
}
}
```
## Resumable Streaming
Streams automatically resume if client disconnects and reconnects:
1. Chunks buffered to SQLite during streaming
2. On reconnect, buffered chunks sent immediately
3. Live streaming continues from where it left off
**Enabled by default.** To disable:
```tsx
const { messages } = useAgentChat({ agent, resume: false });
```
## React Client
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function ChatUI() {
const agent = useAgent({
agent: "Chat",
name: "my-chat-session"
});
const {
messages,
input,
handleInputChange,
handleSubmit,
status
} = useAgentChat({ agent });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
disabled={status === "streaming"}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
```
## Streaming RPC Methods
For non-chat streaming, use `@callable({ streaming: true })`:
```typescript
import { Agent, callable, StreamingResponse } from "agents";
export class MyAgent extends Agent<Env> {
@callable({ streaming: true })
async streamData(stream: StreamingResponse, query: string) {
for (let i = 0; i < 10; i++) {
stream.send(`Result ${i}: ${query}`);
await sleep(100);
}
stream.close();
}
}
```
Client receives streamed messages via WebSocket RPC.
## Key Properties
| Property | Purpose |
|----------|---------|
| `this.messages` | All persisted messages |
| `maxPersistedMessages` | Limit stored messages (prune oldest) |
| `messageConcurrency` | `"queue"` (default), `"latest"`, `"merge"`, `"drop"` |
| `chatRecovery` | `"persist"` (default) or `"continue"` on reconnect |
| `waitForMcpConnections` | Wait for MCP servers before first turn |
## Status Values
`useAgentChat` status:
| Status | Meaning |
|--------|---------|
| `ready` | Idle, ready for input |
| `streaming` | Response streaming |
| `submitted` | Request sent, waiting |
| `error` | Error occurred |
Also: `isStreaming`, `isServerStreaming` for distinguishing user vs server-initiated streams.
@@ -0,0 +1,112 @@
# Think (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/think/ for complete documentation.
`@cloudflare/think` — a higher-level chat agent class that handles the `streamText` loop, tool execution, and message persistence for you. You provide `getModel()` and `getSystemPrompt()`; Think handles the rest.
```bash
npm install @cloudflare/think
```
## Minimal Agent
```typescript
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
export class MyAgent extends Think<Env> {
getModel() {
return createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct");
}
getSystemPrompt() {
return "You are a helpful assistant.";
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env)
};
```
## Wrangler Config
```jsonc
{
"compatibility_flags": ["nodejs_compat", "experimental"],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],
"ai": { "binding": "AI" }
}
```
**Note:** Think requires the `experimental` compatibility flag.
## Custom Tools
```typescript
import { tool } from "ai";
import { z } from "zod";
export class MyAgent extends Think<Env> {
getTools() {
return {
getWeather: tool({
description: "Get weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `72°F in ${city}`
})
};
}
}
```
## Lifecycle Hooks
| Hook | When | Use for |
|------|------|---------|
| `configureSession()` | Agent starts | Set up memory, context providers |
| `beforeTurn(ctx)` | Before each LLM call | Per-turn model/tools/system prompt; return `TurnConfig` |
| `onChunk(chunk)` | Each streaming chunk | Progress tracking |
| `onChatResponse(result)` | After LLM turn completes | Chaining, follow-up `saveMessages` |
| `onChatError(error)` | On LLM error | Error handling |
```typescript
async beforeTurn(ctx: TurnContext): Promise<TurnConfig> {
if (ctx.continuation) {
return { model: cheaperModel };
}
return {};
}
```
## Sub-Agents
```typescript
const child = this.subAgent(SpecialistAgent, "specialist-1");
await child.chat("Analyze this data...", (chunk) => {
// stream callback
});
```
## Client
Same React hooks as `AIChatAgent`:
```tsx
const agent = useAgent({ agent: "MyAgent", name: "session-1" });
const { messages, input, handleInputChange, handleSubmit } = useAgentChat({ agent });
```
## Think vs AIChatAgent
| | Think | AIChatAgent |
|-|-------|-------------|
| `streamText` loop | Built-in | You write it |
| Tool execution | Automatic | You wire it |
| Customization | Override hooks | Full control in `onChatMessage` |
| Built-in tools | Workspace, execute, browser | None |
| Compatibility flag | Requires `experimental` | Standard |
@@ -0,0 +1,68 @@
# Voice (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/voice/ for complete documentation.
`@cloudflare/voice` — real-time speech-to-text and text-to-speech for agents. Audio streams over WebSocket.
```bash
npm install @cloudflare/voice
```
## Server
```typescript
import { Agent } from "agents";
import { withVoice, WorkersAITTS, WorkersAINova3STT } from "@cloudflare/voice";
export class VoiceAgent extends withVoice(Agent)<Env> {
transcriber = new WorkersAINova3STT(this);
tts = new WorkersAITTS(this);
async onTurn(transcript: string, context: VoiceTurnContext) {
const result = streamText({
model: createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct"),
messages: [
{ role: "system", content: "You are a voice assistant." },
...context.conversationHistory,
{ role: "user", content: transcript }
]
});
for await (const chunk of result.textStream) {
if (context.signal.aborted) break;
context.speak(chunk);
}
}
}
```
## Lifecycle Hooks
| Hook | Purpose |
|------|---------|
| `onTurn(transcript, ctx)` | Handle transcribed speech (required) |
| `beforeCallStart(conn)` | Auth/validation before call starts |
| `onCallStart(conn)` | Call connected |
| `onCallEnd(conn)` | Call disconnected |
| `onInterrupt()` | User interrupted agent speech |
## Client (React)
```tsx
import { useVoiceAgent } from "@cloudflare/voice/react";
function VoiceUI() {
const { isConnected, isSpeaking, connect, disconnect } = useVoiceAgent({
agent: "VoiceAgent",
name: "session-1"
});
return <button onClick={isConnected ? disconnect : connect}>
{isConnected ? "End Call" : "Start Call"}
</button>;
}
```
## STT/TTS Providers
Workers AI (default), Deepgram, ElevenLabs — install the provider package and swap the `transcriber`/`tts` properties.
@@ -0,0 +1,86 @@
# Webhooks & Push Notifications
## Webhooks
Fetch https://developers.cloudflare.com/agents/api-reference/webhooks/ for complete documentation.
Route external webhooks to agent instances via `onRequest`:
```typescript
export default {
async fetch(req: Request, env: Env) {
const url = new URL(req.url);
if (url.pathname.startsWith("/webhooks/")) {
const entityId = url.pathname.split("/")[2];
const agent = getAgentByName(env.MyAgent, entityId);
return agent.fetch(req);
}
return routeAgentRequest(req, env);
}
};
```
In the agent:
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
const signature = request.headers.get("X-Signature");
if (!verifySignature(signature, await request.text(), this.env.WEBHOOK_SECRET)) {
return new Response("Unauthorized", { status: 401 });
}
const payload = JSON.parse(await request.text());
this.queue("processWebhook", payload);
return new Response("OK", { status: 202 });
}
}
```
**Tips:** Respond quickly (200/202), verify signatures, deduplicate with stored event IDs, use `queue()` for async processing.
## Push Notifications
Fetch https://developers.cloudflare.com/agents/api-reference/push-notifications/ for complete documentation.
Web Push via VAPID from agents. Store subscriptions in agent state, send via `web-push`.
```bash
npm install web-push
```
```typescript
import webpush from "web-push";
export class NotifyAgent extends Agent<Env, State> {
@callable()
async subscribe(subscription: PushSubscription) {
this.setState({
...this.state,
subscriptions: [...this.state.subscriptions, subscription]
});
}
async sendReminder(payload: { message: string }, schedule: Schedule) {
for (const sub of this.state.subscriptions) {
try {
await webpush.sendNotification(sub, JSON.stringify({
title: "Reminder",
body: payload.message
}), {
vapidDetails: {
subject: "mailto:you@example.com",
publicKey: this.env.VAPID_PUBLIC_KEY,
privateKey: this.env.VAPID_PRIVATE_KEY
}
});
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 410) {
// Remove expired subscription
}
}
}
}
}
```
VAPID keys: generate with `npx web-push generate-vapid-keys`, store as secrets.
@@ -0,0 +1,132 @@
# Workflows Integration
Fetch https://developers.cloudflare.com/agents/api-reference/run-workflows/ for complete documentation.
## Overview
Agents handle real-time communication; Workflows handle durable execution. Together they enable:
- Long-running background tasks with automatic retries
- Human-in-the-loop approval flows
- Multi-step pipelines that survive failures
| Use Case | Recommendation |
|----------|----------------|
| Chat/messaging | Agent only |
| Quick API calls (<30s) | Agent only |
| Background processing (<30s) | Agent `queue()` |
| Long-running tasks (>30s) | Agent + Workflow |
| Human approval flows | Agent + Workflow |
## AgentWorkflow Base Class
```typescript
import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";
type TaskParams = { taskId: string; data: string };
export class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {
async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {
const params = event.payload;
// Durable step - retries on failure
const result = await step.do("process", async () => {
return processData(params.data);
});
// Non-durable: progress reporting
await this.reportProgress({ step: "process", percent: 0.5 });
// Non-durable: broadcast to connected clients
this.broadcastToClients({ type: "update", taskId: params.taskId });
// Durable: merge state via step
await step.mergeAgentState({ lastProcessed: params.taskId });
// Durable: report completion
await step.reportComplete(result);
return result;
}
}
```
## Wrangler Configuration
```jsonc
{
"workflows": [
{ "name": "processing-workflow", "binding": "PROCESSING_WORKFLOW", "class_name": "ProcessingWorkflow" }
],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
}
```
## Agent Methods for Workflows
```typescript
// Start a workflow
const instance = await this.runWorkflow("ProcessingWorkflow", { taskId: "123", data: "..." });
// Send event to waiting workflow
await this.sendWorkflowEvent("ProcessingWorkflow", workflowId, { type: "approve" });
// Query workflows
const workflow = await this.getWorkflow(workflowId);
const workflows = await this.getWorkflows({ status: "running" });
// Control workflows
await this.approveWorkflow(workflowId);
await this.rejectWorkflow(workflowId);
await this.terminateWorkflow(workflowId);
await this.pauseWorkflow(workflowId);
await this.resumeWorkflow(workflowId);
// Delete workflows
await this.deleteWorkflow(workflowId);
await this.deleteWorkflows({ status: "complete", before: new Date(...) });
```
## Lifecycle Callbacks
```typescript
export class MyAgent extends Agent<Env, State> {
async onWorkflowProgress(workflowName: string, workflowId: string, progress: unknown) {
// Workflow reported progress via this.reportProgress()
this.broadcast({ type: "progress", workflowId, progress });
}
async onWorkflowComplete(workflowName: string, workflowId: string, result?: unknown) {
// Workflow finished successfully
}
async onWorkflowError(workflowName: string, workflowId: string, error: Error) {
// Workflow failed
}
async onWorkflowEvent(workflowName: string, workflowId: string, event: unknown) {
// Workflow received an event via sendWorkflowEvent()
}
}
```
## Human-in-the-Loop
```typescript
// In workflow: wait for approval
const approved = await step.waitForEvent<{ approved: boolean }>("approval", {
timeout: "7d"
});
if (!approved.approved) {
throw new Error("Rejected");
}
// From agent: approve or reject
await this.approveWorkflow(workflowId); // Sends { approved: true }
await this.rejectWorkflow(workflowId); // Sends { approved: false }
```