mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Cloudflare Agents SDK
|
||||
|
||||
Cloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration.
|
||||
|
||||
## Core Value
|
||||
Build stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows.
|
||||
|
||||
## When to Use
|
||||
- Persistent state + memory required
|
||||
- Real-time WebSocket connections
|
||||
- Long-running workflows (minutes/hours)
|
||||
- Chat interfaces with AI models
|
||||
- Scheduled/recurring tasks with state
|
||||
- DB queries with agent state
|
||||
|
||||
## What Type of Agent?
|
||||
|
||||
| Use Case | Class | Key Features |
|
||||
|----------|-------|--------------|
|
||||
| AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable |
|
||||
| MCP tool provider | `Agent` + MCP | Expose tools to AI systems |
|
||||
| Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL |
|
||||
| Real-time collaboration | `Agent` | WebSocket state, broadcasts |
|
||||
| Email processing | `Agent` | `onEmail()` handler |
|
||||
|
||||
## Quick Start
|
||||
|
||||
**AI Chat Agent:**
|
||||
```typescript
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages,
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Base Agent:**
|
||||
```typescript
|
||||
import { Agent } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env> {
|
||||
onStart() {
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
|
||||
}
|
||||
|
||||
async onRequest(request: Request) {
|
||||
return Response.json({ state: this.state });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Quick start | README only |
|
||||
| Build chat agent | README → api.md (AIChatAgent) → patterns.md |
|
||||
| Setup project | README → configuration.md |
|
||||
| Add React frontend | README → api.md (Client Hooks) → patterns.md |
|
||||
| Build MCP server | api.md (MCP) → patterns.md |
|
||||
| Background tasks | api.md (Scheduling, Task Queue) → patterns.md |
|
||||
| Debug issues | gotchas.md |
|
||||
|
||||
## Package Entry Points
|
||||
|
||||
| Import | Purpose |
|
||||
|--------|---------|
|
||||
| `agents` | Server-side Agent classes, lifecycle |
|
||||
| `agents/react` | `useAgent()` hook for WebSocket connections |
|
||||
| `agents/ai-react` | `useAgentChat()` hook for AI chat UIs |
|
||||
|
||||
## In This Reference
|
||||
- [configuration.md](./configuration.md) - SDK setup, wrangler config, routing
|
||||
- [api.md](./api.md) - Agent classes, lifecycle, client hooks
|
||||
- [patterns.md](./patterns.md) - Common workflows, best practices
|
||||
- [gotchas.md](./gotchas.md) - Common issues, limits
|
||||
|
||||
## See Also
|
||||
- durable-objects - Agent infrastructure
|
||||
- d1 - External database integration
|
||||
- workers-ai - AI model integration
|
||||
- vectorize - Vector search for RAG patterns
|
||||
190
.agents/skills/cloudflare-deploy/references/agents-sdk/api.md
Normal file
190
.agents/skills/cloudflare-deploy/references/agents-sdk/api.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# API Reference
|
||||
|
||||
## Agent Classes
|
||||
|
||||
### AIChatAgent
|
||||
|
||||
For AI chat with auto-streaming, message history, tools, resumable streaming.
|
||||
|
||||
```ts
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages, // Auto-managed message history
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get weather",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: async ({ city }) => `Sunny, 72°F in ${city}`
|
||||
}
|
||||
},
|
||||
onFinish, // Persist response to this.messages
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent (Base Class)
|
||||
|
||||
Full control for custom logic, WebSockets, email, and SQL.
|
||||
|
||||
```ts
|
||||
import { Agent } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env, State> {
|
||||
// Lifecycle methods below
|
||||
}
|
||||
```
|
||||
|
||||
**Type params:** `Agent<Env, State, ConnState>` - Env bindings, agent state, connection state
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
```ts
|
||||
onStart() { // Init/restart
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`;
|
||||
}
|
||||
|
||||
async onRequest(req: Request) { // HTTP
|
||||
const {pathname} = new URL(req.url);
|
||||
if (pathname === "/users") return Response.json(this.sql<{id,name}>`SELECT * FROM users`);
|
||||
return new Response("Not found", {status: 404});
|
||||
}
|
||||
|
||||
async onConnect(conn: Connection<ConnState>, ctx: ConnectionContext) { // WebSocket
|
||||
conn.accept();
|
||||
conn.setState({userId: ctx.request.headers.get("X-User-ID")});
|
||||
conn.send(JSON.stringify({type: "connected", state: this.state}));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection<ConnState>, msg: WSMessage) { // WS messages
|
||||
const m = JSON.parse(msg as string);
|
||||
this.setState({messages: [...this.state.messages, m]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify(m)));
|
||||
}
|
||||
|
||||
async onEmail(email: AgentEmail) { // Email routing
|
||||
this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get("subject")},${await email.text()})`;
|
||||
}
|
||||
```
|
||||
|
||||
## State, SQL, Scheduling
|
||||
|
||||
```ts
|
||||
// State
|
||||
this.setState({count: 42}); // Auto-syncs
|
||||
this.setState({...this.state, count: this.state.count + 1});
|
||||
|
||||
// SQL (parameterized queries prevent injection)
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;
|
||||
this.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`;
|
||||
const users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`;
|
||||
|
||||
// Scheduling
|
||||
await this.schedule(new Date("2026-12-25"), "sendGreeting", {msg:"Hi"}); // Date
|
||||
await this.schedule(60, "checkStatus", {}); // Delay (sec)
|
||||
await this.schedule("0 0 * * *", "dailyCleanup", {}); // Cron
|
||||
await this.cancelSchedule(scheduleId);
|
||||
```
|
||||
|
||||
## RPC Methods (@callable)
|
||||
|
||||
```ts
|
||||
import { Agent, callable } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env> {
|
||||
@callable()
|
||||
async processTask(input: {text: string}): Promise<{result: string}> {
|
||||
return { result: await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt: input.text}) };
|
||||
}
|
||||
}
|
||||
// Client: const result = await agent.processTask({ text: "Hello" });
|
||||
// Must return JSON-serializable values
|
||||
```
|
||||
|
||||
## Connections & AI
|
||||
|
||||
```ts
|
||||
// Connections (type: Agent<Env, State, ConnState>)
|
||||
this.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast
|
||||
conn.setState({userId:"123"}); conn.close(1000, "Goodbye");
|
||||
|
||||
// Workers AI
|
||||
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
|
||||
|
||||
// Manual streaming (prefer AIChatAgent)
|
||||
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
|
||||
for await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
|
||||
```
|
||||
|
||||
**Type-safe state:** `Agent<Env, State, ConnState>` - third param types `conn.state`
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Model Context Protocol for exposing tools:
|
||||
|
||||
```ts
|
||||
// Register & use MCP server
|
||||
await this.mcp.registerServer("github", {
|
||||
url: env.MCP_SERVER_URL,
|
||||
auth: { type: "oauth", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }
|
||||
});
|
||||
const tools = await this.mcp.getAITools(["github"]);
|
||||
return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools, onFinish });
|
||||
```
|
||||
|
||||
## Task Queue
|
||||
|
||||
```ts
|
||||
await this.queue("processVideo", { videoId: "abc123" }); // Add task
|
||||
const tasks = await this.dequeue(10); // Process up to 10
|
||||
```
|
||||
|
||||
## Context & Cleanup
|
||||
|
||||
```ts
|
||||
const agent = getCurrentAgent<MyAgent>(); // Get current instance
|
||||
async destroy() { /* cleanup before agent destroyed */ }
|
||||
```
|
||||
|
||||
## AI Integration
|
||||
|
||||
```ts
|
||||
// Workers AI
|
||||
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
|
||||
|
||||
// Manual streaming (prefer AIChatAgent for auto-streaming)
|
||||
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
|
||||
}
|
||||
```
|
||||
|
||||
## Client Hooks (React)
|
||||
|
||||
```ts
|
||||
// useAgent() - WebSocket connection + RPC
|
||||
import { useAgent } from "agents/react";
|
||||
const agent = useAgent({ agent: "MyAgent", name: "user-123" }); // name for idFromName
|
||||
const result = await agent.processTask({ text: "Hello" }); // Call @callable methods
|
||||
// agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
|
||||
|
||||
// useAgentChat() - AI chat UI
|
||||
import { useAgentChat } from "agents/ai-react";
|
||||
const agent = useAgent({ agent: "ChatAgent" });
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } =
|
||||
useAgentChat({
|
||||
agent,
|
||||
maxSteps: 5, // Max tool iterations
|
||||
resume: true, // Auto-resume on disconnect
|
||||
onToolCall: async (toolCall) => {
|
||||
// Client tools (human-in-the-loop)
|
||||
if (toolCall.toolName === "confirm") return { ok: window.confirm("Proceed?") };
|
||||
}
|
||||
});
|
||||
// status: "ready" | "submitted" | "streaming" | "error"
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
# Configuration
|
||||
|
||||
## Wrangler Setup
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-agents-app",
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{"name": "MyAgent", "class_name": "MyAgent"}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{"tag": "v1", "new_sqlite_classes": ["MyAgent"]}
|
||||
],
|
||||
"ai": {
|
||||
"binding": "AI"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Bindings
|
||||
|
||||
**Type-safe pattern:**
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
AI?: Ai; // Workers AI
|
||||
MyAgent?: DurableObjectNamespace<MyAgent>;
|
||||
ChatAgent?: DurableObjectNamespace<ChatAgent>;
|
||||
DB?: D1Database; // D1 database
|
||||
KV?: KVNamespace; // KV storage
|
||||
R2?: R2Bucket; // R2 bucket
|
||||
OPENAI_API_KEY?: string; // Secrets
|
||||
GITHUB_CLIENT_ID?: string; // MCP OAuth credentials
|
||||
GITHUB_CLIENT_SECRET?: string;
|
||||
QUEUE?: Queue; // Queues
|
||||
}
|
||||
```
|
||||
|
||||
**Best practice:** Define all DO bindings in Env interface for type safety.
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Local dev
|
||||
npx wrangler dev
|
||||
|
||||
# Deploy production
|
||||
npx wrangler deploy
|
||||
|
||||
# Set secrets
|
||||
npx wrangler secret put OPENAI_API_KEY
|
||||
```
|
||||
|
||||
## Agent Routing
|
||||
|
||||
**Recommended: Use route helpers**
|
||||
|
||||
```typescript
|
||||
import { routeAgent } from "agents";
|
||||
|
||||
export default {
|
||||
fetch(request: Request, env: Env) {
|
||||
return routeAgent(request, env);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Helper routes requests to agents automatically based on URL patterns.
|
||||
|
||||
**Manual routing (advanced):**
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Named ID (deterministic)
|
||||
const id = env.MyAgent.idFromName("user-123");
|
||||
|
||||
// Random ID (from URL param)
|
||||
// const id = env.MyAgent.idFromString(url.searchParams.get("id"));
|
||||
|
||||
const stub = env.MyAgent.get(id);
|
||||
return stub.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-agent setup:**
|
||||
|
||||
```typescript
|
||||
import { routeAgent } from "agents";
|
||||
|
||||
export default {
|
||||
fetch(request: Request, env: Env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Route by path
|
||||
if (url.pathname.startsWith("/chat")) {
|
||||
return routeAgent(request, env, "ChatAgent");
|
||||
}
|
||||
if (url.pathname.startsWith("/task")) {
|
||||
return routeAgent(request, env, "TaskAgent");
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Email Routing
|
||||
|
||||
**Code setup:**
|
||||
|
||||
```typescript
|
||||
import { routeAgentEmail } from "agents";
|
||||
|
||||
export default {
|
||||
fetch: (req: Request, env: Env) => routeAgent(req, env),
|
||||
email: (message: ForwardableEmailMessage, env: Env) => {
|
||||
return routeAgentEmail(message, env);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dashboard setup:**
|
||||
|
||||
Configure email routing in Cloudflare dashboard:
|
||||
|
||||
```
|
||||
Destination: Workers with Durable Objects
|
||||
Worker: my-agents-app
|
||||
```
|
||||
|
||||
Then handle in agent:
|
||||
|
||||
```typescript
|
||||
export class EmailAgent extends Agent<Env> {
|
||||
async onEmail(email: AgentEmail) {
|
||||
const text = await email.text();
|
||||
// Process email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI Gateway (Optional)
|
||||
|
||||
```typescript
|
||||
// Enable caching/routing through AI Gateway
|
||||
const response = await this.env.AI.run(
|
||||
"@cf/meta/llama-3.1-8b-instruct",
|
||||
{ prompt },
|
||||
{
|
||||
gateway: {
|
||||
id: "my-gateway-id",
|
||||
skipCache: false,
|
||||
cacheTtl: 3600
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## MCP Configuration (Optional)
|
||||
|
||||
For exposing tools via Model Context Protocol:
|
||||
|
||||
```typescript
|
||||
// wrangler.jsonc - Add MCP OAuth secrets
|
||||
{
|
||||
"vars": {
|
||||
"MCP_SERVER_URL": "https://mcp.example.com"
|
||||
}
|
||||
}
|
||||
|
||||
// Set secrets via CLI
|
||||
// npx wrangler secret put GITHUB_CLIENT_ID
|
||||
// npx wrangler secret put GITHUB_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Then register in agent code (see api.md MCP section).
|
||||
@@ -0,0 +1,158 @@
|
||||
# Gotchas & Best Practices
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "setState() not syncing"
|
||||
|
||||
**Cause:** Mutating state directly or not calling `setState()` after modifications
|
||||
**Solution:** Always use `setState()` with immutable updates:
|
||||
```ts
|
||||
// ❌ this.state.count++
|
||||
// ✅ this.setState({...this.state, count: this.state.count + 1})
|
||||
```
|
||||
|
||||
### "Message history grows unbounded (AIChatAgent)"
|
||||
|
||||
**Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely
|
||||
**Solution:** Manually trim old messages periodically:
|
||||
```ts
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
// Keep only last 50 messages
|
||||
if (this.messages.length > 50) {
|
||||
this.messages = this.messages.slice(-50);
|
||||
}
|
||||
|
||||
return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "SQL injection vulnerability"
|
||||
|
||||
**Cause:** Direct string interpolation in SQL queries
|
||||
**Solution:** Use parameterized queries:
|
||||
```ts
|
||||
// ❌ this.sql`...WHERE id = '${userId}'`
|
||||
// ✅ this.sql`...WHERE id = ${userId}`
|
||||
```
|
||||
|
||||
### "WebSocket connection timeout"
|
||||
|
||||
**Cause:** Not calling `conn.accept()` in `onConnect`
|
||||
**Solution:** Always accept connections:
|
||||
```ts
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: "123"}); }
|
||||
```
|
||||
|
||||
### "Schedule limit exceeded"
|
||||
|
||||
**Cause:** More than 1000 scheduled tasks per agent
|
||||
**Solution:** Clean up old schedules and limit creation rate:
|
||||
```ts
|
||||
async checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn("Near limit!"); }
|
||||
```
|
||||
|
||||
### "AI Gateway unavailable"
|
||||
|
||||
**Cause:** AI service timeout or quota exceeded
|
||||
**Solution:** Add error handling and fallbacks:
|
||||
```ts
|
||||
try {
|
||||
return await this.env.AI.run(model, {prompt});
|
||||
} catch (e) {
|
||||
console.error("AI error:", e);
|
||||
return {error: "Unavailable"};
|
||||
}
|
||||
```
|
||||
|
||||
### "@callable method returns undefined"
|
||||
|
||||
**Cause:** Method doesn't return JSON-serializable value, or has non-serializable types
|
||||
**Solution:** Ensure return values are plain objects/arrays/primitives:
|
||||
```ts
|
||||
// ❌ Returns class instance
|
||||
@callable()
|
||||
async getData() { return new Date(); }
|
||||
|
||||
// ✅ Returns serializable object
|
||||
@callable()
|
||||
async getData() { return { timestamp: Date.now() }; }
|
||||
```
|
||||
|
||||
### "Resumable stream not resuming"
|
||||
|
||||
**Cause:** Stream ID must be deterministic for resumption to work
|
||||
**Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs:
|
||||
```ts
|
||||
// AIChatAgent handles this automatically
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
// Resumption works out of the box
|
||||
}
|
||||
```
|
||||
|
||||
### "MCP connection loss on hibernation"
|
||||
|
||||
**Cause:** MCP server connections don't survive hibernation
|
||||
**Solution:** Re-register servers in `onStart()` or check connection status:
|
||||
```ts
|
||||
onStart() {
|
||||
// Re-register MCP servers after hibernation
|
||||
await this.mcp.registerServer("github", { url: env.MCP_URL, auth: {...} });
|
||||
}
|
||||
```
|
||||
|
||||
### "Agent not found"
|
||||
|
||||
**Cause:** Durable Object binding missing or incorrect class name
|
||||
**Solution:** Verify DO binding in wrangler.jsonc and class name matches
|
||||
|
||||
## Rate Limits & Quotas
|
||||
|
||||
| Resource/Limit | Value | Notes |
|
||||
|----------------|-------|-------|
|
||||
| CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc |
|
||||
| Memory per instance | 128MB | Shared with WebSockets |
|
||||
| Storage per agent | 10GB | SQLite storage |
|
||||
| Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` |
|
||||
| WebSocket connections | Unlimited | Within memory limits |
|
||||
| SQL columns | 100 | Per table |
|
||||
| SQL row size | 2MB | Key + value |
|
||||
| WebSocket message | 32MiB | Max size |
|
||||
| DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed |
|
||||
| AI Gateway (Workers AI) | Model-specific | Check dashboard for limits |
|
||||
| MCP requests | Depends on server | Implement retry/backoff |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### State Management
|
||||
- Use immutable updates: `setState({...this.state, key: newValue})`
|
||||
- Trim unbounded arrays (messages, logs) periodically
|
||||
- Store large data in SQL, not state
|
||||
|
||||
### SQL Usage
|
||||
- Create tables in `onStart()`, not `onRequest()`
|
||||
- Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``)
|
||||
- Index frequently queried columns
|
||||
|
||||
### Scheduling
|
||||
- Monitor schedule count: `await this.getSchedules()`
|
||||
- Cancel completed tasks to stay under 1000 limit
|
||||
- Use cron strings for recurring tasks
|
||||
|
||||
### WebSockets
|
||||
- Always call `conn.accept()` in `onConnect()`
|
||||
- Handle client disconnects gracefully
|
||||
- Broadcast to `this.connections` efficiently
|
||||
|
||||
### AI Integration
|
||||
- Use `AIChatAgent` for chat interfaces (auto-streaming, resumption)
|
||||
- Trim message history to avoid token limits
|
||||
- Handle AI errors with try/catch and fallbacks
|
||||
|
||||
### Production Deployment
|
||||
- **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s)
|
||||
- **Monitoring:** Log critical errors, track schedule count, monitor storage usage
|
||||
- **Graceful degradation:** Handle AI service outages with fallbacks
|
||||
- **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent
|
||||
- **MCP reliability:** Re-register servers on hibernation, implement retry logic
|
||||
@@ -0,0 +1,192 @@
|
||||
# Patterns & Use Cases
|
||||
|
||||
## AI Chat w/Tools
|
||||
|
||||
**Server (AIChatAgent):**
|
||||
|
||||
```ts
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages, // Auto-managed
|
||||
tools: {
|
||||
getWeather: tool({
|
||||
description: "Get current weather",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F`
|
||||
}),
|
||||
searchDocs: tool({
|
||||
description: "Search documentation",
|
||||
parameters: z.object({ query: z.string() }),
|
||||
execute: async ({ query }) => JSON.stringify(
|
||||
this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}`
|
||||
)
|
||||
})
|
||||
},
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client (React):**
|
||||
|
||||
```tsx
|
||||
import { useAgent } from "agents/react";
|
||||
import { useAgentChat } from "agents/ai-react";
|
||||
|
||||
function ChatUI() {
|
||||
const agent = useAgent({ agent: "ChatAgent" });
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={input} onChange={handleInputChange} disabled={isLoading} />
|
||||
<button disabled={isLoading}>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Human-in-the-Loop (Client Tools)
|
||||
|
||||
Server defines tool, client executes:
|
||||
|
||||
```ts
|
||||
// Server
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages,
|
||||
tools: {
|
||||
confirmAction: tool({
|
||||
description: "Ask user to confirm",
|
||||
parameters: z.object({ action: z.string() }),
|
||||
execute: "client", // Client-side execution
|
||||
})
|
||||
},
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Client
|
||||
const { messages } = useAgentChat({
|
||||
agent,
|
||||
onToolCall: async (toolCall) => {
|
||||
if (toolCall.toolName === "confirmAction") {
|
||||
return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) };
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Task Queue & Scheduled Processing
|
||||
|
||||
```ts
|
||||
export class TaskAgent extends Agent<Env> {
|
||||
onStart() {
|
||||
this.schedule("*/5 * * * *", "processQueue", {}); // Every 5 min
|
||||
this.schedule("0 0 * * *", "dailyCleanup", {}); // Daily
|
||||
}
|
||||
|
||||
async onRequest(req: Request) {
|
||||
await this.queue("processVideo", { videoId: (await req.json()).videoId });
|
||||
return Response.json({ queued: true });
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
const tasks = await this.dequeue(10);
|
||||
for (const task of tasks) {
|
||||
if (task.name === "processVideo") await this.processVideo(task.data.videoId);
|
||||
}
|
||||
}
|
||||
|
||||
async dailyCleanup() {
|
||||
this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manual WebSocket Chat
|
||||
|
||||
Custom protocols (non-AI):
|
||||
|
||||
```ts
|
||||
export class ChatAgent extends Agent<Env> {
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) {
|
||||
conn.accept();
|
||||
conn.setState({userId: ctx.request.headers.get("X-User-ID") || "anon"});
|
||||
conn.send(JSON.stringify({type: "history", messages: this.state.messages}));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection, msg: WSMessage) {
|
||||
const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()};
|
||||
this.setState({messages: [...this.state.messages, newMsg]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify(newMsg)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Email Processing w/AI
|
||||
|
||||
```ts
|
||||
export class EmailAgent extends Agent<Env> {
|
||||
async onEmail(email: AgentEmail) {
|
||||
const [text, from, subject] = [await email.text(), email.from, email.headers.get("subject") || ""];
|
||||
this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`;
|
||||
|
||||
const { text: summary } = await generateText({
|
||||
model: openai("gpt-4o-mini"), prompt: `Summarize: ${subject}\n\n${text}`
|
||||
});
|
||||
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "new_email", from, summary})));
|
||||
if (summary.includes("urgent")) await this.schedule(0, "sendAutoReply", { to: from });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-time Collaboration
|
||||
|
||||
```ts
|
||||
export class GameAgent extends Agent<Env> {
|
||||
initialState = { players: [], gameStarted: false };
|
||||
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) {
|
||||
conn.accept();
|
||||
const playerId = ctx.request.headers.get("X-Player-ID") || crypto.randomUUID();
|
||||
conn.setState({ playerId });
|
||||
|
||||
const newPlayer = { id: playerId, score: 0 };
|
||||
this.setState({...this.state, players: [...this.state.players, newPlayer]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "player_joined", player: newPlayer})));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection, msg: WSMessage) {
|
||||
const m = JSON.parse(msg as string);
|
||||
|
||||
if (m.type === "move") {
|
||||
this.setState({
|
||||
...this.state,
|
||||
players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p)
|
||||
});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "player_moved", playerId: conn.state.playerId})));
|
||||
}
|
||||
|
||||
if (m.type === "start" && this.state.players.length >= 2) {
|
||||
this.setState({...this.state, gameStarted: true});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "game_started"})));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user