History

Replace repository contents with v4 snapshot

Rhys Sullivan committed 7 months ago

Commit 4fcdba41 parent be4bbf5865 files · +36247 −404676Browse files

Showing the changes to packages/kernel/runtime-quickjs/src (2 of 865 files). Show all

Unified · Split

2 files
  1. packages/kernel/runtime-quickjs/src/index.test.ts +127 −255
  2. packages/kernel/runtime-quickjs/src/index.ts +24 −8
packages/kernel/runtime-quickjs/src/index.test.ts+127−255
@@ -1,314 +1,186 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4
5import {
6 allowAllToolInteractions,
7 makeToolInvokerFromTools,
8 toExecutorTool,
9} from "@executor/codemode-core";
10 4
5import type { SandboxToolInvoker } from "@executor/codemode-core";
11import { makeQuickJsExecutor } from "./index";6import { makeQuickJsExecutor } from "./index";
12 7
13const numberPairInputSchema = Schema.standardSchemaV1(8class UnknownToolError extends Data.TaggedError("UnknownToolError")<{
14 Schema.Struct({9 readonly path: string;
15 a: Schema.Number,10}> {}
16 b: Schema.Number,11
17 }),12const makeTestInvoker = (
18);13 handlers: Record<string, (args: unknown) => unknown>,
19 14): SandboxToolInvoker => ({
20const messageInputSchema = Schema.standardSchemaV1(15 invoke: ({ path, args }) => {
21 Schema.Struct({16 const handler = handlers[path];
22 message: Schema.String,17 if (!handler) {
23 }),18 return Effect.fail(new UnknownToolError({ path }));
24);19 }
25 20 return Effect.try(() => handler(args));
26const discoverInputSchema = Schema.standardSchemaV1(
27 Schema.Struct({
28 query: Schema.String,
29 limit: Schema.optional(Schema.Number),
30 }),
31);
32
33const describeToolInputSchema = Schema.standardSchemaV1(
34 Schema.Struct({
35 path: Schema.String,
36 includeSchemas: Schema.optional(Schema.Boolean),
37 }),
38);
39
40const repoInputSchema = Schema.standardSchemaV1(
41 Schema.Struct({
42 owner: Schema.String,
43 repo: Schema.String,
44 }),
45);
46
47const tools = {
48 "math.add": {
49 description: "Add two numbers",
50 inputSchema: numberPairInputSchema,
51 execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
52 },21 },
53 "notifications.send": toExecutorTool({22});
54 tool: {
55 description: "Send a message",
56 inputSchema: messageInputSchema,
57 execute: ({ message }: { message: string }) => ({
58 delivered: true,
59 message,
60 }),
61 },
62 metadata: {
63 interaction: "required",
64 },
65 }),
66};
67
68describe("runtime-quickjs", () => {
69 it.effect("executes simple code and returns result", () =>
70 Effect.gen(function* () {
71 const executor = makeQuickJsExecutor();
72 const toolInvoker = makeToolInvokerFromTools({ tools });
73
74 const output = yield* executor.execute(
75 "return 1 + 2;",
76 toolInvoker,
77 );
78
79 expect(output.result).toBe(3);
80 expect(output.error).toBeUndefined();
81 }),
82 );
83
84 it.effect("executes code with tool calls", () =>
85 Effect.gen(function* () {
86 const executor = makeQuickJsExecutor();
87 const toolInvoker = makeToolInvokerFromTools({
88 tools,
89 onToolInteraction: allowAllToolInteractions,
90 });
91
92 const output = yield* executor.execute(
93 [
94 "const math = await tools.math.add({ a: 19, b: 23 });",
95 "await tools.notifications.send({ message: `sum is ${math.sum}` });",
96 "return math;",
97 ].join("\n"),
98 toolInvoker,
99 );
100
101 expect(output.result).toEqual({ sum: 42 });
102 expect(output.error).toBeUndefined();
103 }),
104 );
105
106 it.effect("captures console output in logs", () =>
107 Effect.gen(function* () {
108 const executor = makeQuickJsExecutor();
109 const toolInvoker = makeToolInvokerFromTools({ tools });
110
111 const output = yield* executor.execute(
112 [
113 'console.log("hello from quickjs");',
114 'console.warn("a warning");',
115 'console.error("an error");',
116 "return 42;",
117 ].join("\n"),
118 toolInvoker,
119 );
120 23
121 expect(output.result).toBe(42);24const executor = makeQuickJsExecutor({ timeoutMs: 5_000 });
122 expect(output.logs).toContain("[log] hello from quickjs");
123 expect(output.logs).toContain("[warn] a warning");
124 expect(output.logs).toContain("[error] an error");
125 }),
126 );
127 25
128 it.effect("reports execution errors without crashing", () =>26describe("quickjs executor", () => {
27 it.effect("runs plain code", () =>
129 Effect.gen(function* () {28 Effect.gen(function* () {
130 const executor = makeQuickJsExecutor();29 const result = yield* executor.execute(
131 const toolInvoker = makeToolInvokerFromTools({ tools });30 `return 1 + 2`,
132 31 makeTestInvoker({}),
133 const output = yield* executor.execute(
134 'throw new Error("boom");',
135 toolInvoker,
136 );32 );
137 33 expect(result.result).toBe(3);
138 expect(output.result).toBeNull();34 expect(result.error).toBeUndefined();
139 expect(output.error).toContain("boom");
140 }),35 }),
141 );36 );
142 37
143 it.effect("handles tool call errors gracefully", () =>38 it.effect("invokes a tool and returns its result", () =>
144 Effect.gen(function* () {39 Effect.gen(function* () {
145 const failingTools = {40 const invoker = makeTestInvoker({
146 "broken.thing": {41 "math.add": (args) => {
147 description: "Always fails",42 const { a, b } = args as { a: number; b: number };
148 inputSchema: Schema.standardSchemaV1(Schema.Struct({})),43 return { sum: a + b };
149 execute: () => {
150 throw new Error("tool is broken");
151 },
152 },44 },
153 };
154
155 const executor = makeQuickJsExecutor();
156 const toolInvoker = makeToolInvokerFromTools({
157 tools: failingTools,
158 });45 });
159 46
160 const output = yield* executor.execute(47 const result = yield* executor.execute(
161 "return await tools.broken.thing({});",48 `
162 toolInvoker,49 const res = await tools.math.add({ a: 5, b: 3 });
50 return res.sum;
51 `,
52 invoker,
163 );53 );
164 54
165 expect(output.result).toBeNull();55 expect(result.error).toBeUndefined();
166 expect(output.error).toContain("tool is broken");56 expect(result.result).toBe(8);
167 }),57 }),
168 );58 );
169 59
170 it.effect("respects timeout", () =>60 it.effect("invokes multiple tools in sequence", () =>
171 Effect.gen(function* () {61 Effect.gen(function* () {
172 const executor = makeQuickJsExecutor({62 const invoker = makeTestInvoker({
173 timeoutMs: 250,63 "users.get": (args) => {
64 const { id } = args as { id: number };
65 return { id, name: `User ${id}` };
66 },
67 "users.greet": (args) => {
68 const { name } = args as { name: string };
69 return { message: `Hello, ${name}!` };
70 },
174 });71 });
175 const toolInvoker = makeToolInvokerFromTools({ tools });
176 72
177 const output = yield* executor.execute(73 const result = yield* executor.execute(
178 "while (true) {}",74 `
179 toolInvoker,75 const user = await tools.users.get({ id: 42 });
76 const greeting = await tools.users.greet({ name: user.name });
77 return greeting.message;
78 `,
79 invoker,
180 );80 );
181 81
182 expect(output.result).toBeNull();82 expect(result.error).toBeUndefined();
183 expect(output.error).toContain("timed out");83 expect(result.result).toBe("Hello, User 42!");
184 }),84 }),
185 );85 );
186 86
187 it.effect("network access is denied by default", () =>87 it.effect("handles tool errors", () =>
188 Effect.gen(function* () {88 Effect.gen(function* () {
189 const executor = makeQuickJsExecutor();89 const invoker = makeTestInvoker({
190 const toolInvoker = makeToolInvokerFromTools({ tools });90 "db.query": () => {
91 throw new Error("connection refused");
92 },
93 });
191 94
192 const output = yield* executor.execute(95 const result = yield* executor.execute(
193 'await fetch("https://example.com"); return 1;',96 `
194 toolInvoker,97 try {
98 await tools.db.query({ sql: "SELECT 1" });
99 return "should not reach";
100 } catch (e) {
101 return "caught: " + e.message;
102 }
103 `,
104 invoker,
195 );105 );
196 106
197 expect(output.result).toBeNull();107 expect(result.error).toBeUndefined();
198 expect(output.error).toContain("fetch is disabled in QuickJS executor");108 expect(result.result).toContain("caught:");
199 }),109 }),
200 );110 );
201 111
202 it.effect("multiple sequential tool calls work correctly", () =>112 it.effect("handles unknown tool path", () =>
203 Effect.gen(function* () {113 Effect.gen(function* () {
204 const executor = makeQuickJsExecutor();114 const invoker = makeTestInvoker({});
205 const toolInvoker = makeToolInvokerFromTools({ tools });115
206 116 const result = yield* executor.execute(
207 const output = yield* executor.execute(117 `
208 [118 try {
209 "const r1 = await tools.math.add({ a: 1, b: 2 });",119 await tools.nonexistent.thing({ x: 1 });
210 "const r2 = await tools.math.add({ a: r1.sum, b: 10 });",120 return "should not reach";
211 "const r3 = await tools.math.add({ a: r2.sum, b: 100 });",121 } catch (e) {
212 "return r3;",122 return "caught: " + e.message;
213 ].join("\n"),123 }
214 toolInvoker,124 `,
125 invoker,
215 );126 );
216 127
217 expect(output.result).toEqual({ sum: 113 });128 expect(result.error).toBeUndefined();
218 expect(output.error).toBeUndefined();129 expect(result.result).toContain("caught:");
219 }),130 }),
220 );131 );
221 132
222 it.effect("does not expose internal executor bridge globals to user code", () =>133 it.effect("captures console.log output", () =>
223 Effect.gen(function* () {134 Effect.gen(function* () {
224 const executor = makeQuickJsExecutor();135 const result = yield* executor.execute(
225 const toolInvoker = makeToolInvokerFromTools({ tools });136 `
226 137 console.log("hello from sandbox");
227 const output = yield* executor.execute(138 console.warn("a warning");
228 [139 return "done";
229 "return {",140 `,
230 " hasInvokeTool: typeof globalThis.__executor_invokeTool !== 'undefined',",141 makeTestInvoker({}),
231 " hasLogBridge: typeof globalThis.__executor_log !== 'undefined',",
232 " globalKeys: Object.keys(globalThis).sort(),",
233 "};",
234 ].join("\n"),
235 toolInvoker,
236 );142 );
237 143
238 expect(output.result).toEqual({144 expect(result.error).toBeUndefined();
239 hasInvokeTool: false,145 expect(result.result).toBe("done");
240 hasLogBridge: false,146 expect(result.logs).toContainEqual("[log] hello from sandbox");
241 globalKeys: [],147 expect(result.logs).toContainEqual("[warn] a warning");
242 });
243 expect(output.error).toBeUndefined();
244 }),148 }),
245 );149 );
246 150
247 it.effect("supports the documented discovery workflow shape", () =>151 it.effect("passes tool result into next tool call", () =>
248 Effect.gen(function* () {152 Effect.gen(function* () {
249 const executor = makeQuickJsExecutor();153 const invoker = makeTestInvoker({
250 const toolInvoker = makeToolInvokerFromTools({154 "stripe.customers.list": () => ({
251 tools: {155 data: [
252 discover: {156 { id: "cus_1", email: "alice@example.com" },
253 inputSchema: discoverInputSchema,157 { id: "cus_2", email: "bob@example.com" },
254 execute: () => ({158 ],
255 bestPath: "github.issues.list",159 }),
256 results: [160 "stripe.invoices.create": (args) => {
257 {161 const { customer, amount } = args as { customer: string; amount: number };
258 path: "github.issues.list",162 return { id: "inv_1", customer, amount };
259 score: 0.99,
260 },
261 ],
262 total: 1,
263 }),
264 },
265 "describe.tool": {
266 inputSchema: describeToolInputSchema,
267 execute: ({ path }: { path: string; includeSchemas?: boolean }) => ({
268 path,
269 contract: {
270 inputTypePreview: "{ owner: string; repo: string }",
271 },
272 }),
273 },
274 "github.issues.list": {
275 inputSchema: repoInputSchema,
276 execute: ({ owner, repo }: { owner: string; repo: string }) => ({
277 owner,
278 repo,
279 issues: [{ id: "issue_1" }],
280 }),
281 },
282 },163 },
283 });164 });
284 165
285 const output = yield* executor.execute(166 const result = yield* executor.execute(
286 [167 `
287 'const { results, bestPath } = await tools.discover({ query: "github issues", limit: 5 });',168 const customers = await tools.stripe.customers.list();
288 "const path = bestPath ?? results[0]?.path;",169 const invoice = await tools.stripe.invoices.create({
289 'if (!path) throw new Error("No matching tools found.");',170 customer: customers.data[0].id,
290 "const detail = await tools.describe.tool({ path, includeSchemas: true });",171 amount: 5000,
291 'const issues = await tools.github.issues.list({ owner: "openai", repo: "codex" });',172 });
292 "return { path, detail, issues };",173 return invoice;
293 ].join("\n"),174 `,
294 toolInvoker,175 invoker,
295 );176 );
296 177
297 expect(output.result).toEqual({178 expect(result.error).toBeUndefined();
298 path: "github.issues.list",179 expect(result.result).toEqual({
299 detail: {180 id: "inv_1",
300 path: "github.issues.list",181 customer: "cus_1",
301 contract: {182 amount: 5000,
302 inputTypePreview: "{ owner: string; repo: string }",
303 },
304 },
305 issues: {
306 owner: "openai",
307 repo: "codex",
308 issues: [{ id: "issue_1" }],
309 },
310 });183 });
311 expect(output.error).toBeUndefined();
312 }),184 }),
313 );185 );
314});186});
packages/kernel/runtime-quickjs/src/index.ts+24−8
@@ -1,8 +1,9 @@
1import type {1import type {
2 CodeExecutor,2 CodeExecutor,
3 ExecuteResult,3 ExecuteResult,
4 ToolInvoker,4 SandboxToolInvoker,
5} from "@executor/codemode-core";5} from "@executor/codemode-core";
6import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
7import {8import {
8 getQuickJS,9 getQuickJS,
@@ -11,6 +12,7 @@ import { · 2 unmodified lines
11 type QuickJSDeferredPromise,12 type QuickJSDeferredPromise,
12 type QuickJSHandle,13 type QuickJSHandle,
13 type QuickJSRuntime,14 type QuickJSRuntime,
15 type QuickJSWASMModule,
14} from "quickjs-emscripten";16} from "quickjs-emscripten";
15 17
16export type QuickJsExecutorOptions = {18export type QuickJsExecutorOptions = {
@@ -19,6 +21,20 @@ export type QuickJsExecutorOptions = { · 2 unmodified lines
19 maxStackSizeBytes?: number;21 maxStackSizeBytes?: number;
20};22};
21 23
24// Allow pre-loading a QuickJS module (e.g. with custom WASM bytes for compiled binaries)
25let preloadedModule: QuickJSWASMModule | null = null;
26
27export const setQuickJSModule = (mod: QuickJSWASMModule) => {
28 preloadedModule = mod;
29};
30
31const resolveQuickJS = (): Promise<QuickJSWASMModule> =>
32 preloadedModule ? Promise.resolve(preloadedModule) : getQuickJS();
33
34class QuickJsExecutionError extends Data.TaggedError("QuickJsExecutionError")<{
35 readonly message: string;
36}> {}
37
22const DEFAULT_TIMEOUT_MS = 5 * 60_000;38const DEFAULT_TIMEOUT_MS = 5 * 60_000;
23const EXECUTION_FILENAME = "executor-quickjs-runtime.js";39const EXECUTION_FILENAME = "executor-quickjs-runtime.js";
24 40
@@ -178,7 +194,7 @@ const createLogBridge = ( · 153 unmodified lines
178 194
179const createToolBridge = (195const createToolBridge = (
180 context: QuickJSContext,196 context: QuickJSContext,
181 toolInvoker: ToolInvoker,197 toolInvoker: SandboxToolInvoker,
182 pendingDeferreds: Set<QuickJSDeferredPromise>,198 pendingDeferreds: Set<QuickJSDeferredPromise>,
183): QuickJSHandle =>199): QuickJSHandle =>
184 context.newFunction("__executor_invokeTool", (pathHandle, argsHandle) => {200 context.newFunction("__executor_invokeTool", (pathHandle, argsHandle) => {
@@ -288,13 +304,13 @@ const drainAsync = async ( · 103 unmodified lines
288const evaluateInQuickJs = async (304const evaluateInQuickJs = async (
289 options: QuickJsExecutorOptions,305 options: QuickJsExecutorOptions,
290 code: string,306 code: string,
291 toolInvoker: ToolInvoker,307 toolInvoker: SandboxToolInvoker,
292): Promise<ExecuteResult> => {308): Promise<ExecuteResult> => {
293 const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);309 const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
294 const deadlineMs = Date.now() + timeoutMs;310 const deadlineMs = Date.now() + timeoutMs;
295 const logs: string[] = [];311 const logs: string[] = [];
296 const pendingDeferreds = new Set<QuickJSDeferredPromise>();312 const pendingDeferreds = new Set<QuickJSDeferredPromise>();
297 const QuickJS = await getQuickJS();313 const QuickJS = await resolveQuickJS();
298 const runtime = QuickJS.newRuntime();314 const runtime = QuickJS.newRuntime();
299 315
300 try {316 try {
@@ -399,16 +415,16 @@ const evaluateInQuickJs = async ( · 98 unmodified lines
399const runInQuickJs = (415const runInQuickJs = (
400 options: QuickJsExecutorOptions,416 options: QuickJsExecutorOptions,
401 code: string,417 code: string,
402 toolInvoker: ToolInvoker,418 toolInvoker: SandboxToolInvoker,
403): Effect.Effect<ExecuteResult, Error> =>419): Effect.Effect<ExecuteResult, QuickJsExecutionError> =>
404 Effect.tryPromise({420 Effect.tryPromise({
405 try: () => evaluateInQuickJs(options, code, toolInvoker),421 try: () => evaluateInQuickJs(options, code, toolInvoker),
406 catch: toError,422 catch: (cause) => new QuickJsExecutionError({ message: String(cause) }),
407 });423 });
408 424
409export const makeQuickJsExecutor = (425export const makeQuickJsExecutor = (
410 options: QuickJsExecutorOptions = {},426 options: QuickJsExecutorOptions = {},
411): CodeExecutor => ({427): CodeExecutor => ({
412 execute: (code: string, toolInvoker: ToolInvoker) =>428 execute: (code: string, toolInvoker: SandboxToolInvoker) =>
413 runInQuickJs(options, code, toolInvoker),429 runInQuickJs(options, code, toolInvoker),
414});430});