@cloudflare/codemode 0.0.8 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +174 -247
- package/dist/ai.d.ts +27 -27
- package/dist/ai.js +76 -136
- package/dist/ai.js.map +1 -1
- package/dist/executor-Czw9jKZH.d.ts +96 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/types-CpkEgXwN.js +392 -0
- package/dist/types-CpkEgXwN.js.map +1 -0
- package/e2e/codemode.spec.ts +124 -0
- package/e2e/playwright.config.ts +24 -0
- package/e2e/worker.ts +144 -0
- package/e2e/wrangler.jsonc +14 -0
- package/package.json +15 -4
- package/scripts/build.ts +1 -2
- package/src/ai.ts +1 -247
- package/src/executor.ts +170 -0
- package/src/index.ts +13 -0
- package/src/tests/cloudflare-test.d.ts +5 -0
- package/src/tests/executor.test.ts +224 -0
- package/src/tests/schema-conversion.test.ts +1068 -0
- package/src/tests/tool.test.ts +454 -0
- package/src/tests/tsconfig.json +10 -0
- package/src/tests/types.test.ts +446 -0
- package/src/tool.ts +152 -0
- package/src/types.ts +677 -0
- package/vitest.config.ts +17 -0
- package/wrangler.jsonc +16 -0
package/e2e/worker.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Agent, routeAgentRequest, getAgentByName } from "agents";
|
|
2
|
+
import {
|
|
3
|
+
streamText,
|
|
4
|
+
convertToModelMessages,
|
|
5
|
+
stepCountIs,
|
|
6
|
+
tool,
|
|
7
|
+
type UIMessage
|
|
8
|
+
} from "ai";
|
|
9
|
+
import { createWorkersAI } from "workers-ai-provider";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { createCodeTool } from "../src/ai";
|
|
12
|
+
import { DynamicWorkerExecutor, generateTypes } from "../src/index";
|
|
13
|
+
|
|
14
|
+
type Env = {
|
|
15
|
+
AI: Ai;
|
|
16
|
+
LOADER: WorkerLoader;
|
|
17
|
+
CodemodeAgent: DurableObjectNamespace<CodemodeAgent>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pmTools = {
|
|
21
|
+
createProject: tool({
|
|
22
|
+
description: "Create a new project",
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
name: z.string().describe("Project name"),
|
|
25
|
+
description: z.string().optional().describe("Project description")
|
|
26
|
+
}),
|
|
27
|
+
execute: async ({ name, description }) => ({
|
|
28
|
+
id: crypto.randomUUID(),
|
|
29
|
+
name,
|
|
30
|
+
description: description ?? ""
|
|
31
|
+
})
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
listProjects: tool({
|
|
35
|
+
description: "List all projects",
|
|
36
|
+
inputSchema: z.object({}),
|
|
37
|
+
execute: async () => [
|
|
38
|
+
{ id: "proj-1", name: "Alpha", description: "First project" },
|
|
39
|
+
{ id: "proj-2", name: "Beta", description: "Second project" }
|
|
40
|
+
]
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
addNumbers: tool({
|
|
44
|
+
description: "Add two numbers together",
|
|
45
|
+
inputSchema: z.object({
|
|
46
|
+
a: z.number().describe("First number"),
|
|
47
|
+
b: z.number().describe("Second number")
|
|
48
|
+
}),
|
|
49
|
+
execute: async ({ a, b }) => ({ result: a + b })
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
getWeather: tool({
|
|
53
|
+
description: "Get the current weather for a city",
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
city: z.string().describe("The city name")
|
|
56
|
+
}),
|
|
57
|
+
execute: async ({ city }) => ({
|
|
58
|
+
city,
|
|
59
|
+
temperature: 22,
|
|
60
|
+
condition: "Sunny"
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class CodemodeAgent extends Agent<Env> {
|
|
66
|
+
observability = undefined;
|
|
67
|
+
|
|
68
|
+
async onRequest(request: Request): Promise<Response> {
|
|
69
|
+
const url = new URL(request.url);
|
|
70
|
+
|
|
71
|
+
if (url.pathname.endsWith("/chat") && request.method === "POST") {
|
|
72
|
+
return this.handleChat(request);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (url.pathname.endsWith("/generate-types")) {
|
|
76
|
+
return Response.json({ types: generateTypes(pmTools) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Response("Not found", { status: 404 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async handleChat(request: Request): Promise<Response> {
|
|
83
|
+
const body = (await request.json()) as { messages: UIMessage[] };
|
|
84
|
+
|
|
85
|
+
const workersai = createWorkersAI({ binding: this.env.AI });
|
|
86
|
+
const model = workersai("@cf/zai-org/glm-4.7-flash");
|
|
87
|
+
|
|
88
|
+
const executor = new DynamicWorkerExecutor({
|
|
89
|
+
loader: this.env.LOADER
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const codemode = createCodeTool({
|
|
93
|
+
tools: pmTools,
|
|
94
|
+
executor
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = streamText({
|
|
98
|
+
model,
|
|
99
|
+
system: `You are a helpful assistant with access to a codemode tool.
|
|
100
|
+
When asked to perform operations, use the codemode tool to write JavaScript code that calls the available functions on the \`codemode\` object.
|
|
101
|
+
Keep responses very short (1-2 sentences max).
|
|
102
|
+
When asked to add numbers, use the addNumbers tool via codemode.
|
|
103
|
+
When asked about weather, use the getWeather tool via codemode.
|
|
104
|
+
When asked about projects, use createProject or listProjects via codemode.`,
|
|
105
|
+
messages: await convertToModelMessages(body.messages),
|
|
106
|
+
tools: { codemode },
|
|
107
|
+
stopWhen: stepCountIs(5)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return result.toTextStreamResponse();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
|
|
116
|
+
const url = new URL(request.url);
|
|
117
|
+
|
|
118
|
+
if (url.pathname.startsWith("/agents/")) {
|
|
119
|
+
return (
|
|
120
|
+
(await routeAgentRequest(request, env)) ||
|
|
121
|
+
new Response("Not found", { status: 404 })
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (url.pathname === "/run" && request.method === "POST") {
|
|
126
|
+
const agent = await getAgentByName(env.CodemodeAgent, "e2e-test");
|
|
127
|
+
const agentUrl = new URL(request.url);
|
|
128
|
+
agentUrl.pathname = "/chat";
|
|
129
|
+
return agent.fetch(
|
|
130
|
+
new Request(agentUrl.toString(), {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: request.headers,
|
|
133
|
+
body: request.body
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (url.pathname === "/types") {
|
|
139
|
+
return Response.json({ types: generateTypes(pmTools) });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return new Response("OK");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ai": { "binding": "AI", "remote": true },
|
|
3
|
+
"compatibility_date": "2026-01-28",
|
|
4
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
5
|
+
"define": {
|
|
6
|
+
"__filename": "'index.ts'"
|
|
7
|
+
},
|
|
8
|
+
"durable_objects": {
|
|
9
|
+
"bindings": [{ "class_name": "CodemodeAgent", "name": "CodemodeAgent" }]
|
|
10
|
+
},
|
|
11
|
+
"main": "worker.ts",
|
|
12
|
+
"migrations": [{ "new_sqlite_classes": ["CodemodeAgent"], "tag": "v1" }],
|
|
13
|
+
"worker_loaders": [{ "binding": "LOADER" }]
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudflare/codemode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Code Mode: use LLMs to generate executable code that performs tool calls",
|
|
5
5
|
"repository": {
|
|
6
6
|
"directory": "packages/codemode",
|
|
@@ -11,18 +11,26 @@
|
|
|
11
11
|
"url": "https://github.com/cloudflare/agents/issues"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"acorn": "^8.16.0",
|
|
14
15
|
"zod-to-ts": "^2.0.0"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
|
-
"
|
|
18
|
+
"@playwright/test": "^1.58.2",
|
|
19
|
+
"ai": "^6.0.97",
|
|
20
|
+
"vitest": "3.2.4",
|
|
18
21
|
"zod": "^4.3.6"
|
|
19
22
|
},
|
|
20
23
|
"peerDependencies": {
|
|
21
|
-
"agents": "^0.5.0",
|
|
22
24
|
"ai": "^6.0.0",
|
|
23
25
|
"zod": "^3.25.0 || ^4.0.0"
|
|
24
26
|
},
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
25
28
|
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
},
|
|
26
34
|
"./ai": {
|
|
27
35
|
"types": "./dist/ai.d.ts",
|
|
28
36
|
"import": "./dist/ai.js",
|
|
@@ -30,7 +38,10 @@
|
|
|
30
38
|
}
|
|
31
39
|
},
|
|
32
40
|
"scripts": {
|
|
33
|
-
"build": "tsx ./scripts/build.ts"
|
|
41
|
+
"build": "tsx ./scripts/build.ts",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:e2e": "npx playwright test --config e2e/playwright.config.ts",
|
|
44
|
+
"test:watch": "vitest"
|
|
34
45
|
},
|
|
35
46
|
"keywords": [
|
|
36
47
|
"cloudflare",
|
package/scripts/build.ts
CHANGED
|
@@ -5,7 +5,7 @@ async function main() {
|
|
|
5
5
|
await build({
|
|
6
6
|
clean: true,
|
|
7
7
|
dts: true,
|
|
8
|
-
entry: ["src/ai.ts"],
|
|
8
|
+
entry: ["src/index.ts", "src/ai.ts"],
|
|
9
9
|
skipNodeModulesBundle: true,
|
|
10
10
|
external: ["cloudflare:workers"],
|
|
11
11
|
format: "esm",
|
|
@@ -20,7 +20,6 @@ async function main() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
main().catch((err) => {
|
|
23
|
-
// Build failures should fail
|
|
24
23
|
console.error(err);
|
|
25
24
|
process.exit(1);
|
|
26
25
|
});
|
package/src/ai.ts
CHANGED
|
@@ -1,247 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { compile as compileJsonSchemaToTs } from "json-schema-to-typescript";
|
|
4
|
-
import {
|
|
5
|
-
zodToTs,
|
|
6
|
-
printNode as printNodeZodToTs,
|
|
7
|
-
createTypeAlias
|
|
8
|
-
} from "zod-to-ts";
|
|
9
|
-
import { getAgentByName } from "agents";
|
|
10
|
-
import { env, WorkerEntrypoint } from "cloudflare:workers";
|
|
11
|
-
import { openai } from "@ai-sdk/openai";
|
|
12
|
-
|
|
13
|
-
function toCamelCase(str: string) {
|
|
14
|
-
return str
|
|
15
|
-
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
16
|
-
.replace(/^[a-z]/, (letter) => letter.toUpperCase());
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class CodeModeProxy extends WorkerEntrypoint<
|
|
20
|
-
Cloudflare.Env,
|
|
21
|
-
{
|
|
22
|
-
binding: string;
|
|
23
|
-
name: string;
|
|
24
|
-
callback: string;
|
|
25
|
-
}
|
|
26
|
-
> {
|
|
27
|
-
async callFunction(options: { functionName: string; args: unknown[] }) {
|
|
28
|
-
const stub = (await getAgentByName(
|
|
29
|
-
// @ts-expect-error
|
|
30
|
-
env[this.ctx.props.binding] as DurableObjectNamespace<T>,
|
|
31
|
-
this.ctx.props.name
|
|
32
|
-
)) as DurableObjectStub;
|
|
33
|
-
// @ts-expect-error
|
|
34
|
-
return stub[this.ctx.props.callback](options.functionName, options.args);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function experimental_codemode(options: {
|
|
39
|
-
tools: ToolSet;
|
|
40
|
-
prompt: string;
|
|
41
|
-
globalOutbound: Fetcher;
|
|
42
|
-
loader: WorkerLoader;
|
|
43
|
-
proxy: Fetcher<CodeModeProxy>;
|
|
44
|
-
model?: LanguageModel;
|
|
45
|
-
}): Promise<{
|
|
46
|
-
prompt: string;
|
|
47
|
-
tools: ToolSet;
|
|
48
|
-
}> {
|
|
49
|
-
const generatedTypes = await generateTypes(options.tools);
|
|
50
|
-
const prompt = `${options.prompt}
|
|
51
|
-
You are a helpful assistant. You have access to the "codemode" tool that can do different things:
|
|
52
|
-
|
|
53
|
-
${getToolDescriptions(options.tools)}
|
|
54
|
-
|
|
55
|
-
If the user asks to do anything that be achieveable by the codemode tool, then simply pass over control to it by giving it a simple function description. Don't be too verbose.
|
|
56
|
-
|
|
57
|
-
`;
|
|
58
|
-
|
|
59
|
-
const codemodeTool = tool({
|
|
60
|
-
description: "codemode: a tool that can generate code to achieve a goal",
|
|
61
|
-
inputSchema: z.object({
|
|
62
|
-
functionDescription: z.string()
|
|
63
|
-
}),
|
|
64
|
-
outputSchema: z.object({
|
|
65
|
-
code: z.string(),
|
|
66
|
-
result: z.any()
|
|
67
|
-
}),
|
|
68
|
-
execute: async ({ functionDescription }) => {
|
|
69
|
-
try {
|
|
70
|
-
const response = await generateObject({
|
|
71
|
-
model: options.model ? options.model : openai("gpt-4.1"),
|
|
72
|
-
schema: z.object({
|
|
73
|
-
code: z.string()
|
|
74
|
-
}),
|
|
75
|
-
prompt: `You are a code generating machine.
|
|
76
|
-
|
|
77
|
-
In addition to regular javascript, you can also use the following functions:
|
|
78
|
-
|
|
79
|
-
${generatedTypes}
|
|
80
|
-
|
|
81
|
-
Respond only with the code, nothing else. Output javascript code.
|
|
82
|
-
|
|
83
|
-
Generate an async function that achieves the goal. This async function doesn't accept any arguments.
|
|
84
|
-
|
|
85
|
-
Here is user input: ${functionDescription}` // insert ts types for the tools here
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// console.log("args", response.object.args);
|
|
89
|
-
const evaluator = createEvaluator(response.object.code, {
|
|
90
|
-
proxy: options.proxy,
|
|
91
|
-
loader: options.loader
|
|
92
|
-
});
|
|
93
|
-
const result = await evaluator();
|
|
94
|
-
return { code: response.object.code, result: result };
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error("error", error);
|
|
97
|
-
throw error;
|
|
98
|
-
// return { code: "", result: error };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return { prompt, tools: { codemode: codemodeTool } };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function createEvaluator(
|
|
107
|
-
code: string,
|
|
108
|
-
options: {
|
|
109
|
-
loader: WorkerLoader;
|
|
110
|
-
proxy: Fetcher<CodeModeProxy>;
|
|
111
|
-
}
|
|
112
|
-
) {
|
|
113
|
-
return async () => {
|
|
114
|
-
const worker = options.loader.get(`code-${Math.random()}`, () => {
|
|
115
|
-
return {
|
|
116
|
-
compatibilityDate: "2025-06-01",
|
|
117
|
-
compatibilityFlags: ["nodejs_compat"],
|
|
118
|
-
mainModule: "foo.js",
|
|
119
|
-
modules: {
|
|
120
|
-
"foo.js": `
|
|
121
|
-
import { env, WorkerEntrypoint } from "cloudflare:workers";
|
|
122
|
-
|
|
123
|
-
export default class CodeModeWorker extends WorkerEntrypoint {
|
|
124
|
-
async evaluate() {
|
|
125
|
-
try {
|
|
126
|
-
const { CodeModeProxy } = env;
|
|
127
|
-
const codemode = new Proxy(
|
|
128
|
-
{},
|
|
129
|
-
{
|
|
130
|
-
get: (target, prop) => {
|
|
131
|
-
return (args) => {
|
|
132
|
-
return CodeModeProxy.callFunction({
|
|
133
|
-
functionName: prop,
|
|
134
|
-
args: args,
|
|
135
|
-
});
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
return await ${code}();
|
|
142
|
-
} catch (err) {
|
|
143
|
-
return {
|
|
144
|
-
err: err.message,
|
|
145
|
-
stack: err.stack
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
`
|
|
152
|
-
},
|
|
153
|
-
env: {
|
|
154
|
-
// insert keys and bindings to tools/ts functions here
|
|
155
|
-
CodeModeProxy: options.proxy
|
|
156
|
-
},
|
|
157
|
-
globalOutbound: null
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// @ts-expect-error TODO: fix this
|
|
162
|
-
return await worker.getEntrypoint().evaluate();
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function generateTypes(tools: ToolSet) {
|
|
167
|
-
let availableTools = "";
|
|
168
|
-
let availableTypes = "";
|
|
169
|
-
|
|
170
|
-
for (const [toolName, tool] of Object.entries(tools)) {
|
|
171
|
-
// @ts-expect-error TODO: fix this
|
|
172
|
-
const inputJsonType = tool.inputSchema.jsonSchema
|
|
173
|
-
? await compileJsonSchemaToTs(
|
|
174
|
-
// @ts-expect-error TODO: fix this
|
|
175
|
-
tool.inputSchema.jsonSchema,
|
|
176
|
-
`${toCamelCase(toolName)}Input`,
|
|
177
|
-
{
|
|
178
|
-
format: false,
|
|
179
|
-
bannerComment: " "
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
: printNodeZodToTs(
|
|
183
|
-
createTypeAlias(
|
|
184
|
-
zodToTs(
|
|
185
|
-
// @ts-expect-error TODO: fix this
|
|
186
|
-
tool.inputSchema,
|
|
187
|
-
`${toCamelCase(toolName)}Input`
|
|
188
|
-
).node,
|
|
189
|
-
`${toCamelCase(toolName)}Input`
|
|
190
|
-
)
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const outputJsonType =
|
|
194
|
-
// @ts-expect-error TODO: fix this
|
|
195
|
-
tool.outputSchema?.jsonSchema
|
|
196
|
-
? await compileJsonSchemaToTs(
|
|
197
|
-
// @ts-expect-error TODO: fix this
|
|
198
|
-
tool.outputSchema?.jsonSchema,
|
|
199
|
-
`${toCamelCase(toolName)}Output`,
|
|
200
|
-
{
|
|
201
|
-
format: false,
|
|
202
|
-
bannerComment: " "
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
: tool.outputSchema
|
|
206
|
-
? printNodeZodToTs(
|
|
207
|
-
createTypeAlias(
|
|
208
|
-
zodToTs(
|
|
209
|
-
// @ts-expect-error TODO: fix this
|
|
210
|
-
tool.outputSchema,
|
|
211
|
-
`${toCamelCase(toolName)}Output`
|
|
212
|
-
).node,
|
|
213
|
-
`${toCamelCase(toolName)}Output`
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
: `interface ${toCamelCase(toolName)}Output { [key: string]: any }`;
|
|
217
|
-
|
|
218
|
-
const InputType = inputJsonType
|
|
219
|
-
.trim()
|
|
220
|
-
.replace("export interface", "interface");
|
|
221
|
-
|
|
222
|
-
const OutputType = outputJsonType
|
|
223
|
-
.trim()
|
|
224
|
-
.replace("export interface", "interface");
|
|
225
|
-
|
|
226
|
-
availableTypes += `\n${InputType}`;
|
|
227
|
-
availableTypes += `\n${OutputType}`;
|
|
228
|
-
availableTools += `\n\t/*\n\t${tool.description?.trim()}\n\t*/`;
|
|
229
|
-
availableTools += `\n\t${toolName}: (input: ${toCamelCase(toolName)}Input) => Promise<${toCamelCase(toolName)}Output>;`;
|
|
230
|
-
availableTools += "\n";
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
availableTools = `\ndeclare const codemode: {${availableTools}}`;
|
|
234
|
-
|
|
235
|
-
return `
|
|
236
|
-
${availableTypes}
|
|
237
|
-
${availableTools}
|
|
238
|
-
`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getToolDescriptions(tools: ToolSet) {
|
|
242
|
-
return Object.entries(tools)
|
|
243
|
-
.map(([_toolName, tool]) => {
|
|
244
|
-
return `\n- ${tool.description?.trim()}`;
|
|
245
|
-
})
|
|
246
|
-
.join("");
|
|
247
|
-
}
|
|
1
|
+
export { createCodeTool, type CreateCodeToolOptions } from "./tool";
|
package/src/executor.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor interface and DynamicWorkerExecutor implementation.
|
|
3
|
+
*
|
|
4
|
+
* The Executor interface is the core abstraction — implement it to run
|
|
5
|
+
* LLM-generated code in any sandbox (Workers, QuickJS, Node VM, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RpcTarget } from "cloudflare:workers";
|
|
9
|
+
|
|
10
|
+
export interface ExecuteResult {
|
|
11
|
+
result: unknown;
|
|
12
|
+
error?: string;
|
|
13
|
+
logs?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An executor runs LLM-generated code in a sandbox, making the provided
|
|
18
|
+
* tool functions callable as `codemode.*` inside the sandbox.
|
|
19
|
+
*
|
|
20
|
+
* Implementations should never throw — errors are returned in `ExecuteResult.error`.
|
|
21
|
+
*/
|
|
22
|
+
export interface Executor {
|
|
23
|
+
execute(
|
|
24
|
+
code: string,
|
|
25
|
+
fns: Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
26
|
+
): Promise<ExecuteResult>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -- ToolDispatcher (RPC target for tool calls from sandboxed Workers) --
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An RpcTarget that dispatches tool calls from the sandboxed Worker
|
|
33
|
+
* back to the host. Passed via Workers RPC to the dynamic Worker's
|
|
34
|
+
* evaluate() method — no globalOutbound or Fetcher bindings needed.
|
|
35
|
+
*/
|
|
36
|
+
export class ToolDispatcher extends RpcTarget {
|
|
37
|
+
#fns: Record<string, (...args: unknown[]) => Promise<unknown>>;
|
|
38
|
+
|
|
39
|
+
constructor(fns: Record<string, (...args: unknown[]) => Promise<unknown>>) {
|
|
40
|
+
super();
|
|
41
|
+
this.#fns = fns;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async call(name: string, argsJson: string): Promise<string> {
|
|
45
|
+
const fn = this.#fns[name];
|
|
46
|
+
if (!fn) {
|
|
47
|
+
return JSON.stringify({ error: `Tool "${name}" not found` });
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const args = argsJson ? JSON.parse(argsJson) : {};
|
|
51
|
+
const result = await fn(args);
|
|
52
|
+
return JSON.stringify({ result });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
error: err instanceof Error ? err.message : String(err)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// -- DynamicWorkerExecutor (Cloudflare Workers) --
|
|
62
|
+
|
|
63
|
+
export interface DynamicWorkerExecutorOptions {
|
|
64
|
+
loader: WorkerLoader;
|
|
65
|
+
/**
|
|
66
|
+
* Timeout in milliseconds for code execution. Defaults to 30000 (30s).
|
|
67
|
+
*/
|
|
68
|
+
timeout?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Controls outbound network access from sandboxed code.
|
|
71
|
+
* - `null` (default): fetch() and connect() throw — sandbox is fully isolated.
|
|
72
|
+
* - `undefined`: inherits parent Worker's network access (full internet).
|
|
73
|
+
* - A `Fetcher`: all outbound requests route through this handler.
|
|
74
|
+
*/
|
|
75
|
+
globalOutbound?: Fetcher | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Executes code in an isolated Cloudflare Worker via WorkerLoader.
|
|
80
|
+
* Tool calls are dispatched via Workers RPC — the host passes a
|
|
81
|
+
* ToolDispatcher (RpcTarget) to the Worker's evaluate() method.
|
|
82
|
+
*
|
|
83
|
+
* External fetch() and connect() are blocked by default via
|
|
84
|
+
* `globalOutbound: null` (runtime-enforced). Pass a Fetcher to
|
|
85
|
+
* `globalOutbound` to allow controlled outbound access.
|
|
86
|
+
*/
|
|
87
|
+
export class DynamicWorkerExecutor implements Executor {
|
|
88
|
+
#loader: WorkerLoader;
|
|
89
|
+
#timeout: number;
|
|
90
|
+
#globalOutbound: Fetcher | null;
|
|
91
|
+
|
|
92
|
+
constructor(options: DynamicWorkerExecutorOptions) {
|
|
93
|
+
this.#loader = options.loader;
|
|
94
|
+
this.#timeout = options.timeout ?? 30000;
|
|
95
|
+
this.#globalOutbound = options.globalOutbound ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async execute(
|
|
99
|
+
code: string,
|
|
100
|
+
fns: Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
101
|
+
): Promise<ExecuteResult> {
|
|
102
|
+
const timeoutMs = this.#timeout;
|
|
103
|
+
|
|
104
|
+
const modulePrefix = [
|
|
105
|
+
'import { WorkerEntrypoint } from "cloudflare:workers";',
|
|
106
|
+
"",
|
|
107
|
+
"export default class CodeExecutor extends WorkerEntrypoint {",
|
|
108
|
+
" async evaluate(dispatcher) {",
|
|
109
|
+
" const __logs = [];",
|
|
110
|
+
' console.log = (...a) => { __logs.push(a.map(String).join(" ")); };',
|
|
111
|
+
' console.warn = (...a) => { __logs.push("[warn] " + a.map(String).join(" ")); };',
|
|
112
|
+
' console.error = (...a) => { __logs.push("[error] " + a.map(String).join(" ")); };',
|
|
113
|
+
" const codemode = new Proxy({}, {",
|
|
114
|
+
" get: (_, toolName) => async (args) => {",
|
|
115
|
+
" const resJson = await dispatcher.call(String(toolName), JSON.stringify(args ?? {}));",
|
|
116
|
+
" const data = JSON.parse(resJson);",
|
|
117
|
+
" if (data.error) throw new Error(data.error);",
|
|
118
|
+
" return data.result;",
|
|
119
|
+
" }",
|
|
120
|
+
" });",
|
|
121
|
+
"",
|
|
122
|
+
" try {",
|
|
123
|
+
" const result = await Promise.race([",
|
|
124
|
+
" ("
|
|
125
|
+
].join("\n");
|
|
126
|
+
|
|
127
|
+
const moduleSuffix = [
|
|
128
|
+
")(),",
|
|
129
|
+
' new Promise((_, reject) => setTimeout(() => reject(new Error("Execution timed out")), ' +
|
|
130
|
+
timeoutMs +
|
|
131
|
+
"))",
|
|
132
|
+
" ]);",
|
|
133
|
+
" return { result, logs: __logs };",
|
|
134
|
+
" } catch (err) {",
|
|
135
|
+
" return { result: undefined, error: err.message, logs: __logs };",
|
|
136
|
+
" }",
|
|
137
|
+
" }",
|
|
138
|
+
"}"
|
|
139
|
+
].join("\n");
|
|
140
|
+
|
|
141
|
+
const executorModule = modulePrefix + code + moduleSuffix;
|
|
142
|
+
|
|
143
|
+
const dispatcher = new ToolDispatcher(fns);
|
|
144
|
+
|
|
145
|
+
const worker = this.#loader.get(`codemode-${crypto.randomUUID()}`, () => ({
|
|
146
|
+
compatibilityDate: "2025-06-01",
|
|
147
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
148
|
+
mainModule: "executor.js",
|
|
149
|
+
modules: {
|
|
150
|
+
"executor.js": executorModule
|
|
151
|
+
},
|
|
152
|
+
globalOutbound: this.#globalOutbound
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
const entrypoint = worker.getEntrypoint() as unknown as {
|
|
156
|
+
evaluate(dispatcher: ToolDispatcher): Promise<{
|
|
157
|
+
result: unknown;
|
|
158
|
+
error?: string;
|
|
159
|
+
logs?: string[];
|
|
160
|
+
}>;
|
|
161
|
+
};
|
|
162
|
+
const response = await entrypoint.evaluate(dispatcher);
|
|
163
|
+
|
|
164
|
+
if (response.error) {
|
|
165
|
+
return { result: undefined, error: response.error, logs: response.logs };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { result: response.result, logs: response.logs };
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DynamicWorkerExecutor,
|
|
3
|
+
ToolDispatcher,
|
|
4
|
+
type DynamicWorkerExecutorOptions,
|
|
5
|
+
type Executor,
|
|
6
|
+
type ExecuteResult
|
|
7
|
+
} from "./executor";
|
|
8
|
+
export {
|
|
9
|
+
generateTypes,
|
|
10
|
+
sanitizeToolName,
|
|
11
|
+
type ToolDescriptor,
|
|
12
|
+
type ToolDescriptors
|
|
13
|
+
} from "./types";
|