import { createWorkersAI } from "workers-ai-provider"; import { routeAgentRequest, callable } from "agents"; import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat"; import { streamText, convertToModelMessages, pruneMessages, tool, stepCountIs } from "ai"; import { z } from "zod"; /** * AI Chat Agent showcasing @cloudflare/ai-chat features: * - streamText with toUIMessageStreamResponse (simplest pattern) * - Server-side tools with execute * - Client-side tools (no execute, handled via onToolCall) * - Tool approval with needsApproval * - Message pruning for long conversations * - Storage management with maxPersistedMessages */ export class ChatAgent extends AIChatAgent { // Keep the last 200 messages in SQLite storage maxPersistedMessages = 200; // Wait for MCP connections to restore after hibernation before processing messages waitForMcpConnections = true; onStart() { // Configure OAuth popup behavior for MCP servers that require authentication this.mcp.configureOAuthCallback({ customHandler: (result) => { if (result.authSuccess) { return new Response("", { headers: { "content-type": "text/html" }, status: 200 }); } return new Response( `Authentication Failed: ${result.authError || "Unknown error"}`, { headers: { "content-type": "text/plain" }, status: 400 } ); } }); } @callable() async addServer(name: string, url: string, host: string) { return await this.addMcpServer(name, url, { callbackHost: host }); } @callable() async removeServer(serverId: string) { await this.removeMcpServer(serverId); } async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) { const mcpTools = this.mcp.getAITools(); const workersai = createWorkersAI({ binding: this.env.AI }); const result = streamText({ abortSignal: options?.abortSignal, model: workersai("@cf/moonshotai/kimi-k2.5", { sessionAffinity: this.sessionAffinity }), system: "You are a helpful assistant. You can check the weather, get the user's timezone, " + "and run calculations. For calculations with large numbers (over 1000), you need user approval first.", // Prune old tool calls and reasoning to save tokens on long conversations messages: pruneMessages({ messages: await convertToModelMessages(this.messages), toolCalls: "before-last-2-messages", reasoning: "before-last-message" }), tools: { // MCP tools from connected servers ...mcpTools, // Server-side tool: executes automatically getWeather: tool({ description: "Get the current weather for a city", inputSchema: z.object({ city: z.string().describe("City name") }), execute: async ({ city }) => { // In a real app, call a weather API const conditions = ["sunny", "cloudy", "rainy", "snowy"]; const temp = Math.floor(Math.random() * 30) + 5; return { city, temperature: temp, condition: conditions[Math.floor(Math.random() * conditions.length)], unit: "celsius" }; } }), // Client-side tool: no execute, handled by onToolCall in the client getUserTimezone: tool({ description: "Get the user's timezone from their browser. Use this when you need to know the user's local time.", inputSchema: z.object({}) // No execute -- the client provides the result via onToolCall }), // Tool with approval: requires user confirmation before executing calculate: tool({ description: "Perform a math calculation with two numbers. Requires approval for large numbers.", inputSchema: z.object({ a: z.number().describe("First number"), b: z.number().describe("Second number"), operator: z .enum(["+", "-", "*", "/", "%"]) .describe("Arithmetic operator") }), needsApproval: async ({ a, b }) => Math.abs(a) > 1000 || Math.abs(b) > 1000, execute: async ({ a, b, operator }) => { const ops: Record number> = { "+": (x, y) => x + y, "-": (x, y) => x - y, "*": (x, y) => x * y, "/": (x, y) => x / y, "%": (x, y) => x % y }; if (operator === "/" && b === 0) { return { error: "Division by zero" }; } return { expression: `${a} ${operator} ${b}`, result: ops[operator](a, b) }; } }) }, stopWhen: stepCountIs(5) }); return result.toUIMessageStreamResponse(); } } export default { async fetch(request: Request, env: Env) { return ( (await routeAgentRequest(request, env)) || new Response("Not found", { status: 404 }) ); } } satisfies ExportedHandler;