Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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.
|
||||
101
plugin.lock.json
Normal file
101
plugin.lock.json
Normal 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
94
templates/basic-agent.ts
Normal 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
197
templates/browser-agent.ts
Normal 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;
|
||||
125
templates/calling-agents-worker.ts
Normal file
125
templates/calling-agents-worker.ts
Normal 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)})`;
|
||||
}
|
||||
}
|
||||
120
templates/chat-agent-streaming.ts
Normal file
120
templates/chat-agent-streaming.ts
Normal 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
250
templates/hitl-agent.ts
Normal 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;
|
||||
182
templates/mcp-server-basic.ts
Normal file
182
templates/mcp-server-basic.ts
Normal 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
196
templates/rag-agent.ts
Normal 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;
|
||||
235
templates/react-useagent-client.tsx
Normal file
235
templates/react-useagent-client.tsx
Normal 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') || '';
|
||||
}
|
||||
173
templates/scheduled-agent.ts
Normal file
173
templates/scheduled-agent.ts
Normal 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;
|
||||
280
templates/simple-chat-no-agents-sdk.ts
Normal file
280
templates/simple-chat-no-agents-sdk.ts
Normal 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).
|
||||
*/
|
||||
160
templates/state-sync-agent.ts
Normal file
160
templates/state-sync-agent.ts
Normal 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;
|
||||
145
templates/websocket-agent.ts
Normal file
145
templates/websocket-agent.ts
Normal 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
134
templates/workflow-agent.ts
Normal 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;
|
||||
95
templates/wrangler-agents-config.jsonc
Normal file
95
templates/wrangler-agents-config.jsonc
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user