|
| 1 | +/** |
| 2 | + * Code mode: let LLMs write code that orchestrates tools instead of calling them one at a time. |
| 3 | + * |
| 4 | + * Inspired by Cloudflare's Code Mode and CodeAct — LLMs are better at writing code |
| 5 | + * than making individual tool calls because they've seen millions of lines of real-world |
| 6 | + * TypeScript but only contrived tool-calling examples. |
| 7 | + */ |
| 8 | + |
| 9 | +import { z } from "zod"; |
| 10 | +import { isFunctionTool } from "../hosted-tool"; |
| 11 | +import { tool } from "../tool"; |
| 12 | +import type { FunctionTool } from "../tool"; |
| 13 | +import type { AgentTool } from "../hosted-tool"; |
| 14 | +import type { Executor } from "./executor"; |
| 15 | +import { generateTypes, normalizeCode, sanitizeToolName } from "./types"; |
| 16 | + |
| 17 | +export type { Executor, ExecuteResult } from "./executor"; |
| 18 | +export { FunctionExecutor } from "./executor"; |
| 19 | +export type { FunctionExecutorOptions } from "./executor"; |
| 20 | +export { generateTypes, normalizeCode, sanitizeToolName } from "./types"; |
| 21 | + |
| 22 | +// ── Default description ──────────────────────────────────────────── |
| 23 | + |
| 24 | +const DEFAULT_DESCRIPTION = `Execute code to achieve a goal. |
| 25 | +
|
| 26 | +Available: |
| 27 | +{{types}} |
| 28 | +
|
| 29 | +Write an async arrow function in JavaScript that returns the result. |
| 30 | +Do NOT use TypeScript syntax — no type annotations, interfaces, or generics. |
| 31 | +Do NOT define named functions then call them — just write the arrow function body directly. |
| 32 | +
|
| 33 | +Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`; |
| 34 | + |
| 35 | +// ── createCodeModeTool ───────────────────────────────────────────── |
| 36 | + |
| 37 | +export interface CodeModeToolOptions { |
| 38 | + /** The tools to make available inside the code sandbox. Hosted tools are filtered out. */ |
| 39 | + tools: AgentTool[]; |
| 40 | + /** The executor to run generated code in. */ |
| 41 | + executor: Executor; |
| 42 | + /** Custom tool description. Use `{{types}}` as a placeholder for the generated type definitions. */ |
| 43 | + description?: string; |
| 44 | +} |
| 45 | + |
| 46 | +export interface CodeModeOutput { |
| 47 | + code: string; |
| 48 | + result: unknown; |
| 49 | + logs?: string[]; |
| 50 | +} |
| 51 | + |
| 52 | +const codeSchema = z.object({ |
| 53 | + code: z.string().describe("JavaScript async arrow function to execute"), |
| 54 | +}); |
| 55 | + |
| 56 | +/** |
| 57 | + * Create a code mode tool that allows LLMs to write and execute code |
| 58 | + * with access to your tools in a sandboxed environment. |
| 59 | + * |
| 60 | + * Returns a Stratus `FunctionTool` that can be added to any agent's tools array. |
| 61 | + * |
| 62 | + * @example |
| 63 | + * ```ts |
| 64 | + * import { Agent, tool } from "stratus"; |
| 65 | + * import { createCodeModeTool, FunctionExecutor } from "stratus/core/codemode"; |
| 66 | + * |
| 67 | + * const weatherTool = tool({ |
| 68 | + * name: "get_weather", |
| 69 | + * description: "Get weather for a location", |
| 70 | + * parameters: z.object({ location: z.string() }), |
| 71 | + * execute: async (ctx, { location }) => `72°F, sunny in ${location}`, |
| 72 | + * }); |
| 73 | + * |
| 74 | + * const executor = new FunctionExecutor({ timeout: 10_000 }); |
| 75 | + * const codemode = createCodeModeTool({ |
| 76 | + * tools: [weatherTool], |
| 77 | + * executor, |
| 78 | + * }); |
| 79 | + * |
| 80 | + * const agent = new Agent({ |
| 81 | + * name: "assistant", |
| 82 | + * instructions: "You are a helpful assistant.", |
| 83 | + * tools: [codemode], |
| 84 | + * }); |
| 85 | + * ``` |
| 86 | + */ |
| 87 | +export function createCodeModeTool<TContext = unknown>( |
| 88 | + options: CodeModeToolOptions, |
| 89 | +): FunctionTool<{ code: string }, TContext> { |
| 90 | + // Filter to only function tools (hosted tools can't be called locally) |
| 91 | + const functionTools = options.tools.filter(isFunctionTool); |
| 92 | + const types = generateTypes(functionTools); |
| 93 | + const executor = options.executor; |
| 94 | + |
| 95 | + return tool({ |
| 96 | + name: "execute_code", |
| 97 | + description: (options.description ?? DEFAULT_DESCRIPTION).replace("{{types}}", types), |
| 98 | + parameters: codeSchema, |
| 99 | + async execute(context: TContext, { code }: { code: string }) { |
| 100 | + // Build the function map for the sandbox |
| 101 | + const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {}; |
| 102 | + for (const t of functionTools) { |
| 103 | + const safeName = sanitizeToolName(t.name); |
| 104 | + fns[safeName] = async (args: unknown) => { |
| 105 | + const validated = t.parameters.parse(args); |
| 106 | + const result = await t.execute(context, validated); |
| 107 | + // Try to parse as JSON, fall back to raw string |
| 108 | + try { |
| 109 | + return JSON.parse(result); |
| 110 | + } catch { |
| 111 | + return result; |
| 112 | + } |
| 113 | + }; |
| 114 | + } |
| 115 | + |
| 116 | + const normalizedCode = normalizeCode(code); |
| 117 | + const executeResult = await executor.execute(normalizedCode, fns); |
| 118 | + |
| 119 | + if (executeResult.error) { |
| 120 | + const logCtx = executeResult.logs?.length |
| 121 | + ? `\n\nConsole output:\n${executeResult.logs.join("\n")}` |
| 122 | + : ""; |
| 123 | + throw new Error(`Code execution failed: ${executeResult.error}${logCtx}`); |
| 124 | + } |
| 125 | + |
| 126 | + const output: CodeModeOutput = { |
| 127 | + code, |
| 128 | + result: executeResult.result, |
| 129 | + }; |
| 130 | + if (executeResult.logs?.length) { |
| 131 | + output.logs = executeResult.logs; |
| 132 | + } |
| 133 | + return JSON.stringify(output); |
| 134 | + }, |
| 135 | + }); |
| 136 | +} |
0 commit comments