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;