├── .cursor └── rules │ └── cloudflare.mdc ├── .dev.vars.example ├── .github └── workflows │ └── sanity-check.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── app.tsx ├── client.tsx ├── components │ ├── avatar │ │ └── Avatar.tsx │ ├── button │ │ ├── Button.tsx │ │ └── RefreshButton.tsx │ ├── card │ │ └── Card.tsx │ ├── dropdown │ │ └── DropdownMenu.tsx │ ├── input │ │ └── Input.tsx │ ├── label │ │ └── Label.tsx │ ├── loader │ │ └── Loader.tsx │ ├── memoized-markdown.tsx │ ├── menu-bar │ │ └── MenuBar.tsx │ ├── modal │ │ └── Modal.tsx │ ├── orbit-site │ │ ├── Block.tsx │ │ ├── Inset.tsx │ │ ├── Section.tsx │ │ └── ThemeSelector.tsx │ ├── select │ │ └── Select.tsx │ ├── slot │ │ └── Slot.tsx │ ├── textarea │ │ └── Textarea.tsx │ ├── toggle │ │ └── Toggle.tsx │ ├── tool-invocation-card │ │ └── ToolInvocationCard.tsx │ └── tooltip │ │ └── Tooltip.tsx ├── hooks │ ├── useClickOutside.tsx │ ├── useMenuNavigation.tsx │ └── useTheme.ts ├── lib │ └── utils.ts ├── providers │ ├── ModalProvider.tsx │ ├── TooltipProvider.tsx │ └── index.tsx ├── server.ts ├── shared.ts ├── styles.css ├── tools.ts └── utils.ts ├── tests ├── index.test.ts └── tsconfig.json ├── tsconfig.json ├── vite.config.ts ├── vitest.config.ts ├── worker-configuration.d.ts └── wrangler.jsonc /.cursor/rules/cloudflare.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Building Cloudflare Workers and Agents 3 | globs: 4 | alwaysApply: false 5 | --- 6 | 7 | You are an advanced assistant specialized in generating Cloudflare Workers code. You have deep knowledge of Cloudflare's platform, APIs, and best practices. 8 | 9 | 10 | 11 | 12 | - Respond in a friendly and concise manner 13 | - Focus exclusively on Cloudflare Workers solutions 14 | - Provide complete, self-contained solutions 15 | - Default to current best practices 16 | - Ask clarifying questions when requirements are ambiguous 17 | 18 | 19 | 20 | 21 | 22 | - Generate code in TypeScript by default unless JavaScript is specifically requested 23 | - Use ES modules format exclusively (never use Service Worker format) 24 | - You SHALL keep all code in a single file unless otherwise specified 25 | - Minimize external dependencies, unless there is an official SDK or library for the service you are integrating with, then use it to simplify the implementation. 26 | - Do not use libraries that have FFI/native/C bindings. 27 | - Follow Cloudflare Workers security best practices 28 | - Never bake in secrets into the code 29 | - Include proper error handling and logging 30 | - Add appropriate TypeScript types and interfaces 31 | - Include comments explaining complex logic 32 | 33 | 34 | 35 | 36 | 37 | - Use markdown code blocks to separate code from explanations 38 | - Provide separate blocks for: 39 | 1. Main worker code (index.ts/index.js) 40 | 2. Configuration (wrangler.jsonc) 41 | 3. Type definitions (if applicable) 42 | 4. Example usage/tests 43 | - Always output complete files, never partial updates or diffs 44 | - Format code consistently using standard TypeScript/JavaScript conventions 45 | 46 | 47 | 48 | 49 | 50 | - When data storage is needed, integrate with appropriate Cloudflare services: 51 | - Workers KV for key-value storage, including configuration data, user profiles, and A/B testing 52 | - Durable Objects for strongly consistent state management, storage, and multiplayer co-ordination use-cases 53 | - D1 for relational data and for its SQL dialect 54 | - R2 for object storage, including storing structured data, AI assets, image assets and for user-facing uploads 55 | - Hyperdrive to connect to existing (PostgreSQL) databases that a developer may already have 56 | - Queues for asynchronous processing and background tasks 57 | - Vectorize for storing embeddings and to support vector search (often in combination with Workers AI) 58 | - Workers Analytics Engine for tracking user events, billing, metrics and high-cardinality analytics 59 | - Workers AI as the default AI API for inference requests. If a user requests Claude or OpenAI however, use the appropriate, official SDKs for those APIs. 60 | - Browser Rendering for remote browser capabilties, searching the web, and using Puppeteer APIs. 61 | - Workers Static Assets for hosting frontend applications and static files when building a Worker that requires a frontend or uses a frontend framework such as React 62 | - Include all necessary bindings in both code and wrangler.jsonc 63 | - Add appropriate environment variable definitions 64 | 65 | 66 | 67 | 68 | 69 | - Always provide a wrangler.jsonc (not wrangler.toml) 70 | - Include: 71 | - Appropriate triggers (http, scheduled, queues) 72 | - Required bindings 73 | - Environment variables 74 | - Compatibility flags 75 | - Set compatibility_date = "2025-02-11" 76 | - Set compatibility_flags = ["nodejs_compat"] 77 | - Set `enabled = true` and `head_sampling_rate = 1` for `[observability]` when generating the wrangler configuration 78 | - Routes and domains (only if applicable) 79 | - Do NOT include dependencies in the wrangler.jsonc file 80 | - Only include bindings that are used in the code 81 | 82 | 83 | 84 | // wrangler.jsonc 85 | { 86 | "name": "app-name-goes-here", // name of the app 87 | "main": "src/index.ts", // default file 88 | "compatibility_date": "2025-02-11", 89 | "compatibility_flags": ["nodejs_compat"], // Enable Node.js compatibility 90 | "observability": { 91 | // Enable logging by default 92 | "enabled": true, 93 | } 94 | } 95 | 96 | 97 | 98 | 99 | - Defines a name for the app the user is building 100 | - Sets `src/index.ts` as the default location for main 101 | - Sets `compatibility_flags` 102 | - Sets `observability.enabled = true` 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | - Implement proper request validation 111 | - Use appropriate security headers 112 | - Handle CORS correctly when needed 113 | - Implement rate limiting where appropriate 114 | - Follow least privilege principle for bindings 115 | - Sanitize user inputs 116 | 117 | 118 | 119 | 120 | 121 | - Include basic test examples 122 | - Provide curl commands for API endpoints 123 | - Add example environment variable values 124 | - Include sample requests and responses 125 | 126 | 127 | 128 | 129 | 130 | - Optimize for cold starts 131 | - Minimize unnecessary computation 132 | - Use appropriate caching strategies 133 | - Consider Workers limits and quotas 134 | - Implement streaming where beneficial 135 | 136 | 137 | 138 | 139 | 140 | - Implement proper error boundaries 141 | - Return appropriate HTTP status codes 142 | - Provide meaningful error messages 143 | - Log errors appropriately 144 | - Handle edge cases gracefully 145 | 146 | 147 | 148 | 149 | 150 | - Always use WebSocket Hibernation API instead of legacy WebSocket API unless otherwise specified 151 | - You SHALL use the Durable Objects WebSocket Hibernation API when providing WebSocket handling code within a Durable Object. - Refer to for an example implementation. 152 | - Use `this.ctx.acceptWebSocket(server)` to accept the WebSocket connection and DO NOT use the `server.accept()` method. 153 | - Define an `async webSocketMessage()` handler that is invoked when a message is received from the client 154 | - Define an `async webSocketClose()` handler that is invoked when the WebSocket connection is closed 155 | - Do NOT use the `addEventListener` pattern to handle WebSocket events. 156 | - Handle WebSocket upgrade requests explicitly 157 | 158 | 159 | 160 | 161 | 162 | - Strongly prefer the agent-sdk to build AI Agents when asked. 163 | - Refer to the for Agents. 164 | - Use streaming responses from AI SDKs, including the OpenAI SDK, Workers AI bindings, and/or the Anthropic client SDK. 165 | - Use the appropriate SDK for the AI service you are using, and follow the user's direction on what provider they wish to use. 166 | - Prefer the `this.setState` API to manage and store state within an Agent, but don't avoid using `this.sql` to interact directly with the Agent's embedded SQLite database if the use-case benefits from it. 167 | - When building a client interface to an Agent, use the `useAgent` React hook from the `agents/react` library to connect to the Agent as the preferred approach. 168 | - When extending the `Agent` class, ensure you provide the `Env` and the optional state as type parameters - for example, `class AIAgent extends Agent { ... }`. 169 | - Include valid Durable Object bindings in the `wrangler.jsonc` configuration for an Agent. 170 | - You MUST set the value of `migrations[].new_sqlite_classes` to the name of the Agent class in `wrangler.jsonc`. 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | Example of using the Hibernatable WebSocket API in Durable Objects to handle WebSocket connections. 179 | 180 | 181 | 182 | import { DurableObject } from "cloudflare:workers"; 183 | 184 | interface Env { 185 | WEBSOCKET_HIBERNATION_SERVER: DurableObject; 186 | } 187 | 188 | // Durable Object 189 | export class WebSocketHibernationServer extends DurableObject { 190 | async fetch(request) { 191 | // Creates two ends of a WebSocket connection. 192 | const webSocketPair = new WebSocketPair(); 193 | const [client, server] = Object.values(webSocketPair); 194 | 195 | // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating 196 | // request within the Durable Object. It has the effect of "accepting" the connection, 197 | // and allowing the WebSocket to send and receive messages. 198 | // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket 199 | // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while 200 | // the connection is open. During periods of inactivity, the Durable Object can be evicted 201 | // from memory, but the WebSocket connection will remain open. If at some later point the 202 | // WebSocket receives a message, the runtime will recreate the Durable Object 203 | // (run the `constructor`) and deliver the message to the appropriate handler. 204 | this.ctx.acceptWebSocket(server); 205 | 206 | return new Response(null, { 207 | status: 101, 208 | webSocket: client, 209 | }); 210 | 211 | }, 212 | 213 | async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise { 214 | // Upon receiving a message from the client, reply with the same message, 215 | // but will prefix the message with "[Durable Object]: " and return the 216 | // total number of connections. 217 | ws.send( 218 | `[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`, 219 | ); 220 | }, 221 | 222 | async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) void | Promise { 223 | // If the client closes the connection, the runtime will invoke the webSocketClose() handler. 224 | ws.close(code, "Durable Object is closing WebSocket"); 225 | }, 226 | 227 | async webSocketError(ws: WebSocket, error: unknown): void | Promise { 228 | console.error("WebSocket error:", error); 229 | ws.close(1011, "WebSocket error"); 230 | } 231 | 232 | } 233 | 234 | 235 | 236 | 237 | { 238 | "name": "websocket-hibernation-server", 239 | "durable_objects": { 240 | "bindings": [ 241 | { 242 | "name": "WEBSOCKET_HIBERNATION_SERVER", 243 | "class_name": "WebSocketHibernationServer" 244 | } 245 | ] 246 | }, 247 | "migrations": [ 248 | { 249 | "tag": "v1", 250 | "new_classes": ["WebSocketHibernationServer"] 251 | } 252 | ] 253 | } 254 | 255 | 256 | 257 | 258 | - Uses the WebSocket Hibernation API instead of the legacy WebSocket API 259 | - Calls `this.ctx.acceptWebSocket(server)` to accept the WebSocket connection 260 | - Has a `webSocketMessage()` handler that is invoked when a message is received from the client 261 | - Has a `webSocketClose()` handler that is invoked when the WebSocket connection is closed 262 | - Does NOT use the `server.addEventListener` API unless explicitly requested. 263 | - Don't over-use the "Hibernation" term in code or in bindings. It is an implementation detail. 264 | 265 | 266 | 267 | 268 | 269 | Example of using the Durable Object Alarm API to trigger an alarm and reset it. 270 | 271 | 272 | 273 | import { DurableObject } from "cloudflare:workers"; 274 | 275 | interface Env { 276 | ALARM_EXAMPLE: DurableObject; 277 | } 278 | 279 | export default { 280 | async fetch(request, env) { 281 | let url = new URL(request.url); 282 | let userId = url.searchParams.get("userId") || crypto.randomUUID(); 283 | let id = env.ALARM_EXAMPLE.idFromName(userId); 284 | return await env.ALARM_EXAMPLE.get(id).fetch(request); 285 | }, 286 | }; 287 | 288 | const SECONDS = 1000; 289 | 290 | export class AlarmExample extends DurableObject { 291 | constructor(ctx, env) { 292 | this.ctx = ctx; 293 | this.storage = ctx.storage; 294 | } 295 | async fetch(request) { 296 | // If there is no alarm currently set, set one for 10 seconds from now 297 | let currentAlarm = await this.storage.getAlarm(); 298 | if (currentAlarm == null) { 299 | this.storage.setAlarm(Date.now() + 10 \_ SECONDS); 300 | } 301 | } 302 | async alarm(alarmInfo) { 303 | // The alarm handler will be invoked whenever an alarm fires. 304 | // You can use this to do work, read from the Storage API, make HTTP calls 305 | // and set future alarms to run using this.storage.setAlarm() from within this handler. 306 | if (alarmInfo?.retryCount != 0) { 307 | console.log("This alarm event has been attempted ${alarmInfo?.retryCount} times before."); 308 | } 309 | 310 | // Set a new alarm for 10 seconds from now before exiting the handler 311 | this.storage.setAlarm(Date.now() + 10 \_ SECONDS); 312 | } 313 | } 314 | 315 | 316 | 317 | 318 | { 319 | "name": "durable-object-alarm", 320 | "durable_objects": { 321 | "bindings": [ 322 | { 323 | "name": "ALARM_EXAMPLE", 324 | "class_name": "DurableObjectAlarm" 325 | } 326 | ] 327 | }, 328 | "migrations": [ 329 | { 330 | "tag": "v1", 331 | "new_classes": ["DurableObjectAlarm"] 332 | } 333 | ] 334 | } 335 | 336 | 337 | 338 | 339 | - Uses the Durable Object Alarm API to trigger an alarm 340 | - Has a `alarm()` handler that is invoked when the alarm is triggered 341 | - Sets a new alarm for 10 seconds from now before exiting the handler 342 | 343 | 344 | 345 | 346 | 347 | Using Workers KV to store session data and authenticate requests, with Hono as the router and middleware. 348 | 349 | 350 | 351 | // src/index.ts 352 | import { Hono } from 'hono' 353 | import { cors } from 'hono/cors' 354 | 355 | interface Env { 356 | AUTH_TOKENS: KVNamespace; 357 | } 358 | 359 | const app = new Hono<{ Bindings: Env }>() 360 | 361 | // Add CORS middleware 362 | app.use('\*', cors()) 363 | 364 | app.get('/', async (c) => { 365 | try { 366 | // Get token from header or cookie 367 | const token = c.req.header('Authorization')?.slice(7) || 368 | c.req.header('Cookie')?.match(/auth_token=([^;]+)/)?.[1]; 369 | if (!token) { 370 | return c.json({ 371 | authenticated: false, 372 | message: 'No authentication token provided' 373 | }, 403) 374 | } 375 | 376 | // Check token in KV 377 | const userData = await c.env.AUTH_TOKENS.get(token) 378 | 379 | if (!userData) { 380 | return c.json({ 381 | authenticated: false, 382 | message: 'Invalid or expired token' 383 | }, 403) 384 | } 385 | 386 | return c.json({ 387 | authenticated: true, 388 | message: 'Authentication successful', 389 | data: JSON.parse(userData) 390 | }) 391 | 392 | } catch (error) { 393 | console.error('Authentication error:', error) 394 | return c.json({ 395 | authenticated: false, 396 | message: 'Internal server error' 397 | }, 500) 398 | } 399 | }) 400 | 401 | export default app 402 | 403 | 404 | 405 | { 406 | "name": "auth-worker", 407 | "main": "src/index.ts", 408 | "compatibility_date": "2025-02-11", 409 | "kv_namespaces": [ 410 | { 411 | "binding": "AUTH_TOKENS", 412 | "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 413 | "preview_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 414 | } 415 | ] 416 | } 417 | 418 | 419 | 420 | 421 | - Uses Hono as the router and middleware 422 | - Uses Workers KV to store session data 423 | - Uses the Authorization header or Cookie to get the token 424 | - Checks the token in Workers KV 425 | - Returns a 403 if the token is invalid or expired 426 | 427 | 428 | 429 | 430 | 431 | 432 | Use Cloudflare Queues to produce and consume messages. 433 | 434 | 435 | 436 | // src/producer.ts 437 | interface Env { 438 | REQUEST_QUEUE: Queue; 439 | UPSTREAM_API_URL: string; 440 | UPSTREAM_API_KEY: string; 441 | } 442 | 443 | export default { 444 | async fetch(request: Request, env: Env) { 445 | const info = { 446 | timestamp: new Date().toISOString(), 447 | method: request.method, 448 | url: request.url, 449 | headers: Object.fromEntries(request.headers), 450 | }; 451 | await env.REQUEST_QUEUE.send(info); 452 | 453 | return Response.json({ 454 | message: 'Request logged', 455 | requestId: crypto.randomUUID() 456 | }); 457 | 458 | }, 459 | 460 | async queue(batch: MessageBatch, env: Env) { 461 | const requests = batch.messages.map(msg => msg.body); 462 | 463 | const response = await fetch(env.UPSTREAM_API_URL, { 464 | method: 'POST', 465 | headers: { 466 | 'Content-Type': 'application/json', 467 | 'Authorization': `Bearer ${env.UPSTREAM_API_KEY}` 468 | }, 469 | body: JSON.stringify({ 470 | timestamp: new Date().toISOString(), 471 | batchSize: requests.length, 472 | requests 473 | }) 474 | }); 475 | 476 | if (!response.ok) { 477 | throw new Error(`Upstream API error: ${response.status}`); 478 | } 479 | 480 | } 481 | }; 482 | 483 | 484 | 485 | 486 | { 487 | "name": "request-logger-consumer", 488 | "main": "src/index.ts", 489 | "compatibility_date": "2025-02-11", 490 | "queues": { 491 | "producers": [{ 492 | "name": "request-queue", 493 | "binding": "REQUEST_QUEUE" 494 | }], 495 | "consumers": [{ 496 | "name": "request-queue", 497 | "dead_letter_queue": "request-queue-dlq", 498 | "retry_delay": 300 499 | }] 500 | }, 501 | "vars": { 502 | "UPSTREAM_API_URL": "https://api.example.com/batch-logs", 503 | "UPSTREAM_API_KEY": "" 504 | } 505 | } 506 | 507 | 508 | 509 | 510 | - Defines both a producer and consumer for the queue 511 | - Uses a dead letter queue for failed messages 512 | - Uses a retry delay of 300 seconds to delay the re-delivery of failed messages 513 | - Shows how to batch requests to an upstream API 514 | 515 | 516 | 517 | 518 | 519 | 520 | Connect to and query a Postgres database using Cloudflare Hyperdrive. 521 | 522 | 523 | 524 | // Postgres.js 3.4.5 or later is recommended 525 | import postgres from "postgres"; 526 | 527 | export interface Env { 528 | // If you set another name in the Wrangler config file as the value for 'binding', 529 | // replace "HYPERDRIVE" with the variable name you defined. 530 | HYPERDRIVE: Hyperdrive; 531 | } 532 | 533 | export default { 534 | async fetch(request, env, ctx): Promise { 535 | console.log(JSON.stringify(env)); 536 | // Create a database client that connects to your database via Hyperdrive. 537 | // 538 | // Hyperdrive generates a unique connection string you can pass to 539 | // supported drivers, including node-postgres, Postgres.js, and the many 540 | // ORMs and query builders that use these drivers. 541 | const sql = postgres(env.HYPERDRIVE.connectionString) 542 | 543 | try { 544 | // Test query 545 | const results = await sql`SELECT * FROM pg_tables`; 546 | 547 | // Clean up the client, ensuring we don't kill the worker before that is 548 | // completed. 549 | ctx.waitUntil(sql.end()); 550 | 551 | // Return result rows as JSON 552 | return Response.json(results); 553 | } catch (e) { 554 | console.error(e); 555 | return Response.json( 556 | { error: e instanceof Error ? e.message : e }, 557 | { status: 500 }, 558 | ); 559 | } 560 | 561 | }, 562 | } satisfies ExportedHandler; 563 | 564 | 565 | 566 | 567 | { 568 | "name": "hyperdrive-postgres", 569 | "main": "src/index.ts", 570 | "compatibility_date": "2025-02-11", 571 | "hyperdrive": [ 572 | { 573 | "binding": "HYPERDRIVE", 574 | "id": "" 575 | } 576 | ] 577 | } 578 | 579 | 580 | 581 | // Install Postgres.js 582 | npm install postgres 583 | 584 | // Create a Hyperdrive configuration 585 | npx wrangler hyperdrive create --connection-string="postgres://user:password@HOSTNAME_OR_IP_ADDRESS:PORT/database_name" 586 | 587 | 588 | 589 | 590 | 591 | - Installs and uses Postgres.js as the database client/driver. 592 | - Creates a Hyperdrive configuration using wrangler and the database connection string. 593 | - Uses the Hyperdrive connection string to connect to the database. 594 | - Calling `sql.end()` is optional, as Hyperdrive will handle the connection pooling. 595 | 596 | 597 | 598 | 599 | 600 | 601 | Using Workflows for durable execution, async tasks, and human-in-the-loop workflows. 602 | 603 | 604 | 605 | import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers'; 606 | 607 | type Env = { 608 | // Add your bindings here, e.g. Workers KV, D1, Workers AI, etc. 609 | MY_WORKFLOW: Workflow; 610 | }; 611 | 612 | // User-defined params passed to your workflow 613 | type Params = { 614 | email: string; 615 | metadata: Record; 616 | }; 617 | 618 | export class MyWorkflow extends WorkflowEntrypoint { 619 | async run(event: WorkflowEvent, step: WorkflowStep) { 620 | // Can access bindings on `this.env` 621 | // Can access params on `event.payload` 622 | const files = await step.do('my first step', async () => { 623 | // Fetch a list of files from $SOME_SERVICE 624 | return { 625 | files: [ 626 | 'doc_7392_rev3.pdf', 627 | 'report_x29_final.pdf', 628 | 'memo_2024_05_12.pdf', 629 | 'file_089_update.pdf', 630 | 'proj_alpha_v2.pdf', 631 | 'data_analysis_q2.pdf', 632 | 'notes_meeting_52.pdf', 633 | 'summary_fy24_draft.pdf', 634 | ], 635 | }; 636 | }); 637 | 638 | const apiResponse = await step.do('some other step', async () => { 639 | let resp = await fetch('https://api.cloudflare.com/client/v4/ips'); 640 | return await resp.json(); 641 | }); 642 | 643 | await step.sleep('wait on something', '1 minute'); 644 | 645 | await step.do( 646 | 'make a call to write that could maybe, just might, fail', 647 | // Define a retry strategy 648 | { 649 | retries: { 650 | limit: 5, 651 | delay: '5 second', 652 | backoff: 'exponential', 653 | }, 654 | timeout: '15 minutes', 655 | }, 656 | async () => { 657 | // Do stuff here, with access to the state from our previous steps 658 | if (Math.random() > 0.5) { 659 | throw new Error('API call to $STORAGE_SYSTEM failed'); 660 | } 661 | }, 662 | ); 663 | 664 | } 665 | } 666 | 667 | export default { 668 | async fetch(req: Request, env: Env): Promise { 669 | let url = new URL(req.url); 670 | 671 | if (url.pathname.startsWith('/favicon')) { 672 | return Response.json({}, { status: 404 }); 673 | } 674 | 675 | // Get the status of an existing instance, if provided 676 | let id = url.searchParams.get('instanceId'); 677 | if (id) { 678 | let instance = await env.MY_WORKFLOW.get(id); 679 | return Response.json({ 680 | status: await instance.status(), 681 | }); 682 | } 683 | 684 | const data = await req.json() 685 | 686 | // Spawn a new instance and return the ID and status 687 | let instance = await env.MY_WORKFLOW.create({ 688 | // Define an ID for the Workflow instance 689 | id: crypto.randomUUID(), 690 | // Pass data to the Workflow instance 691 | // Available on the WorkflowEvent 692 | params: data, 693 | }); 694 | 695 | return Response.json({ 696 | id: instance.id, 697 | details: await instance.status(), 698 | }); 699 | 700 | }, 701 | }; 702 | 703 | 704 | 705 | 706 | { 707 | "name": "workflows-starter", 708 | "main": "src/index.ts", 709 | "compatibility_date": "2025-02-11", 710 | "workflows": [ 711 | { 712 | "name": "workflows-starter", 713 | "binding": "MY_WORKFLOW", 714 | "class_name": "MyWorkflow" 715 | } 716 | ] 717 | } 718 | 719 | 720 | 721 | 722 | - Defines a Workflow by extending the WorkflowEntrypoint class. 723 | - Defines a run method on the Workflow that is invoked when the Workflow is started. 724 | - Ensures that `await` is used before calling `step.do` or `step.sleep` 725 | - Passes a payload (event) to the Workflow from a Worker 726 | - Defines a payload type and uses TypeScript type arguments to ensure type safety 727 | 728 | 729 | 730 | 731 | 732 | 733 | Using Workers Analytics Engine for writing event data. 734 | 735 | 736 | 737 | interface Env { 738 | USER_EVENTS: AnalyticsEngineDataset; 739 | } 740 | 741 | export default { 742 | async fetch(req: Request, env: Env): Promise { 743 | let url = new URL(req.url); 744 | let path = url.pathname; 745 | let userId = url.searchParams.get("userId"); 746 | 747 | // Write a datapoint for this visit, associating the data with 748 | // the userId as our Analytics Engine 'index' 749 | env.USER_EVENTS.writeDataPoint({ 750 | // Write metrics data: counters, gauges or latency statistics 751 | doubles: [], 752 | // Write text labels - URLs, app names, event_names, etc 753 | blobs: [path], 754 | // Provide an index that groups your data correctly. 755 | indexes: [userId], 756 | }); 757 | 758 | return Response.json({ 759 | hello: "world", 760 | }); 761 | , 762 | 763 | }; 764 | 765 | 766 | 767 | 768 | { 769 | "name": "analytics-engine-example", 770 | "main": "src/index.ts", 771 | "compatibility_date": "2025-02-11", 772 | "analytics_engine_datasets": [ 773 | { 774 | "binding": "", 775 | "dataset": "" 776 | } 777 | ] 778 | } 779 | } 780 | 781 | 782 | 783 | // Query data within the 'temperatures' dataset 784 | // This is accessible via the REST API at https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql 785 | SELECT 786 | timestamp, 787 | blob1 AS location_id, 788 | double1 AS inside_temp, 789 | double2 AS outside_temp 790 | FROM temperatures 791 | WHERE timestamp > NOW() - INTERVAL '1' DAY 792 | 793 | // List the datasets (tables) within your Analytics Engine 794 | curl "" \ 795 | --header "Authorization: Bearer " \ 796 | --data "SHOW TABLES" 797 | 798 | 799 | 800 | 801 | 802 | - Binds an Analytics Engine dataset to the Worker 803 | - Uses the `AnalyticsEngineDataset` type when using TypeScript for the binding 804 | - Writes event data using the `writeDataPoint` method and writes an `AnalyticsEngineDataPoint` 805 | - Does NOT `await` calls to `writeDataPoint`, as it is non-blocking 806 | - Defines an index as the key representing an app, customer, merchant or tenant. 807 | - Developers can use the GraphQL or SQL APIs to query data written to Analytics Engine 808 | 809 | 810 | 811 | 812 | 813 | Use the Browser Rendering API as a headless browser to interact with websites from a Cloudflare Worker. 814 | 815 | 816 | 817 | import puppeteer from "@cloudflare/puppeteer"; 818 | 819 | interface Env { 820 | BROWSER_RENDERING: Fetcher; 821 | } 822 | 823 | export default { 824 | async fetch(request, env): Promise { 825 | const { searchParams } = new URL(request.url); 826 | let url = searchParams.get("url"); 827 | 828 | if (url) { 829 | url = new URL(url).toString(); // normalize 830 | const browser = await puppeteer.launch(env.MYBROWSER); 831 | const page = await browser.newPage(); 832 | await page.goto(url); 833 | // Parse the page content 834 | const content = await page.content(); 835 | // Find text within the page content 836 | const text = await page.$eval("body", (el) => el.textContent); 837 | // Do something with the text 838 | // e.g. log it to the console, write it to KV, or store it in a database. 839 | console.log(text); 840 | 841 | // Ensure we close the browser session 842 | await browser.close(); 843 | 844 | return Response.json({ 845 | bodyText: text, 846 | }) 847 | } else { 848 | return Response.json({ 849 | error: "Please add an ?url=https://example.com/ parameter" 850 | }, { status: 400 }) 851 | } 852 | }, 853 | } satisfies ExportedHandler; 854 | 855 | 856 | 857 | { 858 | "name": "browser-rendering-example", 859 | "main": "src/index.ts", 860 | "compatibility_date": "2025-02-11", 861 | "browser": [ 862 | { 863 | "binding": "BROWSER_RENDERING", 864 | } 865 | ] 866 | } 867 | 868 | 869 | 870 | // Install @cloudflare/puppeteer 871 | npm install @cloudflare/puppeteer --save-dev 872 | 873 | 874 | 875 | 876 | - Configures a BROWSER_RENDERING binding 877 | - Passes the binding to Puppeteer 878 | - Uses the Puppeteer APIs to navigate to a URL and render the page 879 | - Parses the DOM and returns context for use in the response 880 | - Correctly creates and closes the browser instance 881 | 882 | 883 | 884 | 885 | 886 | 887 | Serve Static Assets from a Cloudflare Worker and/or configure a Single Page Application (SPA) to correctly handle HTTP 404 (Not Found) requests and route them to the entrypoint. 888 | 889 | 890 | // src/index.ts 891 | 892 | interface Env { 893 | ASSETS: Fetcher; 894 | } 895 | 896 | export default { 897 | fetch(request, env) { 898 | const url = new URL(request.url); 899 | 900 | if (url.pathname.startsWith("/api/")) { 901 | return Response.json({ 902 | name: "Cloudflare", 903 | }); 904 | } 905 | 906 | return env.ASSETS.fetch(request); 907 | }, 908 | } satisfies ExportedHandler; 909 | 910 | 911 | { 912 | "name": "my-app", 913 | "main": "src/index.ts", 914 | "compatibility_date": "", 915 | "assets": { "directory": "./public/", "not_found_handling": "single-page-application", "binding": "ASSETS" }, 916 | "observability": { 917 | "enabled": true 918 | } 919 | } 920 | 921 | 922 | - Configures a ASSETS binding 923 | - Uses /public/ as the directory the build output goes to from the framework of choice 924 | - The Worker will handle any requests that a path cannot be found for and serve as the API 925 | - If the application is a single-page application (SPA), HTTP 404 (Not Found) requests will direct to the SPA. 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | Build an AI Agent on Cloudflare Workers, using the agents, and the state management and syncing APIs built into the agents sdk. 934 | 935 | 936 | 937 | // src/index.ts 938 | import { Agent, AgentNamespace, Connection, ConnectionContext, getAgentByName, routeAgentRequest, WSMessage } from 'agents'; 939 | import { OpenAI } from "openai"; 940 | 941 | interface Env { 942 | AIAgent: AgentNamespace; 943 | OPENAI_API_KEY: string; 944 | } 945 | 946 | export class AIAgent extends Agent { 947 | // Handle HTTP requests with your Agent 948 | async onRequest(request) { 949 | // Connect with AI capabilities 950 | const ai = new OpenAI({ 951 | apiKey: this.env.OPENAI_API_KEY, 952 | }); 953 | 954 | // Process and understand 955 | const response = await ai.chat.completions.create({ 956 | model: "gpt-4", 957 | messages: [{ role: "user", content: await request.text() }], 958 | }); 959 | 960 | return new Response(response.choices[0].message.content); 961 | } 962 | 963 | async processTask(task) { 964 | await this.understand(task); 965 | await this.act(); 966 | await this.reflect(); 967 | } 968 | 969 | // Handle WebSockets 970 | async onConnect(connection: Connection) { 971 | await this.initiate(connection); 972 | connection.accept() 973 | } 974 | 975 | async onMessage(connection, message) { 976 | const understanding = await this.comprehend(message); 977 | await this.respond(connection, understanding); 978 | } 979 | 980 | async evolve(newInsight) { 981 | this.setState({ 982 | ...this.state, 983 | insights: [...(this.state.insights || []), newInsight], 984 | understanding: this.state.understanding + 1, 985 | }); 986 | } 987 | 988 | onStateUpdate(state, source) { 989 | console.log("Understanding deepened:", { 990 | newState: state, 991 | origin: source, 992 | }); 993 | } 994 | 995 | // Scheduling APIs 996 | // An Agent can schedule tasks to be run in the future by calling this.schedule(when, callback, data), where when can be a delay, a Date, or a cron string; callback the function name to call, and data is an object of data to pass to the function. 997 | // 998 | // Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read+write state: scheduled tasks can invoke any regular method on your Agent. 999 | async scheduleExamples() { 1000 | // schedule a task to run in 10 seconds 1001 | let task = await this.schedule(10, "someTask", { message: "hello" }); 1002 | 1003 | // schedule a task to run at a specific date 1004 | let task = await this.schedule(new Date("2025-01-01"), "someTask", {}); 1005 | 1006 | // schedule a task to run every 10 seconds 1007 | let { id } = await this.schedule("*/10 * * * *", "someTask", { message: "hello" }); 1008 | 1009 | // schedule a task to run every 10 seconds, but only on Mondays 1010 | let task = await this.schedule("0 0 * * 1", "someTask", { message: "hello" }); 1011 | 1012 | // cancel a scheduled task 1013 | this.cancelSchedule(task.id); 1014 | 1015 | // Get a specific schedule by ID 1016 | // Returns undefined if the task does not exist 1017 | let task = await this.getSchedule(task.id) 1018 | 1019 | // Get all scheduled tasks 1020 | // Returns an array of Schedule objects 1021 | let tasks = this.getSchedules(); 1022 | 1023 | // Cancel a task by its ID 1024 | // Returns true if the task was cancelled, false if it did not exist 1025 | await this.cancelSchedule(task.id); 1026 | 1027 | // Filter for specific tasks 1028 | // e.g. all tasks starting in the next hour 1029 | let tasks = this.getSchedules({ 1030 | timeRange: { 1031 | start: new Date(Date.now()), 1032 | end: new Date(Date.now() + 60 * 60 * 1000), 1033 | } 1034 | }); 1035 | } 1036 | 1037 | async someTask(data) { 1038 | await this.callReasoningModel(data.message); 1039 | } 1040 | 1041 | // Use the this.sql API within the Agent to access the underlying SQLite database 1042 | async callReasoningModel(prompt: Prompt) { 1043 | interface Prompt { 1044 | userId: string; 1045 | user: string; 1046 | system: string; 1047 | metadata: Record; 1048 | } 1049 | 1050 | interface History { 1051 | timestamp: Date; 1052 | entry: string; 1053 | } 1054 | 1055 | let result = this.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; 1056 | let context = []; 1057 | for await (const row of result) { 1058 | context.push(row.entry); 1059 | } 1060 | 1061 | const client = new OpenAI({ 1062 | apiKey: this.env.OPENAI_API_KEY, 1063 | }); 1064 | 1065 | // Combine user history with the current prompt 1066 | const systemPrompt = prompt.system || 'You are a helpful assistant.'; 1067 | const userPrompt = `${prompt.user}\n\nUser history:\n${context.join('\n')}`; 1068 | 1069 | try { 1070 | const completion = await client.chat.completions.create({ 1071 | model: this.env.MODEL || 'o3-mini', 1072 | messages: [ 1073 | { role: 'system', content: systemPrompt }, 1074 | { role: 'user', content: userPrompt }, 1075 | ], 1076 | temperature: 0.7, 1077 | max_tokens: 1000, 1078 | }); 1079 | 1080 | // Store the response in history 1081 | this 1082 | .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; 1083 | 1084 | return completion.choices[0].message.content; 1085 | } catch (error) { 1086 | console.error('Error calling reasoning model:', error); 1087 | throw error; 1088 | } 1089 | } 1090 | 1091 | // Use the SQL API with a type parameter 1092 | async queryUser(userId: string) { 1093 | type User = { 1094 | id: string; 1095 | name: string; 1096 | email: string; 1097 | }; 1098 | // Supply the type paramter to the query when calling this.sql 1099 | // This assumes the results returns one or more User rows with "id", "name", and "email" columns 1100 | // You do not need to specify an array type (`User[]` or `Array`) as `this.sql` will always return an array of the specified type. 1101 | const user = await this.sql`SELECT * FROM users WHERE id = ${userId}`; 1102 | return user 1103 | } 1104 | 1105 | // Run and orchestrate Workflows from Agents 1106 | async runWorkflow(data) { 1107 | let instance = await env.MY_WORKFLOW.create({ 1108 | id: data.id, 1109 | params: data, 1110 | }) 1111 | 1112 | // Schedule another task that checks the Workflow status every 5 minutes... 1113 | await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id }); 1114 | } 1115 | } 1116 | 1117 | export default { 1118 | async fetch(request, env, ctx): Promise { 1119 | // Routed addressing 1120 | // Automatically routes HTTP requests and/or WebSocket connections to /agents/:agent/:name 1121 | // Best for: connecting React apps directly to Agents using useAgent from @cloudflare/agents/react 1122 | return (await routeAgentRequest(request, env)) || Response.json({ msg: 'no agent here' }, { status: 404 }); 1123 | 1124 | // Named addressing 1125 | // Best for: convenience method for creating or retrieving an agent by name/ID. 1126 | let namedAgent = getAgentByName(env.AIAgent, 'agent-456'); 1127 | // Pass the incoming request straight to your Agent 1128 | let namedResp = (await namedAgent).fetch(request); 1129 | return namedResp; 1130 | 1131 | // Durable Objects-style addressing 1132 | // Best for: controlling ID generation, associating IDs with your existing systems, 1133 | // and customizing when/how an Agent is created or invoked 1134 | const id = env.AIAgent.newUniqueId(); 1135 | const agent = env.AIAgent.get(id); 1136 | // Pass the incoming request straight to your Agent 1137 | let resp = await agent.fetch(request); 1138 | 1139 | // return Response.json({ hello: 'visit https://developers.cloudflare.com/agents for more' }); 1140 | }, 1141 | } satisfies ExportedHandler; 1142 | 1143 | 1144 | 1145 | // client.js 1146 | import { AgentClient } from "agents/client"; 1147 | 1148 | const connection = new AgentClient({ 1149 | agent: "dialogue-agent", 1150 | name: "insight-seeker", 1151 | }); 1152 | 1153 | connection.addEventListener("message", (event) => { 1154 | console.log("Received:", event.data); 1155 | }); 1156 | 1157 | connection.send( 1158 | JSON.stringify({ 1159 | type: "inquiry", 1160 | content: "What patterns do you see?", 1161 | }) 1162 | ); 1163 | 1164 | 1165 | 1166 | // app.tsx 1167 | // React client hook for the agents sdk 1168 | import { useAgent } from "agents/react"; 1169 | import { useState } from "react"; 1170 | 1171 | // useAgent client API 1172 | function AgentInterface() { 1173 | const connection = useAgent({ 1174 | agent: "dialogue-agent", 1175 | name: "insight-seeker", 1176 | onMessage: (message) => { 1177 | console.log("Understanding received:", message.data); 1178 | }, 1179 | onOpen: () => console.log("Connection established"), 1180 | onClose: () => console.log("Connection closed"), 1181 | }); 1182 | 1183 | const inquire = () => { 1184 | connection.send( 1185 | JSON.stringify({ 1186 | type: "inquiry", 1187 | content: "What insights have you gathered?", 1188 | }) 1189 | ); 1190 | }; 1191 | 1192 | return ( 1193 |
1194 | 1195 |
1196 | ); 1197 | } 1198 | 1199 | // State synchronization 1200 | function StateInterface() { 1201 | const [state, setState] = useState({ counter: 0 }); 1202 | 1203 | const agent = useAgent({ 1204 | agent: "thinking-agent", 1205 | onStateUpdate: (newState) => setState(newState), 1206 | }); 1207 | 1208 | const increment = () => { 1209 | agent.setState({ counter: state.counter + 1 }); 1210 | }; 1211 | 1212 | return ( 1213 |
1214 |
Count: {state.counter}
1215 | 1216 |
1217 | ); 1218 | } 1219 |
1220 | 1221 | 1222 | { 1223 | "durable_objects": { 1224 | "bindings": [ 1225 | { 1226 | "binding": "AIAgent", 1227 | "class_name": "AIAgent" 1228 | } 1229 | ] 1230 | }, 1231 | "migrations": [ 1232 | { 1233 | "tag": "v1", 1234 | // Mandatory for the Agent to store state 1235 | "new_sqlite_classes": ["AIAgent"] 1236 | } 1237 | ] 1238 | } 1239 | 1240 | 1241 | 1242 | - Imports the `Agent` class from the `agents` package 1243 | - Extends the `Agent` class and implements the methods exposed by the `Agent`, including `onRequest` for HTTP requests, or `onConnect` and `onMessage` for WebSockets. 1244 | - Uses the `this.schedule` scheduling API to schedule future tasks. 1245 | - Uses the `this.setState` API within the Agent for syncing state, and uses type parameters to ensure the state is typed. 1246 | - Uses the `this.sql` as a lower-level query API. 1247 | - For frontend applications, uses the optional `useAgent` hook to connect to the Agent via WebSockets 1248 | 1249 | 1250 |
1251 | 1252 | 1253 | 1254 | Workers AI supports structured JSON outputs with JSON mode, which supports the `response_format` API provided by the OpenAI SDK. 1255 | 1256 | 1257 | import { OpenAI } from "openai"; 1258 | 1259 | interface Env { 1260 | OPENAI_API_KEY: string; 1261 | } 1262 | 1263 | // Define your JSON schema for a calendar event 1264 | const CalendarEventSchema = { 1265 | type: 'object', 1266 | properties: { 1267 | name: { type: 'string' }, 1268 | date: { type: 'string' }, 1269 | participants: { type: 'array', items: { type: 'string' } }, 1270 | }, 1271 | required: ['name', 'date', 'participants'] 1272 | }; 1273 | 1274 | export default { 1275 | async fetch(request: Request, env: Env) { 1276 | const client = new OpenAI({ 1277 | apiKey: env.OPENAI_API_KEY, 1278 | // Optional: use AI Gateway to bring logs, evals & caching to your AI requests 1279 | // https://developers.cloudflare.com/ai-gateway/providers/openai/ 1280 | // baseUrl: "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai" 1281 | }); 1282 | 1283 | const response = await client.chat.completions.create({ 1284 | model: 'gpt-4o-2024-08-06', 1285 | messages: [ 1286 | { role: 'system', content: 'Extract the event information.' }, 1287 | { role: 'user', content: 'Alice and Bob are going to a science fair on Friday.' }, 1288 | ], 1289 | // Use the `response_format` option to request a structured JSON output 1290 | response_format: { 1291 | // Set json_schema and provide ra schema, or json_object and parse it yourself 1292 | type: 'json_schema', 1293 | schema: CalendarEventSchema, // provide a schema 1294 | }, 1295 | }); 1296 | 1297 | // This will be of type CalendarEventSchema 1298 | const event = response.choices[0].message.parsed; 1299 | 1300 | return Response.json({ 1301 | "calendar_event": event, 1302 | }) 1303 | } 1304 | } 1305 | 1306 | 1307 | { 1308 | "name": "my-app", 1309 | "main": "src/index.ts", 1310 | "compatibility_date": "$CURRENT_DATE", 1311 | "observability": { 1312 | "enabled": true 1313 | } 1314 | } 1315 | 1316 | 1317 | 1318 | - Defines a JSON Schema compatible object that represents the structured format requested from the model 1319 | - Sets `response_format` to `json_schema` and provides a schema to parse the response 1320 | - This could also be `json_object`, which can be parsed after the fact. 1321 | - Optionally uses AI Gateway to cache, log and instrument requests and responses between a client and the AI provider/API. 1322 | 1323 | 1324 | 1325 | 1326 |
1327 | 1328 | 1329 | 1330 | 1331 | 1332 | Fan-in/fan-out for WebSockets. Uses the Hibernatable WebSockets API within Durable Objects. Does NOT use the legacy addEventListener API. 1333 | 1334 | 1335 | export class WebSocketHibernationServer extends DurableObject { 1336 | async fetch(request: Request, env: Env, ctx: ExecutionContext) { 1337 | // Creates two ends of a WebSocket connection. 1338 | const webSocketPair = new WebSocketPair(); 1339 | const [client, server] = Object.values(webSocketPair); 1340 | 1341 | // Call this to accept the WebSocket connection. 1342 | // Do NOT call server.accept() (this is the legacy approach and is not preferred) 1343 | this.ctx.acceptWebSocket(server); 1344 | 1345 | return new Response(null, { 1346 | status: 101, 1347 | webSocket: client, 1348 | }); 1349 | }, 1350 | 1351 | async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise { 1352 | // Invoked on each WebSocket message. 1353 | ws.send(message) 1354 | }, 1355 | 1356 | async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) void | Promise { 1357 | // Invoked when a client closes the connection. 1358 | ws.close(code, ""); 1359 | }, 1360 | 1361 | async webSocketError(ws: WebSocket, error: unknown): void | Promise { 1362 | // Handle WebSocket errors 1363 | } 1364 | } 1365 | 1366 | 1367 | 1368 | 1369 | 1370 | {user_prompt} 1371 | -------------------------------------------------------------------------------- /.dev.vars.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-proj-1234567890 2 | # Optional - Cloudflare AI Gateway https://developers.cloudflare.com/ai-gateway/ 3 | # GATEWAY_BASE_URL=https://gateway.ai.cloudflare.com/v1/.. -------------------------------------------------------------------------------- /.github/workflows/sanity-check.yml: -------------------------------------------------------------------------------- 1 | name: Sanity Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | timeout-minutes: 5 14 | strategy: 15 | matrix: 16 | os: [ 17 | ubuntu-24.04, 18 | # windows-latest, 19 | # macos-latest, 20 | ] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 1 26 | 27 | - uses: actions/setup-node@v3 28 | 29 | - run: npm install 30 | - run: npm run check 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # MacOS 139 | .DS_Store 140 | 141 | # Cloudflare 142 | .wrangler 143 | .dev.vars 144 | 145 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | worker-configuration.d.ts 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2025 Cloudflare Inc. 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 Chat Agent Starter Kit 2 | 3 | ![agents-header](https://github.com/user-attachments/assets/f6d99eeb-1803-4495-9c5e-3cf07a37b402) 4 | 5 | Deploy to Cloudflare 6 | 7 | A starter template for building AI-powered chat agents using Cloudflare's Agent platform, powered by [`agents`](https://www.npmjs.com/package/agents). This project provides a foundation for creating interactive chat experiences with AI, complete with a modern UI and tool integration capabilities. 8 | 9 | ## Features 10 | 11 | - 💬 Interactive chat interface with AI 12 | - 🛠️ Built-in tool system with human-in-the-loop confirmation 13 | - 📅 Advanced task scheduling (one-time, delayed, and recurring via cron) 14 | - 🌓 Dark/Light theme support 15 | - ⚡️ Real-time streaming responses 16 | - 🔄 State management and chat history 17 | - 🎨 Modern, responsive UI 18 | 19 | ## Prerequisites 20 | 21 | - Cloudflare account 22 | - OpenAI API key 23 | 24 | ## Quick Start 25 | 26 | 1. Create a new project: 27 | 28 | ```bash 29 | npx create-cloudflare@latest --template cloudflare/agents-starter 30 | ``` 31 | 32 | 2. Install dependencies: 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | 3. Set up your environment: 39 | 40 | Create a `.dev.vars` file: 41 | 42 | ```env 43 | OPENAI_API_KEY=your_openai_api_key 44 | ``` 45 | 46 | 4. Run locally: 47 | 48 | ```bash 49 | npm start 50 | ``` 51 | 52 | 5. Deploy: 53 | 54 | ```bash 55 | npm run deploy 56 | ``` 57 | 58 | ## Project Structure 59 | 60 | ``` 61 | ├── src/ 62 | │ ├── app.tsx # Chat UI implementation 63 | │ ├── server.ts # Chat agent logic 64 | │ ├── tools.ts # Tool definitions 65 | │ ├── utils.ts # Helper functions 66 | │ └── styles.css # UI styling 67 | ``` 68 | 69 | ## Customization Guide 70 | 71 | ### Adding New Tools 72 | 73 | Add new tools in `tools.ts` using the tool builder: 74 | 75 | ```typescript 76 | // Example of a tool that requires confirmation 77 | const searchDatabase = tool({ 78 | description: "Search the database for user records", 79 | parameters: z.object({ 80 | query: z.string(), 81 | limit: z.number().optional(), 82 | }), 83 | // No execute function = requires confirmation 84 | }); 85 | 86 | // Example of an auto-executing tool 87 | const getCurrentTime = tool({ 88 | description: "Get current server time", 89 | parameters: z.object({}), 90 | execute: async () => new Date().toISOString(), 91 | }); 92 | 93 | // Scheduling tool implementation 94 | const scheduleTask = tool({ 95 | description: 96 | "schedule a task to be executed at a later time. 'when' can be a date, a delay in seconds, or a cron pattern.", 97 | parameters: z.object({ 98 | type: z.enum(["scheduled", "delayed", "cron"]), 99 | when: z.union([z.number(), z.string()]), 100 | payload: z.string(), 101 | }), 102 | execute: async ({ type, when, payload }) => { 103 | // ... see the implementation in tools.ts 104 | }, 105 | }); 106 | ``` 107 | 108 | To handle tool confirmations, add execution functions to the `executions` object: 109 | 110 | ```typescript 111 | export const executions = { 112 | searchDatabase: async ({ 113 | query, 114 | limit, 115 | }: { 116 | query: string; 117 | limit?: number; 118 | }) => { 119 | // Implementation for when the tool is confirmed 120 | const results = await db.search(query, limit); 121 | return results; 122 | }, 123 | // Add more execution handlers for other tools that require confirmation 124 | }; 125 | ``` 126 | 127 | Tools can be configured in two ways: 128 | 129 | 1. With an `execute` function for automatic execution 130 | 2. Without an `execute` function, requiring confirmation and using the `executions` object to handle the confirmed action 131 | 132 | ### Use a different AI model provider 133 | 134 | The starting [`server.ts`](https://github.com/cloudflare/agents-starter/blob/main/src/server.ts) implementation uses the [`ai-sdk`](https://sdk.vercel.ai/docs/introduction) and the [OpenAI provider](https://sdk.vercel.ai/providers/ai-sdk-providers/openai), but you can use any AI model provider by: 135 | 136 | 1. Installing an alternative AI provider for the `ai-sdk`, such as the [`workers-ai-provider`](https://sdk.vercel.ai/providers/community-providers/cloudflare-workers-ai) or [`anthropic`](https://sdk.vercel.ai/providers/ai-sdk-providers/anthropic) provider: 137 | 2. Replacing the AI SDK with the [OpenAI SDK](https://github.com/openai/openai-node) 138 | 3. Using the Cloudflare [Workers AI + AI Gateway](https://developers.cloudflare.com/ai-gateway/providers/workersai/#workers-binding) binding API directly 139 | 140 | For example, to use the [`workers-ai-provider`](https://sdk.vercel.ai/providers/community-providers/cloudflare-workers-ai), install the package: 141 | 142 | ```sh 143 | npm install workers-ai-provider 144 | ``` 145 | 146 | Add an `ai` binding to `wrangler.jsonc`: 147 | 148 | ```jsonc 149 | // rest of file 150 | "ai": { 151 | "binding": "AI" 152 | } 153 | // rest of file 154 | ``` 155 | 156 | Replace the `@ai-sdk/openai` import and usage with the `workers-ai-provider`: 157 | 158 | ```diff 159 | // server.ts 160 | // Change the imports 161 | - import { openai } from "@ai-sdk/openai"; 162 | + import { createWorkersAI } from 'workers-ai-provider'; 163 | 164 | // Create a Workers AI instance 165 | + const workersai = createWorkersAI({ binding: env.AI }); 166 | 167 | // Use it when calling the streamText method (or other methods) 168 | // from the ai-sdk 169 | - const model = openai("gpt-4o-2024-11-20"); 170 | + const model = workersai("@cf/deepseek-ai/deepseek-r1-distill-qwen-32b") 171 | ``` 172 | 173 | Commit your changes and then run the `agents-starter` as per the rest of this README. 174 | 175 | ### Modifying the UI 176 | 177 | The chat interface is built with React and can be customized in `app.tsx`: 178 | 179 | - Modify the theme colors in `styles.css` 180 | - Add new UI components in the chat container 181 | - Customize message rendering and tool confirmation dialogs 182 | - Add new controls to the header 183 | 184 | ### Example Use Cases 185 | 186 | 1. **Customer Support Agent** 187 | 188 | - Add tools for: 189 | - Ticket creation/lookup 190 | - Order status checking 191 | - Product recommendations 192 | - FAQ database search 193 | 194 | 2. **Development Assistant** 195 | 196 | - Integrate tools for: 197 | - Code linting 198 | - Git operations 199 | - Documentation search 200 | - Dependency checking 201 | 202 | 3. **Data Analysis Assistant** 203 | 204 | - Build tools for: 205 | - Database querying 206 | - Data visualization 207 | - Statistical analysis 208 | - Report generation 209 | 210 | 4. **Personal Productivity Assistant** 211 | 212 | - Implement tools for: 213 | - Task scheduling with flexible timing options 214 | - One-time, delayed, and recurring task management 215 | - Task tracking with reminders 216 | - Email drafting 217 | - Note taking 218 | 219 | 5. **Scheduling Assistant** 220 | - Build tools for: 221 | - One-time event scheduling using specific dates 222 | - Delayed task execution (e.g., "remind me in 30 minutes") 223 | - Recurring tasks using cron patterns 224 | - Task payload management 225 | - Flexible scheduling patterns 226 | 227 | Each use case can be implemented by: 228 | 229 | 1. Adding relevant tools in `tools.ts` 230 | 2. Customizing the UI for specific interactions 231 | 3. Extending the agent's capabilities in `server.ts` 232 | 4. Adding any necessary external API integrations 233 | 234 | ## Learn More 235 | 236 | - [`agents`](https://github.com/cloudflare/agents/blob/main/packages/agents/README.md) 237 | - [Cloudflare Agents Documentation](https://developers.cloudflare.com/agents/) 238 | - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) 239 | 240 | ## License 241 | 242 | MIT 243 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["node_modules", "dist", "./worker-configuration.d.ts"] 11 | }, 12 | "formatter": { 13 | "enabled": false, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true, 23 | "style": { 24 | "noNonNullAssertion": "off" 25 | } 26 | } 27 | }, 28 | "javascript": { 29 | "formatter": { 30 | "quoteStyle": "double" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "phosphor" 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | AI Chat Agent 13 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-agent-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "private": true, 7 | "scripts": { 8 | "start": "vite dev", 9 | "deploy": "vite build && wrangler deploy", 10 | "test": "vitest", 11 | "types": "wrangler types", 12 | "format": "prettier --write .", 13 | "check": "prettier . --check && biome lint && tsc" 14 | }, 15 | "keywords": [ 16 | "cloudflare", 17 | "ai", 18 | "agents" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@biomejs/biome": "^1.9.4", 24 | "@cloudflare/vite-plugin": "1.1.1", 25 | "@cloudflare/vitest-pool-workers": "^0.8.27", 26 | "@cloudflare/workers-types": "^4.20250513.0", 27 | "@tailwindcss/vite": "^4.1.6", 28 | "@types/node": "^22.15.17", 29 | "@types/react": "^19.1.4", 30 | "@types/react-dom": "^19.1.5", 31 | "@vitejs/plugin-react": "^4.4.1", 32 | "prettier": "^3.5.3", 33 | "tailwindcss": "^4.1.6", 34 | "typescript": "^5.8.3", 35 | "vite": "^6.3.5", 36 | "vitest": "3.1.3", 37 | "wrangler": "^4.14.4" 38 | }, 39 | "dependencies": { 40 | "@ai-sdk/openai": "^1.3.22", 41 | "@ai-sdk/react": "^1.2.12", 42 | "@ai-sdk/ui-utils": "^1.2.11", 43 | "@phosphor-icons/react": "^2.1.7", 44 | "@radix-ui/react-avatar": "^1.1.9", 45 | "@radix-ui/react-dropdown-menu": "^2.1.14", 46 | "@radix-ui/react-slot": "^1.2.2", 47 | "@radix-ui/react-switch": "^1.2.4", 48 | "@types/marked": "^6.0.0", 49 | "agents": "^0.0.86", 50 | "ai": "^4.3.15", 51 | "class-variance-authority": "^0.7.1", 52 | "clsx": "^2.1.1", 53 | "marked": "^15.0.11", 54 | "react": "^19.1.0", 55 | "react-dom": "^19.1.0", 56 | "react-markdown": "^10.1.0", 57 | "remark-gfm": "^4.0.1", 58 | "tailwind-merge": "^3.3.0", 59 | "zod": "^3.24.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/agents-starter/76323189ed8a95a987e17356bac832ec3c005a6f/public/favicon.ico -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useCallback, use } from "react"; 2 | import { useAgent } from "agents/react"; 3 | import { useAgentChat } from "agents/ai-react"; 4 | import type { Message } from "@ai-sdk/react"; 5 | import type { tools } from "./tools"; 6 | 7 | // Component imports 8 | import { Button } from "@/components/button/Button"; 9 | import { Card } from "@/components/card/Card"; 10 | import { Avatar } from "@/components/avatar/Avatar"; 11 | import { Toggle } from "@/components/toggle/Toggle"; 12 | import { Textarea } from "@/components/textarea/Textarea"; 13 | import { MemoizedMarkdown } from "@/components/memoized-markdown"; 14 | import { ToolInvocationCard } from "@/components/tool-invocation-card/ToolInvocationCard"; 15 | 16 | // Icon imports 17 | import { 18 | Bug, 19 | Moon, 20 | Robot, 21 | Sun, 22 | Trash, 23 | PaperPlaneTilt, 24 | Stop, 25 | } from "@phosphor-icons/react"; 26 | 27 | // List of tools that require human confirmation 28 | const toolsRequiringConfirmation: (keyof typeof tools)[] = [ 29 | "getWeatherInformation", 30 | ]; 31 | 32 | export default function Chat() { 33 | const [theme, setTheme] = useState<"dark" | "light">(() => { 34 | // Check localStorage first, default to dark if not found 35 | const savedTheme = localStorage.getItem("theme"); 36 | return (savedTheme as "dark" | "light") || "dark"; 37 | }); 38 | const [showDebug, setShowDebug] = useState(false); 39 | const [textareaHeight, setTextareaHeight] = useState("auto"); 40 | const messagesEndRef = useRef(null); 41 | 42 | const scrollToBottom = useCallback(() => { 43 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 44 | }, []); 45 | 46 | useEffect(() => { 47 | // Apply theme class on mount and when theme changes 48 | if (theme === "dark") { 49 | document.documentElement.classList.add("dark"); 50 | document.documentElement.classList.remove("light"); 51 | } else { 52 | document.documentElement.classList.remove("dark"); 53 | document.documentElement.classList.add("light"); 54 | } 55 | 56 | // Save theme preference to localStorage 57 | localStorage.setItem("theme", theme); 58 | }, [theme]); 59 | 60 | // Scroll to bottom on mount 61 | useEffect(() => { 62 | scrollToBottom(); 63 | }, [scrollToBottom]); 64 | 65 | const toggleTheme = () => { 66 | const newTheme = theme === "dark" ? "light" : "dark"; 67 | setTheme(newTheme); 68 | }; 69 | 70 | const agent = useAgent({ 71 | agent: "chat", 72 | }); 73 | 74 | const { 75 | messages: agentMessages, 76 | input: agentInput, 77 | handleInputChange: handleAgentInputChange, 78 | handleSubmit: handleAgentSubmit, 79 | addToolResult, 80 | clearHistory, 81 | isLoading, 82 | stop, 83 | } = useAgentChat({ 84 | agent, 85 | maxSteps: 5, 86 | }); 87 | 88 | // Scroll to bottom when messages change 89 | useEffect(() => { 90 | agentMessages.length > 0 && scrollToBottom(); 91 | }, [agentMessages, scrollToBottom]); 92 | 93 | const pendingToolCallConfirmation = agentMessages.some((m: Message) => 94 | m.parts?.some( 95 | (part) => 96 | part.type === "tool-invocation" && 97 | part.toolInvocation.state === "call" && 98 | toolsRequiringConfirmation.includes( 99 | part.toolInvocation.toolName as keyof typeof tools 100 | ) 101 | ) 102 | ); 103 | 104 | const formatTime = (date: Date) => { 105 | return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); 106 | }; 107 | 108 | return ( 109 |
110 | 111 |
112 |
113 |
114 | 120 | Cloudflare Agents 121 | 122 | 126 | 127 | 128 | 129 |
130 | 131 |
132 |

AI Chat Agent

133 |
134 | 135 |
136 | 137 | setShowDebug((prev) => !prev)} 141 | /> 142 |
143 | 144 | 153 | 154 | 163 |
164 | 165 | {/* Messages */} 166 |
167 | {agentMessages.length === 0 && ( 168 |
169 | 170 |
171 |
172 | 173 |
174 |

Welcome to AI Chat

175 |

176 | Start a conversation with your AI assistant. Try asking 177 | about: 178 |

179 |
    180 |
  • 181 | 182 | Weather information for any city 183 |
  • 184 |
  • 185 | 186 | Local time in different locations 187 |
  • 188 |
189 |
190 |
191 |
192 | )} 193 | 194 | {agentMessages.map((m: Message, index) => { 195 | const isUser = m.role === "user"; 196 | const showAvatar = 197 | index === 0 || agentMessages[index - 1]?.role !== m.role; 198 | 199 | return ( 200 |
201 | {showDebug && ( 202 |
203 |                     {JSON.stringify(m, null, 2)}
204 |                   
205 | )} 206 |
209 |
214 | {showAvatar && !isUser ? ( 215 | 216 | ) : ( 217 | !isUser &&
218 | )} 219 | 220 |
221 |
222 | {m.parts?.map((part, i) => { 223 | if (part.type === "text") { 224 | return ( 225 | // biome-ignore lint/suspicious/noArrayIndexKey: immutable index 226 |
227 | 238 | {part.text.startsWith( 239 | "scheduled message" 240 | ) && ( 241 | 242 | 🕒 243 | 244 | )} 245 | 252 | 253 |

258 | {formatTime( 259 | new Date(m.createdAt as unknown as string) 260 | )} 261 |

262 |
263 | ); 264 | } 265 | 266 | if (part.type === "tool-invocation") { 267 | const toolInvocation = part.toolInvocation; 268 | const toolCallId = toolInvocation.toolCallId; 269 | const needsConfirmation = 270 | toolsRequiringConfirmation.includes( 271 | toolInvocation.toolName as keyof typeof tools 272 | ); 273 | 274 | // Skip rendering the card in debug mode 275 | if (showDebug) return null; 276 | 277 | return ( 278 | 286 | ); 287 | } 288 | return null; 289 | })} 290 |
291 |
292 |
293 |
294 |
295 | ); 296 | })} 297 |
298 |
299 | 300 | {/* Input Area */} 301 |
{ 303 | e.preventDefault(); 304 | handleAgentSubmit(e, { 305 | data: { 306 | annotations: { 307 | hello: "world", 308 | }, 309 | }, 310 | }); 311 | setTextareaHeight("auto"); // Reset height after submission 312 | }} 313 | className="p-3 bg-neutral-50 absolute bottom-0 left-0 right-0 z-10 border-t border-neutral-300 dark:border-neutral-800 dark:bg-neutral-900" 314 | > 315 |
316 |
317 |