@neurocode-ai/codemode 1.18.8

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.
@@ -0,0 +1,3465 @@
1
+ import { parse } from "acorn"
2
+ import { Cause, Effect, Exit, Fiber, Semaphore } from "effect"
3
+ import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
4
+ import {
5
+ copyIn,
6
+ copyOut,
7
+ isBlockedMember,
8
+ ToolReference,
9
+ ToolRuntime,
10
+ ToolRuntimeError,
11
+ type HostTools,
12
+ type SafeObject,
13
+ type Services,
14
+ } from "../tool-runtime.js"
15
+ import { ToolError } from "../tool-error.js"
16
+ import type {
17
+ DataValue,
18
+ Diagnostic,
19
+ DiagnosticKind,
20
+ ExecuteOptions,
21
+ ResolvedExecutionLimits,
22
+ Result,
23
+ } from "../codemode.js"
24
+ import {
25
+ type AstNode,
26
+ asNode,
27
+ type Binding,
28
+ CodeModeFunction,
29
+ CoercionFunction,
30
+ ComputedValue,
31
+ ErrorConstructorReference,
32
+ GlobalMethodReference,
33
+ GlobalNamespace,
34
+ type GlobalNamespaceName,
35
+ formatLocation,
36
+ getArray,
37
+ getBoolean,
38
+ getNode,
39
+ getOptionalNode,
40
+ getString,
41
+ IntrinsicReference,
42
+ InterpreterRuntimeError,
43
+ isRecord,
44
+ type MemberReference,
45
+ OptionalShortCircuit,
46
+ PromiseMethodReference,
47
+ type PromiseMethodName,
48
+ PromiseNamespace,
49
+ ProgramThrow,
50
+ type ProgramNode,
51
+ type StatementResult,
52
+ sourceLocation,
53
+ supportedSyntaxMessage,
54
+ unsupportedSyntax,
55
+ UriFunction,
56
+ } from "./model.js"
57
+ import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js"
58
+ import { consoleMethods, MAX_CONSOLE_DEPTH } from "../stdlib/console.js"
59
+ import { dateMethods, dateStatics, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
60
+ import { invokeJsonMethod } from "../stdlib/json.js"
61
+ import { invokeMathMethod, mathConstants } from "../stdlib/math.js"
62
+ import {
63
+ invokeNumberMethod,
64
+ invokeNumberStatic,
65
+ numberConstants,
66
+ numberMethods,
67
+ numberStatics,
68
+ } from "../stdlib/number.js"
69
+ import { invokeObjectMethod } from "../stdlib/object.js"
70
+ import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js"
71
+ import {
72
+ escapeRegexHint,
73
+ invokeRegExpMethod,
74
+ matchToValue,
75
+ regexpMethods,
76
+ regexpProperties,
77
+ regexFailureReason,
78
+ toHostRegex,
79
+ } from "../stdlib/regexp.js"
80
+ import { invokeStringStatic, stringMethods, stringStatics } from "../stdlib/string.js"
81
+ import {
82
+ urlMethods,
83
+ urlProperties,
84
+ urlSearchParamsMethods,
85
+ urlStatics,
86
+ urlWritableProperties,
87
+ invokeUriFunction,
88
+ invokeURLMethod,
89
+ invokeURLStatic,
90
+ uriArgument,
91
+ urlArgument,
92
+ } from "../stdlib/url.js"
93
+ import {
94
+ boundedData,
95
+ coerceToNumber,
96
+ coerceToString,
97
+ compoundOperators,
98
+ createErrorValue,
99
+ errorBrandName,
100
+ errorConstructors,
101
+ invokeCoercion,
102
+ valueConstructors,
103
+ } from "../stdlib/value.js"
104
+ import {
105
+ isSandboxValue,
106
+ SandboxDate,
107
+ SandboxMap,
108
+ SandboxPromise,
109
+ SandboxRegExp,
110
+ SandboxSet,
111
+ SandboxURL,
112
+ SandboxURLSearchParams,
113
+ } from "../values.js"
114
+
115
+ const parseProgram = (code: string): ProgramNode => {
116
+ const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, {
117
+ reportDiagnostics: true,
118
+ compilerOptions: {
119
+ target: ScriptTarget.ESNext,
120
+ module: ModuleKind.ESNext,
121
+ },
122
+ })
123
+ const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
124
+
125
+ if (diagnostic) {
126
+ throw new InterpreterRuntimeError(
127
+ `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
128
+ undefined,
129
+ "ParseError",
130
+ )
131
+ }
132
+
133
+ const bodyStart = transpiled.outputText.indexOf("{") + 1
134
+ const bodyEnd = transpiled.outputText.lastIndexOf("}")
135
+ const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd)
136
+ const parsed = parse(executableCode, {
137
+ ecmaVersion: "latest",
138
+ sourceType: "script",
139
+ allowReturnOutsideFunction: true,
140
+ allowAwaitOutsideFunction: true,
141
+ locations: true,
142
+ }) as unknown
143
+
144
+ if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) {
145
+ throw new InterpreterRuntimeError("Failed to parse script as a Program node.")
146
+ }
147
+
148
+ return parsed as ProgramNode
149
+ }
150
+
151
+ const publicErrorMessage = (message: string): string =>
152
+ message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "<redacted-path>")
153
+
154
+ const normalizeError = (error: unknown): Diagnostic => {
155
+ if (error instanceof InterpreterRuntimeError) {
156
+ return {
157
+ kind: error.kind,
158
+ message: `${error.message}${formatLocation(error.node)}`,
159
+ ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}),
160
+ ...(error.suggestions ? { suggestions: error.suggestions } : {}),
161
+ }
162
+ }
163
+
164
+ if (error instanceof ToolRuntimeError) {
165
+ return {
166
+ kind: error.kind,
167
+ message: error.message,
168
+ ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}),
169
+ }
170
+ }
171
+
172
+ if (error instanceof ToolError) {
173
+ return { kind: "ToolFailure", message: publicErrorMessage(error.message) }
174
+ }
175
+
176
+ if (error instanceof ProgramThrow) {
177
+ const value = error.value
178
+ let message: string
179
+ if (containsRuntimeReference(value)) {
180
+ // A thrown tool/function reference must not leak its internal structure.
181
+ message = "a non-data value"
182
+ } else if (typeof value === "string") {
183
+ message = value
184
+ } else if (
185
+ value !== null &&
186
+ typeof value === "object" &&
187
+ typeof (value as { message?: unknown }).message === "string"
188
+ ) {
189
+ message = (value as { message: string }).message
190
+ } else {
191
+ try {
192
+ message = JSON.stringify(copyOut(value)) ?? String(value)
193
+ } catch {
194
+ message = String(value)
195
+ }
196
+ }
197
+ return { kind: "ExecutionFailure", message: `Uncaught: ${message}` }
198
+ }
199
+
200
+ if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) {
201
+ return {
202
+ kind: "ExecutionFailure",
203
+ message: "Execution exceeded the maximum nesting depth.",
204
+ }
205
+ }
206
+
207
+ if (error instanceof Error) {
208
+ return {
209
+ kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure",
210
+ message: publicErrorMessage(error.message),
211
+ }
212
+ }
213
+
214
+ // A non-Error thrown by a host tool (raw string / number / Symbol) still routes through
215
+ // path redaction so filesystem paths can never leak through the catch-all branch.
216
+ return {
217
+ kind: "ExecutionFailure",
218
+ message: publicErrorMessage(String(error)),
219
+ }
220
+ }
221
+
222
+ // Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers.
223
+ const caughtErrorValue = (thrown: unknown): unknown => {
224
+ if (thrown instanceof ProgramThrow) return thrown.value
225
+ if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
226
+ const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error"
227
+ return createErrorValue(name, normalizeError(thrown).message)
228
+ }
229
+
230
+ const isRuntimeReference = (value: unknown): boolean =>
231
+ value instanceof CodeModeFunction ||
232
+ value instanceof ToolReference ||
233
+ value instanceof IntrinsicReference ||
234
+ value instanceof GlobalNamespace ||
235
+ value instanceof GlobalMethodReference ||
236
+ value instanceof PromiseNamespace ||
237
+ value instanceof PromiseMethodReference ||
238
+ value instanceof SandboxPromise ||
239
+ value instanceof CoercionFunction ||
240
+ value instanceof UriFunction ||
241
+ value instanceof ErrorConstructorReference ||
242
+ isSandboxValue(value)
243
+
244
+ const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
245
+ if (isRuntimeReference(value)) return true
246
+ if (value === null || typeof value !== "object") return false
247
+ if (seen.has(value)) return false
248
+ seen.add(value)
249
+ const contains = Array.isArray(value)
250
+ ? value.some((item) => containsRuntimeReference(item, seen))
251
+ : Object.values(value).some((item) => containsRuntimeReference(item, seen))
252
+ seen.delete(value)
253
+ return contains
254
+ }
255
+
256
+ // Like containsRuntimeReference, but sandbox standard-library values count as data:
257
+ // operators and switch treat them as ordinary object operands (identity equality, ToPrimitive
258
+ // coercion) rather than rejecting them as opaque interpreter machinery.
259
+ const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
260
+ if (isSandboxValue(value)) return false
261
+ if (isRuntimeReference(value)) return true
262
+ if (value === null || typeof value !== "object") return false
263
+ if (seen.has(value)) return false
264
+ seen.add(value)
265
+ const contains = Array.isArray(value)
266
+ ? value.some((item) => containsOpaqueReference(item, seen))
267
+ : Object.values(value).some((item) => containsOpaqueReference(item, seen))
268
+ seen.delete(value)
269
+ return contains
270
+ }
271
+
272
+ // `typeof` never throws in JS; map every interpreter value to its JS-visible category.
273
+ // A SandboxPromise falls through to the final `typeof value` and reports "object", exactly
274
+ // like a real JS promise.
275
+ const typeofValue = (value: unknown): string => {
276
+ if (
277
+ value instanceof CodeModeFunction ||
278
+ value instanceof CoercionFunction ||
279
+ value instanceof IntrinsicReference ||
280
+ value instanceof GlobalMethodReference ||
281
+ value instanceof PromiseMethodReference ||
282
+ value instanceof PromiseNamespace ||
283
+ value instanceof ErrorConstructorReference
284
+ )
285
+ return "function"
286
+ if (value instanceof UriFunction) return "function"
287
+ if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
288
+ if (value instanceof GlobalNamespace) {
289
+ return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function"
290
+ }
291
+ return typeof value
292
+ }
293
+
294
+ // `x instanceof C` against the constructors CodeMode knows. Like `typeof`, it observes any
295
+ // left-hand value (opaque references included) without coercing it. Error checks use the
296
+ // error brand: `instanceof Error` accepts every branded error; a specific error type matches
297
+ // its own brand only (as in JS, where TypeError instances are also Error instances).
298
+ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
299
+ if (rhs instanceof ErrorConstructorReference) {
300
+ const brand = errorBrandName(lhs)
301
+ return brand !== undefined && (rhs.name === "Error" || brand === rhs.name)
302
+ }
303
+ if (rhs instanceof GlobalNamespace) {
304
+ switch (rhs.name) {
305
+ case "Date":
306
+ return lhs instanceof SandboxDate
307
+ case "RegExp":
308
+ return lhs instanceof SandboxRegExp
309
+ case "Map":
310
+ return lhs instanceof SandboxMap
311
+ case "Set":
312
+ return lhs instanceof SandboxSet
313
+ case "URL":
314
+ return lhs instanceof SandboxURL
315
+ case "URLSearchParams":
316
+ return lhs instanceof SandboxURLSearchParams
317
+ case "Array":
318
+ return Array.isArray(lhs)
319
+ case "Object":
320
+ return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function")
321
+ }
322
+ }
323
+ if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise
324
+ // Number/String/Boolean wrap primitives in JS; no boxed values exist in CodeMode, so
325
+ // `x instanceof Number` is always false - exactly what it is for primitives in JS.
326
+ if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) {
327
+ return false
328
+ }
329
+ throw new InterpreterRuntimeError(
330
+ "The right-hand side of 'instanceof' must be a constructor CodeMode knows: Error (or a specific error type like TypeError), Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, or Promise.",
331
+ node,
332
+ )
333
+ }
334
+
335
+ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, node: AstNode): unknown => {
336
+ const str = (index: number): string => {
337
+ const arg = args[index]
338
+ if (typeof arg !== "string")
339
+ throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node)
340
+ return arg
341
+ }
342
+ const num = (index: number): number => {
343
+ const arg = args[index]
344
+ if (typeof arg !== "number")
345
+ throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node)
346
+ return arg
347
+ }
348
+ const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index))
349
+ const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index))
350
+
351
+ let result: unknown
352
+ switch (name) {
353
+ case "toLowerCase":
354
+ result = value.toLowerCase()
355
+ break
356
+ case "toUpperCase":
357
+ result = value.toUpperCase()
358
+ break
359
+ case "trim":
360
+ result = value.trim()
361
+ break
362
+ // trimLeft/trimRight are the legacy aliases of trimStart/trimEnd, kept because models write them.
363
+ case "trimStart":
364
+ case "trimLeft":
365
+ result = value.trimStart()
366
+ break
367
+ case "trimEnd":
368
+ case "trimRight":
369
+ result = value.trimEnd()
370
+ break
371
+ // Locale/options arguments are ignored: comparison runs with the host default locale, and
372
+ // the common use is a sort comparator where any consistent order works.
373
+ case "localeCompare":
374
+ result = value.localeCompare(str(0))
375
+ break
376
+ case "normalize": {
377
+ const form = optStr(0)
378
+ try {
379
+ result = value.normalize(form)
380
+ } catch {
381
+ throw new InterpreterRuntimeError(
382
+ `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`,
383
+ node,
384
+ ).as("RangeError")
385
+ }
386
+ break
387
+ }
388
+ case "split": {
389
+ if (args.length === 0) {
390
+ result = [value]
391
+ break
392
+ }
393
+ if (args[0] instanceof SandboxRegExp) {
394
+ result = value.split((args[0] as SandboxRegExp).regex, optNum(1))
395
+ break
396
+ }
397
+ const requestedLimit = optNum(1)
398
+ result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0)
399
+ break
400
+ }
401
+ case "slice":
402
+ result = value.slice(optNum(0), optNum(1))
403
+ break
404
+ case "includes":
405
+ result = value.includes(str(0), optNum(1))
406
+ break
407
+ case "startsWith":
408
+ result = value.startsWith(str(0), optNum(1))
409
+ break
410
+ case "endsWith":
411
+ result = value.endsWith(str(0), optNum(1))
412
+ break
413
+ case "indexOf":
414
+ result = value.indexOf(str(0), optNum(1))
415
+ break
416
+ case "lastIndexOf":
417
+ result = value.lastIndexOf(str(0), optNum(1))
418
+ break
419
+ case "replace":
420
+ case "replaceAll": {
421
+ if (args[0] instanceof SandboxRegExp) {
422
+ const pattern = (args[0] as SandboxRegExp).regex
423
+ const replacement = str(1)
424
+ if (name === "replaceAll" && !pattern.global) {
425
+ throw new InterpreterRuntimeError(
426
+ `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`,
427
+ node,
428
+ )
429
+ }
430
+ result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement)
431
+ break
432
+ }
433
+ if (name === "replace") {
434
+ result = value.replace(str(0), str(1))
435
+ break
436
+ }
437
+ result = value.replaceAll(str(0), str(1))
438
+ break
439
+ }
440
+ case "match": {
441
+ const pattern = toHostRegex(args[0], name, node)
442
+ const matched = value.match(pattern)
443
+ if (matched === null) return null
444
+ // A global match is a plain array of matched strings; a non-global match carries
445
+ // index/groups own properties, so bypass the copying data checkpoint to keep them.
446
+ if (pattern.global) return boundedData(matched, "String.match result")
447
+ return matchToValue(matched)
448
+ }
449
+ case "matchAll": {
450
+ const pattern = toHostRegex(args[0], name, node, "g")
451
+ if (!pattern.global) {
452
+ throw new InterpreterRuntimeError(
453
+ `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`,
454
+ node,
455
+ )
456
+ }
457
+ // Materialized as an array (not an iterator); each entry is a match array with
458
+ // index/groups own properties. Match count is bounded by the subject length.
459
+ return Array.from(value.matchAll(pattern), matchToValue)
460
+ }
461
+ case "search": {
462
+ result = value.search(toHostRegex(args[0], name, node))
463
+ break
464
+ }
465
+ case "repeat": {
466
+ const count = num(0)
467
+ if (!Number.isFinite(count) || count < 0)
468
+ throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node)
469
+ result = value.repeat(count)
470
+ break
471
+ }
472
+ case "padStart":
473
+ result = value.padStart(num(0), optStr(1))
474
+ break
475
+ case "padEnd":
476
+ result = value.padEnd(num(0), optStr(1))
477
+ break
478
+ case "charAt":
479
+ result = value.charAt(optNum(0) ?? 0)
480
+ break
481
+ case "at":
482
+ result = value.at(optNum(0) ?? 0)
483
+ break
484
+ case "substring":
485
+ result = value.substring(optNum(0) ?? 0, optNum(1))
486
+ break
487
+ case "substr":
488
+ result = value.substr(optNum(0) ?? 0, optNum(1))
489
+ break
490
+ // JS charCodeAt returns NaN out of range; NaN flows as an ordinary in-sandbox value
491
+ // (normalized to null only at the data boundary - see copyOut), so return it as-is.
492
+ case "charCodeAt":
493
+ result = value.charCodeAt(optNum(0) ?? 0)
494
+ break
495
+ case "codePointAt":
496
+ result = value.codePointAt(optNum(0) ?? 0)
497
+ break
498
+ case "toString":
499
+ result = value
500
+ break
501
+ case "concat": {
502
+ result = value.concat(...args.map((_, index) => str(index)))
503
+ break
504
+ }
505
+ default:
506
+ throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node)
507
+ }
508
+ return boundedData(result, `String.${name} result`)
509
+ }
510
+
511
+ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
512
+ switch (name) {
513
+ case "isArray":
514
+ return Array.isArray(args[0])
515
+ case "of":
516
+ return [...args]
517
+ case "from": {
518
+ if (args.length > 1) {
519
+ throw new InterpreterRuntimeError(
520
+ "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.",
521
+ node,
522
+ "UnsupportedSyntax",
523
+ [supportedSyntaxMessage],
524
+ )
525
+ }
526
+ // Map/Set materialize directly (the data checkpoint would serialize them to {}).
527
+ if (args[0] instanceof SandboxMap)
528
+ return Array.from((args[0] as SandboxMap).map.entries(), ([key, item]) => [key, item])
529
+ if (args[0] instanceof SandboxSet) return Array.from((args[0] as SandboxSet).set.values())
530
+ if (args[0] instanceof SandboxURLSearchParams) {
531
+ return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
532
+ }
533
+ const source = boundedData(args[0], "Array.from input")
534
+ if (typeof source === "string") return Array.from(source)
535
+ if (Array.isArray(source)) return [...source]
536
+ if (
537
+ source !== null &&
538
+ typeof source === "object" &&
539
+ typeof (source as { length?: unknown }).length === "number"
540
+ ) {
541
+ return Array.from(source as ArrayLike<unknown>)
542
+ }
543
+ throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
544
+ }
545
+ default:
546
+ throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
547
+ }
548
+ }
549
+
550
+ const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unknown>, node: AstNode): unknown => {
551
+ if (ref.namespace === "console")
552
+ throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node)
553
+ if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node)
554
+ if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node)
555
+ if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node)
556
+ if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node)
557
+ if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node)
558
+ if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node)
559
+ if (ref.namespace === "Date") {
560
+ if (!dateStatics.has(ref.name))
561
+ throw new InterpreterRuntimeError(`Date.${ref.name} is not available in CodeMode.`, node)
562
+ return invokeDateStatic(ref.name, args, node)
563
+ }
564
+ if (
565
+ ref.namespace === "RegExp" ||
566
+ ref.namespace === "Map" ||
567
+ ref.namespace === "Set" ||
568
+ ref.namespace === "URLSearchParams"
569
+ ) {
570
+ throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
571
+ }
572
+ return invokeJsonMethod(ref.name, args, node)
573
+ }
574
+
575
+ // Every identifier a parameter pattern binds, used to seed TDZ slots before defaults run.
576
+ const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<string> => {
577
+ switch (pattern.type) {
578
+ case "Identifier":
579
+ out.push(getString(pattern, "name"))
580
+ break
581
+ case "AssignmentPattern":
582
+ collectPatternNames(getNode(pattern, "left"), out)
583
+ break
584
+ case "RestElement":
585
+ collectPatternNames(getNode(pattern, "argument"), out)
586
+ break
587
+ case "ArrayPattern":
588
+ for (const element of getArray(pattern, "elements")) {
589
+ if (element !== null) collectPatternNames(asNode(element, "elements"), out)
590
+ }
591
+ break
592
+ case "ObjectPattern":
593
+ for (const property of getArray(pattern, "properties")) {
594
+ const prop = asNode(property, "properties")
595
+ collectPatternNames(prop.type === "RestElement" ? getNode(prop, "argument") : getNode(prop, "value"), out)
596
+ }
597
+ break
598
+ }
599
+ return out
600
+ }
601
+
602
+ class Interpreter<R> {
603
+ private scopes: Array<Map<string, Binding>>
604
+ private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
605
+ // Enumerable namespace/tool names at a node of the host tool tree, threaded from
606
+ // ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
607
+ private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
608
+ private readonly logs: Array<string>
609
+ private lastValue: unknown
610
+ // Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
611
+ private readonly callPermits: Semaphore.Semaphore
612
+ // Fiber-backed promises whose settlement no program construct has observed yet. Successful
613
+ // program completion drains these (like a runtime waiting on in-flight work at exit) and
614
+ // surfaces a never-awaited failure as an unhandled-rejection diagnostic.
615
+ private readonly pendingSettlements = new Set<SandboxPromise>()
616
+
617
+ constructor(
618
+ invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
619
+ toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
620
+ logs: Array<string> = [],
621
+ ) {
622
+ const globalScope = new Map<string, Binding>()
623
+ this.scopes = [globalScope]
624
+ this.invokeTool = invokeTool
625
+ this.toolKeys = toolKeys
626
+ this.logs = logs
627
+ this.lastValue = undefined
628
+ this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
629
+ globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
630
+ globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() })
631
+ globalScope.set("undefined", { mutable: false, value: undefined })
632
+ globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") })
633
+ globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") })
634
+ globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") })
635
+ globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") })
636
+ globalScope.set("String", { mutable: false, value: new CoercionFunction("String") })
637
+ globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") })
638
+ globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") })
639
+ globalScope.set("console", { mutable: false, value: new GlobalNamespace("console") })
640
+ globalScope.set("parseInt", { mutable: false, value: new CoercionFunction("parseInt") })
641
+ globalScope.set("parseFloat", { mutable: false, value: new CoercionFunction("parseFloat") })
642
+ globalScope.set("Date", { mutable: false, value: new GlobalNamespace("Date") })
643
+ globalScope.set("RegExp", { mutable: false, value: new GlobalNamespace("RegExp") })
644
+ globalScope.set("Map", { mutable: false, value: new GlobalNamespace("Map") })
645
+ globalScope.set("Set", { mutable: false, value: new GlobalNamespace("Set") })
646
+ globalScope.set("URL", { mutable: false, value: new GlobalNamespace("URL") })
647
+ globalScope.set("URLSearchParams", { mutable: false, value: new GlobalNamespace("URLSearchParams") })
648
+ globalScope.set("encodeURI", { mutable: false, value: new UriFunction("encodeURI") })
649
+ globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") })
650
+ globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") })
651
+ globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") })
652
+ // Error constructors are real values, so `x instanceof Error` works and `Error("msg")`
653
+ // (with or without `new`) constructs a branded { name, message } error object.
654
+ for (const name of errorConstructors) {
655
+ globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) })
656
+ }
657
+ // NaN/Infinity flow as ordinary in-sandbox values (normalized to null only at the data
658
+ // boundary - see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`.
659
+ globalScope.set("NaN", { mutable: false, value: NaN })
660
+ globalScope.set("Infinity", { mutable: false, value: Infinity })
661
+ }
662
+
663
+ run(program: ProgramNode): Effect.Effect<unknown, unknown, R> {
664
+ const self = this
665
+ // Run the program body in its own module scope on top of the builtin global scope, so
666
+ // top-level declarations (`let undefined = 5`, `const Object = ...`) shadow builtins like
667
+ // JS module scope, instead of colliding with the seeded globals.
668
+ this.pushScope()
669
+ return Effect.gen(function* () {
670
+ self.hoistFunctions(program.body)
671
+ let value: unknown = undefined
672
+ let returned = false
673
+ for (const statement of program.body) {
674
+ const result = yield* self.evaluateStatement(statement)
675
+
676
+ if (result.kind === "return") {
677
+ value = result.value
678
+ returned = true
679
+ break
680
+ }
681
+
682
+ if (result.kind === "break" || result.kind === "continue") {
683
+ throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
684
+ }
685
+
686
+ if (result.kind === "value") {
687
+ self.lastValue = result.value
688
+ }
689
+ }
690
+ if (!returned) value = self.lastValue
691
+
692
+ // The program body runs inside an implicit async function, so a returned promise
693
+ // resolves before crossing the data boundary - `return tools.ns.tool(...)` works
694
+ // without an explicit await, exactly as in JS.
695
+ if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)
696
+ yield* self.drainPendingSettlements()
697
+ return value
698
+ }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
699
+ }
700
+
701
+ // Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
702
+ // their work completes before the execution ends - mirroring a JS runtime waiting on
703
+ // in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
704
+ // diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
705
+ private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
706
+ const self = this
707
+ return Effect.gen(function* () {
708
+ for (const promise of [...self.pendingSettlements]) {
709
+ const exit = yield* self.observePromise(promise)
710
+ if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
711
+ const failure = normalizeError(Cause.squash(exit.cause))
712
+ throw new InterpreterRuntimeError(
713
+ `Unhandled rejection from an un-awaited tool call: ${failure.message}`,
714
+ undefined,
715
+ failure.kind,
716
+ ["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."],
717
+ )
718
+ }
719
+ })
720
+ }
721
+
722
+ // Eagerly starts a tool call on a supervised child fiber (so the execution timeout and
723
+ // scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a
724
+ // first-class promise value. `startImmediately` makes the runtime admit the call - charging
725
+ // the tool-call budget and firing onToolCallStart - at the call site, before any await.
726
+ private createToolCallPromise(
727
+ path: ReadonlyArray<string>,
728
+ args: Array<unknown>,
729
+ ): Effect.Effect<SandboxPromise, never, R> {
730
+ const self = this
731
+ return Effect.map(
732
+ Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), {
733
+ startImmediately: true,
734
+ }),
735
+ (fiber) => {
736
+ const promise = new SandboxPromise(fiber)
737
+ self.pendingSettlements.add(promise)
738
+ return promise
739
+ },
740
+ )
741
+ }
742
+
743
+ // The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking.
744
+ // Fiber settlement is idempotent, so observing the same promise repeatedly (await twice,
745
+ // Promise.all([p, p])) never re-runs the underlying call.
746
+ private observePromise(promise: SandboxPromise): Effect.Effect<Exit.Exit<unknown, unknown>> {
747
+ this.pendingSettlements.delete(promise)
748
+ return promise.fiber !== undefined ? Fiber.await(promise.fiber) : Effect.exit(promise.immediate ?? Effect.void)
749
+ }
750
+
751
+ // `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch
752
+ // observes it exactly like a synchronous throw at the await site.
753
+ private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect<unknown, unknown, never> {
754
+ const self = this
755
+ return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node))
756
+ }
757
+
758
+ private unwrapPromiseExit(
759
+ promise: SandboxPromise | undefined,
760
+ exit: Exit.Exit<unknown, unknown>,
761
+ node?: AstNode,
762
+ ): Effect.Effect<unknown, unknown> {
763
+ if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
764
+ // A call Promise.race interrupted after losing settles as a catchable program failure;
765
+ // any other interruption is execution teardown (timeout/host) and must keep propagating
766
+ // as interruption rather than becoming program-visible data.
767
+ if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) {
768
+ return Effect.fail(
769
+ new InterpreterRuntimeError(
770
+ "This tool call was interrupted because another value settled a Promise.race first.",
771
+ node,
772
+ ),
773
+ )
774
+ }
775
+ return Effect.failCause(exit.cause)
776
+ }
777
+
778
+ private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
779
+ switch (node.type) {
780
+ case "ExpressionStatement":
781
+ return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
782
+ case "VariableDeclaration":
783
+ return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
784
+ case "ReturnStatement": {
785
+ const argumentNode = getOptionalNode(node, "argument")
786
+ return argumentNode
787
+ ? Effect.map(this.evaluateExpression(argumentNode), (value) => ({ kind: "return", value }))
788
+ : Effect.succeed({ kind: "return", value: undefined })
789
+ }
790
+ case "BlockStatement":
791
+ return this.evaluateBlock(node)
792
+ case "IfStatement":
793
+ return this.evaluateIfStatement(node)
794
+ case "SwitchStatement":
795
+ return this.evaluateSwitchStatement(node)
796
+ case "WhileStatement":
797
+ return this.evaluateWhileStatement(node)
798
+ case "DoWhileStatement":
799
+ return this.evaluateDoWhileStatement(node)
800
+ case "ForStatement":
801
+ return this.evaluateForStatement(node)
802
+ case "ForOfStatement":
803
+ return this.evaluateForOfStatement(node)
804
+ case "ForInStatement":
805
+ return this.evaluateForInStatement(node)
806
+ case "BreakStatement":
807
+ return Effect.succeed(this.evaluateBreakStatement(node))
808
+ case "ContinueStatement":
809
+ return Effect.succeed(this.evaluateContinueStatement(node))
810
+ case "ThrowStatement":
811
+ return this.evaluateThrowStatement(node)
812
+ case "TryStatement":
813
+ return this.evaluateTryStatement(node)
814
+ case "EmptyStatement":
815
+ return Effect.succeed({ kind: "none" })
816
+ case "FunctionDeclaration":
817
+ return Effect.succeed({ kind: "none" }) // bound ahead of time by hoistFunctions
818
+ default:
819
+ throw unsupportedSyntax(node.type, node)
820
+ }
821
+ }
822
+
823
+ private evaluateBlock(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
824
+ this.pushScope()
825
+ const self = this
826
+ return Effect.gen(function* () {
827
+ const body = getArray(node, "body")
828
+ self.hoistFunctions(body)
829
+
830
+ for (const statementValue of body) {
831
+ const statement = asNode(statementValue, "body")
832
+ const result = yield* self.evaluateStatement(statement)
833
+
834
+ if (result.kind === "value") {
835
+ self.lastValue = result.value
836
+ continue
837
+ }
838
+
839
+ if (result.kind !== "none") {
840
+ return result
841
+ }
842
+ }
843
+
844
+ return { kind: "none" } satisfies StatementResult
845
+ }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
846
+ }
847
+
848
+ private createFunction(node: AstNode): CodeModeFunction {
849
+ if (node.generator === true) {
850
+ throw new InterpreterRuntimeError(
851
+ "Generator functions are not supported in CodeMode.",
852
+ node,
853
+ "UnsupportedSyntax",
854
+ [supportedSyntaxMessage],
855
+ )
856
+ }
857
+ return new CodeModeFunction(
858
+ getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
859
+ getNode(node, "body"),
860
+ this.scopes.slice(),
861
+ )
862
+ }
863
+
864
+ // Function declarations are hoisted: bound in their scope before the body runs, so a
865
+ // program can call a helper defined further down (matching JavaScript).
866
+ private hoistFunctions(statements: Array<unknown>): void {
867
+ for (const statementValue of statements) {
868
+ if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue
869
+ const node = statementValue as AstNode
870
+ this.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node)
871
+ }
872
+ }
873
+
874
+ private evaluateIfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
875
+ const testNode = getNode(node, "test")
876
+ const consequentNode = getNode(node, "consequent")
877
+ const alternateNode = getOptionalNode(node, "alternate")
878
+
879
+ return Effect.flatMap(this.evaluateExpression(testNode), (test) =>
880
+ test
881
+ ? this.evaluateStatement(consequentNode)
882
+ : alternateNode
883
+ ? this.evaluateStatement(alternateNode)
884
+ : Effect.succeed({ kind: "none" }),
885
+ )
886
+ }
887
+
888
+ private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
889
+ const self = this
890
+ this.pushScope()
891
+ return Effect.gen(function* () {
892
+ const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant"))
893
+ if (containsOpaqueReference(discriminant)) {
894
+ throw new InterpreterRuntimeError(
895
+ "Switch discriminants must be data values in CodeMode.",
896
+ node,
897
+ "InvalidDataValue",
898
+ )
899
+ }
900
+ const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
901
+ let defaultIndex: number | undefined
902
+ let selected: number | undefined
903
+ for (const [index, branch] of cases.entries()) {
904
+ const test = getOptionalNode(branch, "test")
905
+ if (!test) {
906
+ defaultIndex = index
907
+ continue
908
+ }
909
+ const candidate = yield* self.evaluateExpression(test)
910
+ if (containsOpaqueReference(candidate)) {
911
+ throw new InterpreterRuntimeError(
912
+ "Switch case values must be data values in CodeMode.",
913
+ test,
914
+ "InvalidDataValue",
915
+ )
916
+ }
917
+ if (candidate === discriminant) {
918
+ selected = index
919
+ break
920
+ }
921
+ }
922
+ const start = selected ?? defaultIndex
923
+ if (start === undefined) return { kind: "none" } satisfies StatementResult
924
+ for (let index = start; index < cases.length; index += 1) {
925
+ for (const statementValue of getArray(cases[index]!, "consequent")) {
926
+ const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
927
+ if (result.kind === "break") return { kind: "none" } satisfies StatementResult
928
+ if (result.kind === "return" || result.kind === "continue") return result
929
+ if (result.kind === "value") self.lastValue = result.value
930
+ }
931
+ }
932
+ return { kind: "none" } satisfies StatementResult
933
+ }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
934
+ }
935
+
936
+ private evaluateWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
937
+ const testNode = getNode(node, "test")
938
+ const bodyNode = getNode(node, "body")
939
+
940
+ const self = this
941
+ return Effect.gen(function* () {
942
+ while (yield* self.evaluateExpression(testNode)) {
943
+ const result = yield* self.evaluateStatement(bodyNode)
944
+
945
+ if (result.kind === "continue") {
946
+ continue
947
+ }
948
+
949
+ if (result.kind === "break") {
950
+ return { kind: "none" } satisfies StatementResult
951
+ }
952
+
953
+ if (result.kind === "return") {
954
+ return result
955
+ }
956
+
957
+ if (result.kind === "value") {
958
+ self.lastValue = result.value
959
+ }
960
+ }
961
+
962
+ return { kind: "none" } satisfies StatementResult
963
+ })
964
+ }
965
+
966
+ private evaluateDoWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
967
+ const bodyNode = getNode(node, "body")
968
+ const testNode = getNode(node, "test")
969
+
970
+ const self = this
971
+ return Effect.gen(function* () {
972
+ do {
973
+ const result = yield* self.evaluateStatement(bodyNode)
974
+
975
+ if (result.kind === "continue") {
976
+ continue
977
+ }
978
+
979
+ if (result.kind === "break") {
980
+ return { kind: "none" } satisfies StatementResult
981
+ }
982
+
983
+ if (result.kind === "return") {
984
+ return result
985
+ }
986
+
987
+ if (result.kind === "value") {
988
+ self.lastValue = result.value
989
+ }
990
+ } while (yield* self.evaluateExpression(testNode))
991
+
992
+ return { kind: "none" } satisfies StatementResult
993
+ })
994
+ }
995
+
996
+ private evaluateForStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
997
+ this.pushScope()
998
+ const self = this
999
+ return Effect.gen(function* () {
1000
+ const initNode = getOptionalNode(node, "init")
1001
+ const testNode = getOptionalNode(node, "test")
1002
+ const updateNode = getOptionalNode(node, "update")
1003
+ const bodyNode = getNode(node, "body")
1004
+
1005
+ if (initNode) {
1006
+ if (initNode.type === "VariableDeclaration") {
1007
+ yield* self.evaluateVariableDeclaration(initNode)
1008
+ } else {
1009
+ yield* self.evaluateExpression(initNode)
1010
+ }
1011
+ }
1012
+
1013
+ const perIterationBindings =
1014
+ initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var"
1015
+ ? Array.from(self.currentScope().keys())
1016
+ : []
1017
+
1018
+ while (testNode ? yield* self.evaluateExpression(testNode) : true) {
1019
+ let iterationScope: Map<string, Binding> | undefined
1020
+ if (perIterationBindings.length > 0) {
1021
+ iterationScope = new Map(
1022
+ perIterationBindings.map((name) => {
1023
+ const binding = self.currentScope().get(name)!
1024
+ return [name, { ...binding }]
1025
+ }),
1026
+ )
1027
+ self.scopes.push(iterationScope)
1028
+ }
1029
+ const result = yield* self.evaluateStatement(bodyNode).pipe(
1030
+ Effect.ensuring(
1031
+ Effect.sync(() => {
1032
+ if (iterationScope) self.popScope()
1033
+ }),
1034
+ ),
1035
+ )
1036
+
1037
+ if (result.kind === "return") {
1038
+ return result
1039
+ }
1040
+
1041
+ if (result.kind === "break") {
1042
+ return { kind: "none" } satisfies StatementResult
1043
+ }
1044
+
1045
+ if (result.kind === "value") {
1046
+ self.lastValue = result.value
1047
+ }
1048
+
1049
+ if (iterationScope) {
1050
+ const loopScope = self.currentScope()
1051
+ for (const name of perIterationBindings) {
1052
+ loopScope.set(name, { ...iterationScope.get(name)! })
1053
+ }
1054
+ }
1055
+
1056
+ if (updateNode) {
1057
+ yield* self.evaluateExpression(updateNode)
1058
+ }
1059
+
1060
+ if (result.kind === "continue") {
1061
+ continue
1062
+ }
1063
+ }
1064
+
1065
+ return { kind: "none" } satisfies StatementResult
1066
+ }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
1067
+ }
1068
+
1069
+ private evaluateForOfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
1070
+ if (getBoolean(node, "await")) {
1071
+ throw new InterpreterRuntimeError("for await...of is not supported.", node)
1072
+ }
1073
+
1074
+ const self = this
1075
+ return Effect.gen(function* () {
1076
+ const left = getNode(node, "left")
1077
+ const right = yield* self.evaluateExpression(getNode(node, "right"))
1078
+ const body = getNode(node, "body")
1079
+
1080
+ // Arrays iterate in place; strings iterate code points; Maps iterate [key, value]
1081
+ // pairs and Sets iterate values over a snapshot (mutation during iteration is safe).
1082
+ const iterable = Array.isArray(right) ? right : spreadItems(right)
1083
+ if (iterable === undefined) {
1084
+ throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
1085
+ }
1086
+
1087
+ let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
1088
+ let assignmentName: string | undefined
1089
+
1090
+ if (left.type === "VariableDeclaration") {
1091
+ const declarations = getArray(left, "declarations")
1092
+ if (declarations.length !== 1) {
1093
+ throw new InterpreterRuntimeError("for...of supports one declared binding.", left)
1094
+ }
1095
+
1096
+ const declarator = asNode(declarations[0], "declarations[0]")
1097
+ declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
1098
+ } else if (left.type === "Identifier") {
1099
+ assignmentName = getString(left, "name")
1100
+ } else {
1101
+ throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
1102
+ }
1103
+
1104
+ for (const value of iterable) {
1105
+ if (declaration) {
1106
+ self.pushScope()
1107
+ yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
1108
+ } else if (assignmentName) {
1109
+ self.setIdentifierValue(assignmentName, value, left)
1110
+ }
1111
+
1112
+ const result = yield* self.evaluateStatement(body).pipe(
1113
+ Effect.ensuring(
1114
+ Effect.sync(() => {
1115
+ if (declaration) self.popScope()
1116
+ }),
1117
+ ),
1118
+ )
1119
+
1120
+ if (result.kind === "return") {
1121
+ return result
1122
+ }
1123
+
1124
+ if (result.kind === "break") {
1125
+ return { kind: "none" }
1126
+ }
1127
+
1128
+ if (result.kind === "value") {
1129
+ self.lastValue = result.value
1130
+ }
1131
+
1132
+ if (result.kind === "continue") {
1133
+ continue
1134
+ }
1135
+ }
1136
+
1137
+ return { kind: "none" }
1138
+ })
1139
+ }
1140
+
1141
+ // Own enumerable string keys of a value, shared by `for...in` and `Object.keys` over tool
1142
+ // references: plain data objects enumerate their own keys, arrays their index strings (plus
1143
+ // any own non-index properties, e.g. match results' index/groups - exactly Object.keys in
1144
+ // JS), and a tool reference the namespace/tool names at its path in the host tool tree.
1145
+ // Returns undefined for everything else so callers can raise a contextual error.
1146
+ private enumerableKeys(value: unknown): Array<string> | undefined {
1147
+ if (value instanceof ToolReference) {
1148
+ return [...this.toolKeys(value.path)]
1149
+ }
1150
+ if (Array.isArray(value)) {
1151
+ return Object.keys(value)
1152
+ }
1153
+ if (value !== null && typeof value === "object" && !isRuntimeReference(value)) {
1154
+ return Object.keys(value)
1155
+ }
1156
+ return undefined
1157
+ }
1158
+
1159
+ private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
1160
+ const self = this
1161
+ return Effect.gen(function* () {
1162
+ const left = getNode(node, "left")
1163
+ const right = yield* self.evaluateExpression(getNode(node, "right"))
1164
+ const body = getNode(node, "body")
1165
+
1166
+ // Keys are snapshotted up front (mutation during iteration is safe): plain objects
1167
+ // enumerate their own keys, arrays their index strings, and tool references the
1168
+ // namespace/tool names at that node - the same enumeration Object.keys performs.
1169
+ // Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather
1170
+ // than real JS's surprising behavior (indices for strings, zero iterations for
1171
+ // Maps/Sets/null): the hint points at the constructs that do what the program means.
1172
+ const keys = self.enumerableKeys(right)
1173
+ if (keys === undefined) {
1174
+ throw new InterpreterRuntimeError(
1175
+ "for...in requires a plain object, array, or tools reference in CodeMode. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.",
1176
+ node,
1177
+ )
1178
+ }
1179
+
1180
+ let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
1181
+ let assignmentName: string | undefined
1182
+
1183
+ if (left.type === "VariableDeclaration") {
1184
+ const declarations = getArray(left, "declarations")
1185
+ if (declarations.length !== 1) {
1186
+ throw new InterpreterRuntimeError("for...in supports one declared binding.", left)
1187
+ }
1188
+
1189
+ const declarator = asNode(declarations[0], "declarations[0]")
1190
+ declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
1191
+ } else if (left.type === "Identifier") {
1192
+ assignmentName = getString(left, "name")
1193
+ } else {
1194
+ throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
1195
+ }
1196
+
1197
+ for (const key of keys) {
1198
+ if (declaration) {
1199
+ self.pushScope()
1200
+ yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left)
1201
+ } else if (assignmentName) {
1202
+ self.setIdentifierValue(assignmentName, key, left)
1203
+ }
1204
+
1205
+ const result = yield* self.evaluateStatement(body).pipe(
1206
+ Effect.ensuring(
1207
+ Effect.sync(() => {
1208
+ if (declaration) self.popScope()
1209
+ }),
1210
+ ),
1211
+ )
1212
+
1213
+ if (result.kind === "return") {
1214
+ return result
1215
+ }
1216
+
1217
+ if (result.kind === "break") {
1218
+ return { kind: "none" }
1219
+ }
1220
+
1221
+ if (result.kind === "value") {
1222
+ self.lastValue = result.value
1223
+ }
1224
+
1225
+ if (result.kind === "continue") {
1226
+ continue
1227
+ }
1228
+ }
1229
+
1230
+ return { kind: "none" }
1231
+ })
1232
+ }
1233
+
1234
+ private evaluateBreakStatement(node: AstNode): StatementResult {
1235
+ const labelNode = getOptionalNode(node, "label")
1236
+
1237
+ if (labelNode) {
1238
+ throw new InterpreterRuntimeError("Labeled break is not supported in v1.", node)
1239
+ }
1240
+
1241
+ return { kind: "break" }
1242
+ }
1243
+
1244
+ private evaluateContinueStatement(node: AstNode): StatementResult {
1245
+ const labelNode = getOptionalNode(node, "label")
1246
+
1247
+ if (labelNode) {
1248
+ throw new InterpreterRuntimeError("Labeled continue is not supported in v1.", node)
1249
+ }
1250
+
1251
+ return { kind: "continue" }
1252
+ }
1253
+
1254
+ private evaluateThrowStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
1255
+ const argument = getNode(node, "argument")
1256
+ return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value)))
1257
+ }
1258
+
1259
+ private evaluateTryStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
1260
+ const body = getNode(node, "block")
1261
+ const handler = getOptionalNode(node, "handler")
1262
+ const finalizer = getOptionalNode(node, "finalizer")
1263
+ const self = this
1264
+
1265
+ const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), {
1266
+ onFailure: (cause) => {
1267
+ if (cause.reasons.some(Cause.isInterruptReason) || !handler) {
1268
+ return Effect.failCause(cause)
1269
+ }
1270
+
1271
+ // The program sees a plain { message } error (or the thrown value itself) - see
1272
+ // caughtErrorValue, shared with Promise.allSettled rejection reasons.
1273
+ const caught = caughtErrorValue(Cause.squash(cause))
1274
+ const parameter = getOptionalNode(handler, "param")
1275
+ self.pushScope()
1276
+ return Effect.gen(function* () {
1277
+ if (parameter) yield* self.declarePattern(parameter, caught, true, handler)
1278
+ return yield* self.evaluateStatement(getNode(handler, "body"))
1279
+ }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
1280
+ },
1281
+ onSuccess: Effect.succeed,
1282
+ })
1283
+
1284
+ if (!finalizer) return attempted
1285
+
1286
+ const isAbrupt = (result: StatementResult): boolean =>
1287
+ result.kind === "return" || result.kind === "break" || result.kind === "continue"
1288
+
1289
+ return Effect.matchCauseEffect(attempted, {
1290
+ onFailure: (cause) =>
1291
+ cause.reasons.some(Cause.isInterruptReason)
1292
+ ? Effect.failCause(cause)
1293
+ : Effect.flatMap(this.evaluateStatement(finalizer), (final) =>
1294
+ isAbrupt(final) ? Effect.succeed(final) : Effect.failCause(cause),
1295
+ ),
1296
+ onSuccess: (result) =>
1297
+ Effect.flatMap(this.evaluateStatement(finalizer), (final) =>
1298
+ isAbrupt(final) ? Effect.succeed(final) : Effect.succeed(result),
1299
+ ),
1300
+ })
1301
+ }
1302
+
1303
+ private evaluateVariableDeclaration(node: AstNode): Effect.Effect<void, unknown, R> {
1304
+ const kind = getString(node, "kind")
1305
+ const declarations = getArray(node, "declarations")
1306
+ const self = this
1307
+ return Effect.gen(function* () {
1308
+ for (const declarationValue of declarations) {
1309
+ const declaration = asNode(declarationValue, "declarations")
1310
+
1311
+ if (declaration.type !== "VariableDeclarator") {
1312
+ throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration)
1313
+ }
1314
+
1315
+ const init = getOptionalNode(declaration, "init")
1316
+ const value = init ? yield* self.evaluateExpression(init) : undefined
1317
+ yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration)
1318
+ }
1319
+ })
1320
+ }
1321
+
1322
+ private declarePattern(
1323
+ pattern: AstNode,
1324
+ value: unknown,
1325
+ mutable: boolean,
1326
+ node: AstNode,
1327
+ ): Effect.Effect<void, unknown, R> {
1328
+ const self = this
1329
+ return Effect.gen(function* () {
1330
+ if (pattern.type === "Identifier") {
1331
+ self.declare(getString(pattern, "name"), value, mutable, node)
1332
+ return
1333
+ }
1334
+
1335
+ // Default values: `x = expr` / `{ a = 1 }` - the default is evaluated only when the value is undefined.
1336
+ if (pattern.type === "AssignmentPattern") {
1337
+ const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
1338
+ yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node)
1339
+ return
1340
+ }
1341
+
1342
+ if (pattern.type === "ObjectPattern") {
1343
+ if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
1344
+ throw new InterpreterRuntimeError(
1345
+ "Object destructuring requires a data object value.",
1346
+ pattern,
1347
+ "InvalidDataValue",
1348
+ )
1349
+ }
1350
+
1351
+ const consumed = new Set<string>()
1352
+ for (const propertyValue of getArray(pattern, "properties")) {
1353
+ const property = asNode(propertyValue, "properties")
1354
+
1355
+ // Object rest: `{ a, ...others }` - gather the not-yet-consumed own keys.
1356
+ if (property.type === "RestElement") {
1357
+ const rest: SafeObject = Object.create(null) as SafeObject
1358
+ for (const [key, item] of Object.entries(value as SafeObject)) {
1359
+ if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
1360
+ }
1361
+ yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property)
1362
+ continue
1363
+ }
1364
+
1365
+ if (
1366
+ property.type !== "Property" ||
1367
+ getBoolean(property, "computed") ||
1368
+ getString(property, "kind") !== "init"
1369
+ ) {
1370
+ throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
1371
+ }
1372
+
1373
+ const keyNode = getNode(property, "key")
1374
+ const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
1375
+ if (isBlockedMember(key)) {
1376
+ throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
1377
+ }
1378
+ consumed.add(key)
1379
+ yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property)
1380
+ }
1381
+ return
1382
+ }
1383
+
1384
+ if (pattern.type === "ArrayPattern") {
1385
+ if (!Array.isArray(value)) {
1386
+ throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
1387
+ }
1388
+
1389
+ for (const [index, item] of getArray(pattern, "elements").entries()) {
1390
+ if (item === null) continue
1391
+ const element = asNode(item, `elements[${index}]`)
1392
+ // Array rest: `[head, ...tail]` - binds the remaining elements (must be last).
1393
+ if (element.type === "RestElement") {
1394
+ yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element)
1395
+ break
1396
+ }
1397
+ yield* self.declarePattern(element, value[index], mutable, pattern)
1398
+ }
1399
+ return
1400
+ }
1401
+
1402
+ throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
1403
+ })
1404
+ }
1405
+
1406
+ private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1407
+ switch (node.type) {
1408
+ case "Literal": {
1409
+ // A regex literal parses as a Literal node carrying { pattern, flags }; construct the
1410
+ // sandbox regex from those (the host `value` instance is never exposed).
1411
+ const regex = node.regex
1412
+ if (isRecord(regex) && typeof regex.pattern === "string") {
1413
+ return Effect.sync(() =>
1414
+ this.constructRegExp([regex.pattern, typeof regex.flags === "string" ? regex.flags : ""], node),
1415
+ )
1416
+ }
1417
+ return Effect.sync(() => boundedData(node.value, "Literal"))
1418
+ }
1419
+ case "Identifier":
1420
+ return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node))
1421
+ case "BinaryExpression":
1422
+ return this.evaluateBinaryExpression(node)
1423
+ case "LogicalExpression":
1424
+ return this.evaluateLogicalExpression(node)
1425
+ case "UnaryExpression":
1426
+ return this.evaluateUnaryExpression(node)
1427
+ case "AssignmentExpression":
1428
+ return this.evaluateAssignmentExpression(node)
1429
+ case "CallExpression":
1430
+ return this.evaluateCallExpression(node)
1431
+ case "ArrowFunctionExpression":
1432
+ case "FunctionExpression":
1433
+ return Effect.sync(() => this.createFunction(node))
1434
+ case "MemberExpression":
1435
+ return this.readMember(node)
1436
+ case "ChainExpression":
1437
+ return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) =>
1438
+ value === OptionalShortCircuit ? undefined : value,
1439
+ )
1440
+ case "ObjectExpression":
1441
+ return this.evaluateObjectExpression(node)
1442
+ case "ArrayExpression":
1443
+ return this.evaluateArrayExpression(node)
1444
+ case "TemplateLiteral":
1445
+ return this.evaluateTemplateLiteral(node)
1446
+ case "ConditionalExpression":
1447
+ return this.evaluateConditionalExpression(node)
1448
+ case "UpdateExpression":
1449
+ return this.evaluateUpdateExpression(node)
1450
+ case "AwaitExpression": {
1451
+ // `await` resolves a promise value; awaiting anything else is a passthrough no-op,
1452
+ // matching real JS semantics for non-thenables.
1453
+ const self = this
1454
+ return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
1455
+ value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value),
1456
+ )
1457
+ }
1458
+ case "NewExpression":
1459
+ return this.evaluateNewExpression(node)
1460
+ default:
1461
+ throw unsupportedSyntax(node.type, node)
1462
+ }
1463
+ }
1464
+
1465
+ private evaluateNewExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1466
+ const callee = getNode(node, "callee")
1467
+ if (callee.type !== "Identifier") {
1468
+ throw unsupportedSyntax("NewExpression", node)
1469
+ }
1470
+ const name = getString(callee, "name")
1471
+ const argNodes = getArray(node, "arguments")
1472
+ const self = this
1473
+ if (name === "Promise") {
1474
+ throw new InterpreterRuntimeError(
1475
+ "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
1476
+ node,
1477
+ "UnsupportedSyntax",
1478
+ [supportedSyntaxMessage],
1479
+ )
1480
+ }
1481
+ if (errorConstructors.has(name)) {
1482
+ return Effect.gen(function* () {
1483
+ const arg =
1484
+ argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined
1485
+ return createErrorValue(name, arg === undefined ? "" : coerceToString(arg))
1486
+ })
1487
+ }
1488
+ if (valueConstructors.has(name)) {
1489
+ return Effect.gen(function* () {
1490
+ const args = yield* self.evaluateCallArguments(argNodes)
1491
+ switch (name) {
1492
+ case "Date":
1493
+ return self.constructDate(args)
1494
+ case "RegExp":
1495
+ return self.constructRegExp(args, node)
1496
+ case "Map":
1497
+ return self.constructMap(args[0], node)
1498
+ case "Set":
1499
+ return self.constructSet(args[0], node)
1500
+ case "URL":
1501
+ return self.constructURL(args, node)
1502
+ default:
1503
+ return self.constructURLSearchParams(args[0], node)
1504
+ }
1505
+ })
1506
+ }
1507
+ throw unsupportedSyntax("NewExpression", node)
1508
+ }
1509
+
1510
+ private constructDate(args: Array<unknown>): SandboxDate {
1511
+ if (args.length === 0) return new SandboxDate(Date.now())
1512
+ if (args.length === 1) {
1513
+ const arg = args[0]
1514
+ if (arg instanceof SandboxDate) return new SandboxDate(arg.time)
1515
+ if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime())
1516
+ if (typeof arg === "string") return new SandboxDate(Date.parse(arg))
1517
+ return new SandboxDate(Number.NaN)
1518
+ }
1519
+ // new Date(year, month, day?, hours?, ...) - local-time component form.
1520
+ const parts = args.map((arg) => coerceToNumber(arg))
1521
+ return new SandboxDate(new Date(...(parts as [number, number])).getTime())
1522
+ }
1523
+
1524
+ private constructRegExp(args: Array<unknown>, node: AstNode): SandboxRegExp {
1525
+ const first = args[0]
1526
+ const pattern =
1527
+ first instanceof SandboxRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first)
1528
+ const flagsArg = args[1]
1529
+ if (flagsArg !== undefined && typeof flagsArg !== "string") {
1530
+ throw new InterpreterRuntimeError(
1531
+ `RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`,
1532
+ node,
1533
+ )
1534
+ }
1535
+ const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "")
1536
+ try {
1537
+ return new SandboxRegExp(pattern, flags)
1538
+ } catch (error) {
1539
+ // Say which part was rejected and how to fix it, instead of passing the engine
1540
+ // message through bare. A flags failure names the flags; a pattern failure gets the
1541
+ // escaping hint (the usual cause is an unescaped metacharacter in a built-up string).
1542
+ const reason = regexFailureReason(error)
1543
+ throw new InterpreterRuntimeError(
1544
+ /flag/i.test(reason)
1545
+ ? `new RegExp(...) received invalid flags ${JSON.stringify(flags)} (${reason}). Valid flags are d, g, i, m, s, u, v, and y.`
1546
+ : `new RegExp(...) received ${JSON.stringify(pattern)}, which is not a valid regular expression pattern (${reason}). ${escapeRegexHint}`,
1547
+ node,
1548
+ ).as("SyntaxError")
1549
+ }
1550
+ }
1551
+
1552
+ private constructMap(init: unknown, node: AstNode): SandboxMap {
1553
+ const target = new SandboxMap()
1554
+ if (init === undefined || init === null) return target
1555
+ const entries = Array.isArray(init)
1556
+ ? init
1557
+ : init instanceof SandboxMap
1558
+ ? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item])
1559
+ : undefined
1560
+ if (entries === undefined) {
1561
+ throw new InterpreterRuntimeError(
1562
+ "new Map(...) expects an array of [key, value] pairs, a Map, or no argument.",
1563
+ node,
1564
+ )
1565
+ }
1566
+ for (const pair of entries) {
1567
+ if (!Array.isArray(pair)) {
1568
+ throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node)
1569
+ }
1570
+ target.map.set(pair[0], pair[1])
1571
+ }
1572
+ return target
1573
+ }
1574
+
1575
+ private constructSet(init: unknown, node: AstNode): SandboxSet {
1576
+ const target = new SandboxSet()
1577
+ if (init === undefined || init === null) return target
1578
+ const items = Array.isArray(init)
1579
+ ? init
1580
+ : init instanceof SandboxSet
1581
+ ? Array.from(init.set.values())
1582
+ : typeof init === "string"
1583
+ ? Array.from(init)
1584
+ : undefined
1585
+ if (items === undefined) {
1586
+ throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node)
1587
+ }
1588
+ for (const item of items) target.set.add(item)
1589
+ return target
1590
+ }
1591
+
1592
+ private constructURL(args: Array<unknown>, node: AstNode): SandboxURL {
1593
+ if (args.length === 0) {
1594
+ throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as(
1595
+ "TypeError",
1596
+ )
1597
+ }
1598
+ const input = urlArgument(args[0], "new URL input")
1599
+ const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base")
1600
+ try {
1601
+ return new SandboxURL(new URL(input, base))
1602
+ } catch {
1603
+ throw new InterpreterRuntimeError(
1604
+ `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`,
1605
+ node,
1606
+ ).as("TypeError")
1607
+ }
1608
+ }
1609
+
1610
+ private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams {
1611
+ if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams())
1612
+ if (init instanceof SandboxURLSearchParams) {
1613
+ return new SandboxURLSearchParams(new URLSearchParams(init.params))
1614
+ }
1615
+ if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init))
1616
+ if (init === null || typeof init === "number" || typeof init === "boolean") {
1617
+ return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init)))
1618
+ }
1619
+ if (init instanceof SandboxMap) {
1620
+ return this.constructURLSearchParams(
1621
+ Array.from(init.map.entries(), ([key, value]) => [key, value]),
1622
+ node,
1623
+ )
1624
+ }
1625
+ if (Array.isArray(init)) {
1626
+ const entries = init.map((pair) => {
1627
+ if (!Array.isArray(pair) || pair.length !== 2) {
1628
+ throw new InterpreterRuntimeError(
1629
+ "new URLSearchParams(...) expects an array of [name, value] pairs.",
1630
+ node,
1631
+ ).as("TypeError")
1632
+ }
1633
+ return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [
1634
+ string,
1635
+ string,
1636
+ ]
1637
+ })
1638
+ return new SandboxURLSearchParams(new URLSearchParams(entries))
1639
+ }
1640
+ if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams())
1641
+ const data = boundedData(init, "new URLSearchParams input")
1642
+ if (data === null || typeof data !== "object") {
1643
+ throw new InterpreterRuntimeError(
1644
+ "new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.",
1645
+ node,
1646
+ ).as("TypeError")
1647
+ }
1648
+ return new SandboxURLSearchParams(
1649
+ new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
1650
+ )
1651
+ }
1652
+
1653
+ private evaluateBinaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1654
+ const operator = getString(node, "operator")
1655
+ const self = this
1656
+ return Effect.gen(function* () {
1657
+ const lhs = yield* self.evaluateExpression(getNode(node, "left"))
1658
+ const rhs = yield* self.evaluateExpression(getNode(node, "right"))
1659
+ // Like `typeof`, `instanceof` observes any value without coercing it (a promise or
1660
+ // function operand is a legitimate question, not an error), so it is handled before
1661
+ // the data-only operand check.
1662
+ if (operator === "instanceof") return instanceofValue(lhs, rhs, node)
1663
+ return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result")
1664
+ })
1665
+ }
1666
+
1667
+ /**
1668
+ * Applies a binary operator to two already-evaluated operands with CodeMode's coercion
1669
+ * semantics. Shared by binary expressions and compound assignment (`x op= y` must behave
1670
+ * exactly like `x = x op y`, coercion included).
1671
+ */
1672
+ private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
1673
+ if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
1674
+ throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue")
1675
+ }
1676
+ // Data objects/arrays are null-prototype, so JS's ToPrimitive throws an opaque host
1677
+ // "No default value" TypeError when an operator coerces them. Coerce to their JS string
1678
+ // form first (as String(x) / template literals do) so operators behave like JavaScript.
1679
+ // A Date follows its ToPrimitive hints: string for `+` (concatenation), its time value
1680
+ // for arithmetic and ordering - so `end - start` and `a < b` work as in JS.
1681
+ // Identity (=== / !==) and the right operand of `in` keep their raw object value.
1682
+ const coerceOperand = (operand: unknown): unknown => {
1683
+ if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time
1684
+ return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
1685
+ }
1686
+ const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
1687
+ const l = coerceOperand(lhs)
1688
+ const r = coerceOperand(rhs)
1689
+ switch (operator) {
1690
+ case "+":
1691
+ return (l as string) + (r as string)
1692
+ case "-":
1693
+ return (l as number) - (r as number)
1694
+ case "*":
1695
+ return (l as number) * (r as number)
1696
+ case "/":
1697
+ return (l as number) / (r as number)
1698
+ case "%":
1699
+ return (l as number) % (r as number)
1700
+ case "**":
1701
+ return (l as number) ** (r as number)
1702
+ // Two objects compare by identity in JS (no ToPrimitive); only object-vs-primitive coerces.
1703
+ case "==":
1704
+ return bothObjects ? lhs === rhs : l == r
1705
+ case "===":
1706
+ return lhs === rhs
1707
+ case "!=":
1708
+ return bothObjects ? lhs !== rhs : l != r
1709
+ case "!==":
1710
+ return lhs !== rhs
1711
+ case "<":
1712
+ return (l as string) < (r as string)
1713
+ case "<=":
1714
+ return (l as string) <= (r as string)
1715
+ case ">":
1716
+ return (l as string) > (r as string)
1717
+ case ">=":
1718
+ return (l as string) >= (r as string)
1719
+ case "&":
1720
+ return (l as number) & (r as number)
1721
+ case "|":
1722
+ return (l as number) | (r as number)
1723
+ case "^":
1724
+ return (l as number) ^ (r as number)
1725
+ case "<<":
1726
+ return (l as number) << (r as number)
1727
+ case ">>":
1728
+ return (l as number) >> (r as number)
1729
+ case ">>>":
1730
+ return (l as number) >>> (r as number)
1731
+ case "in":
1732
+ if (rhs === null || typeof rhs !== "object") {
1733
+ throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
1734
+ }
1735
+ // Own properties only, so arrays don't leak the host Array.prototype (map/constructor/...).
1736
+ return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey)
1737
+ default:
1738
+ throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
1739
+ }
1740
+ }
1741
+
1742
+ private evaluateLogicalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1743
+ const operator = getString(node, "operator")
1744
+ return Effect.flatMap(this.evaluateExpression(getNode(node, "left")), (left) => {
1745
+ if (operator === "&&") return left ? this.evaluateExpression(getNode(node, "right")) : Effect.succeed(left)
1746
+ if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right"))
1747
+ if (operator === "??")
1748
+ return left !== null && left !== undefined
1749
+ ? Effect.succeed(left)
1750
+ : this.evaluateExpression(getNode(node, "right"))
1751
+ throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node)
1752
+ })
1753
+ }
1754
+
1755
+ private evaluateUnaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1756
+ const operator = getString(node, "operator")
1757
+ const argument = getNode(node, "argument")
1758
+ // `typeof undeclaredIdentifier` is `"undefined"` in JS (never a ReferenceError), so
1759
+ // feature-detection guards like `typeof x !== "undefined"` don't crash. Short-circuit before
1760
+ // evaluating the argument; a declared-but-TDZ binding still falls through to the normal throw.
1761
+ if (operator === "typeof" && argument.type === "Identifier" && !this.resolveBinding(getString(argument, "name"))) {
1762
+ return Effect.succeed("undefined")
1763
+ }
1764
+ return Effect.map(this.evaluateExpression(argument), (value) => {
1765
+ // `typeof` and `!` never throw in JS - they observe any value (functions and runtime
1766
+ // references included) without coercing it, so feature detection and negation work.
1767
+ if (operator === "typeof") return typeofValue(value)
1768
+ if (operator === "!") return !value
1769
+ if (containsOpaqueReference(value)) {
1770
+ throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue")
1771
+ }
1772
+ // Numeric/bitwise unary operators ToPrimitive their operand; a Date yields its time value
1773
+ // (`+date` is the epoch-ms idiom), other null-prototype data objects/arrays coerce to
1774
+ // their JS string form first (see evaluateBinaryExpression).
1775
+ const operand =
1776
+ value instanceof SandboxDate
1777
+ ? value.time
1778
+ : value !== null && typeof value === "object"
1779
+ ? coerceToString(value)
1780
+ : value
1781
+ let result: unknown
1782
+ switch (operator) {
1783
+ case "+":
1784
+ result = +(operand as number)
1785
+ break
1786
+ case "-":
1787
+ result = -(operand as number)
1788
+ break
1789
+ case "~":
1790
+ result = ~(operand as number)
1791
+ break
1792
+ default:
1793
+ throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node)
1794
+ }
1795
+ return boundedData(result, "Unary expression result")
1796
+ })
1797
+ }
1798
+
1799
+ private evaluateAssignmentExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1800
+ const left = getNode(node, "left")
1801
+ const operator = getString(node, "operator")
1802
+ const self = this
1803
+ return Effect.gen(function* () {
1804
+ if (operator === "??=" || operator === "||=" || operator === "&&=") {
1805
+ return yield* self.evaluateLogicalAssignment(node, left, operator)
1806
+ }
1807
+ const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
1808
+ if (left.type === "Identifier") {
1809
+ const name = getString(left, "name")
1810
+ if (operator === "=") return self.setIdentifierValue(name, rightValue, left)
1811
+ const next = boundedData(
1812
+ self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node),
1813
+ "Assignment result",
1814
+ )
1815
+ return self.setIdentifierValue(name, next, left)
1816
+ }
1817
+ if (left.type === "MemberExpression") {
1818
+ if (operator === "=") return yield* self.writeMember(left, rightValue)
1819
+ return yield* self.modifyMember(left, (current) => {
1820
+ const next = boundedData(
1821
+ self.applyCompoundAssignment(operator, current, rightValue, node),
1822
+ "Assignment result",
1823
+ )
1824
+ return Effect.succeed({ write: true, next, result: next })
1825
+ })
1826
+ }
1827
+ throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
1828
+ })
1829
+ }
1830
+
1831
+ private evaluateLogicalAssignment(
1832
+ node: AstNode,
1833
+ left: AstNode,
1834
+ operator: string,
1835
+ ): Effect.Effect<unknown, unknown, R> {
1836
+ const self = this
1837
+ const shouldAssign = (current: unknown): boolean =>
1838
+ operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current)
1839
+ if (left.type === "Identifier") {
1840
+ const name = getString(left, "name")
1841
+ return Effect.gen(function* () {
1842
+ const current = self.getIdentifierValue(name, left)
1843
+ if (!shouldAssign(current)) return current
1844
+ const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
1845
+ return self.setIdentifierValue(name, rightValue, left)
1846
+ })
1847
+ }
1848
+ if (left.type === "MemberExpression") {
1849
+ // Resolve the member exactly once; evaluate the RHS only if we actually assign.
1850
+ return self.modifyMember(left, (current) =>
1851
+ shouldAssign(current)
1852
+ ? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({
1853
+ write: true,
1854
+ next: rightValue,
1855
+ result: rightValue,
1856
+ }))
1857
+ : Effect.succeed({ write: false, next: current, result: current }),
1858
+ )
1859
+ }
1860
+ throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
1861
+ }
1862
+
1863
+ private evaluateUpdateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1864
+ const operator = getString(node, "operator")
1865
+ const argument = getNode(node, "argument")
1866
+ const prefix = getBoolean(node, "prefix")
1867
+
1868
+ const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined
1869
+
1870
+ if (increment === undefined) {
1871
+ throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node)
1872
+ }
1873
+
1874
+ if (argument.type === "Identifier") {
1875
+ return Effect.sync(() => {
1876
+ const name = getString(argument, "name")
1877
+ const current = Number(this.getIdentifierValue(name, argument))
1878
+ const next = current + increment
1879
+ this.setIdentifierValue(name, next, argument)
1880
+ return prefix ? next : current
1881
+ })
1882
+ }
1883
+
1884
+ if (argument.type === "MemberExpression") {
1885
+ return this.modifyMember(argument, (current) => {
1886
+ const value = Number(current)
1887
+ const next = value + increment
1888
+ return Effect.succeed({ write: true, next, result: prefix ? next : value })
1889
+ })
1890
+ }
1891
+
1892
+ throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument)
1893
+ }
1894
+
1895
+ private evaluateCallExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
1896
+ const callee = getNode(node, "callee")
1897
+ const argNodes = getArray(node, "arguments")
1898
+
1899
+ const self = this
1900
+ return Effect.gen(function* () {
1901
+ const callable = yield* self.evaluateExpression(callee)
1902
+ if (callable === OptionalShortCircuit) return OptionalShortCircuit
1903
+ if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit
1904
+
1905
+ const args = yield* self.evaluateCallArguments(argNodes)
1906
+
1907
+ if (callable instanceof ToolReference) {
1908
+ if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee)
1909
+ // An un-awaited tool call is a first-class promise value; the call itself starts now.
1910
+ return yield* self.createToolCallPromise(callable.path, args)
1911
+ }
1912
+ if (callable instanceof PromiseMethodReference) {
1913
+ return yield* self.invokePromiseMethod(callable, args, node)
1914
+ }
1915
+ if (callable instanceof CodeModeFunction) {
1916
+ return yield* self.invokeFunction(callable, args)
1917
+ }
1918
+ if (callable instanceof IntrinsicReference) {
1919
+ return yield* self.invokeIntrinsic(callable, args, node)
1920
+ }
1921
+ if (callable instanceof GlobalMethodReference) {
1922
+ if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
1923
+ if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
1924
+ return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node)
1925
+ }
1926
+ return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
1927
+ }
1928
+ if (callable instanceof CoercionFunction) {
1929
+ return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`)
1930
+ }
1931
+ if (callable instanceof UriFunction) {
1932
+ return invokeUriFunction(callable, args, node)
1933
+ }
1934
+ // `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
1935
+ if (callable instanceof ErrorConstructorReference) {
1936
+ return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0]))
1937
+ }
1938
+ throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
1939
+ })
1940
+ }
1941
+
1942
+ // Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate
1943
+ // namespace/tool names from the host tool tree - the discovery idiom a model reaches for
1944
+ // first. Every other Object helper cannot produce data from a tool reference, so it fails
1945
+ // with a pointer at the working idioms instead of the generic plain-objects-only message.
1946
+ private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown {
1947
+ if (name === "keys") {
1948
+ return boundedData(this.enumerableKeys(ref)!, "Object.keys result")
1949
+ }
1950
+ throw new InterpreterRuntimeError(
1951
+ `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`,
1952
+ node,
1953
+ "InvalidDataValue",
1954
+ )
1955
+ }
1956
+
1957
+ private invokeConsole(name: string, args: Array<unknown>, node: AstNode): undefined {
1958
+ if (!consoleMethods.has(name))
1959
+ throw new InterpreterRuntimeError(`console.${name} is not available in CodeMode.`, node)
1960
+ this.logs.push(publicErrorMessage(this.formatConsoleMessage(name, args, node)))
1961
+ return undefined
1962
+ }
1963
+
1964
+ private formatConsoleMessage(name: string, args: Array<unknown>, node: AstNode): string {
1965
+ if (name === "dir") return args.length === 0 ? "undefined" : this.formatConsoleArgument(args[0])
1966
+ if (name === "table") return this.formatConsoleTable(args[0], args[1], node)
1967
+ const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
1968
+ return `${prefix}${args.map((arg) => this.formatConsoleArgument(arg)).join(" ")}`
1969
+ }
1970
+
1971
+ // Console arguments format deeply and totally: values render as a debugger would show them
1972
+ // rather than as boundary JSON - numbers keep NaN/Infinity (JSON would say null), sandbox
1973
+ // values keep their friendly forms at ANY depth (ISO date, /regex/flags, Map(n) [...],
1974
+ // Set(n) [...]), opaque runtime references become "[CodeMode reference]" markers in place,
1975
+ // and plain objects/arrays render JSON-style. Formatting never fails the program: cycles
1976
+ // render "[Circular]" and extreme depth degrades to "...".
1977
+ private formatConsoleArgument(value: unknown): string {
1978
+ if (value === undefined) return "undefined"
1979
+ // A top-level string prints bare; nested strings are JSON-quoted (see formatConsoleValue).
1980
+ if (typeof value === "string") return value
1981
+ return this.formatConsoleValue(value, new Set(), 0)
1982
+ }
1983
+
1984
+ private formatConsoleValue(value: unknown, seen: Set<object>, depth: number): string {
1985
+ // Nested undefined renders as null, matching what JSON boundary output would show.
1986
+ if (value === null || value === undefined) return "null"
1987
+ if (typeof value === "string") return JSON.stringify(value)
1988
+ // String(value) keeps NaN/Infinity/-Infinity readable; finite numbers match their JSON form.
1989
+ if (typeof value === "number" || typeof value === "boolean") return String(value)
1990
+ if (typeof value !== "object") return String(value)
1991
+ if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]"
1992
+ if (value instanceof SandboxDate) return coerceToString(value)
1993
+ if (value instanceof SandboxRegExp) return coerceToString(value)
1994
+ if (value instanceof SandboxURL) return coerceToString(value)
1995
+ if (value instanceof SandboxURLSearchParams) return coerceToString(value)
1996
+ if (depth > MAX_CONSOLE_DEPTH) return "..."
1997
+ if (seen.has(value)) return "[Circular]"
1998
+ if (value instanceof SandboxMap) {
1999
+ seen.add(value)
2000
+ try {
2001
+ const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
2002
+ return `Map(${value.map.size}) ${this.formatConsoleValue(entries, seen, depth + 1)}`
2003
+ } finally {
2004
+ seen.delete(value)
2005
+ }
2006
+ }
2007
+ if (value instanceof SandboxSet) {
2008
+ seen.add(value)
2009
+ try {
2010
+ return `Set(${value.set.size}) ${this.formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
2011
+ } finally {
2012
+ seen.delete(value)
2013
+ }
2014
+ }
2015
+ if (isRuntimeReference(value)) return "[CodeMode reference]"
2016
+ seen.add(value)
2017
+ try {
2018
+ if (Array.isArray(value)) {
2019
+ return `[${value.map((item) => this.formatConsoleValue(item, seen, depth + 1)).join(",")}]`
2020
+ }
2021
+ return `{${Object.entries(value)
2022
+ .map(([key, item]) => `${JSON.stringify(key)}:${this.formatConsoleValue(item, seen, depth + 1)}`)
2023
+ .join(",")}}`
2024
+ } finally {
2025
+ seen.delete(value)
2026
+ }
2027
+ }
2028
+
2029
+ private formatConsoleTable(value: unknown, columnsArgument: unknown, node: AstNode): string {
2030
+ if (value === undefined) return "undefined"
2031
+ // Sandbox values are legitimate table data (cells render their friendly forms); only
2032
+ // truly opaque references (functions, tools, promises) collapse to the marker.
2033
+ if (containsOpaqueReference(value)) return "[CodeMode reference]"
2034
+ const data = boundedData(value, "console.table argument")
2035
+ const columns = this.consoleTableColumns(columnsArgument, node)
2036
+ const rows = this.consoleTableRows(data, columns)
2037
+ const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values))))
2038
+ const header = ["(index)", ...keys].join("\t")
2039
+ return [
2040
+ header,
2041
+ ...rows.map((row) => [row.index, ...keys.map((key) => this.formatConsoleTableCell(row.values[key]))].join("\t")),
2042
+ ].join("\n")
2043
+ }
2044
+
2045
+ private consoleTableColumns(value: unknown, node: AstNode): ReadonlyArray<string> | undefined {
2046
+ if (value === undefined) return undefined
2047
+ if (containsRuntimeReference(value)) return undefined
2048
+ const columns = copyOut(copyIn(value, "console.table columns"), true)
2049
+ return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined
2050
+ }
2051
+
2052
+ private consoleTableRows(
2053
+ data: unknown,
2054
+ columns: ReadonlyArray<string> | undefined,
2055
+ ): Array<{ readonly index: string; readonly values: Record<string, unknown> }> {
2056
+ if (Array.isArray(data)) {
2057
+ return data.map((item, index) => ({ index: String(index), values: this.consoleTableValues(item, columns) }))
2058
+ }
2059
+ if (data !== null && typeof data === "object" && !isSandboxValue(data)) {
2060
+ return Object.entries(data).map(([index, item]) => ({ index, values: this.consoleTableValues(item, columns) }))
2061
+ }
2062
+ return [{ index: "0", values: { Value: data } }]
2063
+ }
2064
+
2065
+ private consoleTableValues(value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> {
2066
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) {
2067
+ const source = value as Record<string, unknown>
2068
+ if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
2069
+ return Object.fromEntries(Object.entries(source))
2070
+ }
2071
+ return { Value: value }
2072
+ }
2073
+
2074
+ private formatConsoleTableCell(value: unknown): string {
2075
+ if (value === undefined) return ""
2076
+ if (typeof value === "string") return value
2077
+ return this.formatConsoleValue(value, new Set(), 0)
2078
+ }
2079
+
2080
+ private evaluateCallArguments(argNodes: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> {
2081
+ const self = this
2082
+ return Effect.gen(function* () {
2083
+ const args: Array<unknown> = []
2084
+ for (const [index, arg] of argNodes.entries()) {
2085
+ const argNode = asNode(arg, `arguments[${index}]`)
2086
+ if (argNode.type === "SpreadElement") {
2087
+ const spread = yield* self.evaluateExpression(getNode(argNode, "argument"))
2088
+ const items = spreadItems(spread)
2089
+ if (items === undefined)
2090
+ throw new InterpreterRuntimeError(
2091
+ "Spread arguments require an array, string, Map, or Set in CodeMode.",
2092
+ argNode,
2093
+ )
2094
+ args.push(...items)
2095
+ } else {
2096
+ args.push(yield* self.evaluateExpression(argNode))
2097
+ }
2098
+ }
2099
+ return args
2100
+ })
2101
+ }
2102
+
2103
+ // Promise.* over ordinary runtime values. Combinators accept ANY array (or spreadable
2104
+ // collection) mixing promise values and plain data - built inline, beforehand, via spread,
2105
+ // whatever - because tool calls already run eagerly on their own fibers; the combinators
2106
+ // only observe settlements. Joining is therefore sequential (no extra fibers) without
2107
+ // costing parallelism, and the concurrency cap stays where the work is: the fork semaphore.
2108
+ private invokePromiseMethod(
2109
+ ref: PromiseMethodReference,
2110
+ args: Array<unknown>,
2111
+ node: AstNode,
2112
+ ): Effect.Effect<unknown, unknown, R> {
2113
+ const self = this
2114
+ if (ref.name === "resolve") {
2115
+ // Promise.resolve of a promise is that promise (JS flattens); anything else is a
2116
+ // promise already fulfilled with the value.
2117
+ const value = args[0]
2118
+ return Effect.succeed(
2119
+ value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)),
2120
+ )
2121
+ }
2122
+ if (ref.name === "reject") {
2123
+ return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0]))))
2124
+ }
2125
+
2126
+ const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0])
2127
+ if (items === undefined) {
2128
+ throw new InterpreterRuntimeError(
2129
+ `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
2130
+ node,
2131
+ )
2132
+ }
2133
+
2134
+ switch (ref.name) {
2135
+ case "all": {
2136
+ // Mark every promise element observed up-front (Promise.all handles all of its
2137
+ // members' failures, as in JS), then join in index order; the first failure rejects
2138
+ // the whole call while unrelated in-flight members keep running.
2139
+ const settles = items.map((item) =>
2140
+ item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item),
2141
+ )
2142
+ return Effect.gen(function* () {
2143
+ const values: Array<unknown> = []
2144
+ for (const settle of settles) values.push(yield* settle)
2145
+ return values
2146
+ })
2147
+ }
2148
+ case "allSettled": {
2149
+ const observations = items.map((item) =>
2150
+ item instanceof SandboxPromise
2151
+ ? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit }))
2152
+ : Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }),
2153
+ )
2154
+ return Effect.gen(function* () {
2155
+ const outcomes: Array<unknown> = []
2156
+ for (const observation of observations) {
2157
+ const { exit, promise } = yield* observation
2158
+ if (Exit.isSuccess(exit)) {
2159
+ outcomes.push(
2160
+ Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
2161
+ )
2162
+ continue
2163
+ }
2164
+ const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
2165
+ if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
2166
+ // Execution teardown (timeout/host interruption), not a program-level rejection.
2167
+ return yield* Effect.failCause(exit.cause)
2168
+ }
2169
+ const thrown = raceInterrupted
2170
+ ? new InterpreterRuntimeError(
2171
+ "This tool call was interrupted because another value settled a Promise.race first.",
2172
+ node,
2173
+ )
2174
+ : Cause.squash(exit.cause)
2175
+ outcomes.push(
2176
+ Object.assign(Object.create(null) as SafeObject, {
2177
+ status: "rejected",
2178
+ reason: caughtErrorValue(thrown),
2179
+ }),
2180
+ )
2181
+ }
2182
+ return outcomes
2183
+ })
2184
+ }
2185
+ case "race": {
2186
+ if (items.length === 0) {
2187
+ throw new InterpreterRuntimeError(
2188
+ "Promise.race([]) would never settle; provide at least one promise or value.",
2189
+ node,
2190
+ )
2191
+ }
2192
+ const observations = items.map((item, index) =>
2193
+ item instanceof SandboxPromise
2194
+ ? Effect.map(this.observePromise(item), (exit) => ({ index, exit }))
2195
+ : Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
2196
+ )
2197
+ return Effect.gen(function* () {
2198
+ // First settlement (fulfilled OR rejected) wins; the observations never fail, so
2199
+ // racing them yields exactly that. Losing in-flight calls are then interrupted.
2200
+ const winner = yield* Effect.raceAll(observations)
2201
+ for (const [index, item] of items.entries()) {
2202
+ if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
2203
+ item.interrupted = true
2204
+ yield* Fiber.interrupt(item.fiber)
2205
+ }
2206
+ const winningItem = items[winner.index]
2207
+ return yield* self.unwrapPromiseExit(
2208
+ winningItem instanceof SandboxPromise ? winningItem : undefined,
2209
+ winner.exit,
2210
+ node,
2211
+ )
2212
+ })
2213
+ }
2214
+ }
2215
+ }
2216
+
2217
+ private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
2218
+ const self = this
2219
+ return Effect.suspend(() => {
2220
+ const savedScopes = self.scopes
2221
+ self.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
2222
+ const run = Effect.gen(function* () {
2223
+ // Seed every parameter name into the scope as a TDZ slot first, so a default that
2224
+ // references another parameter resolves to that (uninitialized) param rather than
2225
+ // silently falling through to an outer binding of the same name - matching JS.
2226
+ const paramScope = self.currentScope()
2227
+ for (const parameter of fn.parameters) {
2228
+ for (const name of collectPatternNames(parameter)) {
2229
+ paramScope.set(name, { mutable: true, value: undefined, initialized: false })
2230
+ }
2231
+ }
2232
+ for (const [index, parameter] of fn.parameters.entries()) {
2233
+ if (parameter.type === "RestElement") {
2234
+ yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
2235
+ break
2236
+ }
2237
+ yield* self.declarePattern(parameter, args[index], true, parameter)
2238
+ }
2239
+
2240
+ if (fn.body.type === "BlockStatement") {
2241
+ const result = yield* self.evaluateStatement(fn.body)
2242
+ return result.kind === "return" || result.kind === "value" ? result.value : undefined
2243
+ }
2244
+
2245
+ return yield* self.evaluateExpression(fn.body)
2246
+ })
2247
+ return run.pipe(
2248
+ Effect.ensuring(
2249
+ Effect.sync(() => {
2250
+ self.scopes = savedScopes
2251
+ }),
2252
+ ),
2253
+ )
2254
+ })
2255
+ }
2256
+
2257
+ private invokeIntrinsic(
2258
+ ref: IntrinsicReference,
2259
+ args: Array<unknown>,
2260
+ node: AstNode,
2261
+ ): Effect.Effect<unknown, unknown, R> {
2262
+ if (typeof ref.receiver === "string") {
2263
+ if (
2264
+ (ref.name === "replace" || ref.name === "replaceAll") &&
2265
+ (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction)
2266
+ ) {
2267
+ return this.invokeStringReplacer(ref.receiver, ref.name, args, node)
2268
+ }
2269
+ return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node))
2270
+ }
2271
+ if (typeof ref.receiver === "number") {
2272
+ return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
2273
+ }
2274
+ if (Array.isArray(ref.receiver)) {
2275
+ return this.invokeArrayMethod(ref.receiver, ref.name, args, node)
2276
+ }
2277
+ if (ref.receiver instanceof SandboxDate) {
2278
+ return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node))
2279
+ }
2280
+ if (ref.receiver instanceof SandboxRegExp) {
2281
+ return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node))
2282
+ }
2283
+ if (ref.receiver instanceof SandboxMap) {
2284
+ return this.invokeMapMethod(ref.receiver, ref.name, args, node)
2285
+ }
2286
+ if (ref.receiver instanceof SandboxSet) {
2287
+ return this.invokeSetMethod(ref.receiver, ref.name, args, node)
2288
+ }
2289
+ if (ref.receiver instanceof SandboxURL) {
2290
+ return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node))
2291
+ }
2292
+ if (ref.receiver instanceof SandboxURLSearchParams) {
2293
+ return this.invokeURLSearchParamsMethod(ref.receiver, ref.name, args, node)
2294
+ }
2295
+ throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
2296
+ }
2297
+
2298
+ private invokeStringReplacer(
2299
+ value: string,
2300
+ name: "replace" | "replaceAll",
2301
+ args: Array<unknown>,
2302
+ node: AstNode,
2303
+ ): Effect.Effect<unknown, unknown, R> {
2304
+ const apply = this.applyCollectionCallback(args[1], `String.${name}`, node)
2305
+ const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
2306
+ const collect = (...callbackArgs: Array<unknown>): string => {
2307
+ const match = callbackArgs[0]
2308
+ const groups = callbackArgs[callbackArgs.length - 1]
2309
+ const hasGroups = groups !== null && typeof groups === "object"
2310
+ const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)]
2311
+ if (typeof match !== "string" || typeof offset !== "number") {
2312
+ throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node)
2313
+ }
2314
+ if (hasGroups) {
2315
+ const safeGroups: SafeObject = Object.create(null) as SafeObject
2316
+ for (const [key, group] of Object.entries(groups)) {
2317
+ if (!isBlockedMember(key)) safeGroups[key] = group
2318
+ }
2319
+ callbackArgs[callbackArgs.length - 1] = safeGroups
2320
+ }
2321
+ matches.push({ match, offset, args: callbackArgs })
2322
+ return match
2323
+ }
2324
+
2325
+ const pattern = args[0]
2326
+ if (pattern instanceof SandboxRegExp) {
2327
+ if (name === "replaceAll" && !pattern.regex.global) {
2328
+ throw new InterpreterRuntimeError(
2329
+ `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`,
2330
+ node,
2331
+ )
2332
+ }
2333
+ if (name === "replace") value.replace(pattern.regex, collect)
2334
+ else value.replaceAll(pattern.regex, collect)
2335
+ } else {
2336
+ if (typeof pattern !== "string") {
2337
+ throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node)
2338
+ }
2339
+ if (name === "replace") value.replace(pattern, collect)
2340
+ else value.replaceAll(pattern, collect)
2341
+ }
2342
+
2343
+ return Effect.gen(function* () {
2344
+ const output: Array<string> = []
2345
+ let end = 0
2346
+ for (const match of matches) {
2347
+ output.push(
2348
+ value.slice(end, match.offset),
2349
+ coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)),
2350
+ )
2351
+ end = match.offset + match.match.length
2352
+ }
2353
+ output.push(value.slice(end))
2354
+ return boundedData(output.join(""), `String.${name} result`)
2355
+ })
2356
+ }
2357
+
2358
+ // Runs a collection callback accepting a user function or supported builtin callable,
2359
+ // mirroring the array-method callback contract.
2360
+ private applyCollectionCallback(
2361
+ callback: unknown,
2362
+ name: string,
2363
+ node: AstNode,
2364
+ ): (args: Array<unknown>) => Effect.Effect<unknown, unknown, R> {
2365
+ if (
2366
+ !(callback instanceof CodeModeFunction) &&
2367
+ !(callback instanceof CoercionFunction) &&
2368
+ !(callback instanceof UriFunction)
2369
+ ) {
2370
+ throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
2371
+ }
2372
+ return (callbackArgs) =>
2373
+ callback instanceof CoercionFunction
2374
+ ? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
2375
+ : callback instanceof UriFunction
2376
+ ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
2377
+ : this.invokeFunction(callback, callbackArgs)
2378
+ }
2379
+
2380
+ private invokeMapMethod(
2381
+ target: SandboxMap,
2382
+ name: string,
2383
+ args: Array<unknown>,
2384
+ node: AstNode,
2385
+ ): Effect.Effect<unknown, unknown, R> {
2386
+ switch (name) {
2387
+ case "get":
2388
+ return Effect.succeed(target.map.get(args[0]))
2389
+ case "has":
2390
+ return Effect.succeed(target.map.has(args[0]))
2391
+ case "set":
2392
+ return Effect.sync(() => {
2393
+ target.map.set(args[0], args[1])
2394
+ return target
2395
+ })
2396
+ case "delete":
2397
+ return Effect.sync(() => target.map.delete(args[0]))
2398
+ case "clear":
2399
+ return Effect.sync(() => {
2400
+ target.map.clear()
2401
+ return undefined
2402
+ })
2403
+ case "keys":
2404
+ return Effect.sync(() => Array.from(target.map.keys()))
2405
+ case "values":
2406
+ return Effect.sync(() => Array.from(target.map.values()))
2407
+ case "entries":
2408
+ return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item]))
2409
+ case "forEach": {
2410
+ const apply = this.applyCollectionCallback(args[0], "Map.forEach", node)
2411
+ return Effect.gen(function* () {
2412
+ // Snapshot iteration, matching the array-method callback contract.
2413
+ for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
2414
+ return undefined
2415
+ })
2416
+ }
2417
+ default:
2418
+ throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node)
2419
+ }
2420
+ }
2421
+
2422
+ private invokeSetMethod(
2423
+ target: SandboxSet,
2424
+ name: string,
2425
+ args: Array<unknown>,
2426
+ node: AstNode,
2427
+ ): Effect.Effect<unknown, unknown, R> {
2428
+ switch (name) {
2429
+ case "has":
2430
+ return Effect.succeed(target.set.has(args[0]))
2431
+ case "add":
2432
+ return Effect.sync(() => {
2433
+ target.set.add(args[0])
2434
+ return target
2435
+ })
2436
+ case "delete":
2437
+ return Effect.sync(() => target.set.delete(args[0]))
2438
+ case "clear":
2439
+ return Effect.sync(() => {
2440
+ target.set.clear()
2441
+ return undefined
2442
+ })
2443
+ case "keys":
2444
+ case "values":
2445
+ return Effect.sync(() => Array.from(target.set.values()))
2446
+ case "entries":
2447
+ return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item]))
2448
+ case "forEach": {
2449
+ const apply = this.applyCollectionCallback(args[0], "Set.forEach", node)
2450
+ return Effect.gen(function* () {
2451
+ for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
2452
+ return undefined
2453
+ })
2454
+ }
2455
+ default:
2456
+ throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node)
2457
+ }
2458
+ }
2459
+
2460
+ private invokeURLSearchParamsMethod(
2461
+ target: SandboxURLSearchParams,
2462
+ name: string,
2463
+ args: Array<unknown>,
2464
+ node: AstNode,
2465
+ ): Effect.Effect<unknown, unknown, R> {
2466
+ const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`)
2467
+ const requireArgs = (count: number): void => {
2468
+ if (args.length < count) {
2469
+ throw new InterpreterRuntimeError(
2470
+ `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`,
2471
+ node,
2472
+ ).as("TypeError")
2473
+ }
2474
+ }
2475
+ switch (name) {
2476
+ case "append": {
2477
+ requireArgs(2)
2478
+ return Effect.sync(() => {
2479
+ target.params.append(arg(0), arg(1))
2480
+ return undefined
2481
+ })
2482
+ }
2483
+ case "delete": {
2484
+ requireArgs(1)
2485
+ return Effect.sync(() => {
2486
+ if (args[1] !== undefined) target.params.delete(arg(0), arg(1))
2487
+ else target.params.delete(arg(0))
2488
+ return undefined
2489
+ })
2490
+ }
2491
+ case "get":
2492
+ requireArgs(1)
2493
+ return Effect.sync(() => target.params.get(arg(0)))
2494
+ case "getAll":
2495
+ requireArgs(1)
2496
+ return Effect.sync(() => target.params.getAll(arg(0)))
2497
+ case "has":
2498
+ requireArgs(1)
2499
+ return Effect.sync(() =>
2500
+ args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)),
2501
+ )
2502
+ case "set": {
2503
+ requireArgs(2)
2504
+ return Effect.sync(() => {
2505
+ target.params.set(arg(0), arg(1))
2506
+ return undefined
2507
+ })
2508
+ }
2509
+ case "sort":
2510
+ return Effect.sync(() => {
2511
+ target.params.sort()
2512
+ return undefined
2513
+ })
2514
+ case "keys":
2515
+ return Effect.sync(() => Array.from(target.params.keys()))
2516
+ case "values":
2517
+ return Effect.sync(() => Array.from(target.params.values()))
2518
+ case "entries":
2519
+ return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value]))
2520
+ case "toString":
2521
+ return Effect.sync(() => target.params.toString())
2522
+ case "forEach": {
2523
+ requireArgs(1)
2524
+ const apply = this.applyCollectionCallback(args[0], "URLSearchParams.forEach", node)
2525
+ return Effect.gen(function* () {
2526
+ for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target])
2527
+ return undefined
2528
+ })
2529
+ }
2530
+ default:
2531
+ throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node)
2532
+ }
2533
+ }
2534
+
2535
+ private invokeArrayMethod(
2536
+ target: Array<unknown>,
2537
+ name: string,
2538
+ args: Array<unknown>,
2539
+ node: AstNode,
2540
+ ): Effect.Effect<unknown, unknown, R> {
2541
+ const optNumber = (value: unknown, label: string): number | undefined => {
2542
+ if (value === undefined) return undefined
2543
+ if (typeof value !== "number")
2544
+ throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node)
2545
+ return value
2546
+ }
2547
+ switch (name) {
2548
+ case "join": {
2549
+ if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
2550
+ throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
2551
+ }
2552
+ const input = boundedData(target, "Array.join input") as Array<unknown>
2553
+ return Effect.succeed(
2554
+ input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
2555
+ )
2556
+ }
2557
+ case "includes":
2558
+ if (args.length === 0 || args.length > 2)
2559
+ throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node)
2560
+ return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index")))
2561
+ case "indexOf":
2562
+ return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index")))
2563
+ case "lastIndexOf":
2564
+ return Effect.succeed(
2565
+ args[1] === undefined
2566
+ ? target.lastIndexOf(args[0])
2567
+ : target.lastIndexOf(args[0], optNumber(args[1], "start index")),
2568
+ )
2569
+ case "at":
2570
+ return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0))
2571
+ case "slice":
2572
+ return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end")))
2573
+ case "concat":
2574
+ return Effect.succeed(target.concat(...args))
2575
+ case "flat":
2576
+ return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
2577
+ case "reverse":
2578
+ return Effect.succeed([...target].reverse())
2579
+ case "sort":
2580
+ case "toSorted":
2581
+ return this.sortArray(target, args[0], node)
2582
+ case "toReversed":
2583
+ return Effect.succeed([...target].reverse())
2584
+ case "with": {
2585
+ const index = optNumber(args[0], "index") ?? 0
2586
+ const resolved = index < 0 ? target.length + index : index
2587
+ if (resolved < 0 || resolved >= target.length) {
2588
+ throw new InterpreterRuntimeError("Array.with index is out of range.", node)
2589
+ }
2590
+ const copied = [...target]
2591
+ copied[resolved] = args[1]
2592
+ return Effect.succeed(copied)
2593
+ }
2594
+ case "push": {
2595
+ // Validate before mutating (so no rollback is needed): inserting a container into
2596
+ // itself would create a cycle no later walk could survive.
2597
+ for (const item of args) this.rejectCircularInsertion(target, item, "Array.push result", node)
2598
+ target.push(...args)
2599
+ return Effect.succeed(target.length)
2600
+ }
2601
+ case "unshift": {
2602
+ for (const item of args) this.rejectCircularInsertion(target, item, "Array.unshift result", node)
2603
+ target.unshift(...args)
2604
+ return Effect.succeed(target.length)
2605
+ }
2606
+ case "pop":
2607
+ return Effect.succeed(target.pop())
2608
+ case "shift":
2609
+ return Effect.succeed(target.shift())
2610
+ case "splice": {
2611
+ // Mutates in place and returns the removed elements, exactly like JS: one argument
2612
+ // removes to the end, an undefined delete count removes nothing.
2613
+ if (args.length === 0) return Effect.succeed(target.splice(0, 0))
2614
+ const start = optNumber(args[0], "start") ?? 0
2615
+ if (args.length === 1) return Effect.succeed(target.splice(start))
2616
+ const deleteCount = optNumber(args[1], "delete count") ?? 0
2617
+ const inserted = args.slice(2)
2618
+ for (const item of inserted) this.rejectCircularInsertion(target, item, "Array.splice result", node)
2619
+ return Effect.succeed(target.splice(start, deleteCount, ...inserted))
2620
+ }
2621
+ case "fill": {
2622
+ this.rejectCircularInsertion(target, args[0], "Array.fill result", node)
2623
+ return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
2624
+ }
2625
+ case "copyWithin":
2626
+ return Effect.succeed(
2627
+ target.copyWithin(
2628
+ optNumber(args[0], "target index") ?? 0,
2629
+ optNumber(args[1], "start") ?? 0,
2630
+ optNumber(args[2], "end"),
2631
+ ),
2632
+ )
2633
+ // keys/values/entries return arrays (not iterators), matching the Map/Set convention;
2634
+ // they work with for...of and spread either way.
2635
+ case "keys":
2636
+ return Effect.succeed(Array.from(target.keys()))
2637
+ case "values":
2638
+ return Effect.succeed([...target])
2639
+ case "entries":
2640
+ return Effect.succeed(Array.from(target.entries(), ([index, item]): Array<unknown> => [index, item]))
2641
+ }
2642
+
2643
+ const callback = args[0]
2644
+ if (
2645
+ !(callback instanceof CodeModeFunction) &&
2646
+ !(callback instanceof CoercionFunction) &&
2647
+ !(callback instanceof UriFunction)
2648
+ ) {
2649
+ throw new InterpreterRuntimeError(`Array.${name} expects a function callback.`, node)
2650
+ }
2651
+ const self = this
2652
+ // Accept a user function or supported builtin callable, so idioms such as
2653
+ // `filter(Boolean)`, `map(String)`, and `map(encodeURIComponent)` work as in JS. Builtins
2654
+ // are synchronous; only CodeModeFunctions can await tool calls.
2655
+ const apply = (callbackArgs: Array<unknown>): Effect.Effect<unknown, unknown, R> =>
2656
+ callback instanceof CoercionFunction
2657
+ ? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
2658
+ : callback instanceof UriFunction
2659
+ ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
2660
+ : self.invokeFunction(callback, callbackArgs)
2661
+ return Effect.gen(function* () {
2662
+ // Iterate a snapshot taken at call time so a callback that mutates the array can't
2663
+ // self-extend the loop - matching JS, where elements appended during iteration are not visited.
2664
+ const items = target.slice()
2665
+ switch (name) {
2666
+ case "map": {
2667
+ const values: Array<unknown> = []
2668
+ for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items]))
2669
+ return values
2670
+ }
2671
+ case "flatMap": {
2672
+ const values: Array<unknown> = []
2673
+ for (const [index, item] of items.entries()) {
2674
+ const mapped = yield* apply([item, index, items])
2675
+ if (Array.isArray(mapped)) values.push(...mapped)
2676
+ else values.push(mapped)
2677
+ }
2678
+ return values
2679
+ }
2680
+ case "filter": {
2681
+ const values: Array<unknown> = []
2682
+ for (const [index, item] of items.entries()) {
2683
+ if (yield* apply([item, index, items])) values.push(item)
2684
+ }
2685
+ return values
2686
+ }
2687
+ case "find":
2688
+ for (const [index, item] of items.entries()) {
2689
+ if (yield* apply([item, index, items])) return item
2690
+ }
2691
+ return undefined
2692
+ case "findIndex":
2693
+ for (const [index, item] of items.entries()) {
2694
+ if (yield* apply([item, index, items])) return index
2695
+ }
2696
+ return -1
2697
+ case "some":
2698
+ for (const [index, item] of items.entries()) {
2699
+ if (yield* apply([item, index, items])) return true
2700
+ }
2701
+ return false
2702
+ case "every":
2703
+ for (const [index, item] of items.entries()) {
2704
+ if (!(yield* apply([item, index, items]))) return false
2705
+ }
2706
+ return true
2707
+ case "forEach":
2708
+ for (const [index, item] of items.entries()) yield* apply([item, index, items])
2709
+ return undefined
2710
+ case "reduce": {
2711
+ let accumulator: unknown
2712
+ let start: number
2713
+ if (args.length >= 2) {
2714
+ accumulator = args[1]
2715
+ start = 0
2716
+ } else {
2717
+ if (items.length === 0)
2718
+ throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
2719
+ accumulator = items[0]
2720
+ start = 1
2721
+ }
2722
+ for (let index = start; index < items.length; index += 1) {
2723
+ accumulator = yield* apply([accumulator, items[index], index, items])
2724
+ }
2725
+ return accumulator
2726
+ }
2727
+ case "reduceRight": {
2728
+ let accumulator: unknown
2729
+ let start: number
2730
+ if (args.length >= 2) {
2731
+ accumulator = args[1]
2732
+ start = items.length - 1
2733
+ } else {
2734
+ if (items.length === 0)
2735
+ throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
2736
+ accumulator = items[items.length - 1]
2737
+ start = items.length - 2
2738
+ }
2739
+ for (let index = start; index >= 0; index -= 1) {
2740
+ accumulator = yield* apply([accumulator, items[index], index, items])
2741
+ }
2742
+ return accumulator
2743
+ }
2744
+ case "findLast":
2745
+ for (let index = items.length - 1; index >= 0; index -= 1) {
2746
+ if (yield* apply([items[index], index, items])) return items[index]
2747
+ }
2748
+ return undefined
2749
+ case "findLastIndex":
2750
+ for (let index = items.length - 1; index >= 0; index -= 1) {
2751
+ if (yield* apply([items[index], index, items])) return index
2752
+ }
2753
+ return -1
2754
+ }
2755
+ throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node)
2756
+ })
2757
+ }
2758
+
2759
+ private sortArray(
2760
+ target: Array<unknown>,
2761
+ comparator: unknown,
2762
+ node: AstNode,
2763
+ ): Effect.Effect<Array<unknown>, unknown, R> {
2764
+ if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) {
2765
+ throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node)
2766
+ }
2767
+ if (!(comparator instanceof CodeModeFunction)) {
2768
+ return Effect.sync(() =>
2769
+ [...target].sort((a, b) => {
2770
+ const left = coerceToString(a)
2771
+ const right = coerceToString(b)
2772
+ return left < right ? -1 : left > right ? 1 : 0
2773
+ }),
2774
+ )
2775
+ }
2776
+ const self = this
2777
+ const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
2778
+ if (items.length <= 1) return Effect.succeed(items)
2779
+ const midpoint = Math.floor(items.length / 2)
2780
+ return Effect.gen(function* () {
2781
+ const left = yield* mergeSort(items.slice(0, midpoint))
2782
+ const right = yield* mergeSort(items.slice(midpoint))
2783
+ const merged: Array<unknown> = []
2784
+ let leftIndex = 0
2785
+ let rightIndex = 0
2786
+ while (leftIndex < left.length && rightIndex < right.length) {
2787
+ // Coerce the comparator's result like JS ToNumber (data objects -> NaN, never a host
2788
+ // crash) and treat NaN as 0 - the spec's "no consistent order" -> keep the left element.
2789
+ const order = coerceToNumber(yield* self.invokeFunction(comparator, [left[leftIndex], right[rightIndex]]))
2790
+ if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++])
2791
+ else merged.push(right[rightIndex++])
2792
+ }
2793
+ return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)]
2794
+ })
2795
+ }
2796
+ // Per spec, undefined elements sort to the end and the comparator is never called on them.
2797
+ const defined = target.filter((item) => item !== undefined)
2798
+ const undefinedCount = target.length - defined.length
2799
+ return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)])
2800
+ }
2801
+
2802
+ private evaluateObjectExpression(node: AstNode): Effect.Effect<Record<string, unknown>, unknown, R> {
2803
+ const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
2804
+ const properties = getArray(node, "properties")
2805
+ const self = this
2806
+ return Effect.gen(function* () {
2807
+ for (const propertyValue of properties) {
2808
+ const property = asNode(propertyValue, "properties")
2809
+
2810
+ if (property.type === "SpreadElement") {
2811
+ const spread = yield* self.evaluateExpression(getNode(property, "argument"))
2812
+ // JS treats `{ ...null }` / `{ ...undefined }` as a no-op, so the common
2813
+ // `{ ...maybeOpts, override }` merge works when the operand is absent. Sandbox values
2814
+ // have no own enumerable properties in JS, so they are no-ops too.
2815
+ if (spread === null || spread === undefined || isSandboxValue(spread)) continue
2816
+ if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
2817
+ throw new InterpreterRuntimeError(
2818
+ "Object spread requires a data object in CodeMode.",
2819
+ property,
2820
+ "InvalidDataValue",
2821
+ )
2822
+ }
2823
+ for (const [key, value] of Object.entries(spread)) {
2824
+ if (isBlockedMember(key))
2825
+ throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, property)
2826
+ objectValue[key] = value
2827
+ }
2828
+ continue
2829
+ }
2830
+
2831
+ if (property.type !== "Property") {
2832
+ throw new InterpreterRuntimeError("Only standard object properties are supported.", property)
2833
+ }
2834
+
2835
+ if (getString(property, "kind") !== "init") {
2836
+ throw new InterpreterRuntimeError("Only init object properties are supported.", property)
2837
+ }
2838
+
2839
+ const keyNode = getNode(property, "key")
2840
+ const valueNode = getNode(property, "value")
2841
+ const computed = getBoolean(property, "computed")
2842
+
2843
+ let key: PropertyKey
2844
+
2845
+ if (computed) {
2846
+ key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode)
2847
+ } else if (keyNode.type === "Identifier") {
2848
+ key = getString(keyNode, "name")
2849
+ } else if (keyNode.type === "Literal") {
2850
+ key = self.toPropertyKey(keyNode.value, keyNode)
2851
+ } else {
2852
+ throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode)
2853
+ }
2854
+
2855
+ if (isBlockedMember(String(key))) {
2856
+ throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, keyNode)
2857
+ }
2858
+ objectValue[String(key)] = yield* self.evaluateExpression(valueNode)
2859
+ }
2860
+
2861
+ return objectValue
2862
+ })
2863
+ }
2864
+
2865
+ private evaluateArrayExpression(node: AstNode): Effect.Effect<Array<unknown>, unknown, R> {
2866
+ const elements = getArray(node, "elements")
2867
+ const values: Array<unknown> = []
2868
+
2869
+ const self = this
2870
+ return Effect.gen(function* () {
2871
+ for (const elementValue of elements) {
2872
+ if (elementValue === null) {
2873
+ values.push(undefined)
2874
+ continue
2875
+ }
2876
+ const element = asNode(elementValue, "elements")
2877
+ if (element.type === "SpreadElement") {
2878
+ const spread = yield* self.evaluateExpression(getNode(element, "argument"))
2879
+ const items = spreadItems(spread)
2880
+ if (items === undefined)
2881
+ throw new InterpreterRuntimeError(
2882
+ "Array spread requires an array, string, Map, or Set in CodeMode.",
2883
+ element,
2884
+ )
2885
+ values.push(...items)
2886
+ } else {
2887
+ values.push(yield* self.evaluateExpression(element))
2888
+ }
2889
+ }
2890
+ return values
2891
+ })
2892
+ }
2893
+
2894
+ private evaluateTemplateLiteral(node: AstNode): Effect.Effect<string, unknown, R> {
2895
+ const quasis = getArray(node, "quasis")
2896
+ const expressions = getArray(node, "expressions")
2897
+
2898
+ let output = ""
2899
+
2900
+ const self = this
2901
+ return Effect.gen(function* () {
2902
+ for (let index = 0; index < quasis.length; index += 1) {
2903
+ const quasi = asNode(quasis[index], "quasis")
2904
+ const rawValue = quasi.value
2905
+
2906
+ if (!isRecord(rawValue) || typeof rawValue.cooked !== "string") {
2907
+ throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi)
2908
+ }
2909
+
2910
+ output += rawValue.cooked
2911
+
2912
+ if (index < expressions.length) {
2913
+ const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions"))
2914
+ // The preserving checkpoint keeps sandbox values intact, so coerceToString renders
2915
+ // them directly (ISO date, /regex/ literal form) instead of a JSON-serialized husk.
2916
+ output += coerceToString(boundedData(raw, "Template interpolation"))
2917
+ }
2918
+ }
2919
+
2920
+ return output
2921
+ })
2922
+ }
2923
+
2924
+ private evaluateConditionalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
2925
+ return Effect.flatMap(this.evaluateExpression(getNode(node, "test")), (test) =>
2926
+ this.evaluateExpression(getNode(node, test ? "consequent" : "alternate")),
2927
+ )
2928
+ }
2929
+
2930
+ private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown {
2931
+ // `x op= y` is `x = x op y`: dispatch through the shared binary operator implementation
2932
+ // so compound assignment inherits the same coercion semantics (Dates, data objects, ...).
2933
+ // Only the arithmetic/bitwise operators are compoundable; logical assignments (&&=/||=/??=)
2934
+ // short-circuit and are handled by evaluateLogicalAssignment before reaching here.
2935
+ if (!compoundOperators.has(operator)) {
2936
+ throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node)
2937
+ }
2938
+ return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node)
2939
+ }
2940
+
2941
+ private getMemberReference(
2942
+ node: AstNode,
2943
+ ): Effect.Effect<
2944
+ | MemberReference
2945
+ | ToolReference
2946
+ | PromiseMethodReference
2947
+ | IntrinsicReference
2948
+ | GlobalMethodReference
2949
+ | ComputedValue
2950
+ | typeof OptionalShortCircuit
2951
+ | undefined,
2952
+ unknown,
2953
+ R
2954
+ > {
2955
+ const objectNode = getNode(node, "object")
2956
+ const propertyNode = getNode(node, "property")
2957
+ const computed = getBoolean(node, "computed")
2958
+ const optional = node.optional === true
2959
+ const self = this
2960
+ return Effect.gen(function* () {
2961
+ const objectValue = yield* self.evaluateExpression(objectNode)
2962
+ if (objectValue === OptionalShortCircuit) return OptionalShortCircuit
2963
+ if ((objectValue === null || objectValue === undefined) && optional) return OptionalShortCircuit
2964
+
2965
+ const key = computed
2966
+ ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
2967
+ : propertyNode.type === "Identifier"
2968
+ ? getString(propertyNode, "name")
2969
+ : self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
2970
+
2971
+ if (objectValue instanceof ToolReference) {
2972
+ if (typeof key !== "string" || isBlockedMember(key)) {
2973
+ throw new InterpreterRuntimeError("Tool paths must use safe string property names.", propertyNode)
2974
+ }
2975
+ return new ToolReference([...objectValue.path, key])
2976
+ }
2977
+
2978
+ if (objectValue instanceof PromiseNamespace) {
2979
+ if (typeof key === "string" && promiseStatics.has(key as PromiseMethodName)) {
2980
+ return new PromiseMethodReference(key as PromiseMethodName)
2981
+ }
2982
+ throw new InterpreterRuntimeError(
2983
+ `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`,
2984
+ propertyNode,
2985
+ )
2986
+ }
2987
+
2988
+ if (objectValue instanceof GlobalNamespace) {
2989
+ if (typeof key !== "string" || isBlockedMember(key)) {
2990
+ throw new InterpreterRuntimeError(
2991
+ `${objectValue.name}.${String(key)} is not available in CodeMode.`,
2992
+ propertyNode,
2993
+ )
2994
+ }
2995
+ if (objectValue.name === "Math" && mathConstants.has(key)) {
2996
+ return new ComputedValue((Math as unknown as Record<string, number>)[key])
2997
+ }
2998
+ return new GlobalMethodReference(objectValue.name, key)
2999
+ }
3000
+
3001
+ if (typeof objectValue === "string") {
3002
+ if (key === "length") return new ComputedValue(objectValue.length)
3003
+ if (typeof key === "number") return new ComputedValue(objectValue[key])
3004
+ if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)])
3005
+ if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key)
3006
+ // Unknown property on a string reads as `undefined`, matching JS (`"x".foo === undefined`),
3007
+ // instead of throwing - so defensive access like `result?.login ?? result` on a JSON-string
3008
+ // tool result doesn't crash. (Optional chaining only guards null/undefined receivers, so a
3009
+ // real string still reaches here.) Only the method allowlist above yields callables.
3010
+ return new ComputedValue(undefined)
3011
+ }
3012
+
3013
+ if (typeof objectValue === "number") {
3014
+ if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key)
3015
+ // Unknown property on a number reads as `undefined`, matching JS, rather than throwing.
3016
+ return new ComputedValue(undefined)
3017
+ }
3018
+
3019
+ // Number / String expose a small allowlist of statics; everything else stays opaque.
3020
+ if (objectValue instanceof CoercionFunction && typeof key === "string" && !isBlockedMember(key)) {
3021
+ if (objectValue.name === "Number" && numberConstants.has(key)) {
3022
+ return new ComputedValue((Number as unknown as Record<string, number>)[key])
3023
+ }
3024
+ if (objectValue.name === "Number" && numberStatics.has(key)) return new GlobalMethodReference("Number", key)
3025
+ if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key)
3026
+ }
3027
+
3028
+ // Sandbox value types expose their method/property allowlists; any other key reads as
3029
+ // `undefined`, consistent with unknown-property reads on strings/numbers/arrays.
3030
+ if (objectValue instanceof SandboxDate) {
3031
+ if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key)
3032
+ return new ComputedValue(undefined)
3033
+ }
3034
+ if (objectValue instanceof SandboxRegExp) {
3035
+ if (typeof key === "string" && regexpProperties.has(key)) {
3036
+ return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key])
3037
+ }
3038
+ if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key)
3039
+ return new ComputedValue(undefined)
3040
+ }
3041
+ if (objectValue instanceof SandboxMap) {
3042
+ if (key === "size") return new ComputedValue(objectValue.map.size)
3043
+ if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key)
3044
+ return new ComputedValue(undefined)
3045
+ }
3046
+ if (objectValue instanceof SandboxSet) {
3047
+ if (key === "size") return new ComputedValue(objectValue.set.size)
3048
+ if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key)
3049
+ return new ComputedValue(undefined)
3050
+ }
3051
+ if (objectValue instanceof SandboxURL) {
3052
+ if (key === "searchParams") {
3053
+ return new ComputedValue(objectValue.searchParams)
3054
+ }
3055
+ if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key)
3056
+ if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key }
3057
+ return new ComputedValue(undefined)
3058
+ }
3059
+ if (objectValue instanceof SandboxURLSearchParams) {
3060
+ if (key === "size") return new ComputedValue(objectValue.params.size)
3061
+ if (typeof key === "string" && urlSearchParamsMethods.has(key)) {
3062
+ return new IntrinsicReference(objectValue, key)
3063
+ }
3064
+ return new ComputedValue(undefined)
3065
+ }
3066
+
3067
+ // Any property access on a promise is a confused program (`p.then(...)`, `p.value`);
3068
+ // reading `undefined` here would hide the missing await, so both paths get an explicit,
3069
+ // await-hinting error instead of the forgiving unknown-property fallthrough.
3070
+ if (objectValue instanceof SandboxPromise) {
3071
+ if (key === "then" || key === "catch" || key === "finally") {
3072
+ throw new InterpreterRuntimeError(
3073
+ `Promise.prototype.${String(key)} is not supported in CodeMode; use await instead (with try/catch to handle failures) - e.g. \`const result = await tools.ns.tool(...)\`.`,
3074
+ propertyNode,
3075
+ "UnsupportedSyntax",
3076
+ [supportedSyntaxMessage],
3077
+ )
3078
+ }
3079
+ throw new InterpreterRuntimeError(
3080
+ "This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.",
3081
+ objectNode,
3082
+ "InvalidDataValue",
3083
+ )
3084
+ }
3085
+
3086
+ if (isRuntimeReference(objectValue)) {
3087
+ throw new InterpreterRuntimeError(
3088
+ "CodeMode runtime references are opaque and do not expose properties.",
3089
+ objectNode,
3090
+ "InvalidDataValue",
3091
+ )
3092
+ }
3093
+
3094
+ if (typeof objectValue !== "object" || objectValue === null) {
3095
+ throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
3096
+ }
3097
+
3098
+ if (typeof key === "string" && isBlockedMember(key)) {
3099
+ throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, propertyNode)
3100
+ }
3101
+
3102
+ if (Array.isArray(objectValue)) {
3103
+ if (
3104
+ key !== "length" &&
3105
+ !(typeof key === "string" && arrayMethods.has(key)) &&
3106
+ typeof key !== "number" &&
3107
+ !/^\d+$/.test(key)
3108
+ ) {
3109
+ // Own non-index properties read through (match results carry index/groups); like JS,
3110
+ // they are readable in place and dropped by JSON at data boundaries.
3111
+ if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
3112
+ return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
3113
+ }
3114
+ // Unknown property on an array reads as `undefined`, matching JS (`[1,2].foo === undefined`),
3115
+ // instead of throwing - so defensive access under optional chaining behaves as expected.
3116
+ return new ComputedValue(undefined)
3117
+ }
3118
+ return { target: objectValue, key }
3119
+ }
3120
+
3121
+ return { target: objectValue as SafeObject, key }
3122
+ })
3123
+ }
3124
+
3125
+ private readMember(node: AstNode): Effect.Effect<unknown, unknown, R> {
3126
+ return Effect.map(this.getMemberReference(node), (reference) => {
3127
+ if (reference === OptionalShortCircuit) return OptionalShortCircuit
3128
+ if (reference instanceof ComputedValue) return reference.value
3129
+ if (
3130
+ reference === undefined ||
3131
+ reference instanceof ToolReference ||
3132
+ reference instanceof PromiseMethodReference ||
3133
+ reference instanceof IntrinsicReference ||
3134
+ reference instanceof GlobalMethodReference
3135
+ )
3136
+ return reference
3137
+ if (Array.isArray(reference.target)) {
3138
+ if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
3139
+ return new IntrinsicReference(reference.target, reference.key)
3140
+ }
3141
+ return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)]
3142
+ }
3143
+ if (reference.target instanceof SandboxURL) {
3144
+ return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
3145
+ }
3146
+ return reference.target[String(reference.key)]
3147
+ })
3148
+ }
3149
+
3150
+ private writeMember(node: AstNode, value: unknown): Effect.Effect<unknown, unknown, R> {
3151
+ return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
3152
+ }
3153
+
3154
+ // Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression
3155
+ // runs once), then lets `compute` decide whether to write - enabling compound assignment,
3156
+ // updates, plain writes, and short-circuiting logical assignment to share one safe path.
3157
+ private modifyMember(
3158
+ node: AstNode,
3159
+ compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>,
3160
+ ): Effect.Effect<unknown, unknown, R> {
3161
+ const self = this
3162
+ return Effect.gen(function* () {
3163
+ const reference = yield* self.getMemberReference(node)
3164
+ if (
3165
+ reference === OptionalShortCircuit ||
3166
+ reference instanceof ComputedValue ||
3167
+ reference === undefined ||
3168
+ reference instanceof ToolReference ||
3169
+ reference instanceof PromiseMethodReference ||
3170
+ reference instanceof IntrinsicReference ||
3171
+ reference instanceof GlobalMethodReference
3172
+ ) {
3173
+ throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node)
3174
+ }
3175
+ if (Array.isArray(reference.target)) {
3176
+ if (reference.key === "length")
3177
+ throw new InterpreterRuntimeError("Array length cannot be assigned in CodeMode.", node)
3178
+ if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
3179
+ throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node)
3180
+ }
3181
+ }
3182
+ const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key)
3183
+ const current =
3184
+ reference.target instanceof SandboxURL
3185
+ ? (reference.target.url as unknown as Record<string, unknown>)[key]
3186
+ : (reference.target as Record<PropertyKey, unknown>)[key]
3187
+ const { write, next, result } = yield* compute(current)
3188
+ if (write) self.assignToReference(reference, key, next, node)
3189
+ return result
3190
+ })
3191
+ }
3192
+
3193
+ // Rejects inserting a value that (transitively) contains the container it is being inserted
3194
+ // into - the mutation that would create a circular structure no later walk could survive.
3195
+ private rejectCircularInsertion(
3196
+ container: object,
3197
+ value: unknown,
3198
+ label: string,
3199
+ node: AstNode,
3200
+ seen = new Set<object>(),
3201
+ ): void {
3202
+ if (value === container)
3203
+ throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
3204
+ if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return
3205
+ seen.add(value)
3206
+ const items = Array.isArray(value) ? value : Object.values(value)
3207
+ for (const item of items) this.rejectCircularInsertion(container, item, label, node, seen)
3208
+ seen.delete(value)
3209
+ }
3210
+
3211
+ private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void {
3212
+ if (Array.isArray(reference.target)) {
3213
+ const target = reference.target
3214
+ const index = key as number
3215
+ if (!Number.isInteger(index) || index < 0) {
3216
+ throw new InterpreterRuntimeError(
3217
+ "Array assignment index must be a non-negative integer.",
3218
+ node,
3219
+ "InvalidDataValue",
3220
+ )
3221
+ }
3222
+ this.rejectCircularInsertion(target, next, "Array assignment result", node)
3223
+ target[index] = next
3224
+ return
3225
+ }
3226
+ if (reference.target instanceof SandboxURL) {
3227
+ const property = key as string
3228
+ if (!urlWritableProperties.has(property)) {
3229
+ throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError")
3230
+ }
3231
+ try {
3232
+ const url = reference.target.url as unknown as Record<string, string>
3233
+ url[property] = uriArgument(next, `URL.${property} value`)
3234
+ return
3235
+ } catch (error) {
3236
+ if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error
3237
+ throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError")
3238
+ }
3239
+ }
3240
+ const target = reference.target as SafeObject
3241
+ const objectKey = key as string
3242
+ this.rejectCircularInsertion(target, next, "Object assignment result", node)
3243
+ target[objectKey] = next
3244
+ }
3245
+
3246
+ private toPropertyKey(value: unknown, node: AstNode): string | number {
3247
+ if (typeof value === "string" || typeof value === "number") {
3248
+ return value
3249
+ }
3250
+
3251
+ throw new InterpreterRuntimeError("Property key must be a string or number.", node)
3252
+ }
3253
+
3254
+ private declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
3255
+ const scope = this.currentScope()
3256
+
3257
+ // A pre-seeded parameter slot (initialized === false) is being bound for the first time;
3258
+ // anything else already present is a genuine duplicate declaration.
3259
+ const existing = scope.get(name)
3260
+ if (existing && existing.initialized !== false) {
3261
+ throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node)
3262
+ }
3263
+
3264
+ scope.set(name, { mutable, value, initialized: true })
3265
+ }
3266
+
3267
+ private getIdentifierValue(name: string, node: AstNode): unknown {
3268
+ const binding = this.resolveBinding(name)
3269
+
3270
+ if (!binding) {
3271
+ throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
3272
+ }
3273
+
3274
+ // A parameter default that forward-references a later (not-yet-bound) parameter - JS TDZ.
3275
+ if (binding.initialized === false) {
3276
+ throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError")
3277
+ }
3278
+
3279
+ return binding.value
3280
+ }
3281
+
3282
+ private setIdentifierValue(name: string, value: unknown, node: AstNode): unknown {
3283
+ const binding = this.resolveBinding(name)
3284
+
3285
+ if (!binding) {
3286
+ throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
3287
+ }
3288
+
3289
+ if (!binding.mutable) {
3290
+ throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError")
3291
+ }
3292
+
3293
+ binding.value = value
3294
+ return value
3295
+ }
3296
+
3297
+ private resolveBinding(name: string): Binding | undefined {
3298
+ for (let index = this.scopes.length - 1; index >= 0; index -= 1) {
3299
+ const scope = this.scopes[index]
3300
+ const binding = scope?.get(name)
3301
+
3302
+ if (binding) {
3303
+ return binding
3304
+ }
3305
+ }
3306
+
3307
+ return undefined
3308
+ }
3309
+
3310
+ private currentScope(): Map<string, Binding> {
3311
+ const scope = this.scopes[this.scopes.length - 1]
3312
+
3313
+ if (!scope) {
3314
+ throw new InterpreterRuntimeError("Interpreter scope stack is empty.")
3315
+ }
3316
+
3317
+ return scope
3318
+ }
3319
+
3320
+ private pushScope(): void {
3321
+ this.scopes.push(new Map())
3322
+ }
3323
+
3324
+ private popScope(): void {
3325
+ this.scopes.pop()
3326
+ }
3327
+ }
3328
+
3329
+ /**
3330
+ * Executes one Effect-native CodeMode program without constructing a reusable runtime.
3331
+ *
3332
+ * @example
3333
+ * ```ts
3334
+ * const result = yield* CodeMode.execute({
3335
+ * tools: { lookup },
3336
+ * code: `return await tools.lookup({ id: "order_42" })`,
3337
+ * })
3338
+ * ```
3339
+ */
3340
+ export const executeWithLimits = <const Tools extends Record<string, unknown>>(
3341
+ options: ExecuteOptions<Tools>,
3342
+ limits: ResolvedExecutionLimits,
3343
+ searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
3344
+ ): Effect.Effect<Result, never, Services<Tools>> => {
3345
+ const hooks = {
3346
+ ...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }),
3347
+ ...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }),
3348
+ }
3349
+ const tools = ToolRuntime.make(
3350
+ (options.tools ?? {}) as HostTools<Services<Tools>>,
3351
+ limits.maxToolCalls,
3352
+ searchIndex,
3353
+ hooks,
3354
+ )
3355
+ const logs: Array<string> = []
3356
+ const logged = () => (logs.length > 0 ? { logs: [...logs] } : {})
3357
+
3358
+ if (options.code.trim().length === 0) {
3359
+ return Effect.succeed({
3360
+ ok: false,
3361
+ error: { kind: "ParseError", message: "Code cannot be empty." },
3362
+ toolCalls: tools.calls,
3363
+ })
3364
+ }
3365
+
3366
+ const operation = Effect.gen(function* () {
3367
+ const program = parseProgram(options.code)
3368
+ const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.keys, logs)
3369
+ const value = yield* interpreter.run(program)
3370
+ const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
3371
+ return {
3372
+ ok: true,
3373
+ value: result,
3374
+ ...logged(),
3375
+ toolCalls: tools.calls,
3376
+ } satisfies Result
3377
+ }).pipe((program) => {
3378
+ const timeoutMs = limits.timeoutMs
3379
+ if (timeoutMs === undefined) return program
3380
+ return program.pipe(
3381
+ Effect.timeoutOrElse({
3382
+ duration: timeoutMs,
3383
+ orElse: () =>
3384
+ Effect.succeed({
3385
+ ok: false,
3386
+ error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` },
3387
+ ...logged(),
3388
+ toolCalls: tools.calls,
3389
+ } satisfies Result),
3390
+ }),
3391
+ )
3392
+ })
3393
+
3394
+ return operation.pipe(
3395
+ Effect.catchCause((cause) =>
3396
+ Cause.hasInterruptsOnly(cause)
3397
+ ? Effect.interrupt
3398
+ : Effect.succeed({
3399
+ ok: false,
3400
+ error: normalizeError(Cause.squash(cause)),
3401
+ ...logged(),
3402
+ toolCalls: tools.calls,
3403
+ } satisfies Result),
3404
+ ),
3405
+ Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))),
3406
+ )
3407
+ }
3408
+
3409
+ const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength
3410
+
3411
+ // Truncates to a UTF-8 byte budget without splitting a code point (a split multi-byte
3412
+ // sequence decodes to a replacement character, which is dropped).
3413
+ const utf8Truncate = (value: string, maxBytes: number): string => {
3414
+ const bytes = new TextEncoder().encode(value)
3415
+ if (bytes.byteLength <= maxBytes) return value
3416
+ const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes)))
3417
+ return text.endsWith("\uFFFD") ? text.slice(0, -1) : text
3418
+ }
3419
+
3420
+ /**
3421
+ * Bounds the model-facing output (serialized result value plus logs) to `maxOutputBytes`.
3422
+ * Oversized values are replaced by their truncated serialized text with an explanatory marker,
3423
+ * and logs are kept from the start until the remaining budget is exhausted. Truncation never
3424
+ * fails the execution; `truncated: true` marks affected results. Only runs when the host set
3425
+ * `maxOutputBytes` - with the limit absent, output passes through unbounded.
3426
+ */
3427
+ const boundOutput = (result: Result, maxOutputBytes: number): Result => {
3428
+ let truncated = false
3429
+
3430
+ let value: DataValue = null
3431
+ let valueBytes = 0
3432
+ if (result.ok) {
3433
+ const serialized = JSON.stringify(result.value) ?? "null"
3434
+ const bytes = utf8ByteLength(serialized)
3435
+ if (bytes > maxOutputBytes) {
3436
+ truncated = true
3437
+ value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]`
3438
+ valueBytes = maxOutputBytes
3439
+ } else {
3440
+ value = result.value
3441
+ valueBytes = bytes
3442
+ }
3443
+ }
3444
+
3445
+ const logs = result.logs ?? []
3446
+ const kept: Array<string> = []
3447
+ const logBudget = Math.max(0, maxOutputBytes - valueBytes)
3448
+ let logBytes = 0
3449
+ for (const line of logs) {
3450
+ const lineBytes = utf8ByteLength(line) + 1
3451
+ if (logBytes + lineBytes > logBudget) break
3452
+ logBytes += lineBytes
3453
+ kept.push(line)
3454
+ }
3455
+ if (kept.length < logs.length) {
3456
+ truncated = true
3457
+ kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`)
3458
+ }
3459
+
3460
+ if (!truncated) return result
3461
+ const logsPart = kept.length > 0 ? { logs: kept } : {}
3462
+ return result.ok
3463
+ ? { ok: true, value, ...logsPart, truncated: true, toolCalls: result.toolCalls }
3464
+ : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls }
3465
+ }