Keep the Deno and SES runtimes available while moving default execution to a packaged QuickJS sandbox.
Unified · Split
| @@ -38,10 +38,10 @@ If `README.md` answers "what is this product and how do I use it?", this file an · 37 unmodified lines | |||
| 38 | +-- live execution manager | 38 | +-- live execution manager |
| 39 | v | 39 | v |
| 40 | +---------+---------+ | 40 | +---------+---------+ |
| 41 | | SES sandboxed | | 41 | | QuickJS sandboxed | |
| 42 | | executor runtime | | 42 | | executor runtime | |
| 43 | | runtime-ses | | 43 | | runtime-quickjs | |
| 44 | | subprocess worker | | 44 | | default executor | |
| 45 | +--------------------+ | 45 | +--------------------+ |
| 46 | ``` | 46 | ``` |
| 47 | 47 | ||
| @@ -103,7 +103,7 @@ It contains the runtime layer, persistence integration, and the business logic f · 55 unmodified lines | |||
| 103 | 103 | ||
| 104 | If you want to understand the behavior of the product, this is the most important package. | 104 | If you want to understand the behavior of the product, this is the most important package. |
| 105 | 105 | ||
| 106 | ### `packages/runtime-ses`: code execution runtime | 106 | ### `packages/runtime-quickjs`: default code execution runtime |
| 107 | 107 | ||
| 108 | This package provides the TypeScript execution environment used by the local product. | 108 | This package provides the TypeScript execution environment used by the local product. |
| 109 | 109 | ||
| @@ -115,7 +115,7 @@ At a high level it receives: · 5 unmodified lines | |||
| 115 | 115 | ||
| 116 | and runs user-authored code against that environment. | 116 | and runs user-authored code against that environment. |
| 117 | 117 | ||
| 118 | The current runtime executes code in a dedicated SES child process so tool calls stay proxied through the control plane. | 118 | The default runtime executes code inside a QuickJS WebAssembly sandbox so tool calls stay proxied through the control plane. |
| 119 | 119 | ||
| 120 | ### Adapter packages | 120 | ### Adapter packages |
| 121 | 121 | ||
| @@ -228,7 +228,7 @@ At a high level, every execution follows the same loop: · 227 unmodified lines | |||
| 228 | 228 | ||
| 229 | 1. `executor` resolves the current local installation and workspace. | 229 | 1. `executor` resolves the current local installation and workspace. |
| 230 | 2. It builds a tool catalog from built-in tools plus all connected workspace sources. | 230 | 2. It builds a tool catalog from built-in tools plus all connected workspace sources. |
| 231 | 3. It runs your TypeScript inside the SES sandbox runtime. | 231 | 3. It runs your TypeScript inside the QuickJS sandbox runtime by default. |
| 232 | 4. Tool calls are dispatched through `executor` rather than directly from your code. | 232 | 4. Tool calls are dispatched through `executor` rather than directly from your code. |
| 233 | 5. If a tool needs interaction, the run pauses and records a pending interaction. | 233 | 5. If a tool needs interaction, the run pauses and records a pending interaction. |
| 234 | 6. Once the interaction is resolved, the execution continues and eventually completes or fails. | 234 | 6. Once the interaction is resolved, the execution continues and eventually completes or fails. |
| @@ -319,7 +319,8 @@ If you are exploring the repo, these are the directories that matter most: · 84 unmodified lines | |||
| 319 | - `apps/web`: local React web UI | 319 | - `apps/web`: local React web UI |
| 320 | - `packages/server`: local HTTP server that serves API, MCP, and UI | 320 | - `packages/server`: local HTTP server that serves API, MCP, and UI |
| 321 | - `packages/control-plane`: source management, secrets, persistence, execution, and inspection | 321 | - `packages/control-plane`: source management, secrets, persistence, execution, and inspection |
| 322 | - `packages/runtime-ses`: SES sandbox runtime for TypeScript execution | 322 | - `packages/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution |
| 323 | - `packages/runtime-ses`: optional SES sandbox runtime for TypeScript execution | ||
| 323 | - `packages/executor-mcp`: MCP bridge for `execute` and `resume` | 324 | - `packages/executor-mcp`: MCP bridge for `execute` and `resume` |
| 324 | - `packages/codemode-*`: core tool abstractions plus MCP and OpenAPI adapters | 325 | - `packages/codemode-*`: core tool abstractions plus MCP and OpenAPI adapters |
| 325 | 326 | ||
| @@ -86,6 +86,21 @@ const resolvePGliteDistDir = (): string => { · 85 unmodified lines | |||
| 86 | return distDir; | 86 | return distDir; |
| 87 | }; | 87 | }; |
| 88 | 88 | ||
| 89 | const resolveQuickJsWasmPath = (): string => { | ||
| 90 | const requireFromQuickJsRuntime = createRequire( | ||
| 91 | join(repoRoot, "packages/runtime-quickjs/package.json"), | ||
| 92 | ); | ||
| 93 | const wasmPath = requireFromQuickJsRuntime.resolve( | ||
| 94 | "@jitl/quickjs-wasmfile-release-sync/wasm", | ||
| 95 | ); | ||
| 96 | |||
| 97 | if (!existsSync(wasmPath)) { | ||
| 98 | throw new Error(`Unable to locate QuickJS wasm asset at ${wasmPath}`); | ||
| 99 | } | ||
| 100 | |||
| 101 | return wasmPath; | ||
| 102 | }; | ||
| 103 | |||
| 89 | 104 | ||
| 90 | const createPackageJson = (input: { | 105 | const createPackageJson = (input: { |
| 91 | packageName: string; | 106 | packageName: string; |
| @@ -176,7 +191,7 @@ const createPostinstallSource = () => [ · 84 unmodified lines | |||
| 176 | "const denoExecutable = resolveDenoExecutable();", | 191 | "const denoExecutable = resolveDenoExecutable();", |
| 177 | "", | 192 | "", |
| 178 | "if (!isDenoAvailable(denoExecutable)) {", | 193 | "if (!isDenoAvailable(denoExecutable)) {", |
| 179 | ' console.warn(`Deno not found, if you want to use deno for sandboxing install it from ${denoInstallUrl} otherwise JS will execute in SES`);', | 194 | ' console.warn(`Deno not found, if you want to use deno for sandboxing install it from ${denoInstallUrl} otherwise JS will execute in QuickJS`);', |
| 180 | "}", | 195 | "}", |
| 181 | "", | 196 | "", |
| 182 | ].join("\n"); | 197 | ].join("\n"); |
| @@ -196,6 +211,7 @@ export const buildDistributionPackage = async ( · 13 unmodified lines | |||
| 196 | const pgliteDistDir = resolvePGliteDistDir(); | 211 | const pgliteDistDir = resolvePGliteDistDir(); |
| 197 | const pgliteDataPath = join(pgliteDistDir, "pglite.data"); | 212 | const pgliteDataPath = join(pgliteDistDir, "pglite.data"); |
| 198 | const pgliteWasmPath = join(pgliteDistDir, "pglite.wasm"); | 213 | const pgliteWasmPath = join(pgliteDistDir, "pglite.wasm"); |
| 214 | const quickJsWasmPath = resolveQuickJsWasmPath(); | ||
| 199 | const webDistDir = join(repoRoot, "apps/web/dist"); | 215 | const webDistDir = join(repoRoot, "apps/web/dist"); |
| 200 | const readmePath = join(repoRoot, "README.md"); | 216 | const readmePath = join(repoRoot, "README.md"); |
| 201 | const packageName = options.packageName ?? defaults.name; | 217 | const packageName = options.packageName ?? defaults.name; |
| @@ -235,6 +251,7 @@ export const buildDistributionPackage = async ( · 33 unmodified lines | |||
| 235 | }); | 251 | }); |
| 236 | await cp(pgliteDataPath, join(binDir, "pglite.data")); | 252 | await cp(pgliteDataPath, join(binDir, "pglite.data")); |
| 237 | await cp(pgliteWasmPath, join(binDir, "pglite.wasm")); | 253 | await cp(pgliteWasmPath, join(binDir, "pglite.wasm")); |
| 254 | await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm")); | ||
| 238 | await mkdir(join(binDir, "openapi-extractor-wasm"), { recursive: true }); | 255 | await mkdir(join(binDir, "openapi-extractor-wasm"), { recursive: true }); |
| 239 | await cp( | 256 | await cp( |
| 240 | join(repoRoot, "packages/codemode-openapi/src/openapi-extractor-wasm/openapi_extractor_bg.wasm"), | 257 | join(repoRoot, "packages/codemode-openapi/src/openapi-extractor-wasm/openapi_extractor_bg.wasm"), |
| @@ -167,6 +167,7 @@ · 166 unmodified lines | |||
| 167 | "@executor/codemode-mcp": "workspace:*", | 167 | "@executor/codemode-mcp": "workspace:*", |
| 168 | "@executor/codemode-openapi": "workspace:*", | 168 | "@executor/codemode-openapi": "workspace:*", |
| 169 | "@executor/runtime-deno-subprocess": "workspace:*", | 169 | "@executor/runtime-deno-subprocess": "workspace:*", |
| 170 | "@executor/runtime-quickjs": "workspace:*", | ||
| 170 | "@executor/runtime-ses": "workspace:*", | 171 | "@executor/runtime-ses": "workspace:*", |
| 171 | "drizzle-orm": "1.0.0-beta.15-859cf75", | 172 | "drizzle-orm": "1.0.0-beta.15-859cf75", |
| 172 | "effect": "catalog:", | 173 | "effect": "catalog:", |
| @@ -257,6 +258,20 @@ · 84 unmodified lines | |||
| 257 | "vitest": "catalog:", | 258 | "vitest": "catalog:", |
| 258 | }, | 259 | }, |
| 259 | }, | 260 | }, |
| 261 | "packages/runtime-quickjs": { | ||
| 262 | "name": "@executor/runtime-quickjs", | ||
| 263 | "dependencies": { | ||
| 264 | "@executor/codemode-core": "workspace:*", | ||
| 265 | "effect": "catalog:", | ||
| 266 | "quickjs-emscripten": "^0.31.0", | ||
| 267 | }, | ||
| 268 | "devDependencies": { | ||
| 269 | "@effect/vitest": "catalog:", | ||
| 270 | "@types/node": "catalog:", | ||
| 271 | "bun-types": "catalog:", | ||
| 272 | "vitest": "catalog:", | ||
| 273 | }, | ||
| 274 | }, | ||
| 260 | "packages/runtime-ses": { | 275 | "packages/runtime-ses": { |
| 261 | "name": "@executor/runtime-ses", | 276 | "name": "@executor/runtime-ses", |
| 262 | "dependencies": { | 277 | "dependencies": { |
| @@ -509,6 +524,8 @@ · 246 unmodified lines | |||
| 509 | 524 | ||
| 510 | "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/runtime-deno-subprocess"], | 525 | "@executor/runtime-deno-subprocess": ["@executor/runtime-deno-subprocess@workspace:packages/runtime-deno-subprocess"], |
| 511 | 526 | ||
| 527 | "@executor/runtime-quickjs": ["@executor/runtime-quickjs@workspace:packages/runtime-quickjs"], | ||
| 528 | |||
| 512 | "@executor/runtime-ses": ["@executor/runtime-ses@workspace:packages/runtime-ses"], | 529 | "@executor/runtime-ses": ["@executor/runtime-ses@workspace:packages/runtime-ses"], |
| 513 | 530 | ||
| 514 | "@executor/server": ["@executor/server@workspace:packages/server"], | 531 | "@executor/server": ["@executor/server@workspace:packages/server"], |
| @@ -519,6 +536,16 @@ · 4 unmodified lines | |||
| 519 | 536 | ||
| 520 | "@hono/vite-dev-server": ["@hono/vite-dev-server@0.25.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-4j5rs7yUTOObU2/yCEHeWbf3C8m95SD9lhdUUE6CJ462Emp9vS94yiza2dsA8+ly5pdaIQW0cd4nhNVcwYL5tA=="], | 537 | "@hono/vite-dev-server": ["@hono/vite-dev-server@0.25.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-4j5rs7yUTOObU2/yCEHeWbf3C8m95SD9lhdUUE6CJ462Emp9vS94yiza2dsA8+ly5pdaIQW0cd4nhNVcwYL5tA=="], |
| 521 | 538 | ||
| 539 | "@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.31.0", "", {}, "sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw=="], | ||
| 540 | |||
| 541 | "@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w=="], | ||
| 542 | |||
| 543 | "@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw=="], | ||
| 544 | |||
| 545 | "@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg=="], | ||
| 546 | |||
| 547 | "@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw=="], | ||
| 548 | |||
| 522 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], | 549 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], |
| 523 | 550 | ||
| 524 | "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], | 551 | "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], |
| @@ -1415,6 +1442,10 @@ · 890 unmodified lines | |||
| 1415 | 1442 | ||
| 1416 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], | 1443 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], |
| 1417 | 1444 | ||
| 1445 | "quickjs-emscripten": ["quickjs-emscripten@0.31.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.31.0", "@jitl/quickjs-wasmfile-debug-sync": "0.31.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.31.0", "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "quickjs-emscripten-core": "0.31.0" } }, "sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ=="], | ||
| 1446 | |||
| 1447 | "quickjs-emscripten-core": ["quickjs-emscripten-core@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ=="], | ||
| 1448 | |||
| 1418 | "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], | 1449 | "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], |
| 1419 | 1450 | ||
| 1420 | "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], | 1451 | "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], |
| @@ -28,6 +28,7 @@ · 27 unmodified lines | |||
| 28 | "@executor/codemode-core": "workspace:*", | 28 | "@executor/codemode-core": "workspace:*", |
| 29 | "@executor/codemode-google-discovery": "workspace:*", | 29 | "@executor/codemode-google-discovery": "workspace:*", |
| 30 | "@executor/codemode-mcp": "workspace:*", | 30 | "@executor/codemode-mcp": "workspace:*", |
| 31 | "@executor/runtime-quickjs": "workspace:*", | ||
| 31 | "@executor/runtime-ses": "workspace:*", | 32 | "@executor/runtime-ses": "workspace:*", |
| 32 | "@executor/codemode-openapi": "workspace:*", | 33 | "@executor/codemode-openapi": "workspace:*", |
| 33 | "@executor/runtime-deno-subprocess": "workspace:*", | 34 | "@executor/runtime-deno-subprocess": "workspace:*", |
| @@ -9,11 +9,7 @@ import { · 8 unmodified lines | |||
| 9 | type ToolPath, | 9 | type ToolPath, |
| 10 | type ToolSchemaBundle, | 10 | type ToolSchemaBundle, |
| 11 | } from "@executor/codemode-core"; | 11 | } from "@executor/codemode-core"; |
| 12 | import { | 12 | import { makeQuickJsExecutor } from "@executor/runtime-quickjs"; |
| 13 | isDenoAvailable, | ||
| 14 | makeDenoSubprocessExecutor, | ||
| 15 | } from "@executor/runtime-deno-subprocess"; | ||
| 16 | import { makeSesExecutor } from "@executor/runtime-ses"; | ||
| 17 | import { | 13 | import { |
| 18 | SqlControlPlaneRowsService, | 14 | SqlControlPlaneRowsService, |
| 19 | type SqlControlPlaneRows, | 15 | type SqlControlPlaneRows, |
| @@ -775,9 +771,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 755 unmodified lines | |||
| 775 | onElicitation, | 771 | onElicitation, |
| 776 | }); | 772 | }); |
| 777 | 773 | ||
| 778 | const executor = isDenoAvailable() | 774 | const executor = makeQuickJsExecutor(); |
| 779 | ? makeDenoSubprocessExecutor() | ||
| 780 | : makeSesExecutor(); | ||
| 781 | 775 | ||
| 782 | return { | 776 | return { |
| 783 | executor, | 777 | executor, |
| @@ -0,0 +1,24 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/runtime-quickjs", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts" | ||
| 7 | }, | ||
| 8 | "scripts": { | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", | ||
| 10 | "test": "vitest run", | ||
| 11 | "test:watch": "vitest" | ||
| 12 | }, | ||
| 13 | "dependencies": { | ||
| 14 | "@executor/codemode-core": "workspace:*", | ||
| 15 | "effect": "catalog:", | ||
| 16 | "quickjs-emscripten": "^0.31.0" | ||
| 17 | }, | ||
| 18 | "devDependencies": { | ||
| 19 | "@effect/vitest": "catalog:", | ||
| 20 | "@types/node": "catalog:", | ||
| 21 | "bun-types": "catalog:", | ||
| 22 | "vitest": "catalog:" | ||
| 23 | } | ||
| 24 | } | ||
| @@ -0,0 +1,200 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | 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"; | ||
| 10 | |||
| 11 | import { makeQuickJsExecutor } from "./index"; | ||
| 12 | |||
| 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 tools = { | ||
| 27 | "math.add": { | ||
| 28 | description: "Add two numbers", | ||
| 29 | inputSchema: numberPairInputSchema, | ||
| 30 | execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }), | ||
| 31 | }, | ||
| 32 | "notifications.send": toExecutorTool({ | ||
| 33 | tool: { | ||
| 34 | description: "Send a message", | ||
| 35 | inputSchema: messageInputSchema, | ||
| 36 | execute: ({ message }: { message: string }) => ({ | ||
| 37 | delivered: true, | ||
| 38 | message, | ||
| 39 | }), | ||
| 40 | }, | ||
| 41 | metadata: { | ||
| 42 | interaction: "required", | ||
| 43 | }, | ||
| 44 | }), | ||
| 45 | }; | ||
| 46 | |||
| 47 | describe("runtime-quickjs", () => { | ||
| 48 | it.effect("executes simple code and returns result", () => | ||
| 49 | Effect.gen(function* () { | ||
| 50 | const executor = makeQuickJsExecutor(); | ||
| 51 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 52 | |||
| 53 | const output = yield* executor.execute( | ||
| 54 | "return 1 + 2;", | ||
| 55 | toolInvoker, | ||
| 56 | ); | ||
| 57 | |||
| 58 | expect(output.result).toBe(3); | ||
| 59 | expect(output.error).toBeUndefined(); | ||
| 60 | }), | ||
| 61 | ); | ||
| 62 | |||
| 63 | it.effect("executes code with tool calls", () => | ||
| 64 | Effect.gen(function* () { | ||
| 65 | const executor = makeQuickJsExecutor(); | ||
| 66 | const toolInvoker = makeToolInvokerFromTools({ | ||
| 67 | tools, | ||
| 68 | onToolInteraction: allowAllToolInteractions, | ||
| 69 | }); | ||
| 70 | |||
| 71 | const output = yield* executor.execute( | ||
| 72 | [ | ||
| 73 | "const math = await tools.math.add({ a: 19, b: 23 });", | ||
| 74 | "await tools.notifications.send({ message: `sum is ${math.sum}` });", | ||
| 75 | "return math;", | ||
| 76 | ].join("\n"), | ||
| 77 | toolInvoker, | ||
| 78 | ); | ||
| 79 | |||
| 80 | expect(output.result).toEqual({ sum: 42 }); | ||
| 81 | expect(output.error).toBeUndefined(); | ||
| 82 | }), | ||
| 83 | ); | ||
| 84 | |||
| 85 | it.effect("captures console output in logs", () => | ||
| 86 | Effect.gen(function* () { | ||
| 87 | const executor = makeQuickJsExecutor(); | ||
| 88 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 89 | |||
| 90 | const output = yield* executor.execute( | ||
| 91 | [ | ||
| 92 | 'console.log("hello from quickjs");', | ||
| 93 | 'console.warn("a warning");', | ||
| 94 | 'console.error("an error");', | ||
| 95 | "return 42;", | ||
| 96 | ].join("\n"), | ||
| 97 | toolInvoker, | ||
| 98 | ); | ||
| 99 | |||
| 100 | expect(output.result).toBe(42); | ||
| 101 | expect(output.logs).toContain("[log] hello from quickjs"); | ||
| 102 | expect(output.logs).toContain("[warn] a warning"); | ||
| 103 | expect(output.logs).toContain("[error] an error"); | ||
| 104 | }), | ||
| 105 | ); | ||
| 106 | |||
| 107 | it.effect("reports execution errors without crashing", () => | ||
| 108 | Effect.gen(function* () { | ||
| 109 | const executor = makeQuickJsExecutor(); | ||
| 110 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 111 | |||
| 112 | const output = yield* executor.execute( | ||
| 113 | 'throw new Error("boom");', | ||
| 114 | toolInvoker, | ||
| 115 | ); | ||
| 116 | |||
| 117 | expect(output.result).toBeNull(); | ||
| 118 | expect(output.error).toContain("boom"); | ||
| 119 | }), | ||
| 120 | ); | ||
| 121 | |||
| 122 | it.effect("handles tool call errors gracefully", () => | ||
| 123 | Effect.gen(function* () { | ||
| 124 | const failingTools = { | ||
| 125 | "broken.thing": { | ||
| 126 | description: "Always fails", | ||
| 127 | inputSchema: Schema.standardSchemaV1(Schema.Struct({})), | ||
| 128 | execute: () => { | ||
| 129 | throw new Error("tool is broken"); | ||
| 130 | }, | ||
| 131 | }, | ||
| 132 | }; | ||
| 133 | |||
| 134 | const executor = makeQuickJsExecutor(); | ||
| 135 | const toolInvoker = makeToolInvokerFromTools({ | ||
| 136 | tools: failingTools, | ||
| 137 | }); | ||
| 138 | |||
| 139 | const output = yield* executor.execute( | ||
| 140 | "return await tools.broken.thing({});", | ||
| 141 | toolInvoker, | ||
| 142 | ); | ||
| 143 | |||
| 144 | expect(output.result).toBeNull(); | ||
| 145 | expect(output.error).toContain("tool is broken"); | ||
| 146 | }), | ||
| 147 | ); | ||
| 148 | |||
| 149 | it.effect("respects timeout", () => | ||
| 150 | Effect.gen(function* () { | ||
| 151 | const executor = makeQuickJsExecutor({ | ||
| 152 | timeoutMs: 250, | ||
| 153 | }); | ||
| 154 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 155 | |||
| 156 | const output = yield* executor.execute( | ||
| 157 | "while (true) {}", | ||
| 158 | toolInvoker, | ||
| 159 | ); | ||
| 160 | |||
| 161 | expect(output.result).toBeNull(); | ||
| 162 | expect(output.error).toContain("timed out"); | ||
| 163 | }), | ||
| 164 | ); | ||
| 165 | |||
| 166 | it.effect("network access is denied by default", () => | ||
| 167 | Effect.gen(function* () { | ||
| 168 | const executor = makeQuickJsExecutor(); | ||
| 169 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 170 | |||
| 171 | const output = yield* executor.execute( | ||
| 172 | 'await fetch("https://example.com"); return 1;', | ||
| 173 | toolInvoker, | ||
| 174 | ); | ||
| 175 | |||
| 176 | expect(output.result).toBeNull(); | ||
| 177 | expect(output.error).toContain("fetch is disabled in QuickJS executor"); | ||
| 178 | }), | ||
| 179 | ); | ||
| 180 | |||
| 181 | it.effect("multiple sequential tool calls work correctly", () => | ||
| 182 | Effect.gen(function* () { | ||
| 183 | const executor = makeQuickJsExecutor(); | ||
| 184 | const toolInvoker = makeToolInvokerFromTools({ tools }); | ||
| 185 | |||
| 186 | const output = yield* executor.execute( | ||
| 187 | [ | ||
| 188 | "const r1 = await tools.math.add({ a: 1, b: 2 });", | ||
| 189 | "const r2 = await tools.math.add({ a: r1.sum, b: 10 });", | ||
| 190 | "const r3 = await tools.math.add({ a: r2.sum, b: 100 });", | ||
| 191 | "return r3;", | ||
| 192 | ].join("\n"), | ||
| 193 | toolInvoker, | ||
| 194 | ); | ||
| 195 | |||
| 196 | expect(output.result).toEqual({ sum: 113 }); | ||
| 197 | expect(output.error).toBeUndefined(); | ||
| 198 | }), | ||
| 199 | ); | ||
| 200 | }); | ||
| @@ -0,0 +1,410 @@ | |||
| 1 | import type { | ||
| 2 | CodeExecutor, | ||
| 3 | ExecuteResult, | ||
| 4 | ToolInvoker, | ||
| 5 | } from "@executor/codemode-core"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | import { | ||
| 8 | getQuickJS, | ||
| 9 | shouldInterruptAfterDeadline, | ||
| 10 | type QuickJSContext, | ||
| 11 | type QuickJSDeferredPromise, | ||
| 12 | type QuickJSHandle, | ||
| 13 | type QuickJSRuntime, | ||
| 14 | } from "quickjs-emscripten"; | ||
| 15 | |||
| 16 | export type QuickJsExecutorOptions = { | ||
| 17 | timeoutMs?: number; | ||
| 18 | memoryLimitBytes?: number; | ||
| 19 | maxStackSizeBytes?: number; | ||
| 20 | }; | ||
| 21 | |||
| 22 | const DEFAULT_TIMEOUT_MS = 5 * 60_000; | ||
| 23 | const EXECUTION_FILENAME = "executor-quickjs-runtime.js"; | ||
| 24 | |||
| 25 | const toError = (cause: unknown): Error => | ||
| 26 | cause instanceof Error ? cause : new Error(String(cause)); | ||
| 27 | |||
| 28 | const toErrorMessage = (cause: unknown): string => { | ||
| 29 | if (typeof cause === "object" && cause !== null) { | ||
| 30 | const stack = "stack" in cause && typeof cause.stack === "string" | ||
| 31 | ? cause.stack | ||
| 32 | : undefined; | ||
| 33 | const message = "message" in cause && typeof cause.message === "string" | ||
| 34 | ? cause.message | ||
| 35 | : undefined; | ||
| 36 | |||
| 37 | if (stack) { | ||
| 38 | return stack; | ||
| 39 | } | ||
| 40 | |||
| 41 | if (message) { | ||
| 42 | return message; | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | const error = toError(cause); | ||
| 47 | return error.stack ?? error.message; | ||
| 48 | }; | ||
| 49 | |||
| 50 | const serializeJson = (value: unknown, label: string): string | undefined => { | ||
| 51 | if (typeof value === "undefined") { | ||
| 52 | return undefined; | ||
| 53 | } | ||
| 54 | |||
| 55 | try { | ||
| 56 | return JSON.stringify(value); | ||
| 57 | } catch (cause) { | ||
| 58 | throw new Error( | ||
| 59 | `${label} is not JSON serializable: ${toError(cause).message}`, | ||
| 60 | ); | ||
| 61 | } | ||
| 62 | }; | ||
| 63 | |||
| 64 | const looksLikeInterruptedError = (message: string): boolean => | ||
| 65 | /\binterrupted\b/i.test(message); | ||
| 66 | |||
| 67 | const timeoutMessage = (timeoutMs: number): string => | ||
| 68 | `QuickJS execution timed out after ${timeoutMs}ms`; | ||
| 69 | |||
| 70 | const normalizeExecutionError = ( | ||
| 71 | cause: unknown, | ||
| 72 | deadlineMs: number, | ||
| 73 | timeoutMs: number, | ||
| 74 | ): string => { | ||
| 75 | const message = toErrorMessage(cause); | ||
| 76 | return Date.now() >= deadlineMs && looksLikeInterruptedError(message) | ||
| 77 | ? timeoutMessage(timeoutMs) | ||
| 78 | : message; | ||
| 79 | }; | ||
| 80 | |||
| 81 | const buildExecutionSource = (code: string): string => { | ||
| 82 | const trimmed = code.trim(); | ||
| 83 | const looksLikeArrowFunction = | ||
| 84 | (trimmed.startsWith("async") || trimmed.startsWith("(")) | ||
| 85 | && trimmed.includes("=>"); | ||
| 86 | |||
| 87 | const body = looksLikeArrowFunction | ||
| 88 | ? [ | ||
| 89 | `const __fn = (${trimmed});`, | ||
| 90 | "if (typeof __fn !== 'function') throw new Error('Code must evaluate to a function');", | ||
| 91 | "return await __fn();", | ||
| 92 | ].join("\n") | ||
| 93 | : code; | ||
| 94 | |||
| 95 | return [ | ||
| 96 | '"use strict";', | ||
| 97 | "const __formatLogArg = (value) => {", | ||
| 98 | " if (typeof value === 'string') return value;", | ||
| 99 | " try {", | ||
| 100 | " return JSON.stringify(value);", | ||
| 101 | " } catch {", | ||
| 102 | " return String(value);", | ||
| 103 | " }", | ||
| 104 | "};", | ||
| 105 | "const __formatLogLine = (args) => args.map(__formatLogArg).join(' ');", | ||
| 106 | "const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {", | ||
| 107 | " get(_target, prop) {", | ||
| 108 | " if (prop === 'then' || typeof prop === 'symbol') {", | ||
| 109 | " return undefined;", | ||
| 110 | " }", | ||
| 111 | " return __makeToolsProxy([...path, String(prop)]);", | ||
| 112 | " },", | ||
| 113 | " apply(_target, _thisArg, args) {", | ||
| 114 | " const toolPath = path.join('.');", | ||
| 115 | " if (!toolPath) {", | ||
| 116 | " throw new Error('Tool path missing in invocation');", | ||
| 117 | " }", | ||
| 118 | " return Promise.resolve(__executor_invokeTool(toolPath, args[0])).then((raw) => raw === undefined ? undefined : JSON.parse(raw));", | ||
| 119 | " },", | ||
| 120 | "});", | ||
| 121 | "const tools = __makeToolsProxy();", | ||
| 122 | "const console = {", | ||
| 123 | " log: (...args) => __executor_log('log', __formatLogLine(args)),", | ||
| 124 | " warn: (...args) => __executor_log('warn', __formatLogLine(args)),", | ||
| 125 | " error: (...args) => __executor_log('error', __formatLogLine(args)),", | ||
| 126 | " info: (...args) => __executor_log('info', __formatLogLine(args)),", | ||
| 127 | " debug: (...args) => __executor_log('debug', __formatLogLine(args)),", | ||
| 128 | "};", | ||
| 129 | "const fetch = (..._args) => {", | ||
| 130 | " throw new Error('fetch is disabled in QuickJS executor');", | ||
| 131 | "};", | ||
| 132 | "(async () => {", | ||
| 133 | body, | ||
| 134 | "})()", | ||
| 135 | ].join("\n"); | ||
| 136 | }; | ||
| 137 | |||
| 138 | const readPropDump = ( | ||
| 139 | context: QuickJSContext, | ||
| 140 | handle: QuickJSHandle, | ||
| 141 | key: string, | ||
| 142 | ): unknown => { | ||
| 143 | const prop = context.getProp(handle, key); | ||
| 144 | try { | ||
| 145 | return context.dump(prop); | ||
| 146 | } finally { | ||
| 147 | prop.dispose(); | ||
| 148 | } | ||
| 149 | }; | ||
| 150 | |||
| 151 | const readResultState = ( | ||
| 152 | context: QuickJSContext, | ||
| 153 | handle: QuickJSHandle, | ||
| 154 | ): { | ||
| 155 | settled: boolean; | ||
| 156 | value: unknown; | ||
| 157 | error: unknown; | ||
| 158 | } => ({ | ||
| 159 | settled: readPropDump(context, handle, "settled") === true, | ||
| 160 | value: readPropDump(context, handle, "v"), | ||
| 161 | error: readPropDump(context, handle, "e"), | ||
| 162 | }); | ||
| 163 | |||
| 164 | const createLogBridge = ( | ||
| 165 | context: QuickJSContext, | ||
| 166 | logs: string[], | ||
| 167 | ): QuickJSHandle => | ||
| 168 | context.newFunction("__executor_log", (levelHandle, lineHandle) => { | ||
| 169 | const level = context.getString(levelHandle); | ||
| 170 | const line = context.getString(lineHandle); | ||
| 171 | logs.push(`[${level}] ${line}`); | ||
| 172 | return context.undefined; | ||
| 173 | }); | ||
| 174 | |||
| 175 | const createToolBridge = ( | ||
| 176 | context: QuickJSContext, | ||
| 177 | toolInvoker: ToolInvoker, | ||
| 178 | pendingDeferreds: Set<QuickJSDeferredPromise>, | ||
| 179 | ): QuickJSHandle => | ||
| 180 | context.newFunction("__executor_invokeTool", (pathHandle, argsHandle) => { | ||
| 181 | const path = context.getString(pathHandle); | ||
| 182 | const args = | ||
| 183 | argsHandle === undefined || context.typeof(argsHandle) === "undefined" | ||
| 184 | ? undefined | ||
| 185 | : context.dump(argsHandle); | ||
| 186 | const deferred = context.newPromise(); | ||
| 187 | pendingDeferreds.add(deferred); | ||
| 188 | deferred.settled.finally(() => { | ||
| 189 | pendingDeferreds.delete(deferred); | ||
| 190 | }); | ||
| 191 | |||
| 192 | void Effect.runPromise(toolInvoker.invoke({ path, args })).then( | ||
| 193 | (value) => { | ||
| 194 | if (!deferred.alive) { | ||
| 195 | return; | ||
| 196 | } | ||
| 197 | |||
| 198 | const serialized = serializeJson(value, `Tool result for ${path}`); | ||
| 199 | if (typeof serialized === "undefined") { | ||
| 200 | deferred.resolve(); | ||
| 201 | return; | ||
| 202 | } | ||
| 203 | |||
| 204 | const valueHandle = context.newString(serialized); | ||
| 205 | deferred.resolve(valueHandle); | ||
| 206 | valueHandle.dispose(); | ||
| 207 | }, | ||
| 208 | (cause) => { | ||
| 209 | if (!deferred.alive) { | ||
| 210 | return; | ||
| 211 | } | ||
| 212 | |||
| 213 | const errorHandle = context.newError(toErrorMessage(cause)); | ||
| 214 | deferred.reject(errorHandle); | ||
| 215 | errorHandle.dispose(); | ||
| 216 | }, | ||
| 217 | ); | ||
| 218 | |||
| 219 | return deferred.handle; | ||
| 220 | }); | ||
| 221 | |||
| 222 | const drainJobs = ( | ||
| 223 | context: QuickJSContext, | ||
| 224 | runtime: QuickJSRuntime, | ||
| 225 | deadlineMs: number, | ||
| 226 | timeoutMs: number, | ||
| 227 | ): void => { | ||
| 228 | while (runtime.hasPendingJob()) { | ||
| 229 | if (Date.now() >= deadlineMs) { | ||
| 230 | throw new Error(timeoutMessage(timeoutMs)); | ||
| 231 | } | ||
| 232 | |||
| 233 | const pending = runtime.executePendingJobs(); | ||
| 234 | if (pending.error) { | ||
| 235 | const error = context.dump(pending.error); | ||
| 236 | pending.error.dispose(); | ||
| 237 | throw toError(error); | ||
| 238 | } | ||
| 239 | } | ||
| 240 | }; | ||
| 241 | |||
| 242 | const waitForDeferreds = async ( | ||
| 243 | pendingDeferreds: ReadonlySet<QuickJSDeferredPromise>, | ||
| 244 | deadlineMs: number, | ||
| 245 | timeoutMs: number, | ||
| 246 | ): Promise<void> => { | ||
| 247 | const remainingMs = deadlineMs - Date.now(); | ||
| 248 | if (remainingMs <= 0) { | ||
| 249 | throw new Error(timeoutMessage(timeoutMs)); | ||
| 250 | } | ||
| 251 | |||
| 252 | let timer: ReturnType<typeof setTimeout> | undefined; | ||
| 253 | try { | ||
| 254 | await Promise.race([ | ||
| 255 | Promise.race([...pendingDeferreds].map((deferred) => deferred.settled)), | ||
| 256 | new Promise<never>((_, reject) => { | ||
| 257 | timer = setTimeout(() => reject(new Error(timeoutMessage(timeoutMs))), remainingMs); | ||
| 258 | }), | ||
| 259 | ]); | ||
| 260 | } finally { | ||
| 261 | if (timer !== undefined) { | ||
| 262 | clearTimeout(timer); | ||
| 263 | } | ||
| 264 | } | ||
| 265 | }; | ||
| 266 | |||
| 267 | const drainAsync = async ( | ||
| 268 | context: QuickJSContext, | ||
| 269 | runtime: QuickJSRuntime, | ||
| 270 | pendingDeferreds: ReadonlySet<QuickJSDeferredPromise>, | ||
| 271 | deadlineMs: number, | ||
| 272 | timeoutMs: number, | ||
| 273 | ): Promise<void> => { | ||
| 274 | drainJobs(context, runtime, deadlineMs, timeoutMs); | ||
| 275 | |||
| 276 | while (pendingDeferreds.size > 0) { | ||
| 277 | await waitForDeferreds(pendingDeferreds, deadlineMs, timeoutMs); | ||
| 278 | drainJobs(context, runtime, deadlineMs, timeoutMs); | ||
| 279 | } | ||
| 280 | |||
| 281 | drainJobs(context, runtime, deadlineMs, timeoutMs); | ||
| 282 | }; | ||
| 283 | |||
| 284 | const evaluateInQuickJs = async ( | ||
| 285 | options: QuickJsExecutorOptions, | ||
| 286 | code: string, | ||
| 287 | toolInvoker: ToolInvoker, | ||
| 288 | ): Promise<ExecuteResult> => { | ||
| 289 | const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); | ||
| 290 | const deadlineMs = Date.now() + timeoutMs; | ||
| 291 | const logs: string[] = []; | ||
| 292 | const pendingDeferreds = new Set<QuickJSDeferredPromise>(); | ||
| 293 | const QuickJS = await getQuickJS(); | ||
| 294 | const runtime = QuickJS.newRuntime(); | ||
| 295 | |||
| 296 | try { | ||
| 297 | if (options.memoryLimitBytes !== undefined) { | ||
| 298 | runtime.setMemoryLimit(options.memoryLimitBytes); | ||
| 299 | } | ||
| 300 | |||
| 301 | if (options.maxStackSizeBytes !== undefined) { | ||
| 302 | runtime.setMaxStackSize(options.maxStackSizeBytes); | ||
| 303 | } | ||
| 304 | |||
| 305 | runtime.setInterruptHandler(shouldInterruptAfterDeadline(deadlineMs)); | ||
| 306 | |||
| 307 | const context = runtime.newContext(); | ||
| 308 | try { | ||
| 309 | const logBridge = createLogBridge(context, logs); | ||
| 310 | context.setProp(context.global, "__executor_log", logBridge); | ||
| 311 | logBridge.dispose(); | ||
| 312 | |||
| 313 | const toolBridge = createToolBridge(context, toolInvoker, pendingDeferreds); | ||
| 314 | context.setProp(context.global, "__executor_invokeTool", toolBridge); | ||
| 315 | toolBridge.dispose(); | ||
| 316 | |||
| 317 | const evaluated = context.evalCode( | ||
| 318 | buildExecutionSource(code), | ||
| 319 | EXECUTION_FILENAME, | ||
| 320 | ); | ||
| 321 | if (evaluated.error) { | ||
| 322 | const error = context.dump(evaluated.error); | ||
| 323 | evaluated.error.dispose(); | ||
| 324 | return { | ||
| 325 | result: null, | ||
| 326 | error: normalizeExecutionError(error, deadlineMs, timeoutMs), | ||
| 327 | logs, | ||
| 328 | } satisfies ExecuteResult; | ||
| 329 | } | ||
| 330 | |||
| 331 | context.setProp(context.global, "__executor_result", evaluated.value); | ||
| 332 | evaluated.value.dispose(); | ||
| 333 | |||
| 334 | const stateResult = context.evalCode( | ||
| 335 | "(function(p){ var s = { v: void 0, e: void 0, settled: false }; var formatError = function(e){ if (e && typeof e === 'object') { var message = typeof e.message === 'string' ? e.message : ''; var stack = typeof e.stack === 'string' ? e.stack : ''; if (message && stack) { return stack.indexOf(message) === -1 ? message + '\\n' + stack : stack; } if (message) return message; if (stack) return stack; } return String(e); }; p.then(function(v){ s.v = v; s.settled = true; }, function(e){ s.e = formatError(e); s.settled = true; }); return s; })(__executor_result)", | ||
| 336 | ); | ||
| 337 | if (stateResult.error) { | ||
| 338 | const error = context.dump(stateResult.error); | ||
| 339 | stateResult.error.dispose(); | ||
| 340 | return { | ||
| 341 | result: null, | ||
| 342 | error: normalizeExecutionError(error, deadlineMs, timeoutMs), | ||
| 343 | logs, | ||
| 344 | } satisfies ExecuteResult; | ||
| 345 | } | ||
| 346 | |||
| 347 | const stateHandle = stateResult.value; | ||
| 348 | try { | ||
| 349 | await drainAsync(context, runtime, pendingDeferreds, deadlineMs, timeoutMs); | ||
| 350 | const state = readResultState(context, stateHandle); | ||
| 351 | if (!state.settled) { | ||
| 352 | return { | ||
| 353 | result: null, | ||
| 354 | error: timeoutMessage(timeoutMs), | ||
| 355 | logs, | ||
| 356 | } satisfies ExecuteResult; | ||
| 357 | } | ||
| 358 | |||
| 359 | if (typeof state.error !== "undefined") { | ||
| 360 | return { | ||
| 361 | result: null, | ||
| 362 | error: normalizeExecutionError(state.error, deadlineMs, timeoutMs), | ||
| 363 | logs, | ||
| 364 | } satisfies ExecuteResult; | ||
| 365 | } | ||
| 366 | |||
| 367 | return { | ||
| 368 | result: state.value, | ||
| 369 | logs, | ||
| 370 | } satisfies ExecuteResult; | ||
| 371 | } finally { | ||
| 372 | stateHandle.dispose(); | ||
| 373 | } | ||
| 374 | } finally { | ||
| 375 | for (const deferred of pendingDeferreds) { | ||
| 376 | if (deferred.alive) { | ||
| 377 | deferred.dispose(); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | pendingDeferreds.clear(); | ||
| 382 | context.dispose(); | ||
| 383 | } | ||
| 384 | } catch (cause) { | ||
| 385 | return { | ||
| 386 | result: null, | ||
| 387 | error: normalizeExecutionError(cause, deadlineMs, timeoutMs), | ||
| 388 | logs, | ||
| 389 | } satisfies ExecuteResult; | ||
| 390 | } finally { | ||
| 391 | runtime.dispose(); | ||
| 392 | } | ||
| 393 | }; | ||
| 394 | |||
| 395 | const runInQuickJs = ( | ||
| 396 | options: QuickJsExecutorOptions, | ||
| 397 | code: string, | ||
| 398 | toolInvoker: ToolInvoker, | ||
| 399 | ): Effect.Effect<ExecuteResult, Error> => | ||
| 400 | Effect.tryPromise({ | ||
| 401 | try: () => evaluateInQuickJs(options, code, toolInvoker), | ||
| 402 | catch: toError, | ||
| 403 | }); | ||
| 404 | |||
| 405 | export const makeQuickJsExecutor = ( | ||
| 406 | options: QuickJsExecutorOptions = {}, | ||
| 407 | ): CodeExecutor => ({ | ||
| 408 | execute: (code: string, toolInvoker: ToolInvoker) => | ||
| 409 | runInQuickJs(options, code, toolInvoker), | ||
| 410 | }); | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": [ | ||
| 13 | "src/**/*.ts" | ||
| 14 | ] | ||
| 15 | } | ||