mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 19:32:43 +00:00
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:
1 parent
3c691e8dc5
commit
20891f3910
476 files changed
+59086
-7
No files matched your search
@@ -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 }
|
||||
```
|
||||
Reference in new issue
Block a user