@ny4n/codemode 1.1.0 → 1.3.0

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.
Files changed (50) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +56 -7
  3. package/dist/declarations.d.ts +6 -21
  4. package/dist/declarations.d.ts.map +1 -1
  5. package/dist/declarations.js +100 -122
  6. package/dist/declarations.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/portable.d.ts +1 -1
  12. package/dist/portable.d.ts.map +1 -1
  13. package/dist/portable.js.map +1 -1
  14. package/dist/runtime/execution.d.ts +14 -2
  15. package/dist/runtime/execution.d.ts.map +1 -1
  16. package/dist/runtime/execution.js +158 -35
  17. package/dist/runtime/execution.js.map +1 -1
  18. package/dist/runtime/host.d.ts.map +1 -1
  19. package/dist/runtime/host.js +25 -11
  20. package/dist/runtime/host.js.map +1 -1
  21. package/dist/runtime/inline.d.ts.map +1 -1
  22. package/dist/runtime/inline.js +1 -0
  23. package/dist/runtime/inline.js.map +1 -1
  24. package/dist/runtime/prelude-source.d.ts +3 -1
  25. package/dist/runtime/prelude-source.d.ts.map +1 -1
  26. package/dist/runtime/prelude-source.js +16 -6
  27. package/dist/runtime/prelude-source.js.map +1 -1
  28. package/dist/runtime/protocol.d.ts +38 -3
  29. package/dist/runtime/protocol.d.ts.map +1 -1
  30. package/dist/runtime/protocol.js +6 -1
  31. package/dist/runtime/protocol.js.map +1 -1
  32. package/dist/runtime/remote.d.ts.map +1 -1
  33. package/dist/runtime/remote.js +5 -0
  34. package/dist/runtime/remote.js.map +1 -1
  35. package/dist/runtime/vm.d.ts +2 -0
  36. package/dist/runtime/vm.d.ts.map +1 -1
  37. package/dist/runtime/vm.js +131 -9
  38. package/dist/runtime/vm.js.map +1 -1
  39. package/dist/search.d.ts +33 -0
  40. package/dist/search.d.ts.map +1 -0
  41. package/dist/search.js +112 -0
  42. package/dist/search.js.map +1 -0
  43. package/dist/source.d.ts +2 -1
  44. package/dist/source.d.ts.map +1 -1
  45. package/dist/source.js +8 -2
  46. package/dist/source.js.map +1 -1
  47. package/dist/types.d.ts +102 -4
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js.map +1 -1
  50. package/package.json +6 -1
@@ -1 +1 @@
1
- {"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/runtime/remote.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runs the VM in another isolate, reached over an RPC boundary such as a Cloudflare Durable Object\n * stub, while tools keep running in the caller.\n *\n * Problem: a VM on the calling thread blocks that thread while the script computes. In a Durable\n * Object that serves an agent session, a CPU-heavy script would stall every other request to the\n * session (including the user's abort) for seconds. Measured on Cloudflare (October 2026), a\n * script that computed for 10 seconds delayed the session's other requests by 10 seconds when it\n * ran in the session object or in a Dynamic Worker, but by about 50 ms when it ran in a Durable\n * Object of a separately deployed Worker.\n *\n * Solution: the caller keeps the host role (`RemoteCodemodeSandbox`: tools, call records,\n * deadline, abort) and the remote side only runs the VM (`serveCodemodeRemote`, behind\n * `CodemodeSandboxDurableObject` in `@ny4n/codemode/cloudflare`).\n *\n * Protocol, one RPC call per execution:\n *\n * client remote\n * remote.execute(request, exchange) ───────▶ starts the VM (in-thread, with interrupt budget)\n * ◀─────── exchange([...outputs, call]) one call per tool call\n * runs the tool, replies with result ──────▶ VM continues\n * ◀─────── exchange([...outputs, done]) terminal, reply ignored\n * execute() resolves ◀────────────────────── VM disposed\n *\n * `exchange` is a function the client passes in; over Workers RPC it becomes a stub that calls back\n * into the client. Output is buffered on the remote side and sent with the next call or the\n * terminal message, so a script that prints in a loop does not make one RPC per line. Each\n * `exchange` carries at most one call, so concurrent tool calls are concurrent RPCs. Calls on one\n * stub are delivered in order, which keeps output ordered before the call or result that follows.\n *\n * Termination: the client cannot interrupt a remote VM that is computing. When the client finishes\n * first (deadline, abort, `close()`), it answers every pending and later `exchange` with\n * `{ type: \"stop\" }`, and the remote stops the VM at that point. A script that computes without\n * calling a tool runs until its interrupt budget is used up. The remote reports that as a\n * `failure` message, which the client turns into the same `kind: \"timeout\"` error an in-thread\n * VM produces.\n *\n * Failure: if the RPC call rejects (the remote isolate crashed, hit its CPU limit, or was\n * redeployed) or returns without a terminal message, the execution fails with `kind: \"sandbox\"`.\n */\nimport type { CodemodeError, CodemodeSandboxBaseOptions } from \"../types.ts\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\nimport { CodemodeSandboxBase, errorMessage, type VmChannel, type VmLauncher } from \"./execution.ts\";\nimport { DEFAULT_INTERRUPT_BUDGET, inlineLauncher } from \"./inline.ts\";\nimport {\n\ttype HostToWorkerMessage,\n\tisWorkerToHostMessage,\n\ttype VmStartData,\n\ttype WorkerToHostMessage,\n} from \"./protocol.ts\";\n\nexport interface CodemodeRemoteRequest {\n\tdata: VmStartData;\n\t/** Requested interrupt budget. The remote may lower it, see {@link CodemodeServeOptions}. */\n\tinterruptBudget?: number;\n}\n\n/** What the remote sends to the client: VM messages, or a failure outside the VM bridge. */\nexport type CodemodeRemoteMessage = WorkerToHostMessage | { type: \"failure\"; error: CodemodeError };\n\n/**\n * The client's answer to an exchange: the result of the call the exchange carried, `null` when it\n * carried no call, or `stop` once the client no longer wants the execution.\n */\nexport type CodemodeRemoteReply = HostToWorkerMessage | { type: \"stop\" } | null;\n\nexport type CodemodeRemoteExchange = (messages: CodemodeRemoteMessage[]) => Promise<CodemodeRemoteReply>;\n\n/**\n * The remote end, usually a Durable Object stub for `CodemodeSandboxDurableObject`. Anything with\n * this method works, for example an in-process object in tests.\n */\nexport interface CodemodeRemote {\n\texecute(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange): Promise<void>;\n}\n\nexport interface RemoteCodemodeSandboxOptions extends CodemodeSandboxBaseOptions {\n\t/**\n\t * Returns the remote that runs one execution, called once per `execute()`. For a Durable Object,\n\t * pick the instance here, for example `() => env.CODEMODE_SANDBOX.getByName(sessionId)`.\n\t */\n\tremote: () => CodemodeRemote;\n\t/** Interrupt budget requested from the remote. Default: the remote's default. */\n\tinterruptBudget?: number;\n}\n\n/** Output buffered on the remote before it is flushed without waiting for a call. */\nconst FLUSH_ITEMS = 1_000;\nconst FLUSH_CHARS = 1 << 20;\n\nexport interface CodemodeServeOptions {\n\twasm: CodemodeWasmModule | Promise<CodemodeWasmModule>;\n\t/** Upper bound for a request's interrupt budget, and the budget when a request names none. */\n\tmaxInterruptBudget?: number;\n\t/** Upper bound for a request's VM memory, and the limit when a request names none. */\n\tmaxMemoryLimitBytes?: number;\n}\n\n/**\n * Remote side of {@link RemoteCodemodeSandbox}: runs one execution's VM on this thread and\n * forwards everything it reports through `exchange`. Resolves once the VM has stopped and the\n * terminal message has been delivered, or the client asked to stop. Never rejects.\n */\nexport function serveCodemodeRemote(\n\trequest: CodemodeRemoteRequest,\n\texchange: CodemodeRemoteExchange,\n\toptions: CodemodeServeOptions,\n): Promise<void> {\n\tconst maxBudget = options.maxInterruptBudget ?? DEFAULT_INTERRUPT_BUDGET;\n\tconst budget = Math.min(request.interruptBudget ?? maxBudget, maxBudget);\n\tconst requestedMemory = request.data.memoryLimitBytes;\n\tconst maxMemory = options.maxMemoryLimitBytes;\n\tconst memoryLimitBytes =\n\t\trequestedMemory === undefined\n\t\t\t? maxMemory\n\t\t\t: maxMemory === undefined\n\t\t\t\t? requestedMemory\n\t\t\t\t: Math.min(requestedMemory, maxMemory);\n\tconst data: VmStartData = { ...request.data, memoryLimitBytes };\n\n\treturn new Promise<void>((resolve) => {\n\t\tlet channel: VmChannel | undefined;\n\t\tlet ended = false;\n\t\tlet buffered: CodemodeRemoteMessage[] = [];\n\t\tlet bufferedChars = 0;\n\t\t/** Replies that arrived before the launcher returned the channel. */\n\t\tconst unsent: HostToWorkerMessage[] = [];\n\n\t\tconst takeBuffer = (): CodemodeRemoteMessage[] => {\n\t\t\tconst batch = buffered;\n\t\t\tbuffered = [];\n\t\t\tbufferedChars = 0;\n\t\t\treturn batch;\n\t\t};\n\t\tconst end = (terminal: CodemodeRemoteMessage | undefined): void => {\n\t\t\tif (ended) return;\n\t\t\tended = true;\n\t\t\tconst batch = takeBuffer();\n\t\t\tconst delivered = terminal ? exchange([...batch, terminal]).catch(() => null) : Promise.resolve();\n\t\t\tvoid delivered\n\t\t\t\t.then(() => channel?.stop())\n\t\t\t\t.catch(() => undefined)\n\t\t\t\t.then(() => resolve());\n\t\t};\n\t\tconst reply = (message: CodemodeRemoteReply): void => {\n\t\t\tif (ended || message === null) return;\n\t\t\tif (message.type === \"stop\") {\n\t\t\t\tend(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (channel) channel.post(message);\n\t\t\telse unsent.push(message);\n\t\t};\n\t\tconst onMessage = (message: unknown): void => {\n\t\t\tif (ended) return;\n\t\t\tconst typed = message as WorkerToHostMessage;\n\t\t\tswitch (typed.type) {\n\t\t\t\tcase \"output\": {\n\t\t\t\t\tbuffered.push(typed);\n\t\t\t\t\tbufferedChars += typed.item.type === \"text\" ? typed.item.text.length : typed.item.data.length;\n\t\t\t\t\tif (buffered.length >= FLUSH_ITEMS || bufferedChars >= FLUSH_CHARS) {\n\t\t\t\t\t\texchange(takeBuffer()).then(reply, () => end(undefined));\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"call\":\n\t\t\t\t\texchange([...takeBuffer(), typed]).then(reply, () => end(undefined));\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tend(typed);\n\t\t\t}\n\t\t};\n\n\t\tconst launch = inlineLauncher(options.wasm, budget);\n\t\tPromise.resolve(\n\t\t\tlaunch(data, {\n\t\t\t\tmessage: onMessage,\n\t\t\t\tfailure: (error) => end({ type: \"failure\", error }),\n\t\t\t}),\n\t\t).then(\n\t\t\t(started) => {\n\t\t\t\tchannel = started;\n\t\t\t\tif (ended) {\n\t\t\t\t\tvoid started.stop().catch(() => undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const message of unsent.splice(0)) started.post(message);\n\t\t\t},\n\t\t\t(error: unknown) => end({ type: \"failure\", error: { kind: \"sandbox\", message: errorMessage(error) } }),\n\t\t);\n\t});\n}\n\nfunction remoteLauncher(remote: () => CodemodeRemote, interruptBudget: number | undefined): VmLauncher {\n\treturn (data, events) => {\n\t\tlet stopped = false;\n\t\tlet settled = false;\n\t\tconst waiting = new Map<number, (reply: CodemodeRemoteReply) => void>();\n\n\t\tconst exchange: CodemodeRemoteExchange = (messages) =>\n\t\t\tnew Promise<CodemodeRemoteReply>((resolve) => {\n\t\t\t\tif (stopped || !Array.isArray(messages)) {\n\t\t\t\t\tresolve({ type: \"stop\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Register the waiter first: the host may answer a call synchronously, for example when the\n\t\t\t\t// tool throws before returning a promise.\n\t\t\t\tconst call = messages.find((message) => isWorkerToHostMessage(message) && message.type === \"call\");\n\t\t\t\tif (call?.type === \"call\") waiting.set(call.id, resolve);\n\t\t\t\telse resolve(null);\n\t\t\t\tfor (const message of messages) {\n\t\t\t\t\tif (typeof message === \"object\" && message !== null && message.type === \"failure\") {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tevents.failure(message.error);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// Malformed messages still go to the host, which fails the execution as a broken bridge.\n\t\t\t\t\tevents.message(message);\n\t\t\t\t\tif (isWorkerToHostMessage(message) && (message.type === \"done\" || message.type === \"crash\"))\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t}\n\t\t\t});\n\n\t\tlet target: CodemodeRemote;\n\t\ttry {\n\t\t\ttarget = remote();\n\t\t} catch (error) {\n\t\t\tthrow new Error(`Failed to reach the remote sandbox: ${errorMessage(error)}`);\n\t\t}\n\t\tconst request: CodemodeRemoteRequest = interruptBudget === undefined ? { data } : { data, interruptBudget };\n\t\tPromise.resolve()\n\t\t\t.then(() => target.execute(request, exchange))\n\t\t\t.then(\n\t\t\t\t() => {\n\t\t\t\t\tif (!settled && !stopped) {\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: \"Remote sandbox returned before the script settled\" });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t(error: unknown) => {\n\t\t\t\t\tif (!stopped)\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: `Remote sandbox failed: ${errorMessage(error)}` });\n\t\t\t\t},\n\t\t\t);\n\n\t\treturn {\n\t\t\tpost: (message) => {\n\t\t\t\tconst resolve = waiting.get(message.id);\n\t\t\t\tif (!resolve) return;\n\t\t\t\twaiting.delete(message.id);\n\t\t\t\tresolve(message);\n\t\t\t},\n\t\t\tstop: async () => {\n\t\t\t\tstopped = true;\n\t\t\t\tfor (const resolve of waiting.values()) resolve({ type: \"stop\" });\n\t\t\t\twaiting.clear();\n\t\t\t},\n\t\t};\n\t};\n}\n\n/**\n * Runs each execution's VM on a remote, usually a Cloudflare Durable Object deployed with\n * `CodemodeSandboxDurableObject`, while tools run here. Same API and results as\n * `CodemodeSandbox`, so it can replace it. Differences: a script that computes without calling a\n * tool is limited by the remote's interrupt budget rather than `timeoutMs`, and after a deadline or\n * abort the remote VM keeps computing until it next calls a tool or uses up that budget.\n */\nexport class RemoteCodemodeSandbox extends CodemodeSandboxBase {\n\tconstructor(options: RemoteCodemodeSandboxOptions) {\n\t\tsuper(options, remoteLauncher(options.remote, options.interruptBudget));\n\t}\n}\n"]}
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/runtime/remote.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runs the VM in another isolate, reached over an RPC boundary such as a Cloudflare Durable Object\n * stub, while tools keep running in the caller.\n *\n * Problem: a VM on the calling thread blocks that thread while the script computes. In a Durable\n * Object that serves an agent session, a CPU-heavy script would stall every other request to the\n * session (including the user's abort) for seconds. Measured on Cloudflare (October 2026), a\n * script that computed for 10 seconds delayed the session's other requests by 10 seconds when it\n * ran in the session object or in a Dynamic Worker, but by about 50 ms when it ran in a Durable\n * Object of a separately deployed Worker.\n *\n * Solution: the caller keeps the host role (`RemoteCodemodeSandbox`: tools, call records,\n * deadline, abort) and the remote side only runs the VM (`serveCodemodeRemote`, behind\n * `CodemodeSandboxDurableObject` in `@ny4n/codemode/cloudflare`).\n *\n * Protocol, one RPC call per execution:\n *\n * client remote\n * remote.execute(request, exchange) ───────▶ starts the VM (in-thread, with interrupt budget)\n * ◀─────── exchange([...outputs, call]) one call per tool call\n * runs the tool, replies with result ──────▶ VM continues\n * ◀─────── exchange([...outputs, done]) terminal, reply ignored\n * execute() resolves ◀────────────────────── VM disposed\n *\n * `exchange` is a function the client passes in; over Workers RPC it becomes a stub that calls back\n * into the client. Output is buffered on the remote side and sent with the next call or the\n * terminal message, so a script that prints in a loop does not make one RPC per line. Each\n * `exchange` carries at most one call, so concurrent tool calls are concurrent RPCs. Calls on one\n * stub are delivered in order, which keeps output ordered before the call or result that follows.\n *\n * Termination: the client cannot interrupt a remote VM that is computing. When the client finishes\n * first (deadline, abort, `close()`), it answers every pending and later `exchange` with\n * `{ type: \"stop\" }`, and the remote stops the VM at that point. A script that computes without\n * calling a tool runs until its interrupt budget is used up. The remote reports that as a\n * `failure` message, which the client turns into the same `kind: \"timeout\"` error an in-thread\n * VM produces.\n *\n * Failure: if the RPC call rejects (the remote isolate crashed, hit its CPU limit, or was\n * redeployed) or returns without a terminal message, the execution fails with `kind: \"sandbox\"`.\n */\nimport type { CodemodeError, CodemodeSandboxBaseOptions } from \"../types.ts\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\nimport { CodemodeSandboxBase, errorMessage, type VmChannel, type VmLauncher } from \"./execution.ts\";\nimport { DEFAULT_INTERRUPT_BUDGET, inlineLauncher } from \"./inline.ts\";\nimport {\n\ttype HostToWorkerMessage,\n\tisWorkerToHostMessage,\n\ttype VmStartData,\n\ttype WorkerToHostMessage,\n} from \"./protocol.ts\";\n\nexport interface CodemodeRemoteRequest {\n\tdata: VmStartData;\n\t/** Requested interrupt budget. The remote may lower it, see {@link CodemodeServeOptions}. */\n\tinterruptBudget?: number;\n}\n\n/** What the remote sends to the client: VM messages, or a failure outside the VM bridge. */\nexport type CodemodeRemoteMessage = WorkerToHostMessage | { type: \"failure\"; error: CodemodeError };\n\n/**\n * The client's answer to an exchange: the result of the call the exchange carried, `null` when it\n * carried no call, or `stop` once the client no longer wants the execution.\n */\nexport type CodemodeRemoteReply = HostToWorkerMessage | { type: \"stop\" } | null;\n\nexport type CodemodeRemoteExchange = (messages: CodemodeRemoteMessage[]) => Promise<CodemodeRemoteReply>;\n\n/**\n * The remote end, usually a Durable Object stub for `CodemodeSandboxDurableObject`. Anything with\n * this method works, for example an in-process object in tests.\n */\nexport interface CodemodeRemote {\n\texecute(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange): Promise<void>;\n}\n\nexport interface RemoteCodemodeSandboxOptions extends CodemodeSandboxBaseOptions {\n\t/**\n\t * Returns the remote that runs one execution, called once per `execute()`. For a Durable Object,\n\t * pick the instance here, for example `() => env.CODEMODE_SANDBOX.getByName(sessionId)`.\n\t */\n\tremote: () => CodemodeRemote;\n\t/** Interrupt budget requested from the remote. Default: the remote's default. */\n\tinterruptBudget?: number;\n}\n\n/** Output buffered on the remote before it is flushed without waiting for a call. */\nconst FLUSH_ITEMS = 1_000;\nconst FLUSH_CHARS = 1 << 20;\n\nexport interface CodemodeServeOptions {\n\twasm: CodemodeWasmModule | Promise<CodemodeWasmModule>;\n\t/** Upper bound for a request's interrupt budget, and the budget when a request names none. */\n\tmaxInterruptBudget?: number;\n\t/** Upper bound for a request's VM memory, and the limit when a request names none. */\n\tmaxMemoryLimitBytes?: number;\n}\n\n/**\n * Remote side of {@link RemoteCodemodeSandbox}: runs one execution's VM on this thread and\n * forwards everything it reports through `exchange`. Resolves once the VM has stopped and the\n * terminal message has been delivered, or the client asked to stop. Never rejects.\n */\nexport function serveCodemodeRemote(\n\trequest: CodemodeRemoteRequest,\n\texchange: CodemodeRemoteExchange,\n\toptions: CodemodeServeOptions,\n): Promise<void> {\n\tconst maxBudget = options.maxInterruptBudget ?? DEFAULT_INTERRUPT_BUDGET;\n\tconst budget = Math.min(request.interruptBudget ?? maxBudget, maxBudget);\n\tconst requestedMemory = request.data.memoryLimitBytes;\n\tconst maxMemory = options.maxMemoryLimitBytes;\n\tconst memoryLimitBytes =\n\t\trequestedMemory === undefined\n\t\t\t? maxMemory\n\t\t\t: maxMemory === undefined\n\t\t\t\t? requestedMemory\n\t\t\t\t: Math.min(requestedMemory, maxMemory);\n\tconst data: VmStartData = { ...request.data, memoryLimitBytes };\n\n\treturn new Promise<void>((resolve) => {\n\t\tlet channel: VmChannel | undefined;\n\t\tlet ended = false;\n\t\tlet buffered: CodemodeRemoteMessage[] = [];\n\t\tlet bufferedChars = 0;\n\t\t/** Replies that arrived before the launcher returned the channel. */\n\t\tconst unsent: HostToWorkerMessage[] = [];\n\n\t\tconst takeBuffer = (): CodemodeRemoteMessage[] => {\n\t\t\tconst batch = buffered;\n\t\t\tbuffered = [];\n\t\t\tbufferedChars = 0;\n\t\t\treturn batch;\n\t\t};\n\t\tconst end = (terminal: CodemodeRemoteMessage | undefined): void => {\n\t\t\tif (ended) return;\n\t\t\tended = true;\n\t\t\tconst batch = takeBuffer();\n\t\t\tconst delivered = terminal ? exchange([...batch, terminal]).catch(() => null) : Promise.resolve();\n\t\t\tvoid delivered\n\t\t\t\t.then(() => channel?.stop())\n\t\t\t\t.catch(() => undefined)\n\t\t\t\t.then(() => resolve());\n\t\t};\n\t\tconst reply = (message: CodemodeRemoteReply): void => {\n\t\t\tif (ended || message === null) return;\n\t\t\tif (message.type === \"stop\") {\n\t\t\t\tend(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (channel) channel.post(message);\n\t\t\telse unsent.push(message);\n\t\t};\n\t\tconst onMessage = (message: unknown): void => {\n\t\t\tif (ended) return;\n\t\t\tconst typed = message as WorkerToHostMessage;\n\t\t\tswitch (typed.type) {\n\t\t\t\tcase \"output\": {\n\t\t\t\t\tbuffered.push(typed);\n\t\t\t\t\tbufferedChars += typed.item.type === \"text\" ? typed.item.text.length : typed.item.data.length;\n\t\t\t\t\tif (buffered.length >= FLUSH_ITEMS || bufferedChars >= FLUSH_CHARS) {\n\t\t\t\t\t\texchange(takeBuffer()).then(reply, () => end(undefined));\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"started\":\n\t\t\t\t\tbuffered.push(typed);\n\t\t\t\t\tbreak;\n\t\t\t\t// Sent at once: a host replaying a script waits for it to see that the script issued no further call.\n\t\t\t\tcase \"call\":\n\t\t\t\tcase \"idle\":\n\t\t\t\t\texchange([...takeBuffer(), typed]).then(reply, () => end(undefined));\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tend(typed);\n\t\t\t}\n\t\t};\n\n\t\tconst launch = inlineLauncher(options.wasm, budget);\n\t\tPromise.resolve(\n\t\t\tlaunch(data, {\n\t\t\t\tmessage: onMessage,\n\t\t\t\tfailure: (error) => end({ type: \"failure\", error }),\n\t\t\t}),\n\t\t).then(\n\t\t\t(started) => {\n\t\t\t\tchannel = started;\n\t\t\t\tif (ended) {\n\t\t\t\t\tvoid started.stop().catch(() => undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const message of unsent.splice(0)) started.post(message);\n\t\t\t},\n\t\t\t(error: unknown) => end({ type: \"failure\", error: { kind: \"sandbox\", message: errorMessage(error) } }),\n\t\t);\n\t});\n}\n\nfunction remoteLauncher(remote: () => CodemodeRemote, interruptBudget: number | undefined): VmLauncher {\n\treturn (data, events) => {\n\t\tlet stopped = false;\n\t\tlet settled = false;\n\t\tconst waiting = new Map<number, (reply: CodemodeRemoteReply) => void>();\n\n\t\tconst exchange: CodemodeRemoteExchange = (messages) =>\n\t\t\tnew Promise<CodemodeRemoteReply>((resolve) => {\n\t\t\t\tif (stopped || !Array.isArray(messages)) {\n\t\t\t\t\tresolve({ type: \"stop\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Register the waiter first: the host may answer a call synchronously, for example when the\n\t\t\t\t// tool throws before returning a promise.\n\t\t\t\tconst call = messages.find((message) => isWorkerToHostMessage(message) && message.type === \"call\");\n\t\t\t\tif (call?.type === \"call\") waiting.set(call.id, resolve);\n\t\t\t\telse resolve(null);\n\t\t\t\tfor (const message of messages) {\n\t\t\t\t\tif (typeof message === \"object\" && message !== null && message.type === \"failure\") {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tevents.failure(message.error);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// Malformed messages still go to the host, which fails the execution as a broken bridge.\n\t\t\t\t\tevents.message(message);\n\t\t\t\t\tif (isWorkerToHostMessage(message) && (message.type === \"done\" || message.type === \"crash\"))\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t}\n\t\t\t});\n\n\t\tlet target: CodemodeRemote;\n\t\ttry {\n\t\t\ttarget = remote();\n\t\t} catch (error) {\n\t\t\tthrow new Error(`Failed to reach the remote sandbox: ${errorMessage(error)}`);\n\t\t}\n\t\tconst request: CodemodeRemoteRequest = interruptBudget === undefined ? { data } : { data, interruptBudget };\n\t\tPromise.resolve()\n\t\t\t.then(() => target.execute(request, exchange))\n\t\t\t.then(\n\t\t\t\t() => {\n\t\t\t\t\tif (!settled && !stopped) {\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: \"Remote sandbox returned before the script settled\" });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t(error: unknown) => {\n\t\t\t\t\tif (!stopped)\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: `Remote sandbox failed: ${errorMessage(error)}` });\n\t\t\t\t},\n\t\t\t);\n\n\t\treturn {\n\t\t\tpost: (message) => {\n\t\t\t\tconst resolve = waiting.get(message.id);\n\t\t\t\tif (!resolve) return;\n\t\t\t\twaiting.delete(message.id);\n\t\t\t\tresolve(message);\n\t\t\t},\n\t\t\tstop: async () => {\n\t\t\t\tstopped = true;\n\t\t\t\tfor (const resolve of waiting.values()) resolve({ type: \"stop\" });\n\t\t\t\twaiting.clear();\n\t\t\t},\n\t\t};\n\t};\n}\n\n/**\n * Runs each execution's VM on a remote, usually a Cloudflare Durable Object deployed with\n * `CodemodeSandboxDurableObject`, while tools run here. Same API and results as\n * `CodemodeSandbox`, so it can replace it. Differences: a script that computes without calling a\n * tool is limited by the remote's interrupt budget rather than `timeoutMs`, and after a deadline or\n * abort the remote VM keeps computing until it next calls a tool or uses up that budget.\n */\nexport class RemoteCodemodeSandbox extends CodemodeSandboxBase {\n\tconstructor(options: RemoteCodemodeSandboxOptions) {\n\t\tsuper(options, remoteLauncher(options.remote, options.interruptBudget));\n\t}\n}\n"]}
@@ -15,6 +15,8 @@ export interface StartVmOptions {
15
15
  * (512 KiB), which fits the 4 MB stack of a Node worker thread but not a 1 MB main thread.
16
16
  */
17
17
  maxStackSize?: number;
18
+ /** Interrupt polls this VM may use, if the launcher counts them; part of the fingerprint. */
19
+ interruptBudget?: number;
18
20
  post(message: WorkerToHostMessage): void;
19
21
  shouldInterrupt(): boolean;
20
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../src/runtime/vm.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,gFAAgF;IAChF,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzC,eAAe,IAAI,OAAO,CAAC;CAC3B;AAmCD,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAiIzE"}
1
+ {"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../src/runtime/vm.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,gFAAgF;IAChF,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzC,eAAe,IAAI,OAAO,CAAC;CAC3B;AAiGD,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAqLzE"}
@@ -19,9 +19,77 @@
19
19
  *
20
20
  * Failures after the VM exists are reported as a `crash` message; `startVm` only rejects when the
21
21
  * VM cannot be created at all.
22
+ *
23
+ * Deterministic mode (`data.deterministic`, used by `CodemodeSandboxBase.start()`): a script must make the same
24
+ * calls with the same arguments every time it runs with the same results, so a host can replay it after a restart.
25
+ * The VM is otherwise deterministic, because it only runs when the host delivers something and has no timers.
26
+ * What remains is fixed here:
27
+ * - Time: every WASI clock reads virtual time, `startedAt` and then the `now` of the result being processed, never going backwards.
28
+ * While the context is created, the realtime clock reads a value derived from the seed instead, because QuickJS
29
+ * seeds `Math.random()` from the first clock read, and which read comes first is the engine's business.
30
+ * - Randomness: `random_get` draws from a PRNG over the seed.
31
+ * - Timezone: UTC, not the host's.
32
+ * - Garbage collection: `WeakRef` and `FinalizationRegistry` are not available; collection timing is not.
33
+ * - Delivery order: results carry a `seq` and are applied strictly in that order, buffering early ones, because a
34
+ * transport (RPC replies) may deliver them in another order than the host sent them.
35
+ * The VM reports `started` with its fingerprint before the script runs, and `idle` with the number of calls issued
36
+ * after each drain, so the host can check a replay step by step.
22
37
  */
23
- import { JSException, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
38
+ import { Intrinsics, JSException, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
24
39
  import { PRELUDE_SOURCE } from "./prelude-source.js";
40
+ /**
41
+ * Bumped whenever this module or the prelude changes what a deterministic script observes, beyond what the
42
+ * fingerprint's other parts (engine versions, prelude hash, limits) already capture.
43
+ */
44
+ const DETERMINISTIC_VERSION = 1;
45
+ /** FNV-1a over UTF-16 code units, as 8 hex digits: identifies the prelude in the fingerprint. */
46
+ function fnv1a(text) {
47
+ let hash = 0x811c9dc5;
48
+ for (let index = 0; index < text.length; index++) {
49
+ hash ^= text.charCodeAt(index);
50
+ hash = Math.imul(hash, 0x01000193);
51
+ }
52
+ return (hash >>> 0).toString(16).padStart(8, "0");
53
+ }
54
+ const PRELUDE_HASH = fnv1a(PRELUDE_SOURCE);
55
+ /** mulberry32: a small, fast PRNG; one 32-bit draw per call. */
56
+ function prng(seed) {
57
+ let state = seed >>> 0;
58
+ return () => {
59
+ state = (state + 0x6d2b79f5) >>> 0;
60
+ let value = Math.imul(state ^ (state >>> 15), 1 | state);
61
+ value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
62
+ return (value ^ (value >>> 14)) >>> 0;
63
+ };
64
+ }
65
+ /** WASI clock and random functions for deterministic mode, on top of {@link discardOutput}. */
66
+ function deterministicWasi(clock) {
67
+ const random = prng(clock.seed ^ 0x9e3779b9);
68
+ return (memory) => ({
69
+ ...discardOutput(memory),
70
+ clock_time_get(clockId, _precision, resultPtr) {
71
+ let nanoseconds;
72
+ // QuickJS seeds Math.random() from this reading in microseconds and turns 0 into 1, so seed s reads s + 1.
73
+ if (clock.creating)
74
+ nanoseconds = clockId === 0 ? BigInt(clock.seed + 1) * 1000n : 0n;
75
+ else if (clockId === 0)
76
+ nanoseconds = BigInt(Math.round(clock.now)) * 1000000n;
77
+ else
78
+ nanoseconds = BigInt(Math.round(clock.now - clock.startedAt)) * 1000000n;
79
+ new DataView(memory.buffer).setBigUint64(resultPtr, nanoseconds, true);
80
+ return 0;
81
+ },
82
+ random_get(bufPtr, bufLen) {
83
+ const bytes = new Uint8Array(memory.buffer, bufPtr, bufLen);
84
+ for (let index = 0; index < bufLen; index += 4) {
85
+ const value = random();
86
+ for (let byte = 0; byte < 4 && index + byte < bufLen; byte++)
87
+ bytes[index + byte] = value >>> (byte * 8);
88
+ }
89
+ return 0;
90
+ },
91
+ });
92
+ }
25
93
  /**
26
94
  * QuickJS writes engine diagnostics to fd 1 and 2, which the default shim
27
95
  * forwards to the host's stdout and stderr. That output belongs to the host
@@ -53,25 +121,64 @@ function crashMessage(error) {
53
121
  }
54
122
  export async function startVm(options) {
55
123
  const { data, post } = options;
124
+ const maxStackSize = options.maxStackSize ?? MAX_STACK_SIZE;
125
+ const deterministic = data.deterministic;
126
+ const clock = deterministic && {
127
+ creating: true,
128
+ startedAt: deterministic.startedAt,
129
+ now: deterministic.startedAt,
130
+ seed: deterministic.seed >>> 0,
131
+ };
56
132
  const vm = await QuickJS.create({
57
133
  wasm: options.wasm,
58
134
  memoryLimit: data.memoryLimitBytes,
59
135
  // Without a guard, deep recursion overflows the wasm stack and traps instead of throwing a
60
136
  // catchable RangeError.
61
- maxStackSize: options.maxStackSize ?? MAX_STACK_SIZE,
137
+ maxStackSize,
62
138
  interruptHandler: options.shouldInterrupt,
63
- wasi: discardOutput,
139
+ wasi: clock ? deterministicWasi(clock) : discardOutput,
140
+ ...(clock ? { timezoneOffset: 0, intrinsics: Intrinsics.ALL & ~Intrinsics.WEAK_REF } : {}),
64
141
  });
142
+ if (clock)
143
+ clock.creating = false;
65
144
  let disposed = false;
145
+ /** Calls the script has issued, tools and globals; reported with every `idle`. */
146
+ let issued = 0;
147
+ /** Deterministic mode: the next result to apply, and results that arrived before it. */
148
+ let nextSeq = 1;
149
+ /** The result being processed, 0 during the script's first run; reported with `done`. */
150
+ let current = 0;
151
+ const early = new Map();
152
+ const apply = (message) => {
153
+ vm.withScope(() => {
154
+ vm.callFunction(settle, api, vm.newNumber(message.id), message.ok ? vm.true : vm.false, message.payload === undefined ? vm.undefined : vm.newString(message.payload), message.properties === undefined ? vm.undefined : vm.newString(message.properties));
155
+ });
156
+ drain();
157
+ };
66
158
  const session = {
67
159
  deliver(message) {
68
160
  if (disposed)
69
161
  return;
70
162
  try {
71
- vm.withScope(() => {
72
- vm.callFunction(settle, api, vm.newNumber(message.id), message.ok ? vm.true : vm.false, message.payload === undefined ? vm.undefined : vm.newString(message.payload));
73
- });
74
- drain();
163
+ if (!clock) {
164
+ apply(message);
165
+ return;
166
+ }
167
+ const { seq } = message;
168
+ if (seq === undefined || !Number.isInteger(seq) || seq < nextSeq || early.has(seq)) {
169
+ throw new Error(`Result ${message.id} has sequence number ${seq}, expected ${nextSeq} or later, once`);
170
+ }
171
+ early.set(seq, message);
172
+ for (let next = early.get(nextSeq); next !== undefined; next = early.get(nextSeq)) {
173
+ early.delete(nextSeq);
174
+ // Never backwards: a resumed host's clock may be behind the one that started the run.
175
+ if (next.now !== undefined && Number.isFinite(next.now))
176
+ clock.now = Math.max(clock.now, next.now);
177
+ current = nextSeq;
178
+ apply(next);
179
+ post({ type: "idle", seq: nextSeq, issued });
180
+ nextSeq++;
181
+ }
75
182
  }
76
183
  catch (error) {
77
184
  post(crashMessage(error));
@@ -108,6 +215,7 @@ export async function startVm(options) {
108
215
  switch (kind.toString()) {
109
216
  case "call":
110
217
  case "global":
218
+ issued++;
111
219
  post({
112
220
  type: "call",
113
221
  id: a.toNumber(),
@@ -126,19 +234,23 @@ export async function startVm(options) {
126
234
  : { type: "text", text: b.toString() },
127
235
  });
128
236
  break;
129
- case "done":
237
+ case "done": {
238
+ // The host stops the VM on `done`, so the `idle` of this drain never arrives: report its numbers here.
239
+ const during = clock ? { during: { seq: current, issued } } : {};
130
240
  if (a.toBoolean()) {
131
241
  post({
132
242
  type: "done",
133
243
  ok: true,
134
244
  value: b === undefined || b.isUndefined ? undefined : b.toString(),
135
245
  writes: c.toString(),
246
+ ...during,
136
247
  });
137
248
  }
138
249
  else {
139
- post({ type: "done", ok: false, error: b.toString() });
250
+ post({ type: "done", ok: false, error: b.toString(), ...during });
140
251
  }
141
252
  break;
253
+ }
142
254
  }
143
255
  return vm.undefined;
144
256
  });
@@ -147,6 +259,14 @@ export async function startVm(options) {
147
259
  settle = api.getProp("settle");
148
260
  const run = api.getProp("run");
149
261
  stalled = api.getProp("stalled");
262
+ if (clock) {
263
+ const versions = vm.versions;
264
+ const budget = options.interruptBudget ?? "none";
265
+ post({
266
+ type: "started",
267
+ fingerprint: `quickjs-wasi ${versions["quickjs-wasi"]}, quickjs ${versions.quickjs}, prelude ${PRELUDE_HASH}, deterministic ${DETERMINISTIC_VERSION}, stack ${maxStackSize}, heap ${data.memoryLimitBytes ?? "none"}, budget ${budget}`,
268
+ });
269
+ }
150
270
  // The prefix shares the first line with the script so reported line numbers
151
271
  // match the script as written.
152
272
  let fn;
@@ -162,6 +282,8 @@ export async function startVm(options) {
162
282
  vm.callFunction(run, api, fn).dispose();
163
283
  fn.dispose();
164
284
  drain();
285
+ if (clock)
286
+ post({ type: "idle", seq: 0, issued });
165
287
  }
166
288
  catch (error) {
167
289
  post(crashMessage(error));
@@ -1 +1 @@
1
- {"version":3,"file":"vm.js","sourceRoot":"","sources":["../../src/runtime/vm.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Drives one QuickJS VM through one script, independent of the transport that connects it to the\n * host. The worker thread, the in-thread launcher, and the remote server all run the VM through\n * this module, so the bridge between prelude and host exists once.\n *\n * Contract with the caller:\n * - `post` receives every message for the host, synchronously from inside VM callbacks. The\n * caller must not re-enter the VM from `post` (no `deliver`, no `dispose`); a worker posts to\n * another thread, and the in-thread launcher defers delivery to a microtask.\n * - `deliver` hands the VM a tool result and runs the script until it waits again. It must only be\n * called while the VM is idle, which holds for any caller that never re-enters from `post`.\n * - `shouldInterrupt` is polled by QuickJS while the script runs, and here between batches of\n * promise jobs. Returning true throws an uncatchable error in the script; the VM then reports\n * `crash` or a failed `done`. The job batches matter for a script such as `while (true) await\n * null`: every iteration is a separate job that executes only a few opcodes, so QuickJS's own\n * polling, which counts opcodes, would let it run for a long time between polls.\n * - The VM never decides on its own when it is finished: after `done` or `crash` it stays alive\n * until the caller disposes it (or terminates the worker that hosts it).\n *\n * Failures after the VM exists are reported as a `crash` message; `startVm` only rejects when the\n * VM cannot be created at all.\n */\nimport { JSException, type JSValueHandle, MAX_STACK_SIZE, QuickJS } from \"quickjs-wasi\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\nimport { PRELUDE_SOURCE } from \"./prelude-source.ts\";\nimport type { HostToWorkerMessage, VmStartData, WorkerToHostMessage } from \"./protocol.ts\";\n\nexport interface VmSession {\n\tdeliver(message: HostToWorkerMessage): void;\n\t/** Releases the VM's wasm instance. The session must not be used afterwards. */\n\tdispose(): void;\n}\n\nexport interface StartVmOptions {\n\tdata: VmStartData;\n\twasm: CodemodeWasmModule;\n\t/**\n\t * QuickJS's guard on its own stack, which turns deep recursion into a catchable `RangeError`.\n\t * The wasm frames also use the host thread's native stack, so the guard must trip before that\n\t * stack runs out: a host overflow fails the whole execution instead. Default: `MAX_STACK_SIZE`\n\t * (512 KiB), which fits the 4 MB stack of a Node worker thread but not a 1 MB main thread.\n\t */\n\tmaxStackSize?: number;\n\tpost(message: WorkerToHostMessage): void;\n\tshouldInterrupt(): boolean;\n}\n\n/**\n * QuickJS writes engine diagnostics to fd 1 and 2, which the default shim\n * forwards to the host's stdout and stderr. That output belongs to the host\n * application (for example a TUI), so it is discarded. Reporting every byte as\n * written keeps libc from retrying.\n */\nfunction discardOutput(memory: { readonly buffer: ArrayBufferLike }) {\n\treturn {\n\t\tfd_write(_fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number {\n\t\t\tconst view = new DataView(memory.buffer);\n\t\t\tlet written = 0;\n\t\t\tfor (let i = 0; i < iovsLen; i++) {\n\t\t\t\twritten += view.getUint32(iovsPtr + i * 8 + 4, true);\n\t\t\t}\n\t\t\tview.setUint32(nwrittenPtr, written, true);\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n\nfunction describeException(error: JSException): string {\n\tconst head = error.message ? `${error.name}: ${error.message}` : error.name;\n\tconst stack = error.stack?.trimEnd();\n\treturn JSON.stringify({ name: error.name, message: error.message, stack: stack ? `${head}\\n${stack}` : head });\n}\n\n/** Promise jobs run between two `shouldInterrupt` polls while draining the job queue. */\nconst JOBS_PER_POLL = 50;\n\nfunction crashMessage(error: unknown): WorkerToHostMessage {\n\treturn { type: \"crash\", message: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };\n}\n\nexport async function startVm(options: StartVmOptions): Promise<VmSession> {\n\tconst { data, post } = options;\n\tconst vm = await QuickJS.create({\n\t\twasm: options.wasm,\n\t\tmemoryLimit: data.memoryLimitBytes,\n\t\t// Without a guard, deep recursion overflows the wasm stack and traps instead of throwing a\n\t\t// catchable RangeError.\n\t\tmaxStackSize: options.maxStackSize ?? MAX_STACK_SIZE,\n\t\tinterruptHandler: options.shouldInterrupt,\n\t\twasi: discardOutput,\n\t});\n\tlet disposed = false;\n\tconst session: VmSession = {\n\t\tdeliver(message) {\n\t\t\tif (disposed) return;\n\t\t\ttry {\n\t\t\t\tvm.withScope(() => {\n\t\t\t\t\tvm.callFunction(\n\t\t\t\t\t\tsettle,\n\t\t\t\t\t\tapi,\n\t\t\t\t\t\tvm.newNumber(message.id),\n\t\t\t\t\t\tmessage.ok ? vm.true : vm.false,\n\t\t\t\t\t\tmessage.payload === undefined ? vm.undefined : vm.newString(message.payload),\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t\tdrain();\n\t\t\t} catch (error) {\n\t\t\t\tpost(crashMessage(error));\n\t\t\t}\n\t\t},\n\t\tdispose() {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tvm.dispose();\n\t\t},\n\t};\n\n\tlet api: JSValueHandle;\n\tlet settle: JSValueHandle;\n\tlet stalled: JSValueHandle;\n\t// `executePendingJobs()` runs the queue to empty without a chance to interrupt, so jobs run one at a\n\t// time through the (internal, but public) exports; quickjs-wasi is pinned to an exact version.\n\tconst exports = vm._getExports();\n\t/** Run queued jobs, then fail a script that waits on nothing that can ever resume it. */\n\tconst drain = () => {\n\t\tlet jobs = 0;\n\t\twhile (exports.qjs_is_job_pending()) {\n\t\t\t// A job only fails for uncatchable errors, such as an interrupt.\n\t\t\tif (exports.qjs_execute_pending_job() < 0) throw new Error(\"Promise job failed\");\n\t\t\tif (++jobs % JOBS_PER_POLL === 0 && options.shouldInterrupt()) throw new Error(\"Execution interrupted\");\n\t\t}\n\t\tvm.callFunction(stalled, api).dispose();\n\t};\n\n\ttry {\n\t\t// Called from the prelude with primitives only.\n\t\tconst bridge = vm.newFunction(\"bridge\", (kind, a, b, c) => {\n\t\t\tswitch (kind.toString()) {\n\t\t\t\tcase \"call\":\n\t\t\t\tcase \"global\":\n\t\t\t\t\tpost({\n\t\t\t\t\t\ttype: \"call\",\n\t\t\t\t\t\tid: a.toNumber(),\n\t\t\t\t\t\ttarget: kind.toString() === \"call\" ? \"tool\" : \"global\",\n\t\t\t\t\t\tname: b.toString(),\n\t\t\t\t\t\targs: c === undefined || c.isUndefined ? undefined : c.toString(),\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"output\":\n\t\t\t\t\tpost({\n\t\t\t\t\t\ttype: \"output\",\n\t\t\t\t\t\titem:\n\t\t\t\t\t\t\ta.toString() === \"image\"\n\t\t\t\t\t\t\t\t? { type: \"image\", data: b.toString(), mimeType: c.toString() }\n\t\t\t\t\t\t\t\t: a.toString() === \"console\"\n\t\t\t\t\t\t\t\t\t? { type: \"text\", text: b.toString(), console: true }\n\t\t\t\t\t\t\t\t\t: { type: \"text\", text: b.toString() },\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"done\":\n\t\t\t\t\tif (a.toBoolean()) {\n\t\t\t\t\t\tpost({\n\t\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tvalue: b === undefined || b.isUndefined ? undefined : b.toString(),\n\t\t\t\t\t\t\twrites: c.toString(),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpost({ type: \"done\", ok: false, error: b.toString() });\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\treturn vm.undefined;\n\t\t});\n\n\t\t// These handles live as long as the VM, which the caller disposes as a whole.\n\t\tapi = vm.withScope((scope) =>\n\t\t\tscope.escape(\n\t\t\t\tvm.callFunction(\n\t\t\t\t\tvm.evalCode(PRELUDE_SOURCE, \"codemode-prelude.js\"),\n\t\t\t\t\tvm.undefined,\n\t\t\t\t\tbridge,\n\t\t\t\t\tvm.newString(JSON.stringify(data.tools)),\n\t\t\t\t\tvm.newString(JSON.stringify(data.globals)),\n\t\t\t\t\tvm.newString(JSON.stringify(data.store)),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\tsettle = api.getProp(\"settle\");\n\t\tconst run = api.getProp(\"run\");\n\t\tstalled = api.getProp(\"stalled\");\n\n\t\t// The prefix shares the first line with the script so reported line numbers\n\t\t// match the script as written.\n\t\tlet fn: JSValueHandle;\n\t\ttry {\n\t\t\tfn = vm.evalCode(`(async (tools, console) => {${data.code}\\n})`, \"codemode.js\");\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof JSException)) throw error;\n\t\t\tpost({ type: \"done\", ok: false, error: describeException(error) });\n\t\t\treturn session;\n\t\t}\n\t\tvm.callFunction(run, api, fn).dispose();\n\t\tfn.dispose();\n\t\tdrain();\n\t} catch (error) {\n\t\tpost(crashMessage(error));\n\t}\n\treturn session;\n}\n"]}
1
+ {"version":3,"file":"vm.js","sourceRoot":"","sources":["../../src/runtime/vm.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Drives one QuickJS VM through one script, independent of the transport that connects it to the\n * host. The worker thread, the in-thread launcher, and the remote server all run the VM through\n * this module, so the bridge between prelude and host exists once.\n *\n * Contract with the caller:\n * - `post` receives every message for the host, synchronously from inside VM callbacks. The\n * caller must not re-enter the VM from `post` (no `deliver`, no `dispose`); a worker posts to\n * another thread, and the in-thread launcher defers delivery to a microtask.\n * - `deliver` hands the VM a tool result and runs the script until it waits again. It must only be\n * called while the VM is idle, which holds for any caller that never re-enters from `post`.\n * - `shouldInterrupt` is polled by QuickJS while the script runs, and here between batches of\n * promise jobs. Returning true throws an uncatchable error in the script; the VM then reports\n * `crash` or a failed `done`. The job batches matter for a script such as `while (true) await\n * null`: every iteration is a separate job that executes only a few opcodes, so QuickJS's own\n * polling, which counts opcodes, would let it run for a long time between polls.\n * - The VM never decides on its own when it is finished: after `done` or `crash` it stays alive\n * until the caller disposes it (or terminates the worker that hosts it).\n *\n * Failures after the VM exists are reported as a `crash` message; `startVm` only rejects when the\n * VM cannot be created at all.\n *\n * Deterministic mode (`data.deterministic`, used by `CodemodeSandboxBase.start()`): a script must make the same\n * calls with the same arguments every time it runs with the same results, so a host can replay it after a restart.\n * The VM is otherwise deterministic, because it only runs when the host delivers something and has no timers.\n * What remains is fixed here:\n * - Time: every WASI clock reads virtual time, `startedAt` and then the `now` of the result being processed, never going backwards.\n * While the context is created, the realtime clock reads a value derived from the seed instead, because QuickJS\n * seeds `Math.random()` from the first clock read, and which read comes first is the engine's business.\n * - Randomness: `random_get` draws from a PRNG over the seed.\n * - Timezone: UTC, not the host's.\n * - Garbage collection: `WeakRef` and `FinalizationRegistry` are not available; collection timing is not.\n * - Delivery order: results carry a `seq` and are applied strictly in that order, buffering early ones, because a\n * transport (RPC replies) may deliver them in another order than the host sent them.\n * The VM reports `started` with its fingerprint before the script runs, and `idle` with the number of calls issued\n * after each drain, so the host can check a replay step by step.\n */\nimport { Intrinsics, JSException, type JSValueHandle, MAX_STACK_SIZE, QuickJS } from \"quickjs-wasi\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\nimport { PRELUDE_SOURCE } from \"./prelude-source.ts\";\nimport type { HostToWorkerMessage, VmStartData, WorkerToHostMessage } from \"./protocol.ts\";\n\nexport interface VmSession {\n\tdeliver(message: HostToWorkerMessage): void;\n\t/** Releases the VM's wasm instance. The session must not be used afterwards. */\n\tdispose(): void;\n}\n\nexport interface StartVmOptions {\n\tdata: VmStartData;\n\twasm: CodemodeWasmModule;\n\t/**\n\t * QuickJS's guard on its own stack, which turns deep recursion into a catchable `RangeError`.\n\t * The wasm frames also use the host thread's native stack, so the guard must trip before that\n\t * stack runs out: a host overflow fails the whole execution instead. Default: `MAX_STACK_SIZE`\n\t * (512 KiB), which fits the 4 MB stack of a Node worker thread but not a 1 MB main thread.\n\t */\n\tmaxStackSize?: number;\n\t/** Interrupt polls this VM may use, if the launcher counts them; part of the fingerprint. */\n\tinterruptBudget?: number;\n\tpost(message: WorkerToHostMessage): void;\n\tshouldInterrupt(): boolean;\n}\n\n/**\n * Bumped whenever this module or the prelude changes what a deterministic script observes, beyond what the\n * fingerprint's other parts (engine versions, prelude hash, limits) already capture.\n */\nconst DETERMINISTIC_VERSION = 1;\n\n/** FNV-1a over UTF-16 code units, as 8 hex digits: identifies the prelude in the fingerprint. */\nfunction fnv1a(text: string): string {\n\tlet hash = 0x811c9dc5;\n\tfor (let index = 0; index < text.length; index++) {\n\t\thash ^= text.charCodeAt(index);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\nconst PRELUDE_HASH = fnv1a(PRELUDE_SOURCE);\n\n/** mulberry32: a small, fast PRNG; one 32-bit draw per call. */\nfunction prng(seed: number): () => number {\n\tlet state = seed >>> 0;\n\treturn () => {\n\t\tstate = (state + 0x6d2b79f5) >>> 0;\n\t\tlet value = Math.imul(state ^ (state >>> 15), 1 | state);\n\t\tvalue = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;\n\t\treturn (value ^ (value >>> 14)) >>> 0;\n\t};\n}\n\n/** The deterministic VM's clock: seed-derived while the context is created, then virtual time. */\ninterface VirtualClock {\n\tcreating: boolean;\n\tstartedAt: number;\n\tnow: number;\n\tseed: number;\n}\n\n/** WASI clock and random functions for deterministic mode, on top of {@link discardOutput}. */\nfunction deterministicWasi(clock: VirtualClock) {\n\tconst random = prng(clock.seed ^ 0x9e3779b9);\n\treturn (memory: { readonly buffer: ArrayBufferLike }) => ({\n\t\t...discardOutput(memory),\n\t\tclock_time_get(clockId: number, _precision: bigint, resultPtr: number): number {\n\t\t\tlet nanoseconds: bigint;\n\t\t\t// QuickJS seeds Math.random() from this reading in microseconds and turns 0 into 1, so seed s reads s + 1.\n\t\t\tif (clock.creating) nanoseconds = clockId === 0 ? BigInt(clock.seed + 1) * 1000n : 0n;\n\t\t\telse if (clockId === 0) nanoseconds = BigInt(Math.round(clock.now)) * 1_000_000n;\n\t\t\telse nanoseconds = BigInt(Math.round(clock.now - clock.startedAt)) * 1_000_000n;\n\t\t\tnew DataView(memory.buffer).setBigUint64(resultPtr, nanoseconds, true);\n\t\t\treturn 0;\n\t\t},\n\t\trandom_get(bufPtr: number, bufLen: number): number {\n\t\t\tconst bytes = new Uint8Array(memory.buffer, bufPtr, bufLen);\n\t\t\tfor (let index = 0; index < bufLen; index += 4) {\n\t\t\t\tconst value = random();\n\t\t\t\tfor (let byte = 0; byte < 4 && index + byte < bufLen; byte++) bytes[index + byte] = value >>> (byte * 8);\n\t\t\t}\n\t\t\treturn 0;\n\t\t},\n\t});\n}\n\n/**\n * QuickJS writes engine diagnostics to fd 1 and 2, which the default shim\n * forwards to the host's stdout and stderr. That output belongs to the host\n * application (for example a TUI), so it is discarded. Reporting every byte as\n * written keeps libc from retrying.\n */\nfunction discardOutput(memory: { readonly buffer: ArrayBufferLike }) {\n\treturn {\n\t\tfd_write(_fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number {\n\t\t\tconst view = new DataView(memory.buffer);\n\t\t\tlet written = 0;\n\t\t\tfor (let i = 0; i < iovsLen; i++) {\n\t\t\t\twritten += view.getUint32(iovsPtr + i * 8 + 4, true);\n\t\t\t}\n\t\t\tview.setUint32(nwrittenPtr, written, true);\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n\nfunction describeException(error: JSException): string {\n\tconst head = error.message ? `${error.name}: ${error.message}` : error.name;\n\tconst stack = error.stack?.trimEnd();\n\treturn JSON.stringify({ name: error.name, message: error.message, stack: stack ? `${head}\\n${stack}` : head });\n}\n\n/** Promise jobs run between two `shouldInterrupt` polls while draining the job queue. */\nconst JOBS_PER_POLL = 50;\n\nfunction crashMessage(error: unknown): WorkerToHostMessage {\n\treturn { type: \"crash\", message: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };\n}\n\nexport async function startVm(options: StartVmOptions): Promise<VmSession> {\n\tconst { data, post } = options;\n\tconst maxStackSize = options.maxStackSize ?? MAX_STACK_SIZE;\n\tconst deterministic = data.deterministic;\n\tconst clock: VirtualClock | undefined = deterministic && {\n\t\tcreating: true,\n\t\tstartedAt: deterministic.startedAt,\n\t\tnow: deterministic.startedAt,\n\t\tseed: deterministic.seed >>> 0,\n\t};\n\tconst vm = await QuickJS.create({\n\t\twasm: options.wasm,\n\t\tmemoryLimit: data.memoryLimitBytes,\n\t\t// Without a guard, deep recursion overflows the wasm stack and traps instead of throwing a\n\t\t// catchable RangeError.\n\t\tmaxStackSize,\n\t\tinterruptHandler: options.shouldInterrupt,\n\t\twasi: clock ? deterministicWasi(clock) : discardOutput,\n\t\t...(clock ? { timezoneOffset: 0, intrinsics: Intrinsics.ALL & ~Intrinsics.WEAK_REF } : {}),\n\t});\n\tif (clock) clock.creating = false;\n\tlet disposed = false;\n\t/** Calls the script has issued, tools and globals; reported with every `idle`. */\n\tlet issued = 0;\n\t/** Deterministic mode: the next result to apply, and results that arrived before it. */\n\tlet nextSeq = 1;\n\t/** The result being processed, 0 during the script's first run; reported with `done`. */\n\tlet current = 0;\n\tconst early = new Map<number, HostToWorkerMessage>();\n\tconst apply = (message: HostToWorkerMessage): void => {\n\t\tvm.withScope(() => {\n\t\t\tvm.callFunction(\n\t\t\t\tsettle,\n\t\t\t\tapi,\n\t\t\t\tvm.newNumber(message.id),\n\t\t\t\tmessage.ok ? vm.true : vm.false,\n\t\t\t\tmessage.payload === undefined ? vm.undefined : vm.newString(message.payload),\n\t\t\t\tmessage.properties === undefined ? vm.undefined : vm.newString(message.properties),\n\t\t\t);\n\t\t});\n\t\tdrain();\n\t};\n\tconst session: VmSession = {\n\t\tdeliver(message) {\n\t\t\tif (disposed) return;\n\t\t\ttry {\n\t\t\t\tif (!clock) {\n\t\t\t\t\tapply(message);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { seq } = message;\n\t\t\t\tif (seq === undefined || !Number.isInteger(seq) || seq < nextSeq || early.has(seq)) {\n\t\t\t\t\tthrow new Error(`Result ${message.id} has sequence number ${seq}, expected ${nextSeq} or later, once`);\n\t\t\t\t}\n\t\t\t\tearly.set(seq, message);\n\t\t\t\tfor (let next = early.get(nextSeq); next !== undefined; next = early.get(nextSeq)) {\n\t\t\t\t\tearly.delete(nextSeq);\n\t\t\t\t\t// Never backwards: a resumed host's clock may be behind the one that started the run.\n\t\t\t\t\tif (next.now !== undefined && Number.isFinite(next.now)) clock.now = Math.max(clock.now, next.now);\n\t\t\t\t\tcurrent = nextSeq;\n\t\t\t\t\tapply(next);\n\t\t\t\t\tpost({ type: \"idle\", seq: nextSeq, issued });\n\t\t\t\t\tnextSeq++;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tpost(crashMessage(error));\n\t\t\t}\n\t\t},\n\t\tdispose() {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tvm.dispose();\n\t\t},\n\t};\n\n\tlet api: JSValueHandle;\n\tlet settle: JSValueHandle;\n\tlet stalled: JSValueHandle;\n\t// `executePendingJobs()` runs the queue to empty without a chance to interrupt, so jobs run one at a\n\t// time through the (internal, but public) exports; quickjs-wasi is pinned to an exact version.\n\tconst exports = vm._getExports();\n\t/** Run queued jobs, then fail a script that waits on nothing that can ever resume it. */\n\tconst drain = () => {\n\t\tlet jobs = 0;\n\t\twhile (exports.qjs_is_job_pending()) {\n\t\t\t// A job only fails for uncatchable errors, such as an interrupt.\n\t\t\tif (exports.qjs_execute_pending_job() < 0) throw new Error(\"Promise job failed\");\n\t\t\tif (++jobs % JOBS_PER_POLL === 0 && options.shouldInterrupt()) throw new Error(\"Execution interrupted\");\n\t\t}\n\t\tvm.callFunction(stalled, api).dispose();\n\t};\n\n\ttry {\n\t\t// Called from the prelude with primitives only.\n\t\tconst bridge = vm.newFunction(\"bridge\", (kind, a, b, c) => {\n\t\t\tswitch (kind.toString()) {\n\t\t\t\tcase \"call\":\n\t\t\t\tcase \"global\":\n\t\t\t\t\tissued++;\n\t\t\t\t\tpost({\n\t\t\t\t\t\ttype: \"call\",\n\t\t\t\t\t\tid: a.toNumber(),\n\t\t\t\t\t\ttarget: kind.toString() === \"call\" ? \"tool\" : \"global\",\n\t\t\t\t\t\tname: b.toString(),\n\t\t\t\t\t\targs: c === undefined || c.isUndefined ? undefined : c.toString(),\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"output\":\n\t\t\t\t\tpost({\n\t\t\t\t\t\ttype: \"output\",\n\t\t\t\t\t\titem:\n\t\t\t\t\t\t\ta.toString() === \"image\"\n\t\t\t\t\t\t\t\t? { type: \"image\", data: b.toString(), mimeType: c.toString() }\n\t\t\t\t\t\t\t\t: a.toString() === \"console\"\n\t\t\t\t\t\t\t\t\t? { type: \"text\", text: b.toString(), console: true }\n\t\t\t\t\t\t\t\t\t: { type: \"text\", text: b.toString() },\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"done\": {\n\t\t\t\t\t// The host stops the VM on `done`, so the `idle` of this drain never arrives: report its numbers here.\n\t\t\t\t\tconst during = clock ? { during: { seq: current, issued } } : {};\n\t\t\t\t\tif (a.toBoolean()) {\n\t\t\t\t\t\tpost({\n\t\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tvalue: b === undefined || b.isUndefined ? undefined : b.toString(),\n\t\t\t\t\t\t\twrites: c.toString(),\n\t\t\t\t\t\t\t...during,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpost({ type: \"done\", ok: false, error: b.toString(), ...during });\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn vm.undefined;\n\t\t});\n\n\t\t// These handles live as long as the VM, which the caller disposes as a whole.\n\t\tapi = vm.withScope((scope) =>\n\t\t\tscope.escape(\n\t\t\t\tvm.callFunction(\n\t\t\t\t\tvm.evalCode(PRELUDE_SOURCE, \"codemode-prelude.js\"),\n\t\t\t\t\tvm.undefined,\n\t\t\t\t\tbridge,\n\t\t\t\t\tvm.newString(JSON.stringify(data.tools)),\n\t\t\t\t\tvm.newString(JSON.stringify(data.globals)),\n\t\t\t\t\tvm.newString(JSON.stringify(data.store)),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\tsettle = api.getProp(\"settle\");\n\t\tconst run = api.getProp(\"run\");\n\t\tstalled = api.getProp(\"stalled\");\n\t\tif (clock) {\n\t\t\tconst versions = vm.versions;\n\t\t\tconst budget = options.interruptBudget ?? \"none\";\n\t\t\tpost({\n\t\t\t\ttype: \"started\",\n\t\t\t\tfingerprint: `quickjs-wasi ${versions[\"quickjs-wasi\"]}, quickjs ${versions.quickjs}, prelude ${PRELUDE_HASH}, deterministic ${DETERMINISTIC_VERSION}, stack ${maxStackSize}, heap ${data.memoryLimitBytes ?? \"none\"}, budget ${budget}`,\n\t\t\t});\n\t\t}\n\n\t\t// The prefix shares the first line with the script so reported line numbers\n\t\t// match the script as written.\n\t\tlet fn: JSValueHandle;\n\t\ttry {\n\t\t\tfn = vm.evalCode(`(async (tools, console) => {${data.code}\\n})`, \"codemode.js\");\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof JSException)) throw error;\n\t\t\tpost({ type: \"done\", ok: false, error: describeException(error) });\n\t\t\treturn session;\n\t\t}\n\t\tvm.callFunction(run, api, fn).dispose();\n\t\tfn.dispose();\n\t\tdrain();\n\t\tif (clock) post({ type: \"idle\", seq: 0, issued });\n\t} catch (error) {\n\t\tpost(crashMessage(error));\n\t}\n\treturn session;\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Tool discovery ranking shared by every host that gives scripts `searchTools()` or the model a `tool_search` tool: a
3
+ * BM25 ranker over tool metadata. The host decides which tools are candidates and builds each tool's search text; this
4
+ * module only ranks.
5
+ */
6
+ /** A tool as the ranker sees it: its name and its search text. */
7
+ export interface ToolSearchDocument {
8
+ name: string;
9
+ text: string;
10
+ }
11
+ export interface ToolSearchMatch {
12
+ name: string;
13
+ score: number;
14
+ }
15
+ /** Ranks tools for a query. BM25 today; a hybrid ranker with embeddings can replace it. */
16
+ export interface ToolRanker {
17
+ rank(query: string, documents: readonly ToolSearchDocument[], limit: number): ToolSearchMatch[];
18
+ }
19
+ /** Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words. */
20
+ export declare function tokenize(text: string): string[];
21
+ /** Appends the descriptions and property names of a JSON Schema, recursively, to `parts`. */
22
+ export declare function collectSchemaText(schema: unknown, parts: string[]): void;
23
+ /** Okapi BM25 with the usual parameters. Ties keep document order. */
24
+ export declare class Bm25Ranker implements ToolRanker {
25
+ private readonly k1;
26
+ private readonly b;
27
+ constructor(options?: {
28
+ k1?: number;
29
+ b?: number;
30
+ });
31
+ rank(query: string, documents: readonly ToolSearchDocument[], limit: number): ToolSearchMatch[];
32
+ }
33
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kEAAkE;AAClE,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IAC1B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,kBAAkB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAAC;CAChG;AAkCD,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ/C;AAMD,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAcxE;AAED,sEAAsE;AACtE,qBAAa,UAAW,YAAW,UAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAS;IAE3B,YAAY,OAAO,GAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAO,EAGpD;IAED,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,kBAAkB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CA4B9F;CACD"}
package/dist/search.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tool discovery ranking shared by every host that gives scripts `searchTools()` or the model a `tool_search` tool: a
3
+ * BM25 ranker over tool metadata. The host decides which tools are candidates and builds each tool's search text; this
4
+ * module only ranks.
5
+ */
6
+ const STOP_WORDS = new Set([
7
+ "a",
8
+ "an",
9
+ "and",
10
+ "are",
11
+ "as",
12
+ "at",
13
+ "be",
14
+ "by",
15
+ "for",
16
+ "from",
17
+ "in",
18
+ "is",
19
+ "it",
20
+ "of",
21
+ "on",
22
+ "or",
23
+ "that",
24
+ "the",
25
+ "this",
26
+ "to",
27
+ "with",
28
+ ]);
29
+ /** Naive singular form, so `issues` matches `issue` and `searches` matches `search`. */
30
+ function stem(term) {
31
+ if (term.length > 4 && term.endsWith("ies"))
32
+ return `${term.slice(0, -3)}y`;
33
+ if (term.length > 4 && /(ches|shes|sses|xes|zes)$/.test(term))
34
+ return term.slice(0, -2);
35
+ if (term.length > 3 && term.endsWith("s") && !term.endsWith("ss"))
36
+ return term.slice(0, -1);
37
+ return term;
38
+ }
39
+ /** Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words. */
40
+ export function tokenize(text) {
41
+ return text
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
43
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
44
+ .toLowerCase()
45
+ .split(/[^a-z0-9]+/)
46
+ .filter((term) => term.length > 0 && !STOP_WORDS.has(term))
47
+ .map(stem);
48
+ }
49
+ function isObject(value) {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+ /** Appends the descriptions and property names of a JSON Schema, recursively, to `parts`. */
53
+ export function collectSchemaText(schema, parts) {
54
+ if (!isObject(schema))
55
+ return;
56
+ if (typeof schema.description === "string")
57
+ parts.push(schema.description);
58
+ if (isObject(schema.properties)) {
59
+ for (const [name, property] of Object.entries(schema.properties)) {
60
+ parts.push(name);
61
+ collectSchemaText(property, parts);
62
+ }
63
+ }
64
+ collectSchemaText(schema.items, parts);
65
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
66
+ const variants = schema[key];
67
+ if (Array.isArray(variants))
68
+ for (const variant of variants)
69
+ collectSchemaText(variant, parts);
70
+ }
71
+ }
72
+ /** Okapi BM25 with the usual parameters. Ties keep document order. */
73
+ export class Bm25Ranker {
74
+ k1;
75
+ b;
76
+ constructor(options = {}) {
77
+ this.k1 = options.k1 ?? 1.2;
78
+ this.b = options.b ?? 0.75;
79
+ }
80
+ rank(query, documents, limit) {
81
+ const queryTerms = [...new Set(tokenize(query))];
82
+ if (queryTerms.length === 0 || documents.length === 0 || limit <= 0)
83
+ return [];
84
+ const termCounts = documents.map((document) => {
85
+ const counts = new Map();
86
+ for (const term of tokenize(document.text))
87
+ counts.set(term, (counts.get(term) ?? 0) + 1);
88
+ return counts;
89
+ });
90
+ const lengths = termCounts.map((counts) => [...counts.values()].reduce((sum, count) => sum + count, 0));
91
+ const averageLength = lengths.reduce((sum, length) => sum + length, 0) / documents.length || 1;
92
+ const idf = new Map(queryTerms.map((term) => {
93
+ const frequency = termCounts.filter((counts) => counts.has(term)).length;
94
+ return [term, Math.log(1 + (documents.length - frequency + 0.5) / (frequency + 0.5))];
95
+ }));
96
+ const matches = [];
97
+ documents.forEach((document, index) => {
98
+ let score = 0;
99
+ for (const term of queryTerms) {
100
+ const count = termCounts[index].get(term);
101
+ if (!count)
102
+ continue;
103
+ const norm = this.k1 * (1 - this.b + (this.b * lengths[index]) / averageLength);
104
+ score += (idf.get(term) ?? 0) * ((count * (this.k1 + 1)) / (count + norm));
105
+ }
106
+ if (score > 0)
107
+ matches.push({ name: document.name, score });
108
+ });
109
+ return matches.sort((a, b) => b.score - a.score).slice(0, limit);
110
+ }
111
+ }
112
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tool discovery ranking shared by every host that gives scripts `searchTools()` or the model a `tool_search` tool: a\n * BM25 ranker over tool metadata. The host decides which tools are candidates and builds each tool's search text; this\n * module only ranks.\n */\n\n/** A tool as the ranker sees it: its name and its search text. */\nexport interface ToolSearchDocument {\n\tname: string;\n\ttext: string;\n}\n\nexport interface ToolSearchMatch {\n\tname: string;\n\tscore: number;\n}\n\n/** Ranks tools for a query. BM25 today; a hybrid ranker with embeddings can replace it. */\nexport interface ToolRanker {\n\trank(query: string, documents: readonly ToolSearchDocument[], limit: number): ToolSearchMatch[];\n}\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"in\",\n\t\"is\",\n\t\"it\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"that\",\n\t\"the\",\n\t\"this\",\n\t\"to\",\n\t\"with\",\n]);\n\n/** Naive singular form, so `issues` matches `issue` and `searches` matches `search`. */\nfunction stem(term: string): string {\n\tif (term.length > 4 && term.endsWith(\"ies\")) return `${term.slice(0, -3)}y`;\n\tif (term.length > 4 && /(ches|shes|sses|xes|zes)$/.test(term)) return term.slice(0, -2);\n\tif (term.length > 3 && term.endsWith(\"s\") && !term.endsWith(\"ss\")) return term.slice(0, -1);\n\treturn term;\n}\n\n/** Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words. */\nexport function tokenize(text: string): string[] {\n\treturn text\n\t\t.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n\t\t.toLowerCase()\n\t\t.split(/[^a-z0-9]+/)\n\t\t.filter((term) => term.length > 0 && !STOP_WORDS.has(term))\n\t\t.map(stem);\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Appends the descriptions and property names of a JSON Schema, recursively, to `parts`. */\nexport function collectSchemaText(schema: unknown, parts: string[]): void {\n\tif (!isObject(schema)) return;\n\tif (typeof schema.description === \"string\") parts.push(schema.description);\n\tif (isObject(schema.properties)) {\n\t\tfor (const [name, property] of Object.entries(schema.properties)) {\n\t\t\tparts.push(name);\n\t\t\tcollectSchemaText(property, parts);\n\t\t}\n\t}\n\tcollectSchemaText(schema.items, parts);\n\tfor (const key of [\"anyOf\", \"oneOf\", \"allOf\"]) {\n\t\tconst variants = schema[key];\n\t\tif (Array.isArray(variants)) for (const variant of variants) collectSchemaText(variant, parts);\n\t}\n}\n\n/** Okapi BM25 with the usual parameters. Ties keep document order. */\nexport class Bm25Ranker implements ToolRanker {\n\tprivate readonly k1: number;\n\tprivate readonly b: number;\n\n\tconstructor(options: { k1?: number; b?: number } = {}) {\n\t\tthis.k1 = options.k1 ?? 1.2;\n\t\tthis.b = options.b ?? 0.75;\n\t}\n\n\trank(query: string, documents: readonly ToolSearchDocument[], limit: number): ToolSearchMatch[] {\n\t\tconst queryTerms = [...new Set(tokenize(query))];\n\t\tif (queryTerms.length === 0 || documents.length === 0 || limit <= 0) return [];\n\t\tconst termCounts = documents.map((document) => {\n\t\t\tconst counts = new Map<string, number>();\n\t\t\tfor (const term of tokenize(document.text)) counts.set(term, (counts.get(term) ?? 0) + 1);\n\t\t\treturn counts;\n\t\t});\n\t\tconst lengths = termCounts.map((counts) => [...counts.values()].reduce((sum, count) => sum + count, 0));\n\t\tconst averageLength = lengths.reduce((sum, length) => sum + length, 0) / documents.length || 1;\n\t\tconst idf = new Map(\n\t\t\tqueryTerms.map((term) => {\n\t\t\t\tconst frequency = termCounts.filter((counts) => counts.has(term)).length;\n\t\t\t\treturn [term, Math.log(1 + (documents.length - frequency + 0.5) / (frequency + 0.5))] as const;\n\t\t\t}),\n\t\t);\n\t\tconst matches: ToolSearchMatch[] = [];\n\t\tdocuments.forEach((document, index) => {\n\t\t\tlet score = 0;\n\t\t\tfor (const term of queryTerms) {\n\t\t\t\tconst count = termCounts[index].get(term);\n\t\t\t\tif (!count) continue;\n\t\t\t\tconst norm = this.k1 * (1 - this.b + (this.b * lengths[index]) / averageLength);\n\t\t\t\tscore += (idf.get(term) ?? 0) * ((count * (this.k1 + 1)) / (count + norm));\n\t\t\t}\n\t\t\tif (score > 0) matches.push({ name: document.name, score });\n\t\t});\n\t\treturn matches.sort((a, b) => b.score - a.score).slice(0, limit);\n\t}\n}\n"]}
package/dist/source.d.ts CHANGED
@@ -29,7 +29,8 @@ export declare class CodemodeSourceError extends Error {
29
29
  }
30
30
  /**
31
31
  * Split an optional first-line `// @options: {...}` from the script. Throws
32
- * {@link CodemodeSourceError} for empty input and invalid options.
32
+ * {@link CodemodeSourceError} for empty input, invalid options, and an options line that is not the
33
+ * first line, which would otherwise be ignored without notice.
33
34
  */
34
35
  export declare function parseCodemodeSource(input: string): ParsedCodemodeSource;
35
36
  //# sourceMappingURL=source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAOtD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAQnC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACrC,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACpC,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,qBAAqB,CAAC;CAC/B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACD;AA8CD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,CAevE"}
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAOtD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAQnC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACrC,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACpC,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,qBAAqB,CAAC;CAC/B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACD;AA8CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,CAwBvE"}
package/dist/source.js CHANGED
@@ -72,14 +72,20 @@ function parseOptions(directive) {
72
72
  }
73
73
  /**
74
74
  * Split an optional first-line `// @options: {...}` from the script. Throws
75
- * {@link CodemodeSourceError} for empty input and invalid options.
75
+ * {@link CodemodeSourceError} for empty input, invalid options, and an options line that is not the
76
+ * first line, which would otherwise be ignored without notice.
76
77
  */
77
78
  export function parseCodemodeSource(input) {
78
79
  if (input.trim() === "") {
79
80
  throw new CodemodeSourceError('Expected JavaScript source text (non-empty). Provide JS only, optionally with a first line `// @options: {"max_output_tokens": 1000}`.');
80
81
  }
82
+ const lines = input.split("\n");
83
+ const misplaced = lines.findIndex((line, index) => index > 0 && line.trimStart().startsWith(CODEMODE_OPTIONS_PREFIX));
84
+ if (misplaced !== -1) {
85
+ throw new CodemodeSourceError(`The ${CODEMODE_OPTIONS_PREFIX} line must be the first line of the input; found one on line ${misplaced + 1}`);
86
+ }
81
87
  const newline = input.indexOf("\n");
82
- const firstLine = (newline === -1 ? input : input.slice(0, newline)).replace(/\r$/, "");
88
+ const firstLine = lines[0].replace(/\r$/, "");
83
89
  const trimmed = firstLine.trimStart();
84
90
  if (!trimmed.startsWith(CODEMODE_OPTIONS_PREFIX))
85
91
  return { code: input, options: {} };