From 08dbde4111591a29af5fe1c58d870694e3da8833 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:24:06 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + SKILL.md | 1403 ++++++++++++++++++++++++ plugin.lock.json | 101 ++ templates/basic-agent.ts | 94 ++ templates/browser-agent.ts | 197 ++++ templates/calling-agents-worker.ts | 125 +++ templates/chat-agent-streaming.ts | 120 ++ templates/hitl-agent.ts | 250 +++++ templates/mcp-server-basic.ts | 182 +++ templates/rag-agent.ts | 196 ++++ templates/react-useagent-client.tsx | 235 ++++ templates/scheduled-agent.ts | 173 +++ templates/simple-chat-no-agents-sdk.ts | 280 +++++ templates/state-sync-agent.ts | 160 +++ templates/websocket-agent.ts | 145 +++ templates/workflow-agent.ts | 134 +++ templates/wrangler-agents-config.jsonc | 95 ++ 18 files changed, 3905 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 plugin.lock.json create mode 100644 templates/basic-agent.ts create mode 100644 templates/browser-agent.ts create mode 100644 templates/calling-agents-worker.ts create mode 100644 templates/chat-agent-streaming.ts create mode 100644 templates/hitl-agent.ts create mode 100644 templates/mcp-server-basic.ts create mode 100644 templates/rag-agent.ts create mode 100644 templates/react-useagent-client.tsx create mode 100644 templates/scheduled-agent.ts create mode 100644 templates/simple-chat-no-agents-sdk.ts create mode 100644 templates/state-sync-agent.ts create mode 100644 templates/websocket-agent.ts create mode 100644 templates/workflow-agent.ts create mode 100644 templates/wrangler-agents-config.jsonc diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..210d696 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "cloudflare-agents", + "description": "Build AI agents with Cloudflare Agents SDK on Workers + Durable Objects. Includes critical guidance on choosing between Agents SDK (infrastructure/state) vs AI SDK (simpler flows). Use when: deciding SDK choice, building WebSocket agents with state, RAG with Vectorize, MCP servers, multi-agent orchestration, or troubleshooting Agent class must extend, new_sqlite_classes, binding errors.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..658f56a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# cloudflare-agents + +Build AI agents with Cloudflare Agents SDK on Workers + Durable Objects. Includes critical guidance on choosing between Agents SDK (infrastructure/state) vs AI SDK (simpler flows). Use when: deciding SDK choice, building WebSocket agents with state, RAG with Vectorize, MCP servers, multi-agent orchestration, or troubleshooting Agent class must extend, new_sqlite_classes, binding errors. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..6ceefcb --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1403 @@ +--- +name: cloudflare-agents +description: | + Build AI agents with Cloudflare Agents SDK on Workers + Durable Objects. Includes critical guidance on choosing between Agents SDK (infrastructure/state) vs AI SDK (simpler flows). + + Use when: deciding SDK choice, building WebSocket agents with state, RAG with Vectorize, MCP servers, multi-agent orchestration, or troubleshooting "Agent class must extend", "new_sqlite_classes", binding errors. +license: MIT +metadata: + keywords: + - Cloudflare Agents + - agents sdk + - Agent class + - Durable Objects agents + - stateful agents + - WebSocket agents + - this.setState + - this.sql + - this.schedule + - cron agents + - agent workflows + - browser rendering + - rag agents + - vectorize agents + - mcp server + - McpAgent + - model context protocol + - routeAgentRequest + - AIChatAgent + - streaming chat + - human in the loop + - multi-agent + - autonomous agents + - AI SDK vs Agents SDK + - Workers AI + - new_sqlite_classes + - migrations required + - binding not found +--- + +# Cloudflare Agents SDK + +**Status**: Production Ready ✅ +**Last Updated**: 2025-11-23 +**Dependencies**: cloudflare-worker-base (recommended) +**Latest Versions**: agents@0.2.23 (Nov 13, 2025), @modelcontextprotocol/sdk@latest +**Production Tested**: Cloudflare's own MCP servers (https://github.com/cloudflare/mcp-server-cloudflare) + +**Recent Updates (2025)**: +- **Sept 2025**: AI SDK v5 compatibility, automatic message migration +- **April 2025**: MCP support (MCPAgent class), `import { context }` from agents +- **March 2025**: Package rename (agents-sdk → agents) + +--- + +## What is Cloudflare Agents? + +The Cloudflare Agents SDK enables building AI-powered autonomous agents that run on Cloudflare Workers + Durable Objects. Agents can: + +- **Communicate in real-time** via WebSockets and Server-Sent Events +- **Persist state** with built-in SQLite database (up to 1GB per agent) +- **Schedule tasks** using delays, specific dates, or cron expressions +- **Run workflows** by triggering asynchronous Cloudflare Workflows +- **Browse the web** using Browser Rendering API + Puppeteer +- **Implement RAG** with Vectorize vector database + Workers AI embeddings +- **Build MCP servers** implementing the Model Context Protocol +- **Support human-in-the-loop** patterns for review and approval +- **Scale to millions** of independent agent instances globally + +Each agent instance is a **globally unique, stateful micro-server** that can run for seconds, minutes, or hours. + +--- + +## Do You Need Agents SDK? + +**STOP**: Before using Agents SDK, ask yourself if you actually need it. + +### Use JUST Vercel AI SDK (Simpler) When: + +- ✅ Building a basic chat interface +- ✅ Server-Sent Events (SSE) streaming is sufficient (one-way: server → client) +- ✅ No persistent agent state needed (or you manage it separately with D1/KV) +- ✅ Single-user, single-conversation scenarios +- ✅ Just need AI responses, no complex workflows or scheduling + +**This covers 80% of chat applications.** For these cases, use [Vercel AI SDK](https://sdk.vercel.ai/) directly on Workers - it's simpler, requires less infrastructure, and handles streaming automatically. + +**Example** (no Agents SDK needed): +```typescript +// worker.ts - Simple chat with AI SDK only +import { streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +export default { + async fetch(request: Request, env: Env) { + const { messages } = await request.json(); + + const result = streamText({ + model: openai('gpt-4o-mini'), + messages + }); + + return result.toTextStreamResponse(); // Automatic SSE streaming + } +} + +// client.tsx - React with built-in hooks +import { useChat } from 'ai/react'; + +function ChatPage() { + const { messages, input, handleSubmit } = useChat({ api: '/api/chat' }); + // Done. No Agents SDK needed. +} +``` + +**Result**: 100 lines of code instead of 500. No Durable Objects setup, no WebSocket complexity, no migrations. + +--- + +### Use Agents SDK When You Need: + +- ✅ **WebSocket connections** (true bidirectional real-time communication) +- ✅ **Durable Objects** (globally unique, stateful agent instances) +- ✅ **Built-in state persistence** (SQLite storage up to 1GB per agent) +- ✅ **Multi-agent coordination** (agents calling and communicating with each other) +- ✅ **Scheduled tasks** (delays, cron expressions, recurring jobs) +- ✅ **Human-in-the-loop workflows** (approval gates, review processes) +- ✅ **Long-running agents** (background processing, autonomous workflows) +- ✅ **MCP servers** with stateful tool execution + +**This is ~20% of applications** - when you need the infrastructure that Agents SDK provides. + +--- + +### Key Understanding: What Agents SDK IS vs IS NOT + +**Agents SDK IS**: +- 🏗️ **Infrastructure layer** for WebSocket connections, Durable Objects, and state management +- 🔧 **Framework** for building stateful, autonomous agents +- 📦 **Wrapper** around Durable Objects with lifecycle methods + +**Agents SDK IS NOT**: +- ❌ **AI inference provider** (you bring your own: AI SDK, Workers AI, OpenAI, etc.) +- ❌ **Streaming response handler** (use AI SDK for automatic parsing) +- ❌ **LLM integration** (that's a separate concern) + +**Think of it this way**: +- **Agents SDK** = The building (WebSockets, state, rooms) +- **AI SDK / Workers AI** = The AI brain (inference, reasoning, responses) + +You can use them together (recommended for most cases), or use Workers AI directly (if you're willing to handle manual SSE parsing). + +--- + +### Decision Flowchart + +``` +Building an AI application? +│ +├─ Need WebSocket bidirectional communication? ───────┐ +│ (Client sends while server streams, agent-initiated messages) +│ +├─ Need Durable Objects stateful instances? ──────────┤ +│ (Globally unique agents with persistent memory) +│ +├─ Need multi-agent coordination? ────────────────────┤ +│ (Agents calling/messaging other agents) +│ +├─ Need scheduled tasks or cron jobs? ────────────────┤ +│ (Delayed execution, recurring tasks) +│ +├─ Need human-in-the-loop workflows? ─────────────────┤ +│ (Approval gates, review processes) +│ +└─ If ALL above are NO ─────────────────────────────→ Use AI SDK directly + (Much simpler approach) + + If ANY above are YES ────────────────────────────→ Use Agents SDK + AI SDK + (More infrastructure, more power) +``` + +--- + +### Architecture Comparison + +| Feature | AI SDK Only | Agents SDK + AI SDK | +|---------|-------------|---------------------| +| **Setup Complexity** | 🟢 Low (npm install, done) | 🔴 Higher (Durable Objects, migrations, bindings) | +| **Code Volume** | 🟢 ~100 lines | 🟡 ~500+ lines | +| **Streaming** | ✅ Automatic (SSE) | ✅ Automatic (AI SDK) or manual (Workers AI) | +| **State Management** | ⚠️ Manual (D1/KV) | ✅ Built-in (SQLite) | +| **WebSockets** | ❌ Manual setup | ✅ Built-in | +| **React Hooks** | ✅ useChat, useCompletion | ⚠️ Custom hooks needed | +| **Multi-agent** | ❌ Not supported | ✅ Built-in (routeAgentRequest) | +| **Scheduling** | ❌ External (Queue/Workflow) | ✅ Built-in (this.schedule) | +| **Use Case** | Simple chat, completions | Complex stateful workflows | + +--- + +### Still Not Sure? + +**Start with AI SDK.** You can always migrate to Agents SDK later if you discover you need WebSockets or Durable Objects. It's easier to add infrastructure later than to remove it. + +**For most developers**: If you're building a chat interface and don't have specific requirements for WebSockets, multi-agent coordination, or scheduled tasks, use AI SDK directly. You'll ship faster and with less complexity. + +**Proceed with Agents SDK only if** you've identified a specific need for its infrastructure capabilities. + +--- + +## Quick Start (10 Minutes) + +### 1. Scaffold Project with Template + +```bash +npm create cloudflare@latest my-agent -- \ + --template=cloudflare/agents-starter \ + --ts \ + --git \ + --deploy false +``` + +**What this creates:** +- Complete Agent project structure +- TypeScript configuration +- wrangler.jsonc with Durable Objects bindings +- Example chat agent implementation +- React client with useAgent hook + +### 2. Or Add to Existing Worker + +```bash +cd my-existing-worker +npm install agents +``` + +**Then create an Agent class:** + +```typescript +// src/index.ts +import { Agent, AgentNamespace } from "agents"; + +export class MyAgent extends Agent { + async onRequest(request: Request): Promise { + return new Response("Hello from Agent!"); + } +} + +export default MyAgent; +``` + +### 3. Configure Durable Objects Binding + +Create or update `wrangler.jsonc`: + +```jsonc +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "my-agent", + "main": "src/index.ts", + "compatibility_date": "2025-10-21", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [ + { + "name": "MyAgent", // MUST match class name + "class_name": "MyAgent" // MUST match exported class + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MyAgent"] // CRITICAL: Enables SQLite storage + } + ] +} +``` + +**CRITICAL Configuration Rules:** +- ✅ `name` and `class_name` **MUST be identical** +- ✅ `new_sqlite_classes` **MUST be in first migration** (cannot add later) +- ✅ Agent class **MUST be exported** (or binding will fail) +- ✅ Migration tags **CANNOT be reused** (each migration needs unique tag) + +### 4. Deploy + +```bash +npx wrangler@latest deploy +``` + +Your agent is now running at: `https://my-agent..workers.dev` + +--- + +## Architecture Overview: How the Pieces Fit Together + +Understanding what each tool does prevents confusion and helps you choose the right combination. + +### The Stack + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Application │ +│ │ +│ ┌────────────────┐ ┌──────────────────────┐ │ +│ │ Agents SDK │ │ AI Inference │ │ +│ │ (Infra Layer) │ + │ (Brain Layer) │ │ +│ │ │ │ │ │ +│ │ • WebSockets │ │ Choose ONE: │ │ +│ │ • Durable Objs │ │ • Vercel AI SDK ✅ │ │ +│ │ • State (SQL) │ │ • Workers AI ⚠️ │ │ +│ │ • Scheduling │ │ • OpenAI Direct │ │ +│ │ • Multi-agent │ │ • Anthropic Direct │ │ +│ └────────────────┘ └──────────────────────┘ │ +│ ↓ ↓ │ +│ Manages connections Generates responses │ +│ and state and handles streaming │ +└─────────────────────────────────────────────────────────┘ + ↓ + Cloudflare Workers + Durable Objects +``` + +### What Each Tool Provides + +#### 1. Agents SDK (This Skill) + +**Purpose**: Infrastructure for stateful, real-time agents + +**Provides**: +- ✅ WebSocket connection management (bidirectional real-time) +- ✅ Durable Objects wrapper (globally unique agent instances) +- ✅ Built-in state persistence (SQLite up to 1GB) +- ✅ Lifecycle methods (`onStart`, `onConnect`, `onMessage`, `onClose`) +- ✅ Task scheduling (`this.schedule()` with cron/delays) +- ✅ Multi-agent coordination (`routeAgentRequest()`) +- ✅ Client libraries (`useAgent`, `AgentClient`, `agentFetch`) + +**Does NOT Provide**: +- ❌ AI inference (no LLM calls) +- ❌ Streaming response parsing (bring your own) +- ❌ Provider integrations (OpenAI, Anthropic, etc.) + +**Think of it as**: The building and infrastructure (rooms, doors, plumbing) but NOT the residents (AI). + +--- + +#### 2. Vercel AI SDK (Recommended for AI) + +**Purpose**: AI inference with automatic streaming + +**Provides**: +- ✅ Automatic streaming response handling (SSE parsing done for you) +- ✅ Multi-provider support (OpenAI, Anthropic, Google, etc.) +- ✅ React hooks (`useChat`, `useCompletion`, `useAssistant`) +- ✅ Unified API across providers +- ✅ Tool calling / function calling +- ✅ Works on Cloudflare Workers ✅ + +**Example**: +```typescript +import { streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +const result = streamText({ + model: openai('gpt-4o-mini'), + messages: [...] +}); + +// Returns SSE stream - no manual parsing needed +return result.toTextStreamResponse(); +``` + +**When to use with Agents SDK**: +- ✅ Most chat applications +- ✅ When you want React hooks +- ✅ When you use multiple AI providers +- ✅ When you want clean, abstracted AI calls + +**Combine with Agents SDK**: +```typescript +import { AIChatAgent } from "agents/ai-chat-agent"; +import { streamText } from "ai"; + +export class MyAgent extends AIChatAgent { + async onChatMessage(onFinish) { + // Agents SDK provides: WebSocket, state, this.messages + // AI SDK provides: Automatic streaming, provider abstraction + + return streamText({ + model: openai('gpt-4o-mini'), + messages: this.messages // Managed by Agents SDK + }).toTextStreamResponse(); + } +} +``` + +--- + +#### 3. Workers AI (Alternative for AI) + +**Purpose**: Cloudflare's on-platform AI inference + +**Provides**: +- ✅ Cost-effective inference (included in Workers subscription) +- ✅ No external API keys needed +- ✅ Models: LLaMA 3, Qwen, Mistral, embeddings, etc. +- ✅ Runs on Cloudflare's network (low latency) + +**Does NOT Provide**: +- ❌ Automatic streaming parsing (returns raw SSE format) +- ❌ React hooks +- ❌ Multi-provider abstraction + +**Manual parsing required**: +```typescript +const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', { + messages: [...], + stream: true +}); + +// Returns raw SSE format - YOU must parse +for await (const chunk of response) { + const text = new TextDecoder().decode(chunk); // Uint8Array → string + if (text.startsWith('data: ')) { // Check SSE format + const data = JSON.parse(text.slice(6)); // Parse JSON + if (data.response) { // Extract .response field + fullResponse += data.response; + } + } +} +``` + +**When to use**: +- ✅ Cost is critical (embeddings, high-volume) +- ✅ Need Cloudflare-specific models +- ✅ Willing to handle manual SSE parsing +- ✅ No external dependencies allowed + +**Trade-off**: Save money, spend time on manual parsing. + +--- + +### Recommended Combinations + +#### Option A: Agents SDK + Vercel AI SDK (Recommended ⭐) + +**Use when**: You need WebSockets/state AND want clean AI integration + +```typescript +import { AIChatAgent } from "agents/ai-chat-agent"; +import { streamText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage(onFinish) { + return streamText({ + model: openai('gpt-4o-mini'), + messages: this.messages, // Agents SDK manages history + onFinish + }).toTextStreamResponse(); + } +} +``` + +**Pros**: +- ✅ Best developer experience +- ✅ Automatic streaming +- ✅ WebSockets + state from Agents SDK +- ✅ Clean, maintainable code + +**Cons**: +- ⚠️ Requires external API keys +- ⚠️ Additional cost for AI provider + +--- + +#### Option B: Agents SDK + Workers AI + +**Use when**: You need WebSockets/state AND cost is critical + +```typescript +import { Agent } from "agents"; + +export class BudgetAgent extends Agent { + async onMessage(connection, message) { + const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', { + messages: [...], + stream: true + }); + + // Manual SSE parsing required (see Workers AI section above) + for await (const chunk of response) { + // ... manual parsing ... + } + } +} +``` + +**Pros**: +- ✅ Cost-effective +- ✅ No external dependencies +- ✅ WebSockets + state from Agents SDK + +**Cons**: +- ❌ Manual SSE parsing complexity +- ❌ Limited model selection +- ❌ More code to maintain + +--- + +#### Option C: Just Vercel AI SDK (No Agents) + +**Use when**: You DON'T need WebSockets or Durable Objects + +```typescript +// worker.ts - Simple Workers route +export default { + async fetch(request: Request, env: Env) { + const { messages } = await request.json(); + + const result = streamText({ + model: openai('gpt-4o-mini'), + messages + }); + + return result.toTextStreamResponse(); + } +} + +// client.tsx - Built-in React hooks +import { useChat } from 'ai/react'; + +function Chat() { + const { messages, input, handleSubmit } = useChat({ api: '/api/chat' }); + return
...
; +} +``` + +**Pros**: +- ✅ Simplest approach +- ✅ Least code +- ✅ Fast to implement +- ✅ Built-in React hooks + +**Cons**: +- ❌ No WebSockets (only SSE) +- ❌ No Durable Objects state +- ❌ No multi-agent coordination + +**Best for**: 80% of chat applications + +--- + +### Decision Matrix + +| Your Needs | Recommended Stack | Complexity | Cost | +|-----------|------------------|-----------|------| +| Simple chat, no state | AI SDK only | 🟢 Low | $$ (AI provider) | +| Chat + WebSockets + state | Agents SDK + AI SDK | 🟡 Medium | $$$ (infra + AI) | +| Chat + WebSockets + budget | Agents SDK + Workers AI | 🔴 High | $ (infra only) | +| Multi-agent workflows | Agents SDK + AI SDK | 🔴 High | $$$ (infra + AI) | +| MCP server with tools | Agents SDK (McpAgent) | 🟡 Medium | $ (infra only) | + +--- + +### Key Takeaway + +**Agents SDK is infrastructure, not AI.** You combine it with AI inference tools: + +- **For best DX**: Agents SDK + Vercel AI SDK ⭐ +- **For cost savings**: Agents SDK + Workers AI (accept manual parsing) +- **For simplicity**: Just AI SDK (if you don't need WebSockets/state) + +The rest of this skill focuses on Agents SDK (the infrastructure layer). For AI inference patterns, see the `ai-sdk-core` or `cloudflare-workers-ai` skills. + +--- + +## Configuration (wrangler.jsonc) + +**Critical Required Configuration**: +```jsonc +{ + "durable_objects": { + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }] + }, + "migrations": [ + { "tag": "v1", "new_sqlite_classes": ["MyAgent"] } // MUST be in first migration + ] +} +``` + +**Common Optional Bindings**: `ai`, `vectorize`, `browser`, `workflows`, `d1_databases`, `r2_buckets` + +**CRITICAL Migration Rules**: +- ✅ `new_sqlite_classes` MUST be in tag "v1" (cannot add SQLite to existing deployed class) +- ✅ `name` and `class_name` MUST match exactly +- ✅ Migrations are atomic (all instances updated simultaneously) +- ✅ Each tag must be unique, cannot edit/remove previous tags + +**See**: https://developers.cloudflare.com/agents/api-reference/configuration/ + +--- + +## Core Agent Patterns + +**Agent Class Basics** - Extend `Agent` with lifecycle methods: +- `onStart()` - Agent initialization +- `onRequest()` - Handle HTTP requests +- `onConnect/onMessage/onClose()` - WebSocket handling +- `onStateUpdate()` - React to state changes + +**Key Properties**: +- `this.env` - Environment bindings (AI, DB, etc.) +- `this.state` - Current agent state (read-only) +- `this.setState()` - Update persisted state +- `this.sql` - Built-in SQLite database +- `this.name` - Agent instance identifier +- `this.schedule()` - Schedule future tasks + +**See**: Official Agent API docs at https://developers.cloudflare.com/agents/api-reference/agents-api/ + +--- + +## WebSockets & Real-Time Communication + +Agents support WebSockets for bidirectional real-time communication. Use when you need: +- Client can send messages while server streams +- Agent-initiated messages (notifications, updates) +- Long-lived connections with state + +**Basic Pattern**: +```typescript +export class ChatAgent extends Agent { + async onConnect(connection: Connection, ctx: ConnectionContext) { + // Auth check, add to participants, send welcome + } + + async onMessage(connection: Connection, message: WSMessage) { + // Process message, update state, broadcast response + } +} +``` + +**SSE Alternative**: For one-way server → client streaming (simpler, HTTP-based), use Server-Sent Events instead of WebSockets. + +**See**: https://developers.cloudflare.com/agents/api-reference/websockets/ + +--- + +## State Management + +**Two State Mechanisms**: + +1. **`this.setState(newState)`** - JSON-serializable state (up to 1GB) + - Automatically persisted, syncs to WebSocket clients + - Use for: User preferences, session data, small datasets + +2. **`this.sql`** - Built-in SQLite database (up to 1GB) + - Tagged template literals prevent SQL injection + - Use for: Relational data, large datasets, complex queries + +**State Rules**: +- ✅ JSON-serializable only (objects, arrays, primitives, null) +- ✅ Persists across restarts, immediately consistent +- ❌ No functions or circular references +- ❌ 1GB total limit (state + SQL combined) + +**SQL Pattern**: +```typescript +await this.sql`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)` +await this.sql`INSERT INTO users (email) VALUES (${userEmail})` // ← Prepared statement +const users = await this.sql`SELECT * FROM users WHERE email = ${email}` // ← Returns array +``` + +**See**: https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ + +--- + +## Schedule Tasks + +Agents can schedule tasks to run in the future using `this.schedule()`. + +### Delay (Seconds) + +```typescript +export class MyAgent extends Agent { + async onRequest(request: Request): Promise { + // Schedule task to run in 60 seconds + const { id } = await this.schedule(60, "checkStatus", { requestId: "123" }); + + return Response.json({ scheduledTaskId: id }); + } + + // This method will be called in 60 seconds + async checkStatus(data: { requestId: string }) { + console.log('Checking status for request:', data.requestId); + // Perform check, update state, send notification, etc. + } +} +``` + +### Specific Date + +```typescript +export class MyAgent extends Agent { + async scheduleReminder(reminderDate: string) { + const date = new Date(reminderDate); + + const { id } = await this.schedule(date, "sendReminder", { + message: "Time for your appointment!" + }); + + return id; + } + + async sendReminder(data: { message: string }) { + console.log('Sending reminder:', data.message); + // Send email, push notification, etc. + } +} +``` + +### Cron Expressions + +```typescript +export class MyAgent extends Agent { + async setupRecurringTasks() { + // Every 10 minutes + await this.schedule("*/10 * * * *", "checkUpdates", {}); + + // Every day at 8 AM + await this.schedule("0 8 * * *", "dailyReport", {}); + + // Every Monday at 9 AM + await this.schedule("0 9 * * 1", "weeklyReport", {}); + + // Every hour on the hour + await this.schedule("0 * * * *", "hourlyCheck", {}); + } + + async checkUpdates(data: any) { + console.log('Checking for updates...'); + } + + async dailyReport(data: any) { + console.log('Generating daily report...'); + } + + async weeklyReport(data: any) { + console.log('Generating weekly report...'); + } + + async hourlyCheck(data: any) { + console.log('Running hourly check...'); + } +} +``` + +### Managing Scheduled Tasks + +```typescript +export class MyAgent extends Agent { + async manageSchedules() { + // Get all scheduled tasks + const allTasks = this.getSchedules(); + console.log('Total tasks:', allTasks.length); + + // Get specific task by ID + const taskId = "some-task-id"; + const task = await this.getSchedule(taskId); + + if (task) { + console.log('Task:', task.callback, 'at', new Date(task.time)); + console.log('Payload:', task.payload); + console.log('Type:', task.type); // "scheduled" | "delayed" | "cron" + + // Cancel the task + const cancelled = await this.cancelSchedule(taskId); + console.log('Cancelled:', cancelled); + } + + // Get tasks in time range + const upcomingTasks = this.getSchedules({ + timeRange: { + start: new Date(), + end: new Date(Date.now() + 24 * 60 * 60 * 1000) // Next 24 hours + } + }); + + console.log('Upcoming tasks:', upcomingTasks.length); + + // Filter by type + const cronTasks = this.getSchedules({ type: "cron" }); + const delayedTasks = this.getSchedules({ type: "delayed" }); + } +} +``` + +**Scheduling Constraints:** +- Each task maps to a SQL database row (max 2 MB per task) +- Total tasks limited by: `(task_size * count) + other_state < 1GB` +- Cron tasks continue running until explicitly cancelled +- Callback method MUST exist on Agent class (throws error if missing) + +**CRITICAL ERROR**: If callback method doesn't exist: +```typescript +// ❌ BAD: Method doesn't exist +await this.schedule(60, "nonExistentMethod", {}); + +// ✅ GOOD: Method exists +await this.schedule(60, "existingMethod", {}); + +async existingMethod(data: any) { + // Implementation +} +``` + +--- + +## Run Workflows + +Agents can trigger asynchronous [Cloudflare Workflows](https://developers.cloudflare.com/workflows/). + +### Workflow Binding Configuration + +`wrangler.jsonc`: + +```jsonc +{ + "workflows": [ + { + "name": "MY_WORKFLOW", + "class_name": "MyWorkflow" + } + ] +} +``` + +If Workflow is in a different script: + +```jsonc +{ + "workflows": [ + { + "name": "EMAIL_WORKFLOW", + "class_name": "EmailWorkflow", + "script_name": "email-workflows" // Different project + } + ] +} +``` + +### Triggering a Workflow + +```typescript +import { Agent } from "agents"; +import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +interface Env { + MY_WORKFLOW: Workflow; + MyAgent: AgentNamespace; +} + +export class MyAgent extends Agent { + async onRequest(request: Request): Promise { + const userId = new URL(request.url).searchParams.get('userId'); + + // Trigger a workflow immediately + const instance = await this.env.MY_WORKFLOW.create({ + id: `user-${userId}`, + params: { userId, action: "process" } + }); + + // Or schedule a delayed workflow trigger + await this.schedule(300, "runWorkflow", { userId }); + + return Response.json({ workflowId: instance.id }); + } + + async runWorkflow(data: { userId: string }) { + const instance = await this.env.MY_WORKFLOW.create({ + id: `delayed-${data.userId}`, + params: data + }); + + // Monitor workflow status periodically + await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id }); + } + + async checkWorkflowStatus(data: { id: string }) { + // Check workflow status (see Workflows docs for details) + console.log('Checking workflow:', data.id); + } +} + +// Workflow definition (can be in same or different file/project) +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) { + // Workflow implementation + const result = await step.do('process-data', async () => { + return { processed: true }; + }); + + return result; + } +} +``` + +### Agents vs Workflows + +| Feature | Agents | Workflows | +|---------|--------|-----------| +| **Purpose** | Interactive, user-facing | Background processing | +| **Duration** | Seconds to hours | Minutes to hours | +| **State** | SQLite database | Step-based checkpoints | +| **Interaction** | WebSockets, HTTP | No direct interaction | +| **Retry** | Manual | Automatic per step | +| **Use Case** | Chat, real-time UI | ETL, batch processing | + +**Best Practice**: Use Agents to **coordinate** multiple Workflows. Agents can trigger, monitor, and respond to Workflow results while maintaining user interaction. + +--- + +## Browse the Web + +Agents can use Browser Rendering for web scraping and automation: + +**Binding**: Add `"browser": { "binding": "BROWSER" }` to wrangler.jsonc +**Package**: `@cloudflare/puppeteer` +**Use Case**: Web scraping, screenshots, automated browsing within agent workflows + +**See**: `cloudflare-browser-rendering` skill for complete Puppeteer + Workers integration guide. + +--- + +## Retrieval Augmented Generation (RAG) + +Agents can implement RAG using Vectorize (vector database) + Workers AI (embeddings): + +**Pattern**: Ingest docs → generate embeddings → store in Vectorize → query → retrieve context → pass to AI + +**Bindings**: +- `"ai": { "binding": "AI" }` - Workers AI for embeddings +- `"vectorize": { "bindings": [{ "binding": "VECTORIZE", "index_name": "my-vectors" }] }` - Vector search + +**Typical Workflow**: +1. Generate embeddings with Workers AI (`@cf/baai/bge-base-en-v1.5`) +2. Upsert vectors to Vectorize (`this.env.VECTORIZE.upsert(vectors)`) +3. Query similar vectors (`this.env.VECTORIZE.query(queryVector, { topK: 5 })`) +4. Use retrieved context in AI prompt + +**See**: `cloudflare-vectorize` skill for complete RAG implementation guide. + +--- + +## Using AI Models + +Agents can call AI models using: +- **Vercel AI SDK** (recommended): Multi-provider, automatic streaming, tool calling +- **Workers AI**: Cloudflare's on-platform inference (cost-effective, manual parsing) + +**Architecture Note**: Agents SDK provides infrastructure (WebSockets, state, scheduling). AI inference is a separate layer - use AI SDK for the "brain". + +**See**: +- `ai-sdk-core` skill for complete AI SDK integration patterns +- `cloudflare-workers-ai` skill for Workers AI streaming parsing + +--- + +## Calling Agents + +**Two Main Patterns**: + +1. **`routeAgentRequest(request, env)`** - Auto-route via URL pattern `/agents/:agent/:name` + - Example: `/agents/my-agent/user-123` routes to MyAgent instance "user-123" + +2. **`getAgentByName(env.AgentBinding, instanceName)`** - Custom routing + - Returns agent stub for calling methods or passing requests + - Example: `const agent = getAgentByName(env.MyAgent, 'user-${userId}')` + +**Multi-Agent Communication**: +```typescript +export class AgentA extends Agent { + async processData(data: any) { + const agentB = getAgentByName(this.env.AgentB, 'processor-1'); + return await (await agentB).analyze(data); + } +} +``` + +**CRITICAL Security**: Always authenticate in Worker BEFORE creating/accessing agents. Agents should assume the caller is authorized. + +**See**: https://developers.cloudflare.com/agents/api-reference/calling-agents/ + +--- + +## Client APIs + +**Browser/React Integration**: +- **`AgentClient`** (from `agents/client`) - WebSocket client for browser +- **`agentFetch`** (from `agents/client`) - HTTP requests to agents +- **`useAgent`** (from `agents/react`) - React hook for WebSocket connections + state sync +- **`useAgentChat`** (from `agents/ai-react`) - Pre-built chat UI hook + +**All client libraries automatically handle**: WebSocket connections, state synchronization, reconnection logic. + +**See**: https://developers.cloudflare.com/agents/api-reference/client-apis/ + +--- + +## Model Context Protocol (MCP) + +Build MCP servers using the Agents SDK. + +### MCP Server Setup + +```bash +npm install @modelcontextprotocol/sdk agents +``` + +### Basic MCP Server + +```typescript +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +export class MyMCP extends McpAgent { + server = new McpServer({ name: "Demo", version: "1.0.0" }); + + async init() { + // Define a tool + this.server.tool( + "add", + "Add two numbers together", + { + a: z.number().describe("First number"), + b: z.number().describe("Second number") + }, + async ({ a, b }) => ({ + content: [{ type: "text", text: String(a + b) }] + }) + ); + } +} +``` + +### Stateful MCP Server + +```typescript +type State = { counter: number }; + +export class StatefulMCP extends McpAgent { + server = new McpServer({ name: "Counter", version: "1.0.0" }); + + initialState: State = { counter: 0 }; + + async init() { + // Resource + this.server.resource( + "counter", + "mcp://resource/counter", + (uri) => ({ + contents: [{ uri: uri.href, text: String(this.state.counter) }] + }) + ); + + // Tool + this.server.tool( + "increment", + "Increment the counter", + { amount: z.number() }, + async ({ amount }) => { + this.setState({ + ...this.state, + counter: this.state.counter + amount + }); + + return { + content: [{ + type: "text", + text: `Counter is now ${this.state.counter}` + }] + }; + } + ); + } +} +``` + +### MCP Transport Configuration + +```typescript +import { Hono } from 'hono'; + +const app = new Hono(); + +// Modern streamable HTTP transport (recommended) +app.mount('/mcp', MyMCP.serve('/mcp').fetch, { replaceRequest: false }); + +// Legacy SSE transport (deprecated) +app.mount('/sse', MyMCP.serveSSE('/sse').fetch, { replaceRequest: false }); + +export default app; +``` + +**Transport Comparison:** +- **/mcp**: Streamable HTTP (modern, recommended) +- **/sse**: Server-Sent Events (legacy, deprecated) + +### MCP with OAuth + +```typescript +import { OAuthProvider } from '@cloudflare/workers-oauth-provider'; + +export default new OAuthProvider({ + apiHandlers: { + '/sse': MyMCP.serveSSE('/sse'), + '/mcp': MyMCP.serve('/mcp') + }, + // OAuth configuration + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + // ... other OAuth settings +}); +``` + +### Testing MCP Server + +```bash +# Run MCP inspector +npx @modelcontextprotocol/inspector@latest + +# Connect to: http://localhost:8788/mcp +``` + +**Cloudflare's MCP Servers**: See [reference](https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/) for production examples. + +--- + + +## Critical Rules + +### Always Do ✅ + +1. **Export Agent class** - Must be exported for binding to work +2. **Include new_sqlite_classes in v1 migration** - Cannot add SQLite later +3. **Match binding name to class name** - Prevents "binding not found" errors +4. **Authenticate in Worker, not Agent** - Security best practice +5. **Use tagged template literals for SQL** - Prevents SQL injection +6. **Handle WebSocket disconnections** - State persists, connections don't +7. **Verify scheduled task callback exists** - Throws error if method missing +8. **Use global unique instance names** - Same name = same agent globally +9. **Check state size limits** - Max 1GB total per agent +10. **Monitor task payload size** - Max 2MB per scheduled task +11. **Use workflow bindings correctly** - Must be configured in wrangler.jsonc +12. **Create Vectorize indexes before inserting** - Required for metadata filtering +13. **Close browser instances** - Prevent resource leaks +14. **Use setState() for persistence** - Don't just modify this.state +15. **Test migrations locally first** - Migrations are atomic, can't rollback + +### Never Do ❌ + +1. **Don't add SQLite to existing deployed class** - Must be in first migration +2. **Don't gradually deploy migrations** - Atomic only +3. **Don't skip authentication in Worker** - Always auth before agent access +4. **Don't construct SQL strings manually** - Use tagged templates +5. **Don't exceed 1GB state per agent** - Hard limit +6. **Don't schedule tasks with non-existent callbacks** - Runtime error +7. **Don't assume same name = different agent** - Global uniqueness +8. **Don't use SSE for MCP** - Deprecated, use /mcp transport +9. **Don't forget browser binding** - Required for web browsing +10. **Don't modify this.state directly** - Use setState() instead + +--- + +## Known Issues Prevention + +This skill prevents **16+** documented issues: + +### Issue 1: Migrations Not Atomic +**Error**: "Cannot gradually deploy migration" +**Source**: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/ +**Why**: Migrations apply to all instances simultaneously +**Prevention**: Deploy migrations independently of code changes, use `npx wrangler versions deploy` + +### Issue 2: Missing new_sqlite_classes +**Error**: "Cannot enable SQLite on existing class" +**Source**: https://developers.cloudflare.com/agents/api-reference/configuration/ +**Why**: SQLite must be enabled in first migration +**Prevention**: Include `new_sqlite_classes` in tag "v1" migration + +### Issue 3: Agent Class Not Exported +**Error**: "Binding not found" or "Cannot access undefined" +**Source**: https://developers.cloudflare.com/agents/api-reference/agents-api/ +**Why**: Durable Objects require exported class +**Prevention**: `export class MyAgent extends Agent` (with export keyword) + +### Issue 4: Binding Name Mismatch +**Error**: "Binding 'X' not found" +**Source**: https://developers.cloudflare.com/agents/api-reference/configuration/ +**Why**: Binding name must match class name exactly +**Prevention**: Ensure `name` and `class_name` are identical in wrangler.jsonc + +### Issue 5: Global Uniqueness Not Understood +**Error**: Unexpected behavior with agent instances +**Source**: https://developers.cloudflare.com/agents/api-reference/agents-api/ +**Why**: Same name always returns same agent instance globally +**Prevention**: Use unique identifiers (userId, sessionId) for instance names + +### Issue 6: WebSocket State Not Persisted +**Error**: Connection state lost after disconnect +**Source**: https://developers.cloudflare.com/agents/api-reference/websockets/ +**Why**: WebSocket connections don't persist, but agent state does +**Prevention**: Store important data in agent state via setState(), not connection state + +### Issue 7: Scheduled Task Callback Doesn't Exist +**Error**: "Method X does not exist on Agent" +**Source**: https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ +**Why**: this.schedule() calls method that isn't defined +**Prevention**: Ensure callback method exists before scheduling + +### Issue 8: State Size Limit Exceeded +**Error**: "Maximum database size exceeded" +**Source**: https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ +**Why**: Agent state + scheduled tasks exceed 1GB +**Prevention**: Monitor state size, use external storage (D1, R2) for large data + +### Issue 9: Scheduled Task Too Large +**Error**: "Task payload exceeds 2MB" +**Source**: https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ +**Why**: Each task maps to database row with 2MB limit +**Prevention**: Keep task payloads minimal, store large data in agent state/SQL + +### Issue 10: Workflow Binding Missing +**Error**: "Cannot read property 'create' of undefined" +**Source**: https://developers.cloudflare.com/agents/api-reference/run-workflows/ +**Why**: Workflow binding not configured in wrangler.jsonc +**Prevention**: Add workflow binding before using this.env.WORKFLOW + +### Issue 11: Browser Binding Required +**Error**: "BROWSER binding undefined" +**Source**: https://developers.cloudflare.com/agents/api-reference/browse-the-web/ +**Why**: Browser Rendering requires explicit binding +**Prevention**: Add `"browser": { "binding": "BROWSER" }` to wrangler.jsonc + +### Issue 12: Vectorize Index Not Found +**Error**: "Index does not exist" +**Source**: https://developers.cloudflare.com/agents/api-reference/rag/ +**Why**: Vectorize index must be created before use +**Prevention**: Run `wrangler vectorize create` before deploying agent + +### Issue 13: MCP Transport Confusion +**Error**: "SSE transport deprecated" +**Source**: https://developers.cloudflare.com/agents/model-context-protocol/transport/ +**Why**: SSE transport is legacy, streamable HTTP is recommended +**Prevention**: Use `/mcp` endpoint with `MyMCP.serve('/mcp')`, not `/sse` + +### Issue 14: Authentication Bypass +**Error**: Security vulnerability +**Source**: https://developers.cloudflare.com/agents/api-reference/calling-agents/ +**Why**: Authentication done in Agent instead of Worker +**Prevention**: Always authenticate in Worker before calling getAgentByName() + +### Issue 15: Instance Naming Errors +**Error**: Cross-user data leakage +**Source**: https://developers.cloudflare.com/agents/api-reference/calling-agents/ +**Why**: Poor instance naming allows access to wrong agent +**Prevention**: Use namespaced names like `user-${userId}`, validate ownership + +### Issue 16: Workers AI Streaming Requires Manual Parsing +**Error**: "Cannot read property 'response' of undefined" or empty AI responses +**Source**: https://developers.cloudflare.com/workers-ai/platform/streaming/ +**Why**: Workers AI returns streaming responses as `Uint8Array` in Server-Sent Events (SSE) format, not plain objects +**Prevention**: Use `TextDecoder` + SSE parsing pattern (see "Workers AI (Alternative for AI)" section above) + +**The problem** - Attempting to access stream chunks directly fails: +```typescript +const response = await env.AI.run(model, { stream: true }); +for await (const chunk of response) { + console.log(chunk.response); // ❌ undefined - chunk is Uint8Array, not object +} +``` + +**The solution** - Parse SSE format manually: +```typescript +const response = await env.AI.run(model, { stream: true }); +for await (const chunk of response) { + const text = new TextDecoder().decode(chunk); // Step 1: Uint8Array → string + if (text.startsWith('data: ')) { // Step 2: Check SSE format + const jsonStr = text.slice(6).trim(); // Step 3: Extract JSON from "data: {...}" + if (jsonStr === '[DONE]') break; // Step 4: Handle termination + const data = JSON.parse(jsonStr); // Step 5: Parse JSON + if (data.response) { // Step 6: Extract .response field + fullResponse += data.response; + } + } +} +``` + +**Better alternative**: Use Vercel AI SDK which handles this automatically: +```typescript +import { streamText } from 'ai'; +import { createCloudflare } from '@ai-sdk/cloudflare'; + +const cloudflare = createCloudflare(); +const result = streamText({ + model: cloudflare('@cf/meta/llama-3-8b-instruct', { binding: env.AI }), + messages +}); +// No manual parsing needed ✅ +``` + +**When to accept manual parsing**: +- Cost is critical (Workers AI is cheaper) +- No external dependencies allowed +- Willing to maintain SSE parsing code + +**When to use AI SDK instead**: +- Value developer time over compute cost +- Want automatic streaming +- Need multi-provider support + +--- + +## Dependencies + +### Required +- **cloudflare-worker-base** - Foundation (Hono, Vite, Workers setup) + +### Optional (by feature) +- **cloudflare-workers-ai** - For Workers AI model calls +- **cloudflare-vectorize** - For RAG with Vectorize +- **cloudflare-d1** - For additional persistent storage beyond agent state +- **cloudflare-r2** - For file storage +- **cloudflare-queues** - For message queues + +### NPM Packages +- `agents` - Agents SDK (required) +- `@modelcontextprotocol/sdk` - For building MCP servers +- `@cloudflare/puppeteer` - For web browsing +- `ai` - AI SDK for model calls +- `@ai-sdk/openai` - OpenAI models +- `@ai-sdk/anthropic` - Anthropic models + +--- + +## Official Documentation + +- **Agents SDK**: https://developers.cloudflare.com/agents/ +- **API Reference**: https://developers.cloudflare.com/agents/api-reference/ +- **Durable Objects**: https://developers.cloudflare.com/durable-objects/ +- **Workflows**: https://developers.cloudflare.com/workflows/ +- **Vectorize**: https://developers.cloudflare.com/vectorize/ +- **Browser Rendering**: https://developers.cloudflare.com/browser-rendering/ +- **Model Context Protocol**: https://modelcontextprotocol.io/ +- **Cloudflare MCP Servers**: https://github.com/cloudflare/mcp-server-cloudflare + +--- + +## Bundled Resources + +### Templates (templates/) +- `wrangler-agents-config.jsonc` - Complete configuration example +- `basic-agent.ts` - Minimal HTTP agent +- `websocket-agent.ts` - WebSocket handlers +- `state-sync-agent.ts` - State management patterns +- `scheduled-agent.ts` - Task scheduling +- `workflow-agent.ts` - Workflow integration +- `browser-agent.ts` - Web browsing +- `rag-agent.ts` - RAG implementation +- `chat-agent-streaming.ts` - Streaming chat +- `calling-agents-worker.ts` - Agent routing +- `react-useagent-client.tsx` - React client +- `mcp-server-basic.ts` - MCP server +- `hitl-agent.ts` - Human-in-the-loop + +### References (references/) +- `agent-class-api.md` - Complete Agent class reference +- `client-api-reference.md` - Browser client APIs +- `state-management-guide.md` - State and SQL deep dive +- `websockets-sse.md` - WebSocket vs SSE comparison +- `scheduling-api.md` - Task scheduling details +- `workflows-integration.md` - Workflows guide +- `browser-rendering.md` - Web browsing patterns +- `rag-patterns.md` - RAG best practices +- `mcp-server-guide.md` - MCP server development +- `mcp-tools-reference.md` - MCP tools API +- `hitl-patterns.md` - Human-in-the-loop workflows +- `best-practices.md` - Production patterns + +### Examples (examples/) +- `chat-bot-complete.md` - Full chat agent +- `multi-agent-workflow.md` - Agent orchestration +- `scheduled-reports.md` - Recurring tasks +- `browser-scraper-agent.md` - Web scraping +- `rag-knowledge-base.md` - RAG system +- `mcp-remote-server.md` - Production MCP server + +--- + +**Last Verified**: 2025-10-21 +**Package Versions**: agents@latest +**Compliance**: Cloudflare Agents SDK official documentation diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..24a151f --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,101 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/cloudflare-agents", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "2c04d7dee1396ffadc63b1d747184be429d47f70", + "treeHash": "c610f39c205077781ca78481181e555a7829bbb6d56844fa5accacb964721e04", + "generatedAt": "2025-11-28T10:18:57.664327Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "cloudflare-agents", + "description": "Build AI agents with Cloudflare Agents SDK on Workers + Durable Objects. Includes critical guidance on choosing between Agents SDK (infrastructure/state) vs AI SDK (simpler flows). Use when: deciding SDK choice, building WebSocket agents with state, RAG with Vectorize, MCP servers, multi-agent orchestration, or troubleshooting Agent class must extend, new_sqlite_classes, binding errors.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "87d91f29741661c0d543c88117615d3cea277b545ea9fdeab29c08a34d00367e" + }, + { + "path": "SKILL.md", + "sha256": "5b9f11c376f3f962bd67ec429bf3292257f57054bd8a4533157ca257a867e04e" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "2a03fe886ee369b8c0ca74e5885f47f376b9e0d603ef06f3786383755bbd1a85" + }, + { + "path": "templates/react-useagent-client.tsx", + "sha256": "5e98749b2516460780e2aaa86fa0b70f51da06086e93d4513b337f8e04b5c785" + }, + { + "path": "templates/chat-agent-streaming.ts", + "sha256": "07cf303e46bb71c2492047bb5d5e188089f6df18dbe9512e4d986273c9ac39c1" + }, + { + "path": "templates/basic-agent.ts", + "sha256": "8955862f2adb09e8c3b874b9462d3e1e6ee40e550faa41c452ba88247878a52b" + }, + { + "path": "templates/calling-agents-worker.ts", + "sha256": "7c8285c3b568a6251bda198813e4740f080cb5323baac1b6a4309805a7d926f6" + }, + { + "path": "templates/websocket-agent.ts", + "sha256": "e75a6d66088c68e71093bd8b41fae003ecafbede93bd5624e1eeb3e895252c2f" + }, + { + "path": "templates/mcp-server-basic.ts", + "sha256": "e5130212558022851d774ce65b8e4174c148f86d8576838e418604c4190c3ffb" + }, + { + "path": "templates/browser-agent.ts", + "sha256": "97dc2c37dd0b97d0ff9d0fcd3af25e6f4ef4102d4a119cf7d7e989309dcfda87" + }, + { + "path": "templates/workflow-agent.ts", + "sha256": "d98dba3e66f976f5f23a7f4d912d45e31d673d500e40d2a8ec616192f9b06451" + }, + { + "path": "templates/wrangler-agents-config.jsonc", + "sha256": "869b98ff44b4b6fa9e1dd615b9f11ba7e489389afa99c34d92030d7d71db2887" + }, + { + "path": "templates/scheduled-agent.ts", + "sha256": "b678c720ba71fbcf57424fef17ec8c7156aefd8657214b3348fffc54077386a4" + }, + { + "path": "templates/hitl-agent.ts", + "sha256": "a521c8fa441e1a5b8fa9e30ee8eda96bf506eca1e8dfdcbf7675de17fe6e6218" + }, + { + "path": "templates/rag-agent.ts", + "sha256": "3a206fbe0a2a1de0b1833c53115affedf65c6e21dec14f10e8a4d48865238144" + }, + { + "path": "templates/simple-chat-no-agents-sdk.ts", + "sha256": "733e4c8179e1a1dc74e0ee7f17d581a678467cd85e99805358aeb657e8ad8757" + }, + { + "path": "templates/state-sync-agent.ts", + "sha256": "aaec5a48ee4ecddbaf94bafbd2d4c02d9a54fff166fcfd093859f79d3b81fb84" + } + ], + "dirSha256": "c610f39c205077781ca78481181e555a7829bbb6d56844fa5accacb964721e04" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/templates/basic-agent.ts b/templates/basic-agent.ts new file mode 100644 index 0000000..6b8bf9c --- /dev/null +++ b/templates/basic-agent.ts @@ -0,0 +1,94 @@ +// Basic Cloudflare Agent with HTTP request handling + +import { Agent } from "agents"; + +interface Env { + // Add environment variables and bindings here + // Example: OPENAI_API_KEY: string; +} + +interface State { + counter: number; + messages: string[]; + lastUpdated: Date | null; +} + +export class MyAgent extends Agent { + // Set initial state (first time agent is created) + initialState: State = { + counter: 0, + messages: [], + lastUpdated: null + }; + + // Called when agent starts or wakes from hibernation + async onStart() { + console.log('Agent started:', this.name); + console.log('Current state:', this.state); + } + + // Handle HTTP requests + async onRequest(request: Request): Promise { + const url = new URL(request.url); + const method = request.method; + + // GET /status - Return current state + if (method === "GET" && url.pathname === "/status") { + return Response.json({ + agent: this.name, + state: this.state, + timestamp: new Date().toISOString() + }); + } + + // POST /increment - Increment counter + if (method === "POST" && url.pathname === "/increment") { + const newCounter = this.state.counter + 1; + + this.setState({ + ...this.state, + counter: newCounter, + lastUpdated: new Date() + }); + + return Response.json({ + success: true, + counter: newCounter + }); + } + + // POST /message - Add message + if (method === "POST" && url.pathname === "/message") { + const { message } = await request.json(); + + this.setState({ + ...this.state, + messages: [...this.state.messages, message], + lastUpdated: new Date() + }); + + return Response.json({ + success: true, + messageCount: this.state.messages.length + }); + } + + // POST /reset - Reset state + if (method === "POST" && url.pathname === "/reset") { + this.setState(this.initialState); + + return Response.json({ success: true, message: "State reset" }); + } + + // 404 for unknown routes + return new Response("Not Found", { status: 404 }); + } + + // Optional: React to state updates + onStateUpdate(state: State, source: "server" | Connection) { + console.log('State updated:', state); + console.log('Update source:', source); + } +} + +export default MyAgent; diff --git a/templates/browser-agent.ts b/templates/browser-agent.ts new file mode 100644 index 0000000..8b6edc6 --- /dev/null +++ b/templates/browser-agent.ts @@ -0,0 +1,197 @@ +// Agent with Browser Rendering (Puppeteer) + +import { Agent } from "agents"; +import puppeteer from "@cloudflare/puppeteer"; + +interface Env { + BROWSER: Fetcher; // Browser Rendering binding + OPENAI_API_KEY?: string; +} + +export class BrowserAgent extends Agent { + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + // POST /scrape - Scrape single URL + if (url.pathname === "/scrape") { + const { url: targetUrl } = await request.json(); + + const data = await this.scrapeUrl(targetUrl); + + return Response.json({ url: targetUrl, data }); + } + + // POST /screenshot - Capture screenshot + if (url.pathname === "/screenshot") { + const { url: targetUrl } = await request.json(); + + const screenshot = await this.captureScreenshot(targetUrl); + + return new Response(screenshot, { + headers: { 'Content-Type': 'image/png' } + }); + } + + // POST /batch-scrape - Scrape multiple URLs + if (url.pathname === "/batch-scrape") { + const { urls } = await request.json(); + + const results = await this.batchScrape(urls); + + return Response.json({ results }); + } + + return new Response("Not Found", { status: 404 }); + } + + // Scrape single URL + async scrapeUrl(url: string): Promise { + const browser = await puppeteer.launch(this.env.BROWSER); + const page = await browser.newPage(); + + try { + await page.goto(url, { waitUntil: 'networkidle0' }); + + // Wait for body to load + await page.waitForSelector("body"); + + // Extract page content + const data = await page.evaluate(() => ({ + title: document.title, + url: window.location.href, + text: document.body.innerText, + html: document.body.innerHTML + })); + + await browser.close(); + + return data; + } catch (error) { + await browser.close(); + throw error; + } + } + + // Capture screenshot + async captureScreenshot(url: string): Promise { + const browser = await puppeteer.launch(this.env.BROWSER); + const page = await browser.newPage(); + + try { + await page.goto(url, { waitUntil: 'networkidle0' }); + + const screenshot = await page.screenshot({ + fullPage: true, + type: 'png' + }); + + await browser.close(); + + return screenshot; + } catch (error) { + await browser.close(); + throw error; + } + } + + // Batch scrape multiple URLs + async batchScrape(urls: string[]): Promise { + const results = []; + + for (const url of urls) { + try { + const data = await this.scrapeUrl(url); + results.push({ url, success: true, data }); + } catch (error) { + results.push({ + url, + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + + return results; + } + + // Extract structured data with AI + async extractDataWithAI(url: string): Promise { + const browser = await puppeteer.launch(this.env.BROWSER); + const page = await browser.newPage(); + + try { + await page.goto(url); + await page.waitForSelector("body"); + + const bodyContent = await page.$eval("body", el => el.innerHTML); + + await browser.close(); + + // Use OpenAI to extract structured data + if (this.env.OPENAI_API_KEY) { + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [{ + role: 'user', + content: `Extract product information as JSON from this HTML: ${bodyContent.slice(0, 4000)}` + }], + response_format: { type: "json_object" } + }) + }); + + const result = await response.json(); + return JSON.parse(result.choices[0].message.content); + } + + return { html: bodyContent }; + } catch (error) { + await browser.close(); + throw error; + } + } + + // Interactive browsing example + async fillForm(url: string, formData: any): Promise { + const browser = await puppeteer.launch(this.env.BROWSER); + const page = await browser.newPage(); + + try { + await page.goto(url); + + // Fill form fields + if (formData.email) { + await page.type('input[name="email"]', formData.email); + } + + if (formData.password) { + await page.type('input[name="password"]', formData.password); + } + + // Click submit button + await page.click('button[type="submit"]'); + + // Wait for navigation + await page.waitForNavigation(); + + const result = await page.evaluate(() => ({ + url: window.location.href, + title: document.title + })); + + await browser.close(); + + return result; + } catch (error) { + await browser.close(); + throw error; + } + } +} + +export default BrowserAgent; diff --git a/templates/calling-agents-worker.ts b/templates/calling-agents-worker.ts new file mode 100644 index 0000000..9310ad8 --- /dev/null +++ b/templates/calling-agents-worker.ts @@ -0,0 +1,125 @@ +// Worker that calls Agents using routeAgentRequest and getAgentByName + +import { Agent, AgentNamespace, routeAgentRequest, getAgentByName } from 'agents'; + +interface Env { + MyAgent: AgentNamespace; + ChatAgent: AgentNamespace; +} + +// Worker fetch handler +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Pattern 1: Automatic routing with routeAgentRequest + // Routes to: /agents/:agent-name/:instance-name + // Example: /agents/my-agent/user-123 + if (url.pathname.startsWith('/agents/')) { + const response = await routeAgentRequest(request, env); + + if (response) { + return response; + } + + return new Response("Agent not found", { status: 404 }); + } + + // Pattern 2: Custom routing with getAgentByName + // Example: /user/:userId/profile + if (url.pathname.startsWith('/user/')) { + const userId = url.pathname.split('/')[2]; + + // Authenticate first (CRITICAL) + const authHeader = request.headers.get('Authorization'); + if (!authHeader) { + return new Response("Unauthorized", { status: 401 }); + } + + // Verify token and get authenticated userId + const authenticatedUserId = await verifyToken(authHeader); + if (!authenticatedUserId || authenticatedUserId !== userId) { + return new Response("Forbidden", { status: 403 }); + } + + // Get or create agent for this user + const agent = getAgentByName(env.MyAgent, `user-${userId}`); + + // Pass request to agent + return (await agent).fetch(request); + } + + // Pattern 3: Call agent methods directly (RPC) + if (url.pathname === '/api/process') { + const { userId, data } = await request.json(); + + const agent = getAgentByName(env.MyAgent, `user-${userId}`); + + // Call custom method on agent + const result = await (await agent).processData(data); + + return Response.json({ result }); + } + + // Pattern 4: Multi-agent communication + if (url.pathname === '/api/chat-with-context') { + const { userId, message } = await request.json(); + + // Get user agent + const userAgent = getAgentByName(env.MyAgent, `user-${userId}`); + + // Get chat agent + const chatAgent = getAgentByName(env.ChatAgent, `chat-${userId}`); + + // User agent prepares context + const context = await (await userAgent).getContext(); + + // Chat agent generates response + const response = await (await chatAgent).chat(message, context); + + return Response.json({ response }); + } + + return new Response("Not Found", { status: 404 }); + } +} satisfies ExportedHandler; + +// Helper: Verify JWT token +async function verifyToken(token: string): Promise { + try { + // Implement your token verification logic + // Example: JWT verification, session validation, etc. + return "user-123"; // Return userId if valid + } catch (e) { + return null; + } +} + +// Agent definitions + +export class MyAgent extends Agent { + async onRequest(request: Request): Promise { + return Response.json({ + agent: this.name, + message: "Hello from MyAgent" + }); + } + + async processData(data: any): Promise { + return { processed: true, data }; + } + + async getContext(): Promise { + return { user: this.name, preferences: this.state }; + } +} + +export class ChatAgent extends Agent { + async onRequest(request: Request): Promise { + return Response.json({ agent: this.name }); + } + + async chat(message: string, context: any): Promise { + return `Response to: ${message} (with context: ${JSON.stringify(context)})`; + } +} diff --git a/templates/chat-agent-streaming.ts b/templates/chat-agent-streaming.ts new file mode 100644 index 0000000..ea7d7c8 --- /dev/null +++ b/templates/chat-agent-streaming.ts @@ -0,0 +1,120 @@ +/** + * Chat Agent with Streaming AI Responses + * + * ARCHITECTURE DECISION: + * This template uses Agents SDK + Vercel AI SDK together. Understanding WHY helps you decide if you need both. + * + * ┌─────────────────────────────────────────────────────┐ + * │ Agents SDK (AIChatAgent) Vercel AI SDK │ + * │ ├─ WebSocket connections + ├─ AI inference │ + * │ ├─ Durable Objects state ├─ Auto streaming │ + * │ ├─ Message persistence ├─ Multi-provider │ + * │ └─ Real-time sync └─ React hooks │ + * └─────────────────────────────────────────────────────┘ + * + * WHAT AGENTS SDK PROVIDES (AIChatAgent): + * - ✅ WebSocket bidirectional real-time communication + * - ✅ Durable Objects (globally unique agent instance per user/room) + * - ✅ Built-in message history (this.messages) with state persistence + * - ✅ Lifecycle methods (onStart, onConnect, onMessage, onClose) + * - ✅ SQLite storage for custom data (up to 1GB per agent) + * + * WHAT VERCEL AI SDK PROVIDES (streamText): + * - ✅ Automatic streaming response handling (no manual SSE parsing) + * - ✅ Multi-provider support (OpenAI, Anthropic, Google, etc.) + * - ✅ React hooks on client (useChat, useCompletion) + * - ✅ Unified API across providers + * - ✅ Works on Cloudflare Workers + * + * WHEN TO USE THIS COMBINATION: + * - ✅ Need WebSocket real-time chat (bidirectional, client can send while streaming) + * - ✅ Need persistent state across sessions (agent remembers previous conversations) + * - ✅ Need multi-user chat rooms (each room is a unique agent instance) + * - ✅ Want clean AI integration without manual SSE parsing + * + * WHEN YOU DON'T NEED THIS (Simpler Alternative): + * If you just need a basic chat interface with SSE streaming and no persistent state, + * you can use JUST the Vercel AI SDK without Agents SDK: + * + * ```typescript + * // worker.ts - No Agents SDK, 10x less code + * import { streamText } from 'ai'; + * import { openai } from '@ai-sdk/openai'; + * + * export default { + * async fetch(request: Request, env: Env) { + * const { messages } = await request.json(); + * const result = streamText({ model: openai('gpt-4o-mini'), messages }); + * return result.toTextStreamResponse(); // SSE streaming + * } + * } + * + * // client.tsx - Built-in React hooks + * import { useChat } from 'ai/react'; + * function Chat() { + * const { messages, input, handleSubmit } = useChat({ api: '/api/chat' }); + * return
...
; + * } + * ``` + * + * That's it. No Durable Objects, no WebSockets, no migrations. Works for 80% of chat apps. + * + * ALTERNATIVE: Agents SDK + Workers AI (Cost-Optimized) + * If you need Agents SDK infrastructure but want to save money, you can use Workers AI + * instead of external providers. Trade-off: manual SSE parsing required. + * See rag-agent-streaming-workers-ai.ts template for that approach. + * + * VERDICT: Use this template if you need WebSocket + state. Otherwise, start simpler. + */ + +import { AIChatAgent } from "agents/ai-chat-agent"; +import { streamText } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; + +interface Env { + OPENAI_API_KEY?: string; + ANTHROPIC_API_KEY?: string; +} + +export class StreamingChatAgent extends AIChatAgent { + // Handle incoming chat messages and stream response + async onChatMessage(onFinish) { + // Choose model based on available API keys + const model = this.env.ANTHROPIC_API_KEY + ? anthropic('claude-sonnet-4-5') + : openai('gpt-4o-mini'); + + // Stream text generation + const result = streamText({ + model, + messages: this.messages, // Built-in message history + onFinish, // Called when response complete + temperature: 0.7, + maxTokens: 1000 + }); + + // Return streaming response + return result.toTextStreamResponse(); + } + + // Optional: Customize state updates + async onStateUpdate(state, source) { + console.log('Chat updated:'); + console.log(' Messages:', this.messages.length); + console.log(' Last message:', this.messages[this.messages.length - 1]?.content); + } + + // Optional: Custom message persistence + async onStart() { + // Load previous messages from SQL if needed + const saved = await this.sql`SELECT * FROM messages ORDER BY timestamp`; + + if (saved.length > 0) { + // Restore message history + console.log('Restored', saved.length, 'messages'); + } + } +} + +export default StreamingChatAgent; diff --git a/templates/hitl-agent.ts b/templates/hitl-agent.ts new file mode 100644 index 0000000..2f6ec77 --- /dev/null +++ b/templates/hitl-agent.ts @@ -0,0 +1,250 @@ +// Human-in-the-Loop (HITL) Agent + +import { Agent } from "agents"; + +interface Env { + // Add bindings +} + +interface ApprovalRequest { + id: string; + type: 'booking' | 'payment' | 'data_change' | 'high_value'; + data: any; + confidence: number; + requestedAt: number; + status: 'pending' | 'approved' | 'rejected'; + reviewedBy?: string; + reviewedAt?: number; +} + +interface HITLState { + pendingApprovals: ApprovalRequest[]; + approvalHistory: ApprovalRequest[]; + autoProcessedCount: number; + humanReviewCount: number; +} + +export class HITLAgent extends Agent { + initialState: HITLState = { + pendingApprovals: [], + approvalHistory: [], + autoProcessedCount: 0, + humanReviewCount: 0 + }; + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + // POST /process - Process request (auto or human review) + if (url.pathname === "/process") { + const { data, type } = await request.json(); + + const result = await this.processRequest(data, type); + + return Response.json(result); + } + + // GET /pending - Get pending approvals + if (url.pathname === "/pending") { + return Response.json({ + pending: this.state.pendingApprovals, + count: this.state.pendingApprovals.length + }); + } + + // POST /approve/:id - Approve a request + if (url.pathname.startsWith("/approve/")) { + const id = url.pathname.split("/")[2]; + const { reviewedBy } = await request.json(); + + const result = await this.approveRequest(id, reviewedBy); + + return Response.json(result); + } + + // POST /reject/:id - Reject a request + if (url.pathname.startsWith("/reject/")) { + const id = url.pathname.split("/")[2]; + const { reviewedBy, reason } = await request.json(); + + const result = await this.rejectRequest(id, reviewedBy, reason); + + return Response.json(result); + } + + // GET /stats - Get approval statistics + if (url.pathname === "/stats") { + return Response.json({ + autoProcessed: this.state.autoProcessedCount, + humanReviewed: this.state.humanReviewCount, + pending: this.state.pendingApprovals.length, + approvalRate: this.calculateApprovalRate() + }); + } + + return new Response("Not Found", { status: 404 }); + } + + // Process request (decide auto vs human review) + async processRequest(data: any, type: ApprovalRequest['type']) { + // Step 1: Analyze request + const analysis = await this.analyzeRequest(data, type); + + // Step 2: Decide if human review needed + const needsHumanReview = this.needsHumanReview(analysis); + + if (!needsHumanReview) { + // High confidence - process automatically + const result = await this.autoProcess(data, type); + + this.setState({ + ...this.state, + autoProcessedCount: this.state.autoProcessedCount + 1 + }); + + return { + status: 'auto_processed', + result + }; + } + + // Low confidence - request human review + const approvalRequest: ApprovalRequest = { + id: crypto.randomUUID(), + type, + data, + confidence: analysis.confidence, + requestedAt: Date.now(), + status: 'pending' + }; + + this.setState({ + ...this.state, + pendingApprovals: [...this.state.pendingApprovals, approvalRequest], + humanReviewCount: this.state.humanReviewCount + 1 + }); + + // Send notification to human reviewers + await this.notifyReviewers(approvalRequest); + + return { + status: 'pending_review', + approvalId: approvalRequest.id, + reason: analysis.reason + }; + } + + // Analyze request to determine confidence + async analyzeRequest(data: any, type: string) { + // Implement your analysis logic here + // Could use AI model, rules engine, etc. + + // Example logic: + let confidence = 0.5; + let reason = ''; + + if (type === 'high_value' && data.amount > 10000) { + confidence = 0.3; + reason = 'High value transaction requires human approval'; + } else if (type === 'booking' && data.urgent) { + confidence = 0.6; + reason = 'Urgent booking may need verification'; + } else { + confidence = 0.9; + reason = 'Standard request'; + } + + return { confidence, reason }; + } + + // Determine if human review is needed + needsHumanReview(analysis: { confidence: number }): boolean { + const CONFIDENCE_THRESHOLD = 0.8; + return analysis.confidence < CONFIDENCE_THRESHOLD; + } + + // Process automatically (high confidence) + async autoProcess(data: any, type: string) { + console.log('Auto-processing:', type, data); + + // Implement auto-processing logic + return { + processed: true, + timestamp: Date.now() + }; + } + + // Approve a pending request + async approveRequest(id: string, reviewedBy: string) { + const approval = this.state.pendingApprovals.find(a => a.id === id); + + if (!approval) { + return { error: 'Approval request not found' }; + } + + // Update approval + approval.status = 'approved'; + approval.reviewedBy = reviewedBy; + approval.reviewedAt = Date.now(); + + // Process the approved request + const result = await this.autoProcess(approval.data, approval.type); + + // Update state + this.setState({ + ...this.state, + pendingApprovals: this.state.pendingApprovals.filter(a => a.id !== id), + approvalHistory: [...this.state.approvalHistory, approval] + }); + + return { + success: true, + result + }; + } + + // Reject a pending request + async rejectRequest(id: string, reviewedBy: string, reason: string) { + const approval = this.state.pendingApprovals.find(a => a.id === id); + + if (!approval) { + return { error: 'Approval request not found' }; + } + + // Update approval + approval.status = 'rejected'; + approval.reviewedBy = reviewedBy; + approval.reviewedAt = Date.now(); + (approval.data as any).rejectionReason = reason; + + // Update state + this.setState({ + ...this.state, + pendingApprovals: this.state.pendingApprovals.filter(a => a.id !== id), + approvalHistory: [...this.state.approvalHistory, approval] + }); + + return { success: true }; + } + + // Notify human reviewers + async notifyReviewers(approval: ApprovalRequest) { + console.log('Notifying reviewers for:', approval.id); + + // Implement notification logic: + // - Send email + // - Push notification + // - Slack message + // - Update dashboard + } + + // Calculate approval rate + calculateApprovalRate(): number { + const approved = this.state.approvalHistory.filter(a => a.status === 'approved').length; + const total = this.state.approvalHistory.length; + + return total > 0 ? approved / total : 0; + } +} + +export default HITLAgent; diff --git a/templates/mcp-server-basic.ts b/templates/mcp-server-basic.ts new file mode 100644 index 0000000..fe2a623 --- /dev/null +++ b/templates/mcp-server-basic.ts @@ -0,0 +1,182 @@ +// Model Context Protocol (MCP) Server with Agents SDK + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +interface Env { + // Add bindings (AI, DB, etc.) +} + +// Stateless MCP Server (basic tools) +export class BasicMCP extends McpAgent { + server = new McpServer({ + name: "BasicMCP", + version: "1.0.0" + }); + + async init() { + // Tool 1: Add numbers + this.server.tool( + "add", + "Add two numbers together", + { + a: z.number().describe("First number"), + b: z.number().describe("Second number") + }, + async ({ a, b }) => ({ + content: [{ type: "text", text: String(a + b) }] + }) + ); + + // Tool 2: Get current time + this.server.tool( + "get-time", + "Get the current server time", + {}, + async () => ({ + content: [{ + type: "text", + text: new Date().toISOString() + }] + }) + ); + + // Tool 3: Echo message + this.server.tool( + "echo", + "Echo back a message", + { + message: z.string().describe("Message to echo") + }, + async ({ message }) => ({ + content: [{ type: "text", text: message }] + }) + ); + } +} + +// Stateful MCP Server (with Agent state) +type CounterState = { + counter: number; + operations: string[]; +}; + +export class StatefulMCP extends McpAgent { + server = new McpServer({ + name: "StatefulMCP", + version: "1.0.0" + }); + + initialState: CounterState = { + counter: 0, + operations: [] + }; + + async init() { + // Resource: Counter value + this.server.resource( + "counter", + "mcp://resource/counter", + (uri) => ({ + contents: [{ + uri: uri.href, + text: String(this.state.counter) + }] + }) + ); + + // Resource: Operations history + this.server.resource( + "history", + "mcp://resource/history", + (uri) => ({ + contents: [{ + uri: uri.href, + text: JSON.stringify(this.state.operations, null, 2) + }] + }) + ); + + // Tool: Increment counter + this.server.tool( + "increment", + "Increment the counter by a specified amount", + { + amount: z.number().describe("Amount to increment") + }, + async ({ amount }) => { + const newCounter = this.state.counter + amount; + + this.setState({ + counter: newCounter, + operations: [ + ...this.state.operations, + `increment(${amount}) = ${newCounter}` + ] + }); + + return { + content: [{ + type: "text", + text: `Counter is now ${newCounter}` + }] + }; + } + ); + + // Tool: Reset counter + this.server.tool( + "reset", + "Reset the counter to zero", + {}, + async () => { + this.setState({ + counter: 0, + operations: [...this.state.operations, "reset() = 0"] + }); + + return { + content: [{ type: "text", text: "Counter reset to 0" }] + }; + } + ); + } + + // React to state updates + onStateUpdate(state: CounterState) { + console.log('MCP State updated:', state); + } +} + +// Export with Hono for routing +import { Hono } from 'hono'; + +const app = new Hono(); + +// Mount MCP servers +// Streamable HTTP transport (modern, recommended) +app.mount('/mcp', BasicMCP.serve('/mcp').fetch, { replaceRequest: false }); + +// SSE transport (legacy, deprecated) +app.mount('/sse', BasicMCP.serveSSE('/sse').fetch, { replaceRequest: false }); + +// Stateful MCP server +app.mount('/stateful/mcp', StatefulMCP.serve('/stateful/mcp').fetch, { replaceRequest: false }); + +export default app; + +// With OAuth (optional) +/* +import { OAuthProvider } from '@cloudflare/workers-oauth-provider'; + +export default new OAuthProvider({ + apiHandlers: { + '/sse': BasicMCP.serveSSE('/sse'), + '/mcp': BasicMCP.serve('/mcp') + }, + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + // ... other OAuth config +}); +*/ diff --git a/templates/rag-agent.ts b/templates/rag-agent.ts new file mode 100644 index 0000000..b6342b5 --- /dev/null +++ b/templates/rag-agent.ts @@ -0,0 +1,196 @@ +// RAG Agent with Vectorize + Workers AI + +import { Agent } from "agents"; +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +interface Env { + AI: Ai; // Workers AI binding + VECTORIZE: Vectorize; // Vectorize binding + OPENAI_API_KEY?: string; // Optional: OpenAI API key +} + +interface RAGState { + documentsIngested: number; + queriesProcessed: number; + lastQuery: string | null; +} + +export class RAGAgent extends Agent { + initialState: RAGState = { + documentsIngested: 0, + queriesProcessed: 0, + lastQuery: null + }; + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + // POST /ingest - Ingest documents + if (url.pathname === "/ingest") { + const { documents } = await request.json(); + // documents: Array<{ id: string, text: string, metadata: any }> + + const count = await this.ingestDocuments(documents); + + return Response.json({ + ingested: count, + total: this.state.documentsIngested + count + }); + } + + // POST /query - Query knowledge base + if (url.pathname === "/query") { + const { query, topK } = await request.json(); + + const results = await this.queryKnowledge(query, topK || 5); + + return Response.json({ + query, + matches: results.matches, + context: results.context + }); + } + + // POST /chat - RAG-powered chat + if (url.pathname === "/chat") { + const { message } = await request.json(); + + const response = await this.chat(message); + + return Response.json(response); + } + + return new Response("Not Found", { status: 404 }); + } + + // Ingest documents into Vectorize + async ingestDocuments(documents: Array<{ id: string; text: string; metadata?: any }>) { + const vectors = []; + + for (const doc of documents) { + // Generate embedding with Workers AI + const { data } = await this.env.AI.run('@cf/baai/bge-base-en-v1.5', { + text: [doc.text] + }); + + vectors.push({ + id: doc.id, + values: data[0], + metadata: { + text: doc.text, + ...doc.metadata + } + }); + } + + // Upsert into Vectorize (batch operation) + await this.env.VECTORIZE.upsert(vectors); + + // Update state + this.setState({ + ...this.state, + documentsIngested: this.state.documentsIngested + vectors.length + }); + + return vectors.length; + } + + // Query knowledge base + async queryKnowledge(userQuery: string, topK: number = 5) { + // Generate query embedding + const { data } = await this.env.AI.run('@cf/baai/bge-base-en-v1.5', { + text: [userQuery] + }); + + // Search Vectorize + const results = await this.env.VECTORIZE.query(data[0], { + topK, + returnMetadata: 'all' + }); + + // Extract context from matches + const context = results.matches + .map(match => match.metadata?.text) + .filter(Boolean) + .join('\n\n'); + + // Update state + this.setState({ + ...this.state, + queriesProcessed: this.state.queriesProcessed + 1, + lastQuery: userQuery + }); + + return { + matches: results.matches.map(m => ({ + id: m.id, + score: m.score, + metadata: m.metadata + })), + context + }; + } + + // RAG-powered chat + async chat(userMessage: string) { + // 1. Retrieve relevant context + const { context } = await this.queryKnowledge(userMessage); + + // 2. Generate response with context + if (this.env.OPENAI_API_KEY) { + // Use OpenAI with AI SDK + const { text } = await generateText({ + model: openai('gpt-4o-mini'), + messages: [ + { + role: 'system', + content: `You are a helpful assistant. Use the following context to answer questions accurately:\n\n${context}` + }, + { + role: 'user', + content: userMessage + } + ] + }); + + return { response: text, context }; + } else { + // Use Workers AI + const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', { + messages: [ + { + role: 'system', + content: `You are a helpful assistant. Use the following context to answer questions:\n\n${context}` + }, + { + role: 'user', + content: userMessage + } + ] + }); + + return { + response: response.response, + context + }; + } + } + + // Query with metadata filtering + async queryWithFilter(userQuery: string, filters: any) { + const { data } = await this.env.AI.run('@cf/baai/bge-base-en-v1.5', { + text: [userQuery] + }); + + const results = await this.env.VECTORIZE.query(data[0], { + topK: 5, + filter: filters, // e.g., { category: { $eq: "docs" } } + returnMetadata: 'all' + }); + + return results; + } +} + +export default RAGAgent; diff --git a/templates/react-useagent-client.tsx b/templates/react-useagent-client.tsx new file mode 100644 index 0000000..d315606 --- /dev/null +++ b/templates/react-useagent-client.tsx @@ -0,0 +1,235 @@ +// React client using useAgent and useAgentChat hooks + +import { useAgent } from "agents/react"; +import { useAgentChat } from "agents/ai-react"; +import { useState, useEffect } from "react"; + +// Example 1: useAgent with WebSocket connection +export function AgentConnection() { + const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting'); + const [messages, setMessages] = useState([]); + + const connection = useAgent({ + agent: "chat-agent", // Agent class name (kebab-case) + name: "room-123", // Agent instance name + host: window.location.host, // Optional: defaults to current host + + onOpen: () => { + console.log("Connected"); + setStatus('connected'); + }, + + onClose: () => { + console.log("Disconnected"); + setStatus('disconnected'); + }, + + onMessage: (event) => { + const data = JSON.parse(event.data); + console.log("Received:", data); + + if (data.type === 'message') { + setMessages(prev => [...prev, data.message]); + } + }, + + onError: (error) => { + console.error("Connection error:", error); + } + }); + + const sendMessage = (text: string) => { + connection.send(JSON.stringify({ + type: 'chat', + text, + sender: 'user-123' + })); + }; + + return ( +
+

Status: {status}

+ +
+ {messages.map((msg, i) => ( +
{msg.text}
+ ))} +
+ + +
+ ); +} + +// Example 2: useAgent with state synchronization +export function Counter() { + const [count, setCount] = useState(0); + + const agent = useAgent({ + agent: "counter-agent", + name: "my-counter", + + onStateUpdate: (newState) => { + // Automatically called when agent state changes + setCount(newState.counter); + } + }); + + const increment = () => { + // Update agent state (syncs to all connected clients) + agent.setState({ counter: count + 1 }); + }; + + const decrement = () => { + agent.setState({ counter: count - 1 }); + }; + + return ( +
+

Count: {count}

+ + +
+ ); +} + +// Example 3: useAgentChat for AI chat interface +export function ChatInterface() { + const { + messages, // All chat messages + input, // Current input value + handleInputChange, // Update input + handleSubmit, // Send message + isLoading, // Loading state + error, // Error state + reload, // Reload last response + stop // Stop generation + } = useAgentChat({ + agent: "streaming-chat-agent", + name: "chat-session-123", + + // Optional: Custom headers + headers: { + 'Authorization': 'Bearer your-token' + }, + + // Optional: Initial messages + initialMessages: [ + { role: 'system', content: 'You are a helpful assistant.' } + ], + + // Optional: Called when message complete + onFinish: (message) => { + console.log('Message complete:', message); + }, + + // Optional: Called on error + onError: (error) => { + console.error('Chat error:', error); + } + }); + + return ( +
+ {/* Message history */} +
+ {messages.map((msg, i) => ( +
+ {msg.role}: +

{msg.content}

+
+ ))} + + {isLoading &&
Thinking...
} + {error &&
{error.message}
} +
+ + {/* Input form */} +
+ + + + + {isLoading && ( + + )} +
+
+ ); +} + +// Example 4: Multiple agent connections +export function MultiAgentDemo() { + const userAgent = useAgent({ + agent: "user-agent", + name: "user-123" + }); + + const notificationAgent = useAgent({ + agent: "notification-agent", + name: "user-123" + }); + + useEffect(() => { + // Subscribe to notifications + notificationAgent.addEventListener('message', (event) => { + const data = JSON.parse(event.data); + if (data.type === 'notification') { + alert(data.message); + } + }); + }, [notificationAgent]); + + return ( +
+

Multi-Agent Connection

+

Connected to multiple agents

+
+ ); +} + +// Example 5: HTTP requests with agentFetch +import { agentFetch } from "agents/client"; + +export async function fetchAgentData() { + try { + const response = await agentFetch( + { + agent: "data-agent", + name: "user-123" + }, + { + method: "GET", + headers: { + 'Authorization': `Bearer ${getToken()}` + } + } + ); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to fetch:", error); + throw error; + } +} + +function getToken(): string { + // Get auth token from storage or state + return localStorage.getItem('auth-token') || ''; +} diff --git a/templates/scheduled-agent.ts b/templates/scheduled-agent.ts new file mode 100644 index 0000000..669c135 --- /dev/null +++ b/templates/scheduled-agent.ts @@ -0,0 +1,173 @@ +// Agent with task scheduling (delays, dates, cron) + +import { Agent } from "agents"; + +interface Env { + // Add bindings +} + +interface SchedulerState { + tasksScheduled: number; + tasksCompleted: number; + lastTaskRun: Date | null; +} + +export class SchedulerAgent extends Agent { + initialState: SchedulerState = { + tasksScheduled: 0, + tasksCompleted: 0, + lastTaskRun: null + }; + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + // POST /schedule/delay - Schedule task with delay (seconds) + if (url.pathname === "/schedule/delay") { + const { seconds, data } = await request.json(); + + const { id } = await this.schedule(seconds, "runDelayedTask", data); + + this.setState({ + ...this.state, + tasksScheduled: this.state.tasksScheduled + 1 + }); + + return Response.json({ taskId: id, runsIn: seconds }); + } + + // POST /schedule/date - Schedule task for specific date + if (url.pathname === "/schedule/date") { + const { date, data } = await request.json(); + + const targetDate = new Date(date); + const { id } = await this.schedule(targetDate, "runScheduledTask", data); + + return Response.json({ taskId: id, runsAt: targetDate.toISOString() }); + } + + // POST /schedule/cron - Schedule recurring task + if (url.pathname === "/schedule/cron") { + const { cron, data } = await request.json(); + + // Examples: + // "*/10 * * * *" = Every 10 minutes + // "0 8 * * *" = Every day at 8 AM + // "0 9 * * 1" = Every Monday at 9 AM + const { id } = await this.schedule(cron, "runCronTask", data); + + return Response.json({ taskId: id, schedule: cron }); + } + + // GET /tasks - List all scheduled tasks + if (url.pathname === "/tasks") { + const allTasks = this.getSchedules(); + + return Response.json({ + total: allTasks.length, + tasks: allTasks.map(task => ({ + id: task.id, + callback: task.callback, + type: task.type, + time: new Date(task.time).toISOString(), + payload: task.payload + })) + }); + } + + // GET /tasks/upcoming - Get tasks in next 24 hours + if (url.pathname === "/tasks/upcoming") { + const upcomingTasks = this.getSchedules({ + timeRange: { + start: new Date(), + end: new Date(Date.now() + 24 * 60 * 60 * 1000) + } + }); + + return Response.json({ tasks: upcomingTasks }); + } + + // DELETE /tasks/:id - Cancel a task + if (url.pathname.startsWith("/tasks/")) { + const taskId = url.pathname.split("/")[2]; + const cancelled = await this.cancelSchedule(taskId); + + return Response.json({ cancelled }); + } + + // GET /status + if (url.pathname === "/status") { + return Response.json({ + state: this.state, + activeTasks: this.getSchedules().length + }); + } + + return new Response("Not Found", { status: 404 }); + } + + // CRITICAL: Callback methods must exist for scheduled tasks + + // Run delayed task (one-time) + async runDelayedTask(data: any) { + console.log('Running delayed task with data:', data); + + this.setState({ + ...this.state, + tasksCompleted: this.state.tasksCompleted + 1, + lastTaskRun: new Date() + }); + + // Perform task actions... + // Send email, make API call, update database, etc. + } + + // Run scheduled task (specific date) + async runScheduledTask(data: any) { + console.log('Running scheduled task at:', new Date()); + + this.setState({ + ...this.state, + tasksCompleted: this.state.tasksCompleted + 1, + lastTaskRun: new Date() + }); + + // Perform scheduled actions... + } + + // Run cron task (recurring) + async runCronTask(data: any) { + console.log('Running cron task at:', new Date()); + + this.setState({ + ...this.state, + tasksCompleted: this.state.tasksCompleted + 1, + lastTaskRun: new Date() + }); + + // Perform recurring actions... + // Daily report, hourly check, weekly backup, etc. + } + + // Example: Setup recurring tasks on first start + async onStart() { + // Only schedule if not already scheduled + const cronTasks = this.getSchedules({ type: "cron" }); + + if (cronTasks.length === 0) { + // Daily report at 8 AM + await this.schedule("0 8 * * *", "runCronTask", { + type: "daily_report" + }); + + // Hourly check + await this.schedule("0 * * * *", "runCronTask", { + type: "hourly_check" + }); + + console.log('Recurring tasks scheduled'); + } + } +} + +export default SchedulerAgent; diff --git a/templates/simple-chat-no-agents-sdk.ts b/templates/simple-chat-no-agents-sdk.ts new file mode 100644 index 0000000..dfc9f2e --- /dev/null +++ b/templates/simple-chat-no-agents-sdk.ts @@ -0,0 +1,280 @@ +/** + * Simple Chat WITHOUT Agents SDK + * + * This template shows how to build a chat interface using JUST the Vercel AI SDK + * on Cloudflare Workers - no Agents SDK, no Durable Objects, no WebSockets. + * + * WHAT THIS PROVIDES: + * - ✅ AI streaming responses (SSE) + * - ✅ React hooks (useChat, useCompletion) + * - ✅ Multi-provider support + * - ✅ ~100 lines of code vs ~500+ with Agents SDK + * + * WHAT THIS DOESN'T PROVIDE: + * - ❌ WebSocket bidirectional communication (only SSE one-way) + * - ❌ Built-in state persistence (add D1/KV separately if needed) + * - ❌ Durable Objects (single Worker handles all requests) + * - ❌ Multi-agent coordination + * + * USE THIS WHEN: + * - Building a basic chat interface + * - SSE streaming is sufficient (most cases) + * - No persistent state needed per user + * - Want minimal complexity + * + * DON'T USE THIS WHEN: + * - Need WebSocket bidirectional real-time + * - Need stateful agent instances + * - Building multi-agent systems + * - Need scheduled tasks or workflows + * + * MIGRATION PATH: + * If you discover later that you need WebSockets or Durable Objects state, + * you can migrate to Agents SDK. Start simple, add complexity only when needed. + */ + +// ============================================================================ +// BACKEND: Cloudflare Worker with AI SDK +// ============================================================================ + +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +interface Env { + OPENAI_API_KEY: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +// Enable CORS for frontend +app.use('*', cors()); + +// Chat endpoint - handles streaming responses +app.post('/api/chat', async (c) => { + const { messages } = await c.req.json(); + + // Validate input + if (!Array.isArray(messages) || messages.length === 0) { + return c.json({ error: 'Messages array required' }, 400); + } + + // Stream AI response using Vercel AI SDK + const result = streamText({ + model: openai('gpt-4o-mini'), + messages, + system: 'You are a helpful assistant.', + temperature: 0.7, + maxTokens: 1000, + }); + + // Return SSE stream (automatic streaming handled by AI SDK) + return result.toTextStreamResponse(); +}); + +// Optional: Add completion endpoint for non-chat use cases +app.post('/api/completion', async (c) => { + const { prompt } = await c.req.json(); + + const result = streamText({ + model: openai('gpt-4o-mini'), + prompt, + }); + + return result.toTextStreamResponse(); +}); + +export default app; + +/** + * DEPLOYMENT: + * + * 1. Install dependencies: + * npm install hono ai @ai-sdk/openai + * + * 2. Create wrangler.jsonc: + * { + * "name": "simple-chat", + * "main": "src/worker.ts", + * "compatibility_date": "2025-11-19", + * "compatibility_flags": ["nodejs_compat"] + * } + * + * 3. Set secrets: + * npx wrangler secret put OPENAI_API_KEY + * + * 4. Deploy: + * npx wrangler deploy + * + * That's it. No Durable Objects bindings, no migrations, no complexity. + */ + +// ============================================================================ +// FRONTEND: React Client with useChat Hook +// ============================================================================ + +/** + * Save this as: src/ChatPage.tsx + * + * ```typescript + * import { useChat } from 'ai/react'; + * + * export function ChatPage() { + * const { + * messages, + * input, + * handleInputChange, + * handleSubmit, + * isLoading, + * error + * } = useChat({ + * api: '/api/chat', + * }); + * + * return ( + *
+ * {/\* Messages *\/} + *
+ * {messages.map((msg) => ( + *
+ *
+ *

{msg.content}

+ *
+ *
+ * ))} + * + * {isLoading && ( + *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * )} + *
+ * + * {/\* Input Form *\/} + *
+ *
+ * + * + *
+ * {error &&

{error.message}

} + *
+ *
+ * ); + * } + * ``` + */ + +/** + * FEATURES PROVIDED BY useChat HOOK: + * + * - messages: Array of chat messages + * - input: Current input value + * - handleInputChange: Input onChange handler + * - handleSubmit: Form submit handler + * - isLoading: True during streaming + * - error: Error object if request fails + * - reload: Regenerate last response + * - stop: Stop current streaming + * - append: Add message programmatically + * - setMessages: Update messages array + * + * All of this is built-in with AI SDK - no custom state management needed. + */ + +/** + * COMPARISON WITH AGENTS SDK APPROACH: + * + * | Feature | This Template | Agents SDK Template | + * |---------|---------------|-------------------| + * | Lines of code | ~150 | ~500+ | + * | Setup complexity | Low | High | + * | Durable Objects | ❌ | ✅ | + * | WebSockets | ❌ | ✅ | + * | State persistence | Manual (D1/KV) | Built-in (SQLite) | + * | React hooks | ✅ useChat | Custom hooks | + * | Deployment | 1 step | 3+ steps | + * | Migrations | ❌ | ✅ Required | + * | Use case | 80% of chats | 20% (complex) | + * + * START HERE. Migrate to Agents SDK only if you discover you need WebSockets or state. + */ + +/** + * ADDING STATE PERSISTENCE (Optional): + * + * If you need to save chat history but don't want Agents SDK complexity: + * + * ```typescript + * // Add D1 binding to wrangler.jsonc + * // Then in worker: + * + * app.post('/api/chat', async (c) => { + * const { messages, userId } = await c.req.json(); + * + * // Save to D1 before streaming + * await c.env.DB.prepare( + * 'INSERT INTO messages (user_id, content, role) VALUES (?, ?, ?)' + * ).bind(userId, messages[messages.length - 1].content, 'user').run(); + * + * const result = streamText({ + * model: openai('gpt-4o-mini'), + * messages, + * }); + * + * return result.toTextStreamResponse(); + * }); + * ``` + * + * You get persistence without Durable Objects complexity. + */ + +/** + * SWITCHING TO WORKERS AI (Cost Savings): + * + * To use Cloudflare's Workers AI instead of OpenAI: + * + * ```typescript + * import { createCloudflare } from '@ai-sdk/cloudflare'; + * + * const cloudflare = createCloudflare({ + * apiKey: c.env.CLOUDFLARE_API_KEY, + * }); + * + * const result = streamText({ + * model: cloudflare('@cf/meta/llama-3-8b-instruct'), + * messages, + * }); + * ``` + * + * AI SDK handles the SSE parsing automatically (unlike manual Workers AI). + */ diff --git a/templates/state-sync-agent.ts b/templates/state-sync-agent.ts new file mode 100644 index 0000000..aa5aac6 --- /dev/null +++ b/templates/state-sync-agent.ts @@ -0,0 +1,160 @@ +// Agent demonstrating state management with setState() and SQL + +import { Agent } from "agents"; + +interface Env { + // Add bindings +} + +interface UserProfile { + userId: string; + name: string; + email: string; + preferences: { + theme: 'light' | 'dark' | 'system'; + notifications: boolean; + language: string; + }; + loginCount: number; + lastLogin: Date | null; +} + +export class UserAgent extends Agent { + initialState: UserProfile = { + userId: "", + name: "", + email: "", + preferences: { + theme: 'system', + notifications: true, + language: 'en' + }, + loginCount: 0, + lastLogin: null + }; + + // Setup SQL database on first start + async onStart() { + // Create tables if they don't exist + await this.sql` + CREATE TABLE IF NOT EXISTS activity_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + action TEXT NOT NULL, + details TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `; + + await this.sql` + CREATE INDEX IF NOT EXISTS idx_timestamp ON activity_log(timestamp) + `; + + console.log('UserAgent started:', this.name); + } + + // HTTP request handler + async onRequest(request: Request): Promise { + const url = new URL(request.url); + const method = request.method; + + // GET /profile - Return current profile + if (method === "GET" && url.pathname === "/profile") { + return Response.json({ + profile: this.state, + activityCount: await this.getActivityCount() + }); + } + + // POST /login - Record login + if (method === "POST" && url.pathname === "/login") { + const { userId, name, email } = await request.json(); + + // Update state + this.setState({ + ...this.state, + userId, + name, + email, + loginCount: this.state.loginCount + 1, + lastLogin: new Date() + }); + + // Log activity + await this.logActivity('login', `User ${name} logged in`); + + return Response.json({ + success: true, + loginCount: this.state.loginCount + }); + } + + // POST /preferences - Update preferences + if (method === "POST" && url.pathname === "/preferences") { + const { preferences } = await request.json(); + + this.setState({ + ...this.state, + preferences: { ...this.state.preferences, ...preferences } + }); + + await this.logActivity('preferences_updated', JSON.stringify(preferences)); + + return Response.json({ success: true }); + } + + // GET /activity - Get recent activity + if (method === "GET" && url.pathname === "/activity") { + const limit = parseInt(url.searchParams.get('limit') || '10'); + const activities = await this.getRecentActivity(limit); + + return Response.json({ activities }); + } + + return new Response("Not Found", { status: 404 }); + } + + // SQL helper: Log activity + async logActivity(action: string, details: string = "") { + await this.sql` + INSERT INTO activity_log (action, details) + VALUES (${action}, ${details}) + `; + } + + // SQL helper: Get activity count + async getActivityCount(): Promise { + const result = await this.sql` + SELECT COUNT(*) as count FROM activity_log + `; + + return result[0]?.count || 0; + } + + // SQL helper: Get recent activity + async getRecentActivity(limit: number = 10) { + const activities = await this.sql` + SELECT * FROM activity_log + ORDER BY timestamp DESC + LIMIT ${limit} + `; + + return activities; + } + + // React to state updates + onStateUpdate(state: UserProfile, source: "server" | Connection) { + console.log('Profile updated for:', state.userId); + console.log('Login count:', state.loginCount); + + // Trigger actions based on state changes + if (state.loginCount === 1) { + console.log('First login! Send welcome email.'); + } + + if (state.loginCount > 100) { + console.log('Power user detected!'); + } + } +} + +export default UserAgent; diff --git a/templates/websocket-agent.ts b/templates/websocket-agent.ts new file mode 100644 index 0000000..f7709b5 --- /dev/null +++ b/templates/websocket-agent.ts @@ -0,0 +1,145 @@ +// WebSocket Agent with real-time bidirectional communication + +import { Agent, Connection, ConnectionContext, WSMessage } from "agents"; + +interface Env { + // Add bindings here +} + +interface ChatState { + messages: Array<{ + id: string; + text: string; + sender: string; + timestamp: number; + }>; + participants: string[]; + createdAt: number; +} + +export class ChatAgent extends Agent { + initialState: ChatState = { + messages: [], + participants: [], + createdAt: Date.now() + }; + + // Called when a client connects via WebSocket + async onConnect(connection: Connection, ctx: ConnectionContext) { + // Access original HTTP request for authentication + const userId = ctx.request.headers.get('X-User-ID') || 'anonymous'; + const authToken = ctx.request.headers.get('Authorization'); + + // Optional: Close connection if unauthorized + if (!authToken) { + connection.close(401, "Unauthorized"); + return; + } + + console.log(`Client ${connection.id} connected as ${userId}`); + + // Add to participants + if (!this.state.participants.includes(userId)) { + this.setState({ + ...this.state, + participants: [...this.state.participants, userId] + }); + } + + // Send welcome message to this connection + connection.send(JSON.stringify({ + type: 'welcome', + message: `Welcome, ${userId}!`, + participants: this.state.participants, + messageCount: this.state.messages.length + })); + + // Broadcast participant joined to all connected clients (via state sync) + connection.send(JSON.stringify({ + type: 'user_joined', + userId, + participants: this.state.participants + })); + } + + // Called for each message received + async onMessage(connection: Connection, message: WSMessage) { + // Handle string messages (most common) + if (typeof message === 'string') { + try { + const data = JSON.parse(message); + + // Handle chat message + if (data.type === 'chat') { + const newMessage = { + id: crypto.randomUUID(), + text: data.text, + sender: data.sender || 'anonymous', + timestamp: Date.now() + }; + + // Add to state (will sync to all connections) + this.setState({ + ...this.state, + messages: [...this.state.messages, newMessage] + }); + + // Send acknowledgement to sender + connection.send(JSON.stringify({ + type: 'message_sent', + messageId: newMessage.id + })); + } + + // Handle typing indicator + if (data.type === 'typing') { + // Broadcast to all except sender + connection.send(JSON.stringify({ + type: 'user_typing', + userId: data.sender + })); + } + } catch (e) { + // Handle parse error + connection.send(JSON.stringify({ + type: 'error', + message: 'Invalid message format' + })); + } + } + + // Handle binary messages (ArrayBuffer or ArrayBufferView) + if (message instanceof ArrayBuffer) { + console.log('Received binary message:', message.byteLength, 'bytes'); + // Process binary data... + } + } + + // Called when connection has an error + async onError(connection: Connection, error: unknown): Promise { + console.error('WebSocket error:', error); + + // Send error message to client + connection.send(JSON.stringify({ + type: 'error', + message: 'Connection error occurred' + })); + } + + // Called when connection closes + async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise { + console.log(`Connection ${connection.id} closed:`, code, reason, wasClean); + + // Clean up connection-specific state if needed + // Note: Agent state persists even after all connections close + } + + // React to state updates (from any source) + onStateUpdate(state: ChatState, source: "server" | Connection) { + console.log('Chat state updated'); + console.log('Message count:', state.messages.length); + console.log('Participants:', state.participants); + } +} + +export default ChatAgent; diff --git a/templates/workflow-agent.ts b/templates/workflow-agent.ts new file mode 100644 index 0000000..650025d --- /dev/null +++ b/templates/workflow-agent.ts @@ -0,0 +1,134 @@ +// Agent that triggers Cloudflare Workflows + +import { Agent } from "agents"; +import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +interface Env { + MY_WORKFLOW: Workflow; + MyAgent: AgentNamespace; +} + +interface WorkflowState { + workflowsTriggered: number; + activeWorkflows: string[]; +} + +export class WorkflowAgent extends Agent { + initialState: WorkflowState = { + workflowsTriggered: 0, + activeWorkflows: [] + }; + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + // POST /workflow/trigger - Trigger workflow immediately + if (url.pathname === "/workflow/trigger") { + const { userId, data } = await request.json(); + + const instance = await this.env.MY_WORKFLOW.create({ + id: `workflow-${userId}-${Date.now()}`, + params: { userId, ...data } + }); + + this.setState({ + ...this.state, + workflowsTriggered: this.state.workflowsTriggered + 1, + activeWorkflows: [...this.state.activeWorkflows, instance.id] + }); + + return Response.json({ + workflowId: instance.id, + status: 'triggered' + }); + } + + // POST /workflow/schedule - Schedule workflow for later + if (url.pathname === "/workflow/schedule") { + const { delaySeconds, data } = await request.json(); + + // Schedule a task that will trigger the workflow + const { id: taskId } = await this.schedule( + delaySeconds, + "triggerWorkflow", + data + ); + + return Response.json({ + scheduledTaskId: taskId, + runsIn: delaySeconds + }); + } + + // POST /workflow/status - Check workflow status + if (url.pathname === "/workflow/status") { + const { workflowId } = await request.json(); + + // Note: Workflow status checking depends on your Workflow implementation + // See Cloudflare Workflows docs for details + + return Response.json({ + workflowId, + message: 'Status checking not implemented (see Workflows docs)' + }); + } + + return new Response("Not Found", { status: 404 }); + } + + // Triggered by scheduled task + async triggerWorkflow(data: any) { + const instance = await this.env.MY_WORKFLOW.create({ + id: `delayed-workflow-${Date.now()}`, + params: data + }); + + this.setState({ + ...this.state, + workflowsTriggered: this.state.workflowsTriggered + 1, + activeWorkflows: [...this.state.activeWorkflows, instance.id] + }); + + // Schedule another task to check workflow status + await this.schedule("*/5 * * * *", "checkWorkflowStatus", { + workflowId: instance.id + }); + + console.log('Workflow triggered:', instance.id); + } + + // Check workflow status periodically + async checkWorkflowStatus(data: { workflowId: string }) { + console.log('Checking workflow status:', data.workflowId); + + // Implement status checking logic here + // If workflow completed, cancel this recurring task + } +} + +// Example Workflow definition (can be in same or different file/project) +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent<{ userId: string; data: any }>, step: WorkflowStep) { + // Step 1: Process data + const processed = await step.do('process-data', async () => { + console.log('Processing data for user:', event.payload.userId); + return { processed: true, userId: event.payload.userId }; + }); + + // Step 2: Send notification + await step.do('send-notification', async () => { + console.log('Sending notification to:', processed.userId); + // Send email, push notification, etc. + }); + + // Step 3: Update records + await step.do('update-records', async () => { + console.log('Updating records'); + // Update database, etc. + }); + + return { success: true, steps: 3 }; + } +} + +export default WorkflowAgent; diff --git a/templates/wrangler-agents-config.jsonc b/templates/wrangler-agents-config.jsonc new file mode 100644 index 0000000..d5ae086 --- /dev/null +++ b/templates/wrangler-agents-config.jsonc @@ -0,0 +1,95 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "my-agent", + "main": "src/index.ts", + "account_id": "YOUR_ACCOUNT_ID", + "compatibility_date": "2025-10-21", + "compatibility_flags": ["nodejs_compat"], + + // REQUIRED: Durable Objects configuration for Agents + "durable_objects": { + "bindings": [ + { + "name": "MyAgent", // MUST match class name exactly + "class_name": "MyAgent" // MUST match exported class + } + ] + }, + + // REQUIRED: Migrations (SQLite must be in v1) + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MyAgent"] // Enables persistent state + } + ], + + // Optional: Workers AI binding + "ai": { + "binding": "AI" + }, + + // Optional: Vectorize binding (for RAG) + "vectorize": { + "bindings": [ + { + "binding": "VECTORIZE", + "index_name": "my-agent-vectors" + } + ] + }, + + // Optional: Browser Rendering binding + "browser": { + "binding": "BROWSER" + }, + + // Optional: Workflows binding + "workflows": [ + { + "name": "MY_WORKFLOW", + "class_name": "MyWorkflow" + // Add "script_name": "workflow-project" if in different script + } + ], + + // Optional: D1 binding (additional persistent storage) + "d1_databases": [ + { + "binding": "DB", + "database_name": "my-agent-db", + "database_id": "your-database-id-here" + } + ], + + // Optional: R2 binding (file storage) + "r2_buckets": [ + { + "binding": "BUCKET", + "bucket_name": "my-agent-files" + } + ], + + // Optional: KV binding + "kv_namespaces": [ + { + "binding": "KV", + "id": "your-kv-namespace-id" + } + ], + + // Environment variables + "vars": { + "ENVIRONMENT": "production" + }, + + // Secrets (set with: wrangler secret put SECRET_NAME) + // OPENAI_API_KEY + // ANTHROPIC_API_KEY + // etc. + + // Observability + "observability": { + "enabled": true + } +}