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

@@ -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 }
```