@brian-agent/codemode 0.0.0-stage → 0.1.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 (66) hide show
  1. package/README.md +373 -2
  2. package/dist/cloudflare.d.ts +53 -0
  3. package/dist/cloudflare.d.ts.map +1 -0
  4. package/dist/cloudflare.js +64 -0
  5. package/dist/cloudflare.js.map +1 -0
  6. package/dist/declarations.d.ts +43 -0
  7. package/dist/declarations.d.ts.map +1 -0
  8. package/dist/declarations.js +320 -0
  9. package/dist/declarations.js.map +1 -0
  10. package/dist/identifier.d.ts +6 -0
  11. package/dist/identifier.d.ts.map +1 -0
  12. package/dist/identifier.js +13 -0
  13. package/dist/identifier.js.map +1 -0
  14. package/dist/index.d.ts +11 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +10 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/portable.d.ts +15 -0
  19. package/dist/portable.d.ts.map +1 -0
  20. package/dist/portable.js +12 -0
  21. package/dist/portable.js.map +1 -0
  22. package/dist/runtime/execution.d.ts +66 -0
  23. package/dist/runtime/execution.d.ts.map +1 -0
  24. package/dist/runtime/execution.js +494 -0
  25. package/dist/runtime/execution.js.map +1 -0
  26. package/dist/runtime/host.d.ts +13 -0
  27. package/dist/runtime/host.d.ts.map +1 -0
  28. package/dist/runtime/host.js +86 -0
  29. package/dist/runtime/host.js.map +1 -0
  30. package/dist/runtime/inline.d.ts +63 -0
  31. package/dist/runtime/inline.d.ts.map +1 -0
  32. package/dist/runtime/inline.js +80 -0
  33. package/dist/runtime/inline.js.map +1 -0
  34. package/dist/runtime/prelude-source.d.ts +45 -0
  35. package/dist/runtime/prelude-source.d.ts.map +1 -0
  36. package/dist/runtime/prelude-source.js +520 -0
  37. package/dist/runtime/prelude-source.js.map +1 -0
  38. package/dist/runtime/protocol.d.ts +110 -0
  39. package/dist/runtime/protocol.d.ts.map +1 -0
  40. package/dist/runtime/protocol.js +15 -0
  41. package/dist/runtime/protocol.js.map +1 -0
  42. package/dist/runtime/remote.d.ts +102 -0
  43. package/dist/runtime/remote.d.ts.map +1 -0
  44. package/dist/runtime/remote.js +175 -0
  45. package/dist/runtime/remote.js.map +1 -0
  46. package/dist/runtime/vm.d.ts +24 -0
  47. package/dist/runtime/vm.d.ts.map +1 -0
  48. package/dist/runtime/vm.js +293 -0
  49. package/dist/runtime/vm.js.map +1 -0
  50. package/dist/runtime/worker.d.ts +2 -0
  51. package/dist/runtime/worker.d.ts.map +1 -0
  52. package/dist/runtime/worker.js +38 -0
  53. package/dist/runtime/worker.js.map +1 -0
  54. package/dist/source.d.ts +36 -0
  55. package/dist/source.d.ts.map +1 -0
  56. package/dist/source.js +98 -0
  57. package/dist/source.js.map +1 -0
  58. package/dist/types.d.ts +246 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/wasm.d.ts +12 -0
  63. package/dist/wasm.d.ts.map +1 -0
  64. package/dist/wasm.js +24 -0
  65. package/dist/wasm.js.map +1 -0
  66. package/package.json +75 -5
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Drives one QuickJS VM through one script, independent of the transport that connects it to the
3
+ * host. The worker thread, the in-thread launcher, and the remote server all run the VM through
4
+ * this module, so the bridge between prelude and host exists once.
5
+ *
6
+ * Contract with the caller:
7
+ * - `post` receives every message for the host, synchronously from inside VM callbacks. The
8
+ * caller must not re-enter the VM from `post` (no `deliver`, no `dispose`); a worker posts to
9
+ * another thread, and the in-thread launcher defers delivery to a microtask.
10
+ * - `deliver` hands the VM a tool result and runs the script until it waits again. It must only be
11
+ * called while the VM is idle, which holds for any caller that never re-enters from `post`.
12
+ * - `shouldInterrupt` is polled by QuickJS while the script runs, and here between batches of
13
+ * promise jobs. Returning true throws an uncatchable error in the script; the VM then reports
14
+ * `crash` or a failed `done`. The job batches matter for a script such as `while (true) await
15
+ * null`: every iteration is a separate job that executes only a few opcodes, so QuickJS's own
16
+ * polling, which counts opcodes, would let it run for a long time between polls.
17
+ * - The VM never decides on its own when it is finished: after `done` or `crash` it stays alive
18
+ * until the caller disposes it (or terminates the worker that hosts it).
19
+ *
20
+ * Failures after the VM exists are reported as a `crash` message; `startVm` only rejects when the
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.
37
+ */
38
+ import { Intrinsics, JSException, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
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
+ }
93
+ /**
94
+ * QuickJS writes engine diagnostics to fd 1 and 2, which the default shim
95
+ * forwards to the host's stdout and stderr. That output belongs to the host
96
+ * application (for example a TUI), so it is discarded. Reporting every byte as
97
+ * written keeps libc from retrying.
98
+ */
99
+ function discardOutput(memory) {
100
+ return {
101
+ fd_write(_fd, iovsPtr, iovsLen, nwrittenPtr) {
102
+ const view = new DataView(memory.buffer);
103
+ let written = 0;
104
+ for (let i = 0; i < iovsLen; i++) {
105
+ written += view.getUint32(iovsPtr + i * 8 + 4, true);
106
+ }
107
+ view.setUint32(nwrittenPtr, written, true);
108
+ return 0;
109
+ },
110
+ };
111
+ }
112
+ function describeException(error) {
113
+ const head = error.message ? `${error.name}: ${error.message}` : error.name;
114
+ const stack = error.stack?.trimEnd();
115
+ return JSON.stringify({ name: error.name, message: error.message, stack: stack ? `${head}\n${stack}` : head });
116
+ }
117
+ /** Promise jobs run between two `shouldInterrupt` polls while draining the job queue. */
118
+ const JOBS_PER_POLL = 50;
119
+ function crashMessage(error) {
120
+ return { type: "crash", message: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };
121
+ }
122
+ export async function startVm(options) {
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
+ };
132
+ const vm = await QuickJS.create({
133
+ wasm: options.wasm,
134
+ memoryLimit: data.memoryLimitBytes,
135
+ // Without a guard, deep recursion overflows the wasm stack and traps instead of throwing a
136
+ // catchable RangeError.
137
+ maxStackSize,
138
+ interruptHandler: options.shouldInterrupt,
139
+ wasi: clock ? deterministicWasi(clock) : discardOutput,
140
+ ...(clock ? { timezoneOffset: 0, intrinsics: Intrinsics.ALL & ~Intrinsics.WEAK_REF } : {}),
141
+ });
142
+ if (clock)
143
+ clock.creating = false;
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
+ };
158
+ const session = {
159
+ deliver(message) {
160
+ if (disposed)
161
+ return;
162
+ try {
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
+ }
182
+ }
183
+ catch (error) {
184
+ post(crashMessage(error));
185
+ }
186
+ },
187
+ dispose() {
188
+ if (disposed)
189
+ return;
190
+ disposed = true;
191
+ vm.dispose();
192
+ },
193
+ };
194
+ let api;
195
+ let settle;
196
+ let stalled;
197
+ // `executePendingJobs()` runs the queue to empty without a chance to interrupt, so jobs run one at a
198
+ // time through the (internal, but public) exports; quickjs-wasi is pinned to an exact version.
199
+ const exports = vm._getExports();
200
+ /** Run queued jobs, then fail a script that waits on nothing that can ever resume it. */
201
+ const drain = () => {
202
+ let jobs = 0;
203
+ while (exports.qjs_is_job_pending()) {
204
+ // A job only fails for uncatchable errors, such as an interrupt.
205
+ if (exports.qjs_execute_pending_job() < 0)
206
+ throw new Error("Promise job failed");
207
+ if (++jobs % JOBS_PER_POLL === 0 && options.shouldInterrupt())
208
+ throw new Error("Execution interrupted");
209
+ }
210
+ vm.callFunction(stalled, api).dispose();
211
+ };
212
+ try {
213
+ // Called from the prelude with primitives only.
214
+ const bridge = vm.newFunction("bridge", (kind, a, b, c) => {
215
+ switch (kind.toString()) {
216
+ case "call":
217
+ case "global":
218
+ issued++;
219
+ post({
220
+ type: "call",
221
+ id: a.toNumber(),
222
+ target: kind.toString() === "call" ? "tool" : "global",
223
+ name: b.toString(),
224
+ args: c === undefined || c.isUndefined ? undefined : c.toString(),
225
+ });
226
+ break;
227
+ case "output":
228
+ post({
229
+ type: "output",
230
+ item: a.toString() === "image"
231
+ ? { type: "image", data: b.toString(), mimeType: c.toString() }
232
+ : a.toString() === "console"
233
+ ? { type: "text", text: b.toString(), console: true }
234
+ : { type: "text", text: b.toString() },
235
+ });
236
+ break;
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 } } : {};
240
+ if (a.toBoolean()) {
241
+ post({
242
+ type: "done",
243
+ ok: true,
244
+ value: b === undefined || b.isUndefined ? undefined : b.toString(),
245
+ writes: c.toString(),
246
+ ...during,
247
+ });
248
+ }
249
+ else {
250
+ post({ type: "done", ok: false, error: b.toString(), ...during });
251
+ }
252
+ break;
253
+ }
254
+ }
255
+ return vm.undefined;
256
+ });
257
+ // These handles live as long as the VM, which the caller disposes as a whole.
258
+ api = vm.withScope((scope) => scope.escape(vm.callFunction(vm.evalCode(PRELUDE_SOURCE, "codemode-prelude.js"), vm.undefined, bridge, vm.newString(JSON.stringify(data.tools)), vm.newString(JSON.stringify(data.globals)), vm.newString(JSON.stringify(data.store)))));
259
+ settle = api.getProp("settle");
260
+ const run = api.getProp("run");
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
+ }
270
+ // The prefix shares the first line with the script so reported line numbers
271
+ // match the script as written.
272
+ let fn;
273
+ try {
274
+ fn = vm.evalCode(`(async (tools, console) => {${data.code}\n})`, "codemode.js");
275
+ }
276
+ catch (error) {
277
+ if (!(error instanceof JSException))
278
+ throw error;
279
+ post({ type: "done", ok: false, error: describeException(error) });
280
+ return session;
281
+ }
282
+ vm.callFunction(run, api, fn).dispose();
283
+ fn.dispose();
284
+ drain();
285
+ if (clock)
286
+ post({ type: "idle", seq: 0, issued });
287
+ }
288
+ catch (error) {
289
+ post(crashMessage(error));
290
+ }
291
+ return session;
292
+ }
293
+ //# sourceMappingURL=vm.js.map
@@ -0,0 +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 *\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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/runtime/worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Worker thread entry. One worker runs one script inside a fresh QuickJS VM
3
+ * (a separate wasm instance), relays tool calls and output to the host, and
4
+ * reports the result. The host terminates the worker when the script settles,
5
+ * times out, or is aborted; the worker exists so that a spinning script never
6
+ * blocks the host thread.
7
+ *
8
+ * Importing this module starts the worker. Hosts that bundle their code (for
9
+ * example a Bun compiled executable) add a file that imports
10
+ * `@brian-agent/codemode/worker` as a separate entrypoint and pass its URL
11
+ * or embedded-module string specifier as `workerUrl`.
12
+ */
13
+ import { parentPort, workerData } from "node:worker_threads";
14
+ import { isHostToWorkerMessage } from "./protocol.js";
15
+ import { startVm } from "./vm.js";
16
+ function post(message) {
17
+ parentPort?.postMessage(message);
18
+ }
19
+ async function main(data) {
20
+ const interrupt = new Int32Array(data.interrupt);
21
+ // The VM lives until the host terminates the worker, so the session is never disposed.
22
+ const session = await startVm({
23
+ data,
24
+ wasm: data.wasm,
25
+ post,
26
+ shouldInterrupt: () => Atomics.load(interrupt, 0) !== 0,
27
+ });
28
+ parentPort?.on("message", (message) => {
29
+ if (isHostToWorkerMessage(message))
30
+ session.deliver(message);
31
+ });
32
+ }
33
+ if (parentPort) {
34
+ main(workerData).catch((error) => {
35
+ post({ type: "crash", message: error instanceof Error ? `${error.name}: ${error.message}` : String(error) });
36
+ });
37
+ }
38
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/runtime/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAA6C,MAAM,eAAe,CAAC;AACjG,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,SAAS,IAAI,CAAC,OAA4B;IACzC,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAgB;IACnC,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,uFAAuF;IACvF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;QAC7B,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI;QACJ,eAAe,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC;KACvD,CAAC,CAAC;IACH,UAAU,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;QAC9C,IAAI,qBAAqB,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,IAAI,UAAU,EAAE,CAAC;IAChB,IAAI,CAAC,UAAwB,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACvD,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9G,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Worker thread entry. One worker runs one script inside a fresh QuickJS VM\n * (a separate wasm instance), relays tool calls and output to the host, and\n * reports the result. The host terminates the worker when the script settles,\n * times out, or is aborted; the worker exists so that a spinning script never\n * blocks the host thread.\n *\n * Importing this module starts the worker. Hosts that bundle their code (for\n * example a Bun compiled executable) add a file that imports\n * `@brian-agent/codemode/worker` as a separate entrypoint and pass its URL\n * or embedded-module string specifier as `workerUrl`.\n */\nimport { parentPort, workerData } from \"node:worker_threads\";\nimport { isHostToWorkerMessage, type WorkerData, type WorkerToHostMessage } from \"./protocol.ts\";\nimport { startVm } from \"./vm.ts\";\n\nfunction post(message: WorkerToHostMessage): void {\n\tparentPort?.postMessage(message);\n}\n\nasync function main(data: WorkerData): Promise<void> {\n\tconst interrupt = new Int32Array(data.interrupt);\n\t// The VM lives until the host terminates the worker, so the session is never disposed.\n\tconst session = await startVm({\n\t\tdata,\n\t\twasm: data.wasm,\n\t\tpost,\n\t\tshouldInterrupt: () => Atomics.load(interrupt, 0) !== 0,\n\t});\n\tparentPort?.on(\"message\", (message: unknown) => {\n\t\tif (isHostToWorkerMessage(message)) session.deliver(message);\n\t});\n}\n\nif (parentPort) {\n\tmain(workerData as WorkerData).catch((error: unknown) => {\n\t\tpost({ type: \"crash\", message: error instanceof Error ? `${error.name}: ${error.message}` : String(error) });\n\t});\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Codemode source format: JavaScript, optionally preceded by one options line.
3
+ *
4
+ * ```js
5
+ * // @options: {"max_output_tokens": 2000, "timeout_ms": 30000}
6
+ * const text = await tools.read({ path: "package.json" });
7
+ * text(JSON.parse(text).name);
8
+ * ```
9
+ */
10
+ export declare const CODEMODE_OPTIONS_PREFIX = "// @options:";
11
+ /**
12
+ * Lark grammar for providers with grammar-constrained tool input. It only fixes the shape of the
13
+ * options line; the options JSON and the code are checked by {@link parseCodemodeSource}.
14
+ */
15
+ export declare const CODEMODE_SOURCE_GRAMMAR: string;
16
+ export interface CodemodeSourceOptions {
17
+ /** Token budget for the script's output. */
18
+ maxOutputTokens?: number;
19
+ /** Hard deadline for the whole script in milliseconds, including tool calls. */
20
+ timeoutMs?: number;
21
+ }
22
+ export interface ParsedCodemodeSource {
23
+ /** The script with the options line replaced by an empty line, so line numbers are unchanged. */
24
+ code: string;
25
+ options: CodemodeSourceOptions;
26
+ }
27
+ export declare class CodemodeSourceError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ /**
31
+ * Split an optional first-line `// @options: {...}` from the script. Throws
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.
34
+ */
35
+ export declare function parseCodemodeSource(input: string): ParsedCodemodeSource;
36
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +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;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,CAwBvE"}
package/dist/source.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Codemode source format: JavaScript, optionally preceded by one options line.
3
+ *
4
+ * ```js
5
+ * // @options: {"max_output_tokens": 2000, "timeout_ms": 30000}
6
+ * const text = await tools.read({ path: "package.json" });
7
+ * text(JSON.parse(text).name);
8
+ * ```
9
+ */
10
+ export const CODEMODE_OPTIONS_PREFIX = "// @options:";
11
+ const SUPPORTED_FIELDS = ["max_output_tokens", "timeout_ms"];
12
+ const SUPPORTED_FIELDS_TEXT = "`max_output_tokens` and `timeout_ms`";
13
+ /** Largest delay `setTimeout` supports, which bounds `timeout_ms`. */
14
+ const MAX_TIMEOUT_MS = 2_147_483_647;
15
+ /**
16
+ * Lark grammar for providers with grammar-constrained tool input. It only fixes the shape of the
17
+ * options line; the options JSON and the code are checked by {@link parseCodemodeSource}.
18
+ */
19
+ export const CODEMODE_SOURCE_GRAMMAR = String.raw `
20
+ start: options_source | plain_source
21
+ options_source: OPTIONS_LINE NEWLINE SOURCE
22
+ plain_source: SOURCE
23
+
24
+ OPTIONS_LINE: /[ \t]*\/\/ @options:[^\r\n]*/
25
+ NEWLINE: /\r?\n/
26
+ SOURCE: /[\s\S]+/
27
+ `;
28
+ export class CodemodeSourceError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "CodemodeSourceError";
32
+ }
33
+ }
34
+ function isSafeInteger(value) {
35
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
36
+ }
37
+ function parseOptions(directive) {
38
+ if (directive === "") {
39
+ throw new CodemodeSourceError(`@options must be a JSON object with supported fields ${SUPPORTED_FIELDS_TEXT}`);
40
+ }
41
+ let value;
42
+ try {
43
+ value = JSON.parse(directive);
44
+ }
45
+ catch (error) {
46
+ throw new CodemodeSourceError(`@options must be valid JSON with supported fields ${SUPPORTED_FIELDS_TEXT}: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
49
+ throw new CodemodeSourceError(`@options must be a JSON object with supported fields ${SUPPORTED_FIELDS_TEXT}`);
50
+ }
51
+ const fields = value;
52
+ for (const key of Object.keys(fields)) {
53
+ if (!SUPPORTED_FIELDS.includes(key)) {
54
+ throw new CodemodeSourceError(`@options only supports ${SUPPORTED_FIELDS_TEXT}; got \`${key}\``);
55
+ }
56
+ }
57
+ const options = {};
58
+ const { max_output_tokens, timeout_ms } = fields;
59
+ if (max_output_tokens !== undefined) {
60
+ if (!isSafeInteger(max_output_tokens)) {
61
+ throw new CodemodeSourceError("@options field `max_output_tokens` must be a non-negative safe integer");
62
+ }
63
+ options.maxOutputTokens = max_output_tokens;
64
+ }
65
+ if (timeout_ms !== undefined) {
66
+ if (!isSafeInteger(timeout_ms) || timeout_ms === 0 || timeout_ms > MAX_TIMEOUT_MS) {
67
+ throw new CodemodeSourceError(`@options field \`timeout_ms\` must be a positive integer up to ${MAX_TIMEOUT_MS}`);
68
+ }
69
+ options.timeoutMs = timeout_ms;
70
+ }
71
+ return options;
72
+ }
73
+ /**
74
+ * Split an optional first-line `// @options: {...}` from the script. Throws
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.
77
+ */
78
+ export function parseCodemodeSource(input) {
79
+ if (input.trim() === "") {
80
+ throw new CodemodeSourceError('Expected JavaScript source text (non-empty). Provide JS only, optionally with a first line `// @options: {"max_output_tokens": 1000}`.');
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
+ }
87
+ const newline = input.indexOf("\n");
88
+ const firstLine = lines[0].replace(/\r$/, "");
89
+ const trimmed = firstLine.trimStart();
90
+ if (!trimmed.startsWith(CODEMODE_OPTIONS_PREFIX))
91
+ return { code: input, options: {} };
92
+ const code = newline === -1 ? "" : input.slice(newline);
93
+ if (code.trim() === "") {
94
+ throw new CodemodeSourceError("The @options line must be followed by JavaScript source on subsequent lines");
95
+ }
96
+ return { code, options: parseOptions(trimmed.slice(CODEMODE_OPTIONS_PREFIX.length).trim()) };
97
+ }
98
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Codemode source format: JavaScript, optionally preceded by one options line.\n *\n * ```js\n * // @options: {\"max_output_tokens\": 2000, \"timeout_ms\": 30000}\n * const text = await tools.read({ path: \"package.json\" });\n * text(JSON.parse(text).name);\n * ```\n */\n\nexport const CODEMODE_OPTIONS_PREFIX = \"// @options:\";\n\nconst SUPPORTED_FIELDS = [\"max_output_tokens\", \"timeout_ms\"] as const;\nconst SUPPORTED_FIELDS_TEXT = \"`max_output_tokens` and `timeout_ms`\";\n/** Largest delay `setTimeout` supports, which bounds `timeout_ms`. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Lark grammar for providers with grammar-constrained tool input. It only fixes the shape of the\n * options line; the options JSON and the code are checked by {@link parseCodemodeSource}.\n */\nexport const CODEMODE_SOURCE_GRAMMAR = String.raw`\nstart: options_source | plain_source\noptions_source: OPTIONS_LINE NEWLINE SOURCE\nplain_source: SOURCE\n\nOPTIONS_LINE: /[ \\t]*\\/\\/ @options:[^\\r\\n]*/\nNEWLINE: /\\r?\\n/\nSOURCE: /[\\s\\S]+/\n`;\n\nexport interface CodemodeSourceOptions {\n\t/** Token budget for the script's output. */\n\tmaxOutputTokens?: number;\n\t/** Hard deadline for the whole script in milliseconds, including tool calls. */\n\ttimeoutMs?: number;\n}\n\nexport interface ParsedCodemodeSource {\n\t/** The script with the options line replaced by an empty line, so line numbers are unchanged. */\n\tcode: string;\n\toptions: CodemodeSourceOptions;\n}\n\nexport class CodemodeSourceError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"CodemodeSourceError\";\n\t}\n}\n\nfunction isSafeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction parseOptions(directive: string): CodemodeSourceOptions {\n\tif (directive === \"\") {\n\t\tthrow new CodemodeSourceError(`@options must be a JSON object with supported fields ${SUPPORTED_FIELDS_TEXT}`);\n\t}\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(directive);\n\t} catch (error) {\n\t\tthrow new CodemodeSourceError(\n\t\t\t`@options must be valid JSON with supported fields ${SUPPORTED_FIELDS_TEXT}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new CodemodeSourceError(`@options must be a JSON object with supported fields ${SUPPORTED_FIELDS_TEXT}`);\n\t}\n\tconst fields = value as Record<string, unknown>;\n\tfor (const key of Object.keys(fields)) {\n\t\tif (!(SUPPORTED_FIELDS as readonly string[]).includes(key)) {\n\t\t\tthrow new CodemodeSourceError(`@options only supports ${SUPPORTED_FIELDS_TEXT}; got \\`${key}\\``);\n\t\t}\n\t}\n\tconst options: CodemodeSourceOptions = {};\n\tconst { max_output_tokens, timeout_ms } = fields;\n\tif (max_output_tokens !== undefined) {\n\t\tif (!isSafeInteger(max_output_tokens)) {\n\t\t\tthrow new CodemodeSourceError(\"@options field `max_output_tokens` must be a non-negative safe integer\");\n\t\t}\n\t\toptions.maxOutputTokens = max_output_tokens;\n\t}\n\tif (timeout_ms !== undefined) {\n\t\tif (!isSafeInteger(timeout_ms) || timeout_ms === 0 || timeout_ms > MAX_TIMEOUT_MS) {\n\t\t\tthrow new CodemodeSourceError(\n\t\t\t\t`@options field \\`timeout_ms\\` must be a positive integer up to ${MAX_TIMEOUT_MS}`,\n\t\t\t);\n\t\t}\n\t\toptions.timeoutMs = timeout_ms;\n\t}\n\treturn options;\n}\n\n/**\n * Split an optional first-line `// @options: {...}` from the script. Throws\n * {@link CodemodeSourceError} for empty input, invalid options, and an options line that is not the\n * first line, which would otherwise be ignored without notice.\n */\nexport function parseCodemodeSource(input: string): ParsedCodemodeSource {\n\tif (input.trim() === \"\") {\n\t\tthrow new CodemodeSourceError(\n\t\t\t'Expected JavaScript source text (non-empty). Provide JS only, optionally with a first line `// @options: {\"max_output_tokens\": 1000}`.',\n\t\t);\n\t}\n\tconst lines = input.split(\"\\n\");\n\tconst misplaced = lines.findIndex(\n\t\t(line, index) => index > 0 && line.trimStart().startsWith(CODEMODE_OPTIONS_PREFIX),\n\t);\n\tif (misplaced !== -1) {\n\t\tthrow new CodemodeSourceError(\n\t\t\t`The ${CODEMODE_OPTIONS_PREFIX} line must be the first line of the input; found one on line ${misplaced + 1}`,\n\t\t);\n\t}\n\tconst newline = input.indexOf(\"\\n\");\n\tconst firstLine = lines[0].replace(/\\r$/, \"\");\n\tconst trimmed = firstLine.trimStart();\n\tif (!trimmed.startsWith(CODEMODE_OPTIONS_PREFIX)) return { code: input, options: {} };\n\tconst code = newline === -1 ? \"\" : input.slice(newline);\n\tif (code.trim() === \"\") {\n\t\tthrow new CodemodeSourceError(\"The @options line must be followed by JavaScript source on subsequent lines\");\n\t}\n\treturn { code, options: parseOptions(trimmed.slice(CODEMODE_OPTIONS_PREFIX.length).trim()) };\n}\n"]}