Skip to content

Commit 7e9a8bd

Browse files
tylergibbs1claude
andcommitted
Add code mode: let LLMs write code that orchestrates tools
Adapted from Cloudflare's @cloudflare/codemode. Instead of calling tools one at a time, the LLM writes an async arrow function that chains multiple tool calls with conditionals, loops, and error handling — all within a single execute_code invocation. - createCodeModeTool() wraps tools into a single execute_code FunctionTool - generateTypes() converts Zod schemas to TypeScript API definitions - FunctionExecutor runs code via AsyncFunction with console capture + timeout - Executor interface for custom sandboxes (isolated-vm, containers, etc.) - normalizeCode() strips markdown fences, wraps bare statements - sanitizeToolName() handles hyphens, dots, reserved words - 91 tests covering unit, agent integration, and edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 95a4147 commit 7e9a8bd

5 files changed

Lines changed: 2129 additions & 0 deletions

File tree

src/core/codemode/executor.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Code mode executor: runs LLM-generated code in a sandbox with tool access.
3+
*
4+
* The Executor interface is deliberately minimal — implement it to run code
5+
* in any sandbox (Node VM, Bun, QuickJS, containers, Cloudflare Workers, etc.).
6+
*/
7+
8+
export interface ExecuteResult {
9+
result: unknown;
10+
error?: string;
11+
logs?: string[];
12+
}
13+
14+
/**
15+
* An executor runs LLM-generated code in a sandbox, making the provided
16+
* tool functions callable as `codemode.*` inside the sandbox.
17+
*
18+
* Implementations should never throw — errors are returned in `ExecuteResult.error`.
19+
*/
20+
export interface Executor {
21+
execute(
22+
code: string,
23+
fns: Record<string, (...args: unknown[]) => Promise<unknown>>,
24+
): Promise<ExecuteResult>;
25+
}
26+
27+
export interface FunctionExecutorOptions {
28+
/** Timeout in milliseconds for code execution. Defaults to 30000 (30s). */
29+
timeout?: number;
30+
}
31+
32+
/**
33+
* Executes code using AsyncFunction (works in Node.js and Bun).
34+
* Tool calls are injected via the `codemode` parameter.
35+
*
36+
* This is NOT a secure sandbox — it runs in the same V8 isolate.
37+
* For production use with untrusted code, implement a custom Executor
38+
* using isolated-vm, Cloudflare Workers, or containers.
39+
*/
40+
export class FunctionExecutor implements Executor {
41+
readonly #timeout: number;
42+
43+
constructor(options?: FunctionExecutorOptions) {
44+
this.#timeout = options?.timeout ?? 30_000;
45+
}
46+
47+
async execute(
48+
code: string,
49+
fns: Record<string, (...args: unknown[]) => Promise<unknown>>,
50+
): Promise<ExecuteResult> {
51+
const logs: string[] = [];
52+
53+
// Create a console proxy that captures output
54+
const consoleProxy = {
55+
log: (...args: unknown[]) => {
56+
logs.push(args.map(String).join(" "));
57+
},
58+
warn: (...args: unknown[]) => {
59+
logs.push(`[warn] ${args.map(String).join(" ")}`);
60+
},
61+
error: (...args: unknown[]) => {
62+
logs.push(`[error] ${args.map(String).join(" ")}`);
63+
},
64+
};
65+
66+
try {
67+
// biome-ignore lint/security/noGlobalEval: Required for code mode execution
68+
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
69+
...args: string[]
70+
) => (...args: unknown[]) => Promise<unknown>;
71+
72+
const fn = new AsyncFunction(
73+
"codemode",
74+
"console",
75+
`return await (${code})()`,
76+
);
77+
78+
const timeoutMs = this.#timeout;
79+
const result = await Promise.race([
80+
fn(fns, consoleProxy),
81+
new Promise<never>((_, reject) =>
82+
setTimeout(() => reject(new Error("Execution timed out")), timeoutMs),
83+
),
84+
]);
85+
86+
return { result, logs };
87+
} catch (err) {
88+
return {
89+
result: undefined,
90+
error: err instanceof Error ? err.message : String(err),
91+
logs,
92+
};
93+
}
94+
}
95+
}

src/core/codemode/index.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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

Comments
 (0)