Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:06 +08:00
commit 08dbde4111
18 changed files with 3905 additions and 0 deletions

View File

@@ -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": [
"./"
]
}

3
README.md Normal file
View File

@@ -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.

1403
SKILL.md Normal file

File diff suppressed because it is too large Load Diff

101
plugin.lock.json Normal file
View File

@@ -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": []
}
}

94
templates/basic-agent.ts Normal file
View File

@@ -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<Env, State> {
// 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<Response> {
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;

197
templates/browser-agent.ts Normal file
View File

@@ -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<Env> {
async onRequest(request: Request): Promise<Response> {
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<any> {
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<Buffer> {
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<any[]> {
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<any> {
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<any> {
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;

View File

@@ -0,0 +1,125 @@
// Worker that calls Agents using routeAgentRequest and getAgentByName
import { Agent, AgentNamespace, routeAgentRequest, getAgentByName } from 'agents';
interface Env {
MyAgent: AgentNamespace<MyAgent>;
ChatAgent: AgentNamespace<ChatAgent>;
}
// Worker fetch handler
export default {
async fetch(request: Request, env: Env): Promise<Response> {
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>(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>(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>(env.MyAgent, `user-${userId}`);
// Get chat agent
const chatAgent = getAgentByName<Env, ChatAgent>(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<Env>;
// Helper: Verify JWT token
async function verifyToken(token: string): Promise<string | null> {
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<Env> {
async onRequest(request: Request): Promise<Response> {
return Response.json({
agent: this.name,
message: "Hello from MyAgent"
});
}
async processData(data: any): Promise<any> {
return { processed: true, data };
}
async getContext(): Promise<any> {
return { user: this.name, preferences: this.state };
}
}
export class ChatAgent extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
return Response.json({ agent: this.name });
}
async chat(message: string, context: any): Promise<string> {
return `Response to: ${message} (with context: ${JSON.stringify(context)})`;
}
}

View File

@@ -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 <form onSubmit={handleSubmit}>...</form>;
* }
* ```
*
* 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<Env> {
// 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;

250
templates/hitl-agent.ts Normal file
View File

@@ -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<Env, HITLState> {
initialState: HITLState = {
pendingApprovals: [],
approvalHistory: [],
autoProcessedCount: 0,
humanReviewCount: 0
};
async onRequest(request: Request): Promise<Response> {
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;

View File

@@ -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<Env, CounterState> {
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
});
*/

196
templates/rag-agent.ts Normal file
View File

@@ -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<Env, RAGState> {
initialState: RAGState = {
documentsIngested: 0,
queriesProcessed: 0,
lastQuery: null
};
async onRequest(request: Request): Promise<Response> {
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;

View File

@@ -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<any[]>([]);
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 (
<div>
<p>Status: {status}</p>
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
<button onClick={() => sendMessage("Hello!")}>
Send Message
</button>
</div>
);
}
// 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 (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// 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 (
<div className="chat-container">
{/* Message history */}
<div className="messages">
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<strong>{msg.role}:</strong>
<p>{msg.content}</p>
</div>
))}
{isLoading && <div className="loading">Thinking...</div>}
{error && <div className="error">{error.message}</div>}
</div>
{/* Input form */}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading || !input.trim()}>
{isLoading ? "Sending..." : "Send"}
</button>
{isLoading && (
<button type="button" onClick={stop}>
Stop
</button>
)}
</form>
</div>
);
}
// 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 (
<div>
<h2>Multi-Agent Connection</h2>
<p>Connected to multiple agents</p>
</div>
);
}
// 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') || '';
}

View File

@@ -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<Env, SchedulerState> {
initialState: SchedulerState = {
tasksScheduled: 0,
tasksCompleted: 0,
lastTaskRun: null
};
async onRequest(request: Request): Promise<Response> {
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;

View File

@@ -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 (
* <div className="flex flex-col h-screen">
* {/\* Messages *\/}
* <div className="flex-1 overflow-y-auto p-4 space-y-4">
* {messages.map((msg) => (
* <div
* key={msg.id}
* className={`flex ${
* msg.role === 'user' ? 'justify-end' : 'justify-start'
* }`}
* >
* <div
* className={`max-w-xs rounded-lg p-3 ${
* msg.role === 'user'
* ? 'bg-blue-500 text-white'
* : 'bg-gray-200 text-gray-900'
* }`}
* >
* <p className="whitespace-pre-wrap">{msg.content}</p>
* </div>
* </div>
* ))}
*
* {isLoading && (
* <div className="flex justify-start">
* <div className="bg-gray-200 rounded-lg p-3">
* <div className="flex space-x-2">
* <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" />
* <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100" />
* <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200" />
* </div>
* </div>
* </div>
* )}
* </div>
*
* {/\* Input Form *\/}
* <form onSubmit={handleSubmit} className="p-4 border-t">
* <div className="flex space-x-2">
* <input
* value={input}
* onChange={handleInputChange}
* placeholder="Type a message..."
* className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2"
* disabled={isLoading}
* />
* <button
* type="submit"
* disabled={isLoading || !input.trim()}
* className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50"
* >
* Send
* </button>
* </div>
* {error && <p className="text-red-500 text-sm mt-2">{error.message}</p>}
* </form>
* </div>
* );
* }
* ```
*/
/**
* 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).
*/

View File

@@ -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<Env, UserProfile> {
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<Response> {
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<number> {
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;

View File

@@ -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<Env, ChatState> {
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<void> {
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<void> {
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;

134
templates/workflow-agent.ts Normal file
View File

@@ -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<MyAgent>;
}
interface WorkflowState {
workflowsTriggered: number;
activeWorkflows: string[];
}
export class WorkflowAgent extends Agent<Env, WorkflowState> {
initialState: WorkflowState = {
workflowsTriggered: 0,
activeWorkflows: []
};
async onRequest(request: Request): Promise<Response> {
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<Env> {
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;

View File

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