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 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import * as Data from "effect/Data";
23 import * as Effect from "effect/Effect";
3−import * as Schema from "effect/Schema";
4−
5−import {
6− allowAllToolInteractions,
7− makeToolInvokerFromTools,
8− toExecutorTool,
9−} from "@executor/codemode-core";
104
5+import type { SandboxToolInvoker } from "@executor/codemode-core";
116 import { makeQuickJsExecutor } from "./index";
127
13−const numberPairInputSchema = Schema.standardSchemaV1(
14− Schema.Struct({
15− a: Schema.Number,
16− b: Schema.Number,
17− }),
18−);
19−
20−const messageInputSchema = Schema.standardSchemaV1(
21− Schema.Struct({
22− message: Schema.String,
23− }),
24−);
25−
26−const discoverInputSchema = Schema.standardSchemaV1(
27− Schema.Struct({
28− query: Schema.String,
29− limit: Schema.optional(Schema.Number),
30− }),
31−);
32−
33−const describeToolInputSchema = Schema.standardSchemaV1(
34− Schema.Struct({
35− path: Schema.String,
36− includeSchemas: Schema.optional(Schema.Boolean),
37− }),
38−);
39−
40−const repoInputSchema = Schema.standardSchemaV1(
41− Schema.Struct({
42− owner: Schema.String,
43− repo: Schema.String,
44− }),
45−);
46−
47−const tools = {
48− "math.add": {
49− description: "Add two numbers",
50− inputSchema: numberPairInputSchema,
51− execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
8+class UnknownToolError extends Data.TaggedError("UnknownToolError")<{
9+ readonly path: string;
10+}> {}
11+
12+const makeTestInvoker = (
13+ handlers: Record<string, (args: unknown) => unknown>,
14+): SandboxToolInvoker => ({
15+ invoke: ({ path, args }) => {
16+ const handler = handlers[path];
17+ if (!handler) {
18+ return Effect.fail(new UnknownToolError({ path }));
19+ }
20+ return Effect.try(() => handler(args));
5221 },
53− "notifications.send": toExecutorTool({
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−
68−describe("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− );
22+});
12023
121− expect(output.result).toBe(42);
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− );
24+const executor = makeQuickJsExecutor({ timeoutMs: 5_000 });
12725
128− it.effect("reports execution errors without crashing", () =>
26+describe("quickjs executor", () => {
27+ it.effect("runs plain code", () =>
12928 Effect.gen(function* () {
130− const executor = makeQuickJsExecutor();
131− const toolInvoker = makeToolInvokerFromTools({ tools });
132−
133− const output = yield* executor.execute(
134− 'throw new Error("boom");',
135− toolInvoker,
29+ const result = yield* executor.execute(
30+ `return 1 + 2`,
31+ makeTestInvoker({}),
13632 );
137−
138− expect(output.result).toBeNull();
139− expect(output.error).toContain("boom");
33+ expect(result.result).toBe(3);
34+ expect(result.error).toBeUndefined();
14035 }),
14136 );
14237
143− it.effect("handles tool call errors gracefully", () =>
38+ it.effect("invokes a tool and returns its result", () =>
14439 Effect.gen(function* () {
145− const failingTools = {
146− "broken.thing": {
147− description: "Always fails",
148− inputSchema: Schema.standardSchemaV1(Schema.Struct({})),
149− execute: () => {
150− throw new Error("tool is broken");
151− },
40+ const invoker = makeTestInvoker({
41+ "math.add": (args) => {
42+ const { a, b } = args as { a: number; b: number };
43+ return { sum: a + b };
15244 },
153− };
154−
155− const executor = makeQuickJsExecutor();
156− const toolInvoker = makeToolInvokerFromTools({
157− tools: failingTools,
15845 });
15946
160− const output = yield* executor.execute(
161− "return await tools.broken.thing({});",
162− toolInvoker,
47+ const result = yield* executor.execute(
48+ `
49+ const res = await tools.math.add({ a: 5, b: 3 });
50+ return res.sum;
51+ `,
52+ invoker,
16353 );
16454
165− expect(output.result).toBeNull();
166− expect(output.error).toContain("tool is broken");
55+ expect(result.error).toBeUndefined();
56+ expect(result.result).toBe(8);
16757 }),
16858 );
16959
170− it.effect("respects timeout", () =>
60+ it.effect("invokes multiple tools in sequence", () =>
17161 Effect.gen(function* () {
172− const executor = makeQuickJsExecutor({
173− timeoutMs: 250,
62+ const invoker = makeTestInvoker({
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+ },
17471 });
175− const toolInvoker = makeToolInvokerFromTools({ tools });
17672
177− const output = yield* executor.execute(
178− "while (true) {}",
179− toolInvoker,
73+ const result = yield* executor.execute(
74+ `
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,
18080 );
18181
182− expect(output.result).toBeNull();
183− expect(output.error).toContain("timed out");
82+ expect(result.error).toBeUndefined();
83+ expect(result.result).toBe("Hello, User 42!");
18484 }),
18585 );
18686
187− it.effect("network access is denied by default", () =>
87+ it.effect("handles tool errors", () =>
18888 Effect.gen(function* () {
189− const executor = makeQuickJsExecutor();
190− const toolInvoker = makeToolInvokerFromTools({ tools });
89+ const invoker = makeTestInvoker({
90+ "db.query": () => {
91+ throw new Error("connection refused");
92+ },
93+ });
19194
192− const output = yield* executor.execute(
193− 'await fetch("https://example.com"); return 1;',
194− toolInvoker,
95+ const result = yield* executor.execute(
96+ `
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,
195105 );
196106
197− expect(output.result).toBeNull();
198− expect(output.error).toContain("fetch is disabled in QuickJS executor");
107+ expect(result.error).toBeUndefined();
108+ expect(result.result).toContain("caught:");
199109 }),
200110 );
201111
202− it.effect("multiple sequential tool calls work correctly", () =>
112+ it.effect("handles unknown tool path", () =>
203113 Effect.gen(function* () {
204− const executor = makeQuickJsExecutor();
205− const toolInvoker = makeToolInvokerFromTools({ tools });
206−
207− const output = yield* executor.execute(
208− [
209− "const r1 = await tools.math.add({ a: 1, b: 2 });",
210− "const r2 = await tools.math.add({ a: r1.sum, b: 10 });",
211− "const r3 = await tools.math.add({ a: r2.sum, b: 100 });",
212− "return r3;",
213− ].join("\n"),
214− toolInvoker,
114+ const invoker = makeTestInvoker({});
115+
116+ const result = yield* executor.execute(
117+ `
118+ try {
119+ await tools.nonexistent.thing({ x: 1 });
120+ return "should not reach";
121+ } catch (e) {
122+ return "caught: " + e.message;
123+ }
124+ `,
125+ invoker,
215126 );
216127
217− expect(output.result).toEqual({ sum: 113 });
218− expect(output.error).toBeUndefined();
128+ expect(result.error).toBeUndefined();
129+ expect(result.result).toContain("caught:");
219130 }),
220131 );
221132
222− it.effect("does not expose internal executor bridge globals to user code", () =>
133+ it.effect("captures console.log output", () =>
223134 Effect.gen(function* () {
224− const executor = makeQuickJsExecutor();
225− const toolInvoker = makeToolInvokerFromTools({ tools });
226−
227− const output = yield* executor.execute(
228− [
229− "return {",
230− " hasInvokeTool: typeof globalThis.__executor_invokeTool !== 'undefined',",
231− " hasLogBridge: typeof globalThis.__executor_log !== 'undefined',",
232− " globalKeys: Object.keys(globalThis).sort(),",
233− "};",
234− ].join("\n"),
235− toolInvoker,
135+ const result = yield* executor.execute(
136+ `
137+ console.log("hello from sandbox");
138+ console.warn("a warning");
139+ return "done";
140+ `,
141+ makeTestInvoker({}),
236142 );
237143
238− expect(output.result).toEqual({
239− hasInvokeTool: false,
240− hasLogBridge: false,
241− globalKeys: [],
242− });
243− expect(output.error).toBeUndefined();
144+ expect(result.error).toBeUndefined();
145+ expect(result.result).toBe("done");
146+ expect(result.logs).toContainEqual("[log] hello from sandbox");
147+ expect(result.logs).toContainEqual("[warn] a warning");
244148 }),
245149 );
246150
247− it.effect("supports the documented discovery workflow shape", () =>
151+ it.effect("passes tool result into next tool call", () =>
248152 Effect.gen(function* () {
249− const executor = makeQuickJsExecutor();
250− const toolInvoker = makeToolInvokerFromTools({
251− tools: {
252− discover: {
253− inputSchema: discoverInputSchema,
254− execute: () => ({
255− bestPath: "github.issues.list",
256− results: [
257− {
258− path: "github.issues.list",
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− },
153+ const invoker = makeTestInvoker({
154+ "stripe.customers.list": () => ({
155+ data: [
156+ { id: "cus_1", email: "alice@example.com" },
157+ { id: "cus_2", email: "bob@example.com" },
158+ ],
159+ }),
160+ "stripe.invoices.create": (args) => {
161+ const { customer, amount } = args as { customer: string; amount: number };
162+ return { id: "inv_1", customer, amount };
282163 },
283164 });
284165
285− const output = yield* executor.execute(
286− [
287− 'const { results, bestPath } = await tools.discover({ query: "github issues", limit: 5 });',
288− "const path = bestPath ?? results[0]?.path;",
289− 'if (!path) throw new Error("No matching tools found.");',
290− "const detail = await tools.describe.tool({ path, includeSchemas: true });",
291− 'const issues = await tools.github.issues.list({ owner: "openai", repo: "codex" });',
292− "return { path, detail, issues };",
293− ].join("\n"),
294− toolInvoker,
166+ const result = yield* executor.execute(
167+ `
168+ const customers = await tools.stripe.customers.list();
169+ const invoice = await tools.stripe.invoices.create({
170+ customer: customers.data[0].id,
171+ amount: 5000,
172+ });
173+ return invoice;
174+ `,
175+ invoker,
295176 );
296177
297− expect(output.result).toEqual({
298− path: "github.issues.list",
299− detail: {
300− path: "github.issues.list",
301− contract: {
302− inputTypePreview: "{ owner: string; repo: string }",
303− },
304− },
305− issues: {
306− owner: "openai",
307− repo: "codex",
308− issues: [{ id: "issue_1" }],
309− },
178+ expect(result.error).toBeUndefined();
179+ expect(result.result).toEqual({
180+ id: "inv_1",
181+ customer: "cus_1",
182+ amount: 5000,
310183 });
311− expect(output.error).toBeUndefined();
312184 }),
313185 );
314186 });
packages/kernel/runtime-quickjs/src/index.ts+24−8
@@ -1,8 +1,9 @@
11 import type {
22 CodeExecutor,
33 ExecuteResult,
4− ToolInvoker,
4+ SandboxToolInvoker,
55 } from "@executor/codemode-core";
6+import * as Data from "effect/Data";
67 import * as Effect from "effect/Effect";
78 import {
89 getQuickJS,
@@ -11,6 +12,7 @@ import { · 2 unmodified lines
1112 type QuickJSDeferredPromise,
1213 type QuickJSHandle,
1314 type QuickJSRuntime,
15+ type QuickJSWASMModule,
1416 } from "quickjs-emscripten";
1517
1618 export type QuickJsExecutorOptions = {
@@ -19,6 +21,20 @@ export type QuickJsExecutorOptions = { · 2 unmodified lines
1921 maxStackSizeBytes?: number;
2022 };
2123
24+// Allow pre-loading a QuickJS module (e.g. with custom WASM bytes for compiled binaries)
25+let preloadedModule: QuickJSWASMModule | null = null;
26+
27+export const setQuickJSModule = (mod: QuickJSWASMModule) => {
28+ preloadedModule = mod;
29+};
30+
31+const resolveQuickJS = (): Promise<QuickJSWASMModule> =>
32+ preloadedModule ? Promise.resolve(preloadedModule) : getQuickJS();
33+
34+class QuickJsExecutionError extends Data.TaggedError("QuickJsExecutionError")<{
35+ readonly message: string;
36+}> {}
37+
2238 const DEFAULT_TIMEOUT_MS = 5 * 60_000;
2339 const EXECUTION_FILENAME = "executor-quickjs-runtime.js";
2440
@@ -178,7 +194,7 @@ const createLogBridge = ( · 153 unmodified lines
178194
179195 const createToolBridge = (
180196 context: QuickJSContext,
181− toolInvoker: ToolInvoker,
197+ toolInvoker: SandboxToolInvoker,
182198 pendingDeferreds: Set<QuickJSDeferredPromise>,
183199 ): QuickJSHandle =>
184200 context.newFunction("__executor_invokeTool", (pathHandle, argsHandle) => {
@@ -288,13 +304,13 @@ const drainAsync = async ( · 103 unmodified lines
288304 const evaluateInQuickJs = async (
289305 options: QuickJsExecutorOptions,
290306 code: string,
291− toolInvoker: ToolInvoker,
307+ toolInvoker: SandboxToolInvoker,
292308 ): Promise<ExecuteResult> => {
293309 const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
294310 const deadlineMs = Date.now() + timeoutMs;
295311 const logs: string[] = [];
296312 const pendingDeferreds = new Set<QuickJSDeferredPromise>();
297− const QuickJS = await getQuickJS();
313+ const QuickJS = await resolveQuickJS();
298314 const runtime = QuickJS.newRuntime();
299315
300316 try {
@@ -399,16 +415,16 @@ const evaluateInQuickJs = async ( · 98 unmodified lines
399415 const runInQuickJs = (
400416 options: QuickJsExecutorOptions,
401417 code: string,
402− toolInvoker: ToolInvoker,
403−): Effect.Effect<ExecuteResult, Error> =>
418+ toolInvoker: SandboxToolInvoker,
419+): Effect.Effect<ExecuteResult, QuickJsExecutionError> =>
404420 Effect.tryPromise({
405421 try: () => evaluateInQuickJs(options, code, toolInvoker),
406− catch: toError,
422+ catch: (cause) => new QuickJsExecutionError({ message: String(cause) }),
407423 });
408424
409425 export const makeQuickJsExecutor = (
410426 options: QuickJsExecutorOptions = {},
411427 ): CodeExecutor => ({
412− execute: (code: string, toolInvoker: ToolInvoker) =>
428+ execute: (code: string, toolInvoker: SandboxToolInvoker) =>
413429 runInQuickJs(options, code, toolInvoker),
414430 });