branch:
server.ts
5098 bytesRaw
import { routeAgentRequest, getAgentByName, callable } from "agents";
import { AIChatAgent } from "@cloudflare/ai-chat";
import { createCodeTool, generateTypes } from "@cloudflare/codemode/ai";
import { DynamicWorkerExecutor, type Executor } from "@cloudflare/codemode";
import {
streamText,
stepCountIs,
convertToModelMessages,
pruneMessages
} from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { initDatabase, createTools } from "./tools";
import {
NodeServerExecutor,
handleToolCallback
} from "./executors/node-server-client";
export type ExecutorType = "dynamic-worker" | "node-server";
type ToolFns = Record<string, (...args: unknown[]) => Promise<unknown>>;
export class Codemode extends AIChatAgent<Env> {
nodeExecutorRegistry = new Map<string, ToolFns>();
tools!: ReturnType<typeof createTools>;
executorType: ExecutorType = "dynamic-worker";
async onStart() {
initDatabase(this.ctx.storage.sql);
this.tools = createTools(this.ctx.storage.sql);
}
async onRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith("/node-executor-callback/")) {
return handleToolCallback(request, this.nodeExecutorRegistry);
}
return super.onRequest(request);
}
@callable({ description: "Set the executor type" })
setExecutor(executorType: ExecutorType) {
this.executorType = executorType;
return { executor: this.executorType };
}
@callable({ description: "Get tool type definitions" })
getToolTypes() {
// Merge local tools with MCP tools for type generation
const mcpTools = this.mcp.getAITools();
const allTools = { ...this.tools, ...mcpTools };
return generateTypes(allTools);
}
@callable({ description: "Add an MCP server to get additional tools" })
async addMcp(url: string, name?: string) {
const serverName = name || `mcp-${Date.now()}`;
// Use HOST if provided, otherwise it will be derived from the request
// For @callable methods (WebSocket RPC), there's no request context,
// so HOST must be set in wrangler.jsonc vars for production
await this.addMcpServer(serverName, url, {
callbackHost: this.env.HOST
});
return { success: true, name: serverName };
}
@callable({ description: "List connected MCP servers and their tools" })
listMcpTools() {
const tools = this.mcp.listTools();
return tools.map((t) => ({
serverId: t.serverId,
name: t.name,
description: t.description
}));
}
@callable({ description: "Remove an MCP server" })
async removeMcp(serverId: string) {
await this.mcp.removeServer(serverId);
return { success: true, removed: serverId };
}
createExecutor(): Executor {
switch (this.executorType) {
case "node-server":
return new NodeServerExecutor({
serverUrl: "http://localhost:3001",
callbackUrl: `http://localhost:5173/node-executor-callback/${this.name}`,
registry: this.nodeExecutorRegistry
});
case "dynamic-worker":
default:
return new DynamicWorkerExecutor({
loader: this.env.LOADER
});
}
}
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const executor = this.createExecutor();
// Merge local tools with MCP tools
const mcpTools = this.mcp.getAITools();
const allTools = { ...this.tools, ...mcpTools };
const codemode = createCodeTool({
tools: allTools,
executor
});
const result = streamText({
model: workersai("@cf/moonshotai/kimi-k2.5", {
sessionAffinity: this.sessionAffinity
}),
system:
"You are a helpful project management assistant. " +
"You can create and manage projects, tasks, sprints, and comments using the codemode tool. " +
"When you need to perform operations, use the codemode tool to write JavaScript " +
"that calls the available functions on the `codemode` object. " +
`Current executor: ${this.executorType}`,
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
toolCalls: "before-last-2-messages",
reasoning: "before-last-message"
}),
tools: { codemode },
stopWhen: stepCountIs(10)
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname.startsWith("/node-executor-callback/")) {
const parts = url.pathname.split("/").filter(Boolean);
const agentName = parts[1];
if (!agentName) {
return Response.json(
{ error: "Missing agent name in callback URL" },
{ status: 400 }
);
}
const agent = await getAgentByName(env.Codemode, agentName);
return agent.fetch(request);
}
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;