| import { parse } from "acorn" |
| import { Cause, Effect, Exit, Fiber, Semaphore } from "effect" |
| import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" |
| import { |
| copyIn, |
| copyOut, |
| isBlockedMember, |
| ToolReference, |
| ToolRuntime, |
| ToolRuntimeError, |
| type HostTools, |
| type SafeObject, |
| type Services, |
| } from "../tool-runtime.js" |
| import { ToolError } from "../tool-error.js" |
| import type { |
| DataValue, |
| Diagnostic, |
| DiagnosticKind, |
| ExecuteOptions, |
| ResolvedExecutionLimits, |
| Result, |
| } from "../codemode.js" |
| import { |
| type AstNode, |
| asNode, |
| type Binding, |
| CodeModeFunction, |
| CoercionFunction, |
| ComputedValue, |
| ErrorConstructorReference, |
| GlobalMethodReference, |
| GlobalNamespace, |
| type GlobalNamespaceName, |
| formatLocation, |
| getArray, |
| getBoolean, |
| getNode, |
| getOptionalNode, |
| getString, |
| IntrinsicReference, |
| InterpreterRuntimeError, |
| isRecord, |
| type MemberReference, |
| OptionalShortCircuit, |
| PromiseMethodReference, |
| type PromiseMethodName, |
| PromiseNamespace, |
| ProgramThrow, |
| type ProgramNode, |
| type StatementResult, |
| sourceLocation, |
| supportedSyntaxMessage, |
| unsupportedSyntax, |
| UriFunction, |
| } from "./model.js" |
| import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js" |
| import { consoleMethods, MAX_CONSOLE_DEPTH } from "../stdlib/console.js" |
| import { dateMethods, dateStatics, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" |
| import { invokeJsonMethod } from "../stdlib/json.js" |
| import { invokeMathMethod, mathConstants } from "../stdlib/math.js" |
| import { |
| invokeNumberMethod, |
| invokeNumberStatic, |
| numberConstants, |
| numberMethods, |
| numberStatics, |
| } from "../stdlib/number.js" |
| import { invokeObjectMethod } from "../stdlib/object.js" |
| import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js" |
| import { |
| escapeRegexHint, |
| invokeRegExpMethod, |
| matchToValue, |
| regexpMethods, |
| regexpProperties, |
| regexFailureReason, |
| toHostRegex, |
| } from "../stdlib/regexp.js" |
| import { invokeStringStatic, stringMethods, stringStatics } from "../stdlib/string.js" |
| import { |
| urlMethods, |
| urlProperties, |
| urlSearchParamsMethods, |
| urlStatics, |
| urlWritableProperties, |
| invokeUriFunction, |
| invokeURLMethod, |
| invokeURLStatic, |
| uriArgument, |
| urlArgument, |
| } from "../stdlib/url.js" |
| import { |
| boundedData, |
| coerceToNumber, |
| coerceToString, |
| compoundOperators, |
| createErrorValue, |
| errorBrandName, |
| errorConstructors, |
| invokeCoercion, |
| valueConstructors, |
| } from "../stdlib/value.js" |
| import { |
| isSandboxValue, |
| SandboxDate, |
| SandboxMap, |
| SandboxPromise, |
| SandboxRegExp, |
| SandboxSet, |
| SandboxURL, |
| SandboxURLSearchParams, |
| } from "../values.js" |
|
|
| const parseProgram = (code: string): ProgramNode => { |
| const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, { |
| reportDiagnostics: true, |
| compilerOptions: { |
| target: ScriptTarget.ESNext, |
| module: ModuleKind.ESNext, |
| }, |
| }) |
| const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error) |
|
|
| if (diagnostic) { |
| throw new InterpreterRuntimeError( |
| `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, |
| undefined, |
| "ParseError", |
| ) |
| } |
|
|
| const bodyStart = transpiled.outputText.indexOf("{") + 1 |
| const bodyEnd = transpiled.outputText.lastIndexOf("}") |
| const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd) |
| const parsed = parse(executableCode, { |
| ecmaVersion: "latest", |
| sourceType: "script", |
| allowReturnOutsideFunction: true, |
| allowAwaitOutsideFunction: true, |
| locations: true, |
| }) as unknown |
|
|
| if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) { |
| throw new InterpreterRuntimeError("Failed to parse script as a Program node.") |
| } |
|
|
| return parsed as ProgramNode |
| } |
|
|
| const publicErrorMessage = (message: string): string => |
| message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "<redacted-path>") |
|
|
| const normalizeError = (error: unknown): Diagnostic => { |
| if (error instanceof InterpreterRuntimeError) { |
| return { |
| kind: error.kind, |
| message: `${error.message}${formatLocation(error.node)}`, |
| ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}), |
| ...(error.suggestions ? { suggestions: error.suggestions } : {}), |
| } |
| } |
|
|
| if (error instanceof ToolRuntimeError) { |
| return { |
| kind: error.kind, |
| message: error.message, |
| ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}), |
| } |
| } |
|
|
| if (error instanceof ToolError) { |
| return { kind: "ToolFailure", message: publicErrorMessage(error.message) } |
| } |
|
|
| if (error instanceof ProgramThrow) { |
| const value = error.value |
| let message: string |
| if (containsRuntimeReference(value)) { |
| |
| message = "a non-data value" |
| } else if (typeof value === "string") { |
| message = value |
| } else if ( |
| value !== null && |
| typeof value === "object" && |
| typeof (value as { message?: unknown }).message === "string" |
| ) { |
| message = (value as { message: string }).message |
| } else { |
| try { |
| message = JSON.stringify(copyOut(value)) ?? String(value) |
| } catch { |
| message = String(value) |
| } |
| } |
| return { kind: "ExecutionFailure", message: `Uncaught: ${message}` } |
| } |
|
|
| if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) { |
| return { |
| kind: "ExecutionFailure", |
| message: "Execution exceeded the maximum nesting depth.", |
| } |
| } |
|
|
| if (error instanceof Error) { |
| return { |
| kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure", |
| message: publicErrorMessage(error.message), |
| } |
| } |
|
|
| |
| |
| return { |
| kind: "ExecutionFailure", |
| message: publicErrorMessage(String(error)), |
| } |
| } |
|
|
| |
| const caughtErrorValue = (thrown: unknown): unknown => { |
| if (thrown instanceof ProgramThrow) return thrown.value |
| if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message) |
| const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error" |
| return createErrorValue(name, normalizeError(thrown).message) |
| } |
|
|
| const isRuntimeReference = (value: unknown): boolean => |
| value instanceof CodeModeFunction || |
| value instanceof ToolReference || |
| value instanceof IntrinsicReference || |
| value instanceof GlobalNamespace || |
| value instanceof GlobalMethodReference || |
| value instanceof PromiseNamespace || |
| value instanceof PromiseMethodReference || |
| value instanceof SandboxPromise || |
| value instanceof CoercionFunction || |
| value instanceof UriFunction || |
| value instanceof ErrorConstructorReference || |
| isSandboxValue(value) |
|
|
| const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => { |
| if (isRuntimeReference(value)) return true |
| if (value === null || typeof value !== "object") return false |
| if (seen.has(value)) return false |
| seen.add(value) |
| const contains = Array.isArray(value) |
| ? value.some((item) => containsRuntimeReference(item, seen)) |
| : Object.values(value).some((item) => containsRuntimeReference(item, seen)) |
| seen.delete(value) |
| return contains |
| } |
|
|
| |
| |
| |
| const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => { |
| if (isSandboxValue(value)) return false |
| if (isRuntimeReference(value)) return true |
| if (value === null || typeof value !== "object") return false |
| if (seen.has(value)) return false |
| seen.add(value) |
| const contains = Array.isArray(value) |
| ? value.some((item) => containsOpaqueReference(item, seen)) |
| : Object.values(value).some((item) => containsOpaqueReference(item, seen)) |
| seen.delete(value) |
| return contains |
| } |
|
|
| |
| |
| |
| const typeofValue = (value: unknown): string => { |
| if ( |
| value instanceof CodeModeFunction || |
| value instanceof CoercionFunction || |
| value instanceof IntrinsicReference || |
| value instanceof GlobalMethodReference || |
| value instanceof PromiseMethodReference || |
| value instanceof PromiseNamespace || |
| value instanceof ErrorConstructorReference |
| ) |
| return "function" |
| if (value instanceof UriFunction) return "function" |
| if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" |
| if (value instanceof GlobalNamespace) { |
| return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function" |
| } |
| return typeof value |
| } |
|
|
| |
| |
| |
| |
| const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { |
| if (rhs instanceof ErrorConstructorReference) { |
| const brand = errorBrandName(lhs) |
| return brand !== undefined && (rhs.name === "Error" || brand === rhs.name) |
| } |
| if (rhs instanceof GlobalNamespace) { |
| switch (rhs.name) { |
| case "Date": |
| return lhs instanceof SandboxDate |
| case "RegExp": |
| return lhs instanceof SandboxRegExp |
| case "Map": |
| return lhs instanceof SandboxMap |
| case "Set": |
| return lhs instanceof SandboxSet |
| case "URL": |
| return lhs instanceof SandboxURL |
| case "URLSearchParams": |
| return lhs instanceof SandboxURLSearchParams |
| case "Array": |
| return Array.isArray(lhs) |
| case "Object": |
| return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function") |
| } |
| } |
| if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise |
| |
| |
| if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { |
| return false |
| } |
| throw new InterpreterRuntimeError( |
| "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.", |
| node, |
| ) |
| } |
|
|
| const invokeStringMethod = (value: string, name: string, args: Array<unknown>, node: AstNode): unknown => { |
| const str = (index: number): string => { |
| const arg = args[index] |
| if (typeof arg !== "string") |
| throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node) |
| return arg |
| } |
| const num = (index: number): number => { |
| const arg = args[index] |
| if (typeof arg !== "number") |
| throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node) |
| return arg |
| } |
| const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) |
| const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) |
|
|
| let result: unknown |
| switch (name) { |
| case "toLowerCase": |
| result = value.toLowerCase() |
| break |
| case "toUpperCase": |
| result = value.toUpperCase() |
| break |
| case "trim": |
| result = value.trim() |
| break |
| |
| case "trimStart": |
| case "trimLeft": |
| result = value.trimStart() |
| break |
| case "trimEnd": |
| case "trimRight": |
| result = value.trimEnd() |
| break |
| |
| |
| case "localeCompare": |
| result = value.localeCompare(str(0)) |
| break |
| case "normalize": { |
| const form = optStr(0) |
| try { |
| result = value.normalize(form) |
| } catch { |
| throw new InterpreterRuntimeError( |
| `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, |
| node, |
| ).as("RangeError") |
| } |
| break |
| } |
| case "split": { |
| if (args.length === 0) { |
| result = [value] |
| break |
| } |
| if (args[0] instanceof SandboxRegExp) { |
| result = value.split((args[0] as SandboxRegExp).regex, optNum(1)) |
| break |
| } |
| const requestedLimit = optNum(1) |
| result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) |
| break |
| } |
| case "slice": |
| result = value.slice(optNum(0), optNum(1)) |
| break |
| case "includes": |
| result = value.includes(str(0), optNum(1)) |
| break |
| case "startsWith": |
| result = value.startsWith(str(0), optNum(1)) |
| break |
| case "endsWith": |
| result = value.endsWith(str(0), optNum(1)) |
| break |
| case "indexOf": |
| result = value.indexOf(str(0), optNum(1)) |
| break |
| case "lastIndexOf": |
| result = value.lastIndexOf(str(0), optNum(1)) |
| break |
| case "replace": |
| case "replaceAll": { |
| if (args[0] instanceof SandboxRegExp) { |
| const pattern = (args[0] as SandboxRegExp).regex |
| const replacement = str(1) |
| if (name === "replaceAll" && !pattern.global) { |
| throw new InterpreterRuntimeError( |
| `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.`, |
| node, |
| ) |
| } |
| result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) |
| break |
| } |
| if (name === "replace") { |
| result = value.replace(str(0), str(1)) |
| break |
| } |
| result = value.replaceAll(str(0), str(1)) |
| break |
| } |
| case "match": { |
| const pattern = toHostRegex(args[0], name, node) |
| const matched = value.match(pattern) |
| if (matched === null) return null |
| |
| |
| if (pattern.global) return boundedData(matched, "String.match result") |
| return matchToValue(matched) |
| } |
| case "matchAll": { |
| const pattern = toHostRegex(args[0], name, node, "g") |
| if (!pattern.global) { |
| throw new InterpreterRuntimeError( |
| `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.`, |
| node, |
| ) |
| } |
| |
| |
| return Array.from(value.matchAll(pattern), matchToValue) |
| } |
| case "search": { |
| result = value.search(toHostRegex(args[0], name, node)) |
| break |
| } |
| case "repeat": { |
| const count = num(0) |
| if (!Number.isFinite(count) || count < 0) |
| throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node) |
| result = value.repeat(count) |
| break |
| } |
| case "padStart": |
| result = value.padStart(num(0), optStr(1)) |
| break |
| case "padEnd": |
| result = value.padEnd(num(0), optStr(1)) |
| break |
| case "charAt": |
| result = value.charAt(optNum(0) ?? 0) |
| break |
| case "at": |
| result = value.at(optNum(0) ?? 0) |
| break |
| case "substring": |
| result = value.substring(optNum(0) ?? 0, optNum(1)) |
| break |
| case "substr": |
| result = value.substr(optNum(0) ?? 0, optNum(1)) |
| break |
| |
| |
| case "charCodeAt": |
| result = value.charCodeAt(optNum(0) ?? 0) |
| break |
| case "codePointAt": |
| result = value.codePointAt(optNum(0) ?? 0) |
| break |
| case "toString": |
| result = value |
| break |
| case "concat": { |
| result = value.concat(...args.map((_, index) => str(index))) |
| break |
| } |
| default: |
| throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node) |
| } |
| return boundedData(result, `String.${name} result`) |
| } |
|
|
| const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => { |
| switch (name) { |
| case "isArray": |
| return Array.isArray(args[0]) |
| case "of": |
| return [...args] |
| case "from": { |
| if (args.length > 1) { |
| throw new InterpreterRuntimeError( |
| "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.", |
| node, |
| "UnsupportedSyntax", |
| [supportedSyntaxMessage], |
| ) |
| } |
| |
| if (args[0] instanceof SandboxMap) |
| return Array.from((args[0] as SandboxMap).map.entries(), ([key, item]) => [key, item]) |
| if (args[0] instanceof SandboxSet) return Array.from((args[0] as SandboxSet).set.values()) |
| if (args[0] instanceof SandboxURLSearchParams) { |
| return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) |
| } |
| const source = boundedData(args[0], "Array.from input") |
| if (typeof source === "string") return Array.from(source) |
| if (Array.isArray(source)) return [...source] |
| if ( |
| source !== null && |
| typeof source === "object" && |
| typeof (source as { length?: unknown }).length === "number" |
| ) { |
| return Array.from(source as ArrayLike<unknown>) |
| } |
| throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node) |
| } |
| default: |
| throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) |
| } |
| } |
|
|
| const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unknown>, node: AstNode): unknown => { |
| if (ref.namespace === "console") |
| throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node) |
| if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node) |
| if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node) |
| if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node) |
| if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node) |
| if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node) |
| if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node) |
| if (ref.namespace === "Date") { |
| if (!dateStatics.has(ref.name)) |
| throw new InterpreterRuntimeError(`Date.${ref.name} is not available in CodeMode.`, node) |
| return invokeDateStatic(ref.name, args, node) |
| } |
| if ( |
| ref.namespace === "RegExp" || |
| ref.namespace === "Map" || |
| ref.namespace === "Set" || |
| ref.namespace === "URLSearchParams" |
| ) { |
| throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node) |
| } |
| return invokeJsonMethod(ref.name, args, node) |
| } |
|
|
| |
| const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<string> => { |
| switch (pattern.type) { |
| case "Identifier": |
| out.push(getString(pattern, "name")) |
| break |
| case "AssignmentPattern": |
| collectPatternNames(getNode(pattern, "left"), out) |
| break |
| case "RestElement": |
| collectPatternNames(getNode(pattern, "argument"), out) |
| break |
| case "ArrayPattern": |
| for (const element of getArray(pattern, "elements")) { |
| if (element !== null) collectPatternNames(asNode(element, "elements"), out) |
| } |
| break |
| case "ObjectPattern": |
| for (const property of getArray(pattern, "properties")) { |
| const prop = asNode(property, "properties") |
| collectPatternNames(prop.type === "RestElement" ? getNode(prop, "argument") : getNode(prop, "value"), out) |
| } |
| break |
| } |
| return out |
| } |
|
|
| class Interpreter<R> { |
| private scopes: Array<Map<string, Binding>> |
| private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R> |
| |
| |
| private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string> |
| private readonly logs: Array<string> |
| private lastValue: unknown |
| |
| private readonly callPermits: Semaphore.Semaphore |
| |
| |
| |
| private readonly pendingSettlements = new Set<SandboxPromise>() |
|
|
| constructor( |
| invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>, |
| toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>, |
| logs: Array<string> = [], |
| ) { |
| const globalScope = new Map<string, Binding>() |
| this.scopes = [globalScope] |
| this.invokeTool = invokeTool |
| this.toolKeys = toolKeys |
| this.logs = logs |
| this.lastValue = undefined |
| this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) |
| globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) |
| globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() }) |
| globalScope.set("undefined", { mutable: false, value: undefined }) |
| globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") }) |
| globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") }) |
| globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") }) |
| globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") }) |
| globalScope.set("String", { mutable: false, value: new CoercionFunction("String") }) |
| globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") }) |
| globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") }) |
| globalScope.set("console", { mutable: false, value: new GlobalNamespace("console") }) |
| globalScope.set("parseInt", { mutable: false, value: new CoercionFunction("parseInt") }) |
| globalScope.set("parseFloat", { mutable: false, value: new CoercionFunction("parseFloat") }) |
| globalScope.set("Date", { mutable: false, value: new GlobalNamespace("Date") }) |
| globalScope.set("RegExp", { mutable: false, value: new GlobalNamespace("RegExp") }) |
| globalScope.set("Map", { mutable: false, value: new GlobalNamespace("Map") }) |
| globalScope.set("Set", { mutable: false, value: new GlobalNamespace("Set") }) |
| globalScope.set("URL", { mutable: false, value: new GlobalNamespace("URL") }) |
| globalScope.set("URLSearchParams", { mutable: false, value: new GlobalNamespace("URLSearchParams") }) |
| globalScope.set("encodeURI", { mutable: false, value: new UriFunction("encodeURI") }) |
| globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") }) |
| globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") }) |
| globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") }) |
| |
| |
| for (const name of errorConstructors) { |
| globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) }) |
| } |
| |
| |
| globalScope.set("NaN", { mutable: false, value: NaN }) |
| globalScope.set("Infinity", { mutable: false, value: Infinity }) |
| } |
|
|
| run(program: ProgramNode): Effect.Effect<unknown, unknown, R> { |
| const self = this |
| |
| |
| |
| this.pushScope() |
| return Effect.gen(function* () { |
| self.hoistFunctions(program.body) |
| let value: unknown = undefined |
| let returned = false |
| for (const statement of program.body) { |
| const result = yield* self.evaluateStatement(statement) |
|
|
| if (result.kind === "return") { |
| value = result.value |
| returned = true |
| break |
| } |
|
|
| if (result.kind === "break" || result.kind === "continue") { |
| throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement) |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
| } |
| if (!returned) value = self.lastValue |
|
|
| |
| |
| |
| if (value instanceof SandboxPromise) value = yield* self.settlePromise(value) |
| yield* self.drainPendingSettlements() |
| return value |
| }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) |
| } |
|
|
| |
| |
| |
| |
| private drainPendingSettlements(): Effect.Effect<void, unknown, never> { |
| const self = this |
| return Effect.gen(function* () { |
| for (const promise of [...self.pendingSettlements]) { |
| const exit = yield* self.observePromise(promise) |
| if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue |
| const failure = normalizeError(Cause.squash(exit.cause)) |
| throw new InterpreterRuntimeError( |
| `Unhandled rejection from an un-awaited tool call: ${failure.message}`, |
| undefined, |
| failure.kind, |
| ["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."], |
| ) |
| } |
| }) |
| } |
|
|
| |
| |
| |
| |
| private createToolCallPromise( |
| path: ReadonlyArray<string>, |
| args: Array<unknown>, |
| ): Effect.Effect<SandboxPromise, never, R> { |
| const self = this |
| return Effect.map( |
| Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), { |
| startImmediately: true, |
| }), |
| (fiber) => { |
| const promise = new SandboxPromise(fiber) |
| self.pendingSettlements.add(promise) |
| return promise |
| }, |
| ) |
| } |
|
|
| |
| |
| |
| private observePromise(promise: SandboxPromise): Effect.Effect<Exit.Exit<unknown, unknown>> { |
| this.pendingSettlements.delete(promise) |
| return promise.fiber !== undefined ? Fiber.await(promise.fiber) : Effect.exit(promise.immediate ?? Effect.void) |
| } |
|
|
| |
| |
| private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect<unknown, unknown, never> { |
| const self = this |
| return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node)) |
| } |
|
|
| private unwrapPromiseExit( |
| promise: SandboxPromise | undefined, |
| exit: Exit.Exit<unknown, unknown>, |
| node?: AstNode, |
| ): Effect.Effect<unknown, unknown> { |
| if (Exit.isSuccess(exit)) return Effect.succeed(exit.value) |
| |
| |
| |
| if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) { |
| return Effect.fail( |
| new InterpreterRuntimeError( |
| "This tool call was interrupted because another value settled a Promise.race first.", |
| node, |
| ), |
| ) |
| } |
| return Effect.failCause(exit.cause) |
| } |
|
|
| private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| switch (node.type) { |
| case "ExpressionStatement": |
| return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value })) |
| case "VariableDeclaration": |
| return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) |
| case "ReturnStatement": { |
| const argumentNode = getOptionalNode(node, "argument") |
| return argumentNode |
| ? Effect.map(this.evaluateExpression(argumentNode), (value) => ({ kind: "return", value })) |
| : Effect.succeed({ kind: "return", value: undefined }) |
| } |
| case "BlockStatement": |
| return this.evaluateBlock(node) |
| case "IfStatement": |
| return this.evaluateIfStatement(node) |
| case "SwitchStatement": |
| return this.evaluateSwitchStatement(node) |
| case "WhileStatement": |
| return this.evaluateWhileStatement(node) |
| case "DoWhileStatement": |
| return this.evaluateDoWhileStatement(node) |
| case "ForStatement": |
| return this.evaluateForStatement(node) |
| case "ForOfStatement": |
| return this.evaluateForOfStatement(node) |
| case "ForInStatement": |
| return this.evaluateForInStatement(node) |
| case "BreakStatement": |
| return Effect.succeed(this.evaluateBreakStatement(node)) |
| case "ContinueStatement": |
| return Effect.succeed(this.evaluateContinueStatement(node)) |
| case "ThrowStatement": |
| return this.evaluateThrowStatement(node) |
| case "TryStatement": |
| return this.evaluateTryStatement(node) |
| case "EmptyStatement": |
| return Effect.succeed({ kind: "none" }) |
| case "FunctionDeclaration": |
| return Effect.succeed({ kind: "none" }) |
| default: |
| throw unsupportedSyntax(node.type, node) |
| } |
| } |
|
|
| private evaluateBlock(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| this.pushScope() |
| const self = this |
| return Effect.gen(function* () { |
| const body = getArray(node, "body") |
| self.hoistFunctions(body) |
|
|
| for (const statementValue of body) { |
| const statement = asNode(statementValue, "body") |
| const result = yield* self.evaluateStatement(statement) |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| continue |
| } |
|
|
| if (result.kind !== "none") { |
| return result |
| } |
| } |
|
|
| return { kind: "none" } satisfies StatementResult |
| }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) |
| } |
|
|
| private createFunction(node: AstNode): CodeModeFunction { |
| if (node.generator === true) { |
| throw new InterpreterRuntimeError( |
| "Generator functions are not supported in CodeMode.", |
| node, |
| "UnsupportedSyntax", |
| [supportedSyntaxMessage], |
| ) |
| } |
| return new CodeModeFunction( |
| getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), |
| getNode(node, "body"), |
| this.scopes.slice(), |
| ) |
| } |
|
|
| |
| |
| private hoistFunctions(statements: Array<unknown>): void { |
| for (const statementValue of statements) { |
| if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue |
| const node = statementValue as AstNode |
| this.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) |
| } |
| } |
|
|
| private evaluateIfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const testNode = getNode(node, "test") |
| const consequentNode = getNode(node, "consequent") |
| const alternateNode = getOptionalNode(node, "alternate") |
|
|
| return Effect.flatMap(this.evaluateExpression(testNode), (test) => |
| test |
| ? this.evaluateStatement(consequentNode) |
| : alternateNode |
| ? this.evaluateStatement(alternateNode) |
| : Effect.succeed({ kind: "none" }), |
| ) |
| } |
|
|
| private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const self = this |
| this.pushScope() |
| return Effect.gen(function* () { |
| const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant")) |
| if (containsOpaqueReference(discriminant)) { |
| throw new InterpreterRuntimeError( |
| "Switch discriminants must be data values in CodeMode.", |
| node, |
| "InvalidDataValue", |
| ) |
| } |
| const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) |
| let defaultIndex: number | undefined |
| let selected: number | undefined |
| for (const [index, branch] of cases.entries()) { |
| const test = getOptionalNode(branch, "test") |
| if (!test) { |
| defaultIndex = index |
| continue |
| } |
| const candidate = yield* self.evaluateExpression(test) |
| if (containsOpaqueReference(candidate)) { |
| throw new InterpreterRuntimeError( |
| "Switch case values must be data values in CodeMode.", |
| test, |
| "InvalidDataValue", |
| ) |
| } |
| if (candidate === discriminant) { |
| selected = index |
| break |
| } |
| } |
| const start = selected ?? defaultIndex |
| if (start === undefined) return { kind: "none" } satisfies StatementResult |
| for (let index = start; index < cases.length; index += 1) { |
| for (const statementValue of getArray(cases[index]!, "consequent")) { |
| const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) |
| if (result.kind === "break") return { kind: "none" } satisfies StatementResult |
| if (result.kind === "return" || result.kind === "continue") return result |
| if (result.kind === "value") self.lastValue = result.value |
| } |
| } |
| return { kind: "none" } satisfies StatementResult |
| }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) |
| } |
|
|
| private evaluateWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const testNode = getNode(node, "test") |
| const bodyNode = getNode(node, "body") |
|
|
| const self = this |
| return Effect.gen(function* () { |
| while (yield* self.evaluateExpression(testNode)) { |
| const result = yield* self.evaluateStatement(bodyNode) |
|
|
| if (result.kind === "continue") { |
| continue |
| } |
|
|
| if (result.kind === "break") { |
| return { kind: "none" } satisfies StatementResult |
| } |
|
|
| if (result.kind === "return") { |
| return result |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
| } |
|
|
| return { kind: "none" } satisfies StatementResult |
| }) |
| } |
|
|
| private evaluateDoWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const bodyNode = getNode(node, "body") |
| const testNode = getNode(node, "test") |
|
|
| const self = this |
| return Effect.gen(function* () { |
| do { |
| const result = yield* self.evaluateStatement(bodyNode) |
|
|
| if (result.kind === "continue") { |
| continue |
| } |
|
|
| if (result.kind === "break") { |
| return { kind: "none" } satisfies StatementResult |
| } |
|
|
| if (result.kind === "return") { |
| return result |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
| } while (yield* self.evaluateExpression(testNode)) |
|
|
| return { kind: "none" } satisfies StatementResult |
| }) |
| } |
|
|
| private evaluateForStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| this.pushScope() |
| const self = this |
| return Effect.gen(function* () { |
| const initNode = getOptionalNode(node, "init") |
| const testNode = getOptionalNode(node, "test") |
| const updateNode = getOptionalNode(node, "update") |
| const bodyNode = getNode(node, "body") |
|
|
| if (initNode) { |
| if (initNode.type === "VariableDeclaration") { |
| yield* self.evaluateVariableDeclaration(initNode) |
| } else { |
| yield* self.evaluateExpression(initNode) |
| } |
| } |
|
|
| const perIterationBindings = |
| initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var" |
| ? Array.from(self.currentScope().keys()) |
| : [] |
|
|
| while (testNode ? yield* self.evaluateExpression(testNode) : true) { |
| let iterationScope: Map<string, Binding> | undefined |
| if (perIterationBindings.length > 0) { |
| iterationScope = new Map( |
| perIterationBindings.map((name) => { |
| const binding = self.currentScope().get(name)! |
| return [name, { ...binding }] |
| }), |
| ) |
| self.scopes.push(iterationScope) |
| } |
| const result = yield* self.evaluateStatement(bodyNode).pipe( |
| Effect.ensuring( |
| Effect.sync(() => { |
| if (iterationScope) self.popScope() |
| }), |
| ), |
| ) |
|
|
| if (result.kind === "return") { |
| return result |
| } |
|
|
| if (result.kind === "break") { |
| return { kind: "none" } satisfies StatementResult |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
|
|
| if (iterationScope) { |
| const loopScope = self.currentScope() |
| for (const name of perIterationBindings) { |
| loopScope.set(name, { ...iterationScope.get(name)! }) |
| } |
| } |
|
|
| if (updateNode) { |
| yield* self.evaluateExpression(updateNode) |
| } |
|
|
| if (result.kind === "continue") { |
| continue |
| } |
| } |
|
|
| return { kind: "none" } satisfies StatementResult |
| }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) |
| } |
|
|
| private evaluateForOfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| if (getBoolean(node, "await")) { |
| throw new InterpreterRuntimeError("for await...of is not supported.", node) |
| } |
|
|
| const self = this |
| return Effect.gen(function* () { |
| const left = getNode(node, "left") |
| const right = yield* self.evaluateExpression(getNode(node, "right")) |
| const body = getNode(node, "body") |
|
|
| |
| |
| const iterable = Array.isArray(right) ? right : spreadItems(right) |
| if (iterable === undefined) { |
| throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node) |
| } |
|
|
| let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined |
| let assignmentName: string | undefined |
|
|
| if (left.type === "VariableDeclaration") { |
| const declarations = getArray(left, "declarations") |
| if (declarations.length !== 1) { |
| throw new InterpreterRuntimeError("for...of supports one declared binding.", left) |
| } |
|
|
| const declarator = asNode(declarations[0], "declarations[0]") |
| declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } |
| } else if (left.type === "Identifier") { |
| assignmentName = getString(left, "name") |
| } else { |
| throw new InterpreterRuntimeError("Unsupported for...of binding.", left) |
| } |
|
|
| for (const value of iterable) { |
| if (declaration) { |
| self.pushScope() |
| yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left) |
| } else if (assignmentName) { |
| self.setIdentifierValue(assignmentName, value, left) |
| } |
|
|
| const result = yield* self.evaluateStatement(body).pipe( |
| Effect.ensuring( |
| Effect.sync(() => { |
| if (declaration) self.popScope() |
| }), |
| ), |
| ) |
|
|
| if (result.kind === "return") { |
| return result |
| } |
|
|
| if (result.kind === "break") { |
| return { kind: "none" } |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
|
|
| if (result.kind === "continue") { |
| continue |
| } |
| } |
|
|
| return { kind: "none" } |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| private enumerableKeys(value: unknown): Array<string> | undefined { |
| if (value instanceof ToolReference) { |
| return [...this.toolKeys(value.path)] |
| } |
| if (Array.isArray(value)) { |
| return Object.keys(value) |
| } |
| if (value !== null && typeof value === "object" && !isRuntimeReference(value)) { |
| return Object.keys(value) |
| } |
| return undefined |
| } |
|
|
| private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const self = this |
| return Effect.gen(function* () { |
| const left = getNode(node, "left") |
| const right = yield* self.evaluateExpression(getNode(node, "right")) |
| const body = getNode(node, "body") |
|
|
| |
| |
| |
| |
| |
| |
| const keys = self.enumerableKeys(right) |
| if (keys === undefined) { |
| throw new InterpreterRuntimeError( |
| "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.", |
| node, |
| ) |
| } |
|
|
| let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined |
| let assignmentName: string | undefined |
|
|
| if (left.type === "VariableDeclaration") { |
| const declarations = getArray(left, "declarations") |
| if (declarations.length !== 1) { |
| throw new InterpreterRuntimeError("for...in supports one declared binding.", left) |
| } |
|
|
| const declarator = asNode(declarations[0], "declarations[0]") |
| declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } |
| } else if (left.type === "Identifier") { |
| assignmentName = getString(left, "name") |
| } else { |
| throw new InterpreterRuntimeError("Unsupported for...in binding.", left) |
| } |
|
|
| for (const key of keys) { |
| if (declaration) { |
| self.pushScope() |
| yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left) |
| } else if (assignmentName) { |
| self.setIdentifierValue(assignmentName, key, left) |
| } |
|
|
| const result = yield* self.evaluateStatement(body).pipe( |
| Effect.ensuring( |
| Effect.sync(() => { |
| if (declaration) self.popScope() |
| }), |
| ), |
| ) |
|
|
| if (result.kind === "return") { |
| return result |
| } |
|
|
| if (result.kind === "break") { |
| return { kind: "none" } |
| } |
|
|
| if (result.kind === "value") { |
| self.lastValue = result.value |
| } |
|
|
| if (result.kind === "continue") { |
| continue |
| } |
| } |
|
|
| return { kind: "none" } |
| }) |
| } |
|
|
| private evaluateBreakStatement(node: AstNode): StatementResult { |
| const labelNode = getOptionalNode(node, "label") |
|
|
| if (labelNode) { |
| throw new InterpreterRuntimeError("Labeled break is not supported in v1.", node) |
| } |
|
|
| return { kind: "break" } |
| } |
|
|
| private evaluateContinueStatement(node: AstNode): StatementResult { |
| const labelNode = getOptionalNode(node, "label") |
|
|
| if (labelNode) { |
| throw new InterpreterRuntimeError("Labeled continue is not supported in v1.", node) |
| } |
|
|
| return { kind: "continue" } |
| } |
|
|
| private evaluateThrowStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const argument = getNode(node, "argument") |
| return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value))) |
| } |
|
|
| private evaluateTryStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { |
| const body = getNode(node, "block") |
| const handler = getOptionalNode(node, "handler") |
| const finalizer = getOptionalNode(node, "finalizer") |
| const self = this |
|
|
| const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), { |
| onFailure: (cause) => { |
| if (cause.reasons.some(Cause.isInterruptReason) || !handler) { |
| return Effect.failCause(cause) |
| } |
|
|
| |
| |
| const caught = caughtErrorValue(Cause.squash(cause)) |
| const parameter = getOptionalNode(handler, "param") |
| self.pushScope() |
| return Effect.gen(function* () { |
| if (parameter) yield* self.declarePattern(parameter, caught, true, handler) |
| return yield* self.evaluateStatement(getNode(handler, "body")) |
| }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) |
| }, |
| onSuccess: Effect.succeed, |
| }) |
|
|
| if (!finalizer) return attempted |
|
|
| const isAbrupt = (result: StatementResult): boolean => |
| result.kind === "return" || result.kind === "break" || result.kind === "continue" |
|
|
| return Effect.matchCauseEffect(attempted, { |
| onFailure: (cause) => |
| cause.reasons.some(Cause.isInterruptReason) |
| ? Effect.failCause(cause) |
| : Effect.flatMap(this.evaluateStatement(finalizer), (final) => |
| isAbrupt(final) ? Effect.succeed(final) : Effect.failCause(cause), |
| ), |
| onSuccess: (result) => |
| Effect.flatMap(this.evaluateStatement(finalizer), (final) => |
| isAbrupt(final) ? Effect.succeed(final) : Effect.succeed(result), |
| ), |
| }) |
| } |
|
|
| private evaluateVariableDeclaration(node: AstNode): Effect.Effect<void, unknown, R> { |
| const kind = getString(node, "kind") |
| const declarations = getArray(node, "declarations") |
| const self = this |
| return Effect.gen(function* () { |
| for (const declarationValue of declarations) { |
| const declaration = asNode(declarationValue, "declarations") |
|
|
| if (declaration.type !== "VariableDeclarator") { |
| throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration) |
| } |
|
|
| const init = getOptionalNode(declaration, "init") |
| const value = init ? yield* self.evaluateExpression(init) : undefined |
| yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration) |
| } |
| }) |
| } |
|
|
| private declarePattern( |
| pattern: AstNode, |
| value: unknown, |
| mutable: boolean, |
| node: AstNode, |
| ): Effect.Effect<void, unknown, R> { |
| const self = this |
| return Effect.gen(function* () { |
| if (pattern.type === "Identifier") { |
| self.declare(getString(pattern, "name"), value, mutable, node) |
| return |
| } |
|
|
| |
| if (pattern.type === "AssignmentPattern") { |
| const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value |
| yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node) |
| return |
| } |
|
|
| if (pattern.type === "ObjectPattern") { |
| if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { |
| throw new InterpreterRuntimeError( |
| "Object destructuring requires a data object value.", |
| pattern, |
| "InvalidDataValue", |
| ) |
| } |
|
|
| const consumed = new Set<string>() |
| for (const propertyValue of getArray(pattern, "properties")) { |
| const property = asNode(propertyValue, "properties") |
|
|
| |
| if (property.type === "RestElement") { |
| const rest: SafeObject = Object.create(null) as SafeObject |
| for (const [key, item] of Object.entries(value as SafeObject)) { |
| if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item |
| } |
| yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property) |
| continue |
| } |
|
|
| if ( |
| property.type !== "Property" || |
| getBoolean(property, "computed") || |
| getString(property, "kind") !== "init" |
| ) { |
| throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property) |
| } |
|
|
| const keyNode = getNode(property, "key") |
| const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value) |
| if (isBlockedMember(key)) { |
| throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode) |
| } |
| consumed.add(key) |
| yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property) |
| } |
| return |
| } |
|
|
| if (pattern.type === "ArrayPattern") { |
| if (!Array.isArray(value)) { |
| throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) |
| } |
|
|
| for (const [index, item] of getArray(pattern, "elements").entries()) { |
| if (item === null) continue |
| const element = asNode(item, `elements[${index}]`) |
| |
| if (element.type === "RestElement") { |
| yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element) |
| break |
| } |
| yield* self.declarePattern(element, value[index], mutable, pattern) |
| } |
| return |
| } |
|
|
| throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern) |
| }) |
| } |
|
|
| private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| switch (node.type) { |
| case "Literal": { |
| |
| |
| const regex = node.regex |
| if (isRecord(regex) && typeof regex.pattern === "string") { |
| return Effect.sync(() => |
| this.constructRegExp([regex.pattern, typeof regex.flags === "string" ? regex.flags : ""], node), |
| ) |
| } |
| return Effect.sync(() => boundedData(node.value, "Literal")) |
| } |
| case "Identifier": |
| return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node)) |
| case "BinaryExpression": |
| return this.evaluateBinaryExpression(node) |
| case "LogicalExpression": |
| return this.evaluateLogicalExpression(node) |
| case "UnaryExpression": |
| return this.evaluateUnaryExpression(node) |
| case "AssignmentExpression": |
| return this.evaluateAssignmentExpression(node) |
| case "CallExpression": |
| return this.evaluateCallExpression(node) |
| case "ArrowFunctionExpression": |
| case "FunctionExpression": |
| return Effect.sync(() => this.createFunction(node)) |
| case "MemberExpression": |
| return this.readMember(node) |
| case "ChainExpression": |
| return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => |
| value === OptionalShortCircuit ? undefined : value, |
| ) |
| case "ObjectExpression": |
| return this.evaluateObjectExpression(node) |
| case "ArrayExpression": |
| return this.evaluateArrayExpression(node) |
| case "TemplateLiteral": |
| return this.evaluateTemplateLiteral(node) |
| case "ConditionalExpression": |
| return this.evaluateConditionalExpression(node) |
| case "UpdateExpression": |
| return this.evaluateUpdateExpression(node) |
| case "AwaitExpression": { |
| |
| |
| const self = this |
| return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => |
| value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value), |
| ) |
| } |
| case "NewExpression": |
| return this.evaluateNewExpression(node) |
| default: |
| throw unsupportedSyntax(node.type, node) |
| } |
| } |
|
|
| private evaluateNewExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const callee = getNode(node, "callee") |
| if (callee.type !== "Identifier") { |
| throw unsupportedSyntax("NewExpression", node) |
| } |
| const name = getString(callee, "name") |
| const argNodes = getArray(node, "arguments") |
| const self = this |
| if (name === "Promise") { |
| throw new InterpreterRuntimeError( |
| "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.", |
| node, |
| "UnsupportedSyntax", |
| [supportedSyntaxMessage], |
| ) |
| } |
| if (errorConstructors.has(name)) { |
| return Effect.gen(function* () { |
| const arg = |
| argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined |
| return createErrorValue(name, arg === undefined ? "" : coerceToString(arg)) |
| }) |
| } |
| if (valueConstructors.has(name)) { |
| return Effect.gen(function* () { |
| const args = yield* self.evaluateCallArguments(argNodes) |
| switch (name) { |
| case "Date": |
| return self.constructDate(args) |
| case "RegExp": |
| return self.constructRegExp(args, node) |
| case "Map": |
| return self.constructMap(args[0], node) |
| case "Set": |
| return self.constructSet(args[0], node) |
| case "URL": |
| return self.constructURL(args, node) |
| default: |
| return self.constructURLSearchParams(args[0], node) |
| } |
| }) |
| } |
| throw unsupportedSyntax("NewExpression", node) |
| } |
|
|
| private constructDate(args: Array<unknown>): SandboxDate { |
| if (args.length === 0) return new SandboxDate(Date.now()) |
| if (args.length === 1) { |
| const arg = args[0] |
| if (arg instanceof SandboxDate) return new SandboxDate(arg.time) |
| if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime()) |
| if (typeof arg === "string") return new SandboxDate(Date.parse(arg)) |
| return new SandboxDate(Number.NaN) |
| } |
| |
| const parts = args.map((arg) => coerceToNumber(arg)) |
| return new SandboxDate(new Date(...(parts as [number, number])).getTime()) |
| } |
|
|
| private constructRegExp(args: Array<unknown>, node: AstNode): SandboxRegExp { |
| const first = args[0] |
| const pattern = |
| first instanceof SandboxRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) |
| const flagsArg = args[1] |
| if (flagsArg !== undefined && typeof flagsArg !== "string") { |
| throw new InterpreterRuntimeError( |
| `RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`, |
| node, |
| ) |
| } |
| const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "") |
| try { |
| return new SandboxRegExp(pattern, flags) |
| } catch (error) { |
| |
| |
| |
| const reason = regexFailureReason(error) |
| throw new InterpreterRuntimeError( |
| /flag/i.test(reason) |
| ? `new RegExp(...) received invalid flags ${JSON.stringify(flags)} (${reason}). Valid flags are d, g, i, m, s, u, v, and y.` |
| : `new RegExp(...) received ${JSON.stringify(pattern)}, which is not a valid regular expression pattern (${reason}). ${escapeRegexHint}`, |
| node, |
| ).as("SyntaxError") |
| } |
| } |
|
|
| private constructMap(init: unknown, node: AstNode): SandboxMap { |
| const target = new SandboxMap() |
| if (init === undefined || init === null) return target |
| const entries = Array.isArray(init) |
| ? init |
| : init instanceof SandboxMap |
| ? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item]) |
| : undefined |
| if (entries === undefined) { |
| throw new InterpreterRuntimeError( |
| "new Map(...) expects an array of [key, value] pairs, a Map, or no argument.", |
| node, |
| ) |
| } |
| for (const pair of entries) { |
| if (!Array.isArray(pair)) { |
| throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node) |
| } |
| target.map.set(pair[0], pair[1]) |
| } |
| return target |
| } |
|
|
| private constructSet(init: unknown, node: AstNode): SandboxSet { |
| const target = new SandboxSet() |
| if (init === undefined || init === null) return target |
| const items = Array.isArray(init) |
| ? init |
| : init instanceof SandboxSet |
| ? Array.from(init.set.values()) |
| : typeof init === "string" |
| ? Array.from(init) |
| : undefined |
| if (items === undefined) { |
| throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node) |
| } |
| for (const item of items) target.set.add(item) |
| return target |
| } |
|
|
| private constructURL(args: Array<unknown>, node: AstNode): SandboxURL { |
| if (args.length === 0) { |
| throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( |
| "TypeError", |
| ) |
| } |
| const input = urlArgument(args[0], "new URL input") |
| const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") |
| try { |
| return new SandboxURL(new URL(input, base)) |
| } catch { |
| throw new InterpreterRuntimeError( |
| `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, |
| node, |
| ).as("TypeError") |
| } |
| } |
|
|
| private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams { |
| if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams()) |
| if (init instanceof SandboxURLSearchParams) { |
| return new SandboxURLSearchParams(new URLSearchParams(init.params)) |
| } |
| if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init)) |
| if (init === null || typeof init === "number" || typeof init === "boolean") { |
| return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init))) |
| } |
| if (init instanceof SandboxMap) { |
| return this.constructURLSearchParams( |
| Array.from(init.map.entries(), ([key, value]) => [key, value]), |
| node, |
| ) |
| } |
| if (Array.isArray(init)) { |
| const entries = init.map((pair) => { |
| if (!Array.isArray(pair) || pair.length !== 2) { |
| throw new InterpreterRuntimeError( |
| "new URLSearchParams(...) expects an array of [name, value] pairs.", |
| node, |
| ).as("TypeError") |
| } |
| return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [ |
| string, |
| string, |
| ] |
| }) |
| return new SandboxURLSearchParams(new URLSearchParams(entries)) |
| } |
| if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams()) |
| const data = boundedData(init, "new URLSearchParams input") |
| if (data === null || typeof data !== "object") { |
| throw new InterpreterRuntimeError( |
| "new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.", |
| node, |
| ).as("TypeError") |
| } |
| return new SandboxURLSearchParams( |
| new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), |
| ) |
| } |
|
|
| private evaluateBinaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const operator = getString(node, "operator") |
| const self = this |
| return Effect.gen(function* () { |
| const lhs = yield* self.evaluateExpression(getNode(node, "left")) |
| const rhs = yield* self.evaluateExpression(getNode(node, "right")) |
| |
| |
| |
| if (operator === "instanceof") return instanceofValue(lhs, rhs, node) |
| return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result") |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown { |
| if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) { |
| throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue") |
| } |
| |
| |
| |
| |
| |
| |
| const coerceOperand = (operand: unknown): unknown => { |
| if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time |
| return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand |
| } |
| const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" |
| const l = coerceOperand(lhs) |
| const r = coerceOperand(rhs) |
| switch (operator) { |
| case "+": |
| return (l as string) + (r as string) |
| case "-": |
| return (l as number) - (r as number) |
| case "*": |
| return (l as number) * (r as number) |
| case "/": |
| return (l as number) / (r as number) |
| case "%": |
| return (l as number) % (r as number) |
| case "**": |
| return (l as number) ** (r as number) |
| |
| case "==": |
| return bothObjects ? lhs === rhs : l == r |
| case "===": |
| return lhs === rhs |
| case "!=": |
| return bothObjects ? lhs !== rhs : l != r |
| case "!==": |
| return lhs !== rhs |
| case "<": |
| return (l as string) < (r as string) |
| case "<=": |
| return (l as string) <= (r as string) |
| case ">": |
| return (l as string) > (r as string) |
| case ">=": |
| return (l as string) >= (r as string) |
| case "&": |
| return (l as number) & (r as number) |
| case "|": |
| return (l as number) | (r as number) |
| case "^": |
| return (l as number) ^ (r as number) |
| case "<<": |
| return (l as number) << (r as number) |
| case ">>": |
| return (l as number) >> (r as number) |
| case ">>>": |
| return (l as number) >>> (r as number) |
| case "in": |
| if (rhs === null || typeof rhs !== "object") { |
| throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node) |
| } |
| |
| return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey) |
| default: |
| throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node) |
| } |
| } |
|
|
| private evaluateLogicalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const operator = getString(node, "operator") |
| return Effect.flatMap(this.evaluateExpression(getNode(node, "left")), (left) => { |
| if (operator === "&&") return left ? this.evaluateExpression(getNode(node, "right")) : Effect.succeed(left) |
| if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right")) |
| if (operator === "??") |
| return left !== null && left !== undefined |
| ? Effect.succeed(left) |
| : this.evaluateExpression(getNode(node, "right")) |
| throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node) |
| }) |
| } |
|
|
| private evaluateUnaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const operator = getString(node, "operator") |
| const argument = getNode(node, "argument") |
| |
| |
| |
| if (operator === "typeof" && argument.type === "Identifier" && !this.resolveBinding(getString(argument, "name"))) { |
| return Effect.succeed("undefined") |
| } |
| return Effect.map(this.evaluateExpression(argument), (value) => { |
| |
| |
| if (operator === "typeof") return typeofValue(value) |
| if (operator === "!") return !value |
| if (containsOpaqueReference(value)) { |
| throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") |
| } |
| |
| |
| |
| const operand = |
| value instanceof SandboxDate |
| ? value.time |
| : value !== null && typeof value === "object" |
| ? coerceToString(value) |
| : value |
| let result: unknown |
| switch (operator) { |
| case "+": |
| result = +(operand as number) |
| break |
| case "-": |
| result = -(operand as number) |
| break |
| case "~": |
| result = ~(operand as number) |
| break |
| default: |
| throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node) |
| } |
| return boundedData(result, "Unary expression result") |
| }) |
| } |
|
|
| private evaluateAssignmentExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const left = getNode(node, "left") |
| const operator = getString(node, "operator") |
| const self = this |
| return Effect.gen(function* () { |
| if (operator === "??=" || operator === "||=" || operator === "&&=") { |
| return yield* self.evaluateLogicalAssignment(node, left, operator) |
| } |
| const rightValue = yield* self.evaluateExpression(getNode(node, "right")) |
| if (left.type === "Identifier") { |
| const name = getString(left, "name") |
| if (operator === "=") return self.setIdentifierValue(name, rightValue, left) |
| const next = boundedData( |
| self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node), |
| "Assignment result", |
| ) |
| return self.setIdentifierValue(name, next, left) |
| } |
| if (left.type === "MemberExpression") { |
| if (operator === "=") return yield* self.writeMember(left, rightValue) |
| return yield* self.modifyMember(left, (current) => { |
| const next = boundedData( |
| self.applyCompoundAssignment(operator, current, rightValue, node), |
| "Assignment result", |
| ) |
| return Effect.succeed({ write: true, next, result: next }) |
| }) |
| } |
| throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) |
| }) |
| } |
|
|
| private evaluateLogicalAssignment( |
| node: AstNode, |
| left: AstNode, |
| operator: string, |
| ): Effect.Effect<unknown, unknown, R> { |
| const self = this |
| const shouldAssign = (current: unknown): boolean => |
| operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current) |
| if (left.type === "Identifier") { |
| const name = getString(left, "name") |
| return Effect.gen(function* () { |
| const current = self.getIdentifierValue(name, left) |
| if (!shouldAssign(current)) return current |
| const rightValue = yield* self.evaluateExpression(getNode(node, "right")) |
| return self.setIdentifierValue(name, rightValue, left) |
| }) |
| } |
| if (left.type === "MemberExpression") { |
| |
| return self.modifyMember(left, (current) => |
| shouldAssign(current) |
| ? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({ |
| write: true, |
| next: rightValue, |
| result: rightValue, |
| })) |
| : Effect.succeed({ write: false, next: current, result: current }), |
| ) |
| } |
| throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) |
| } |
|
|
| private evaluateUpdateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const operator = getString(node, "operator") |
| const argument = getNode(node, "argument") |
| const prefix = getBoolean(node, "prefix") |
|
|
| const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined |
|
|
| if (increment === undefined) { |
| throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node) |
| } |
|
|
| if (argument.type === "Identifier") { |
| return Effect.sync(() => { |
| const name = getString(argument, "name") |
| const current = Number(this.getIdentifierValue(name, argument)) |
| const next = current + increment |
| this.setIdentifierValue(name, next, argument) |
| return prefix ? next : current |
| }) |
| } |
|
|
| if (argument.type === "MemberExpression") { |
| return this.modifyMember(argument, (current) => { |
| const value = Number(current) |
| const next = value + increment |
| return Effect.succeed({ write: true, next, result: prefix ? next : value }) |
| }) |
| } |
|
|
| throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument) |
| } |
|
|
| private evaluateCallExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| const callee = getNode(node, "callee") |
| const argNodes = getArray(node, "arguments") |
|
|
| const self = this |
| return Effect.gen(function* () { |
| const callable = yield* self.evaluateExpression(callee) |
| if (callable === OptionalShortCircuit) return OptionalShortCircuit |
| if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit |
|
|
| const args = yield* self.evaluateCallArguments(argNodes) |
|
|
| if (callable instanceof ToolReference) { |
| if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee) |
| |
| return yield* self.createToolCallPromise(callable.path, args) |
| } |
| if (callable instanceof PromiseMethodReference) { |
| return yield* self.invokePromiseMethod(callable, args, node) |
| } |
| if (callable instanceof CodeModeFunction) { |
| return yield* self.invokeFunction(callable, args) |
| } |
| if (callable instanceof IntrinsicReference) { |
| return yield* self.invokeIntrinsic(callable, args, node) |
| } |
| if (callable instanceof GlobalMethodReference) { |
| if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node) |
| if (callable.namespace === "Object" && args[0] instanceof ToolReference) { |
| return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node) |
| } |
| return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) |
| } |
| if (callable instanceof CoercionFunction) { |
| return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`) |
| } |
| if (callable instanceof UriFunction) { |
| return invokeUriFunction(callable, args, node) |
| } |
| |
| if (callable instanceof ErrorConstructorReference) { |
| return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0])) |
| } |
| throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee) |
| }) |
| } |
|
|
| |
| |
| |
| |
| private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown { |
| if (name === "keys") { |
| return boundedData(this.enumerableKeys(ref)!, "Object.keys result") |
| } |
| throw new InterpreterRuntimeError( |
| `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`, |
| node, |
| "InvalidDataValue", |
| ) |
| } |
|
|
| private invokeConsole(name: string, args: Array<unknown>, node: AstNode): undefined { |
| if (!consoleMethods.has(name)) |
| throw new InterpreterRuntimeError(`console.${name} is not available in CodeMode.`, node) |
| this.logs.push(publicErrorMessage(this.formatConsoleMessage(name, args, node))) |
| return undefined |
| } |
|
|
| private formatConsoleMessage(name: string, args: Array<unknown>, node: AstNode): string { |
| if (name === "dir") return args.length === 0 ? "undefined" : this.formatConsoleArgument(args[0]) |
| if (name === "table") return this.formatConsoleTable(args[0], args[1], node) |
| const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" |
| return `${prefix}${args.map((arg) => this.formatConsoleArgument(arg)).join(" ")}` |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private formatConsoleArgument(value: unknown): string { |
| if (value === undefined) return "undefined" |
| |
| if (typeof value === "string") return value |
| return this.formatConsoleValue(value, new Set(), 0) |
| } |
|
|
| private formatConsoleValue(value: unknown, seen: Set<object>, depth: number): string { |
| |
| if (value === null || value === undefined) return "null" |
| if (typeof value === "string") return JSON.stringify(value) |
| |
| if (typeof value === "number" || typeof value === "boolean") return String(value) |
| if (typeof value !== "object") return String(value) |
| if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]" |
| if (value instanceof SandboxDate) return coerceToString(value) |
| if (value instanceof SandboxRegExp) return coerceToString(value) |
| if (value instanceof SandboxURL) return coerceToString(value) |
| if (value instanceof SandboxURLSearchParams) return coerceToString(value) |
| if (depth > MAX_CONSOLE_DEPTH) return "..." |
| if (seen.has(value)) return "[Circular]" |
| if (value instanceof SandboxMap) { |
| seen.add(value) |
| try { |
| const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item]) |
| return `Map(${value.map.size}) ${this.formatConsoleValue(entries, seen, depth + 1)}` |
| } finally { |
| seen.delete(value) |
| } |
| } |
| if (value instanceof SandboxSet) { |
| seen.add(value) |
| try { |
| return `Set(${value.set.size}) ${this.formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` |
| } finally { |
| seen.delete(value) |
| } |
| } |
| if (isRuntimeReference(value)) return "[CodeMode reference]" |
| seen.add(value) |
| try { |
| if (Array.isArray(value)) { |
| return `[${value.map((item) => this.formatConsoleValue(item, seen, depth + 1)).join(",")}]` |
| } |
| return `{${Object.entries(value) |
| .map(([key, item]) => `${JSON.stringify(key)}:${this.formatConsoleValue(item, seen, depth + 1)}`) |
| .join(",")}}` |
| } finally { |
| seen.delete(value) |
| } |
| } |
|
|
| private formatConsoleTable(value: unknown, columnsArgument: unknown, node: AstNode): string { |
| if (value === undefined) return "undefined" |
| |
| |
| if (containsOpaqueReference(value)) return "[CodeMode reference]" |
| const data = boundedData(value, "console.table argument") |
| const columns = this.consoleTableColumns(columnsArgument, node) |
| const rows = this.consoleTableRows(data, columns) |
| const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values)))) |
| const header = ["(index)", ...keys].join("\t") |
| return [ |
| header, |
| ...rows.map((row) => [row.index, ...keys.map((key) => this.formatConsoleTableCell(row.values[key]))].join("\t")), |
| ].join("\n") |
| } |
|
|
| private consoleTableColumns(value: unknown, node: AstNode): ReadonlyArray<string> | undefined { |
| if (value === undefined) return undefined |
| if (containsRuntimeReference(value)) return undefined |
| const columns = copyOut(copyIn(value, "console.table columns"), true) |
| return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined |
| } |
|
|
| private consoleTableRows( |
| data: unknown, |
| columns: ReadonlyArray<string> | undefined, |
| ): Array<{ readonly index: string; readonly values: Record<string, unknown> }> { |
| if (Array.isArray(data)) { |
| return data.map((item, index) => ({ index: String(index), values: this.consoleTableValues(item, columns) })) |
| } |
| if (data !== null && typeof data === "object" && !isSandboxValue(data)) { |
| return Object.entries(data).map(([index, item]) => ({ index, values: this.consoleTableValues(item, columns) })) |
| } |
| return [{ index: "0", values: { Value: data } }] |
| } |
|
|
| private consoleTableValues(value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> { |
| if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) { |
| const source = value as Record<string, unknown> |
| if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) |
| return Object.fromEntries(Object.entries(source)) |
| } |
| return { Value: value } |
| } |
|
|
| private formatConsoleTableCell(value: unknown): string { |
| if (value === undefined) return "" |
| if (typeof value === "string") return value |
| return this.formatConsoleValue(value, new Set(), 0) |
| } |
|
|
| private evaluateCallArguments(argNodes: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> { |
| const self = this |
| return Effect.gen(function* () { |
| const args: Array<unknown> = [] |
| for (const [index, arg] of argNodes.entries()) { |
| const argNode = asNode(arg, `arguments[${index}]`) |
| if (argNode.type === "SpreadElement") { |
| const spread = yield* self.evaluateExpression(getNode(argNode, "argument")) |
| const items = spreadItems(spread) |
| if (items === undefined) |
| throw new InterpreterRuntimeError( |
| "Spread arguments require an array, string, Map, or Set in CodeMode.", |
| argNode, |
| ) |
| args.push(...items) |
| } else { |
| args.push(yield* self.evaluateExpression(argNode)) |
| } |
| } |
| return args |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| private invokePromiseMethod( |
| ref: PromiseMethodReference, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| const self = this |
| if (ref.name === "resolve") { |
| |
| |
| const value = args[0] |
| return Effect.succeed( |
| value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)), |
| ) |
| } |
| if (ref.name === "reject") { |
| return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0])))) |
| } |
|
|
| const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0]) |
| if (items === undefined) { |
| throw new InterpreterRuntimeError( |
| `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, |
| node, |
| ) |
| } |
|
|
| switch (ref.name) { |
| case "all": { |
| |
| |
| |
| const settles = items.map((item) => |
| item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item), |
| ) |
| return Effect.gen(function* () { |
| const values: Array<unknown> = [] |
| for (const settle of settles) values.push(yield* settle) |
| return values |
| }) |
| } |
| case "allSettled": { |
| const observations = items.map((item) => |
| item instanceof SandboxPromise |
| ? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit })) |
| : Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }), |
| ) |
| return Effect.gen(function* () { |
| const outcomes: Array<unknown> = [] |
| for (const observation of observations) { |
| const { exit, promise } = yield* observation |
| if (Exit.isSuccess(exit)) { |
| outcomes.push( |
| Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), |
| ) |
| continue |
| } |
| const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause) |
| if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) { |
| |
| return yield* Effect.failCause(exit.cause) |
| } |
| const thrown = raceInterrupted |
| ? new InterpreterRuntimeError( |
| "This tool call was interrupted because another value settled a Promise.race first.", |
| node, |
| ) |
| : Cause.squash(exit.cause) |
| outcomes.push( |
| Object.assign(Object.create(null) as SafeObject, { |
| status: "rejected", |
| reason: caughtErrorValue(thrown), |
| }), |
| ) |
| } |
| return outcomes |
| }) |
| } |
| case "race": { |
| if (items.length === 0) { |
| throw new InterpreterRuntimeError( |
| "Promise.race([]) would never settle; provide at least one promise or value.", |
| node, |
| ) |
| } |
| const observations = items.map((item, index) => |
| item instanceof SandboxPromise |
| ? Effect.map(this.observePromise(item), (exit) => ({ index, exit })) |
| : Effect.succeed({ index, exit: Exit.succeed(item as unknown) }), |
| ) |
| return Effect.gen(function* () { |
| |
| |
| const winner = yield* Effect.raceAll(observations) |
| for (const [index, item] of items.entries()) { |
| if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue |
| item.interrupted = true |
| yield* Fiber.interrupt(item.fiber) |
| } |
| const winningItem = items[winner.index] |
| return yield* self.unwrapPromiseExit( |
| winningItem instanceof SandboxPromise ? winningItem : undefined, |
| winner.exit, |
| node, |
| ) |
| }) |
| } |
| } |
| } |
|
|
| private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> { |
| const self = this |
| return Effect.suspend(() => { |
| const savedScopes = self.scopes |
| self.scopes = [...fn.capturedScopes, new Map<string, Binding>()] |
| const run = Effect.gen(function* () { |
| |
| |
| |
| const paramScope = self.currentScope() |
| for (const parameter of fn.parameters) { |
| for (const name of collectPatternNames(parameter)) { |
| paramScope.set(name, { mutable: true, value: undefined, initialized: false }) |
| } |
| } |
| for (const [index, parameter] of fn.parameters.entries()) { |
| if (parameter.type === "RestElement") { |
| yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter) |
| break |
| } |
| yield* self.declarePattern(parameter, args[index], true, parameter) |
| } |
|
|
| if (fn.body.type === "BlockStatement") { |
| const result = yield* self.evaluateStatement(fn.body) |
| return result.kind === "return" || result.kind === "value" ? result.value : undefined |
| } |
|
|
| return yield* self.evaluateExpression(fn.body) |
| }) |
| return run.pipe( |
| Effect.ensuring( |
| Effect.sync(() => { |
| self.scopes = savedScopes |
| }), |
| ), |
| ) |
| }) |
| } |
|
|
| private invokeIntrinsic( |
| ref: IntrinsicReference, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| if (typeof ref.receiver === "string") { |
| if ( |
| (ref.name === "replace" || ref.name === "replaceAll") && |
| (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction) |
| ) { |
| return this.invokeStringReplacer(ref.receiver, ref.name, args, node) |
| } |
| return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) |
| } |
| if (typeof ref.receiver === "number") { |
| return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) |
| } |
| if (Array.isArray(ref.receiver)) { |
| return this.invokeArrayMethod(ref.receiver, ref.name, args, node) |
| } |
| if (ref.receiver instanceof SandboxDate) { |
| return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) |
| } |
| if (ref.receiver instanceof SandboxRegExp) { |
| return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) |
| } |
| if (ref.receiver instanceof SandboxMap) { |
| return this.invokeMapMethod(ref.receiver, ref.name, args, node) |
| } |
| if (ref.receiver instanceof SandboxSet) { |
| return this.invokeSetMethod(ref.receiver, ref.name, args, node) |
| } |
| if (ref.receiver instanceof SandboxURL) { |
| return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) |
| } |
| if (ref.receiver instanceof SandboxURLSearchParams) { |
| return this.invokeURLSearchParamsMethod(ref.receiver, ref.name, args, node) |
| } |
| throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) |
| } |
|
|
| private invokeStringReplacer( |
| value: string, |
| name: "replace" | "replaceAll", |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| const apply = this.applyCollectionCallback(args[1], `String.${name}`, node) |
| const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = [] |
| const collect = (...callbackArgs: Array<unknown>): string => { |
| const match = callbackArgs[0] |
| const groups = callbackArgs[callbackArgs.length - 1] |
| const hasGroups = groups !== null && typeof groups === "object" |
| const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] |
| if (typeof match !== "string" || typeof offset !== "number") { |
| throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) |
| } |
| if (hasGroups) { |
| const safeGroups: SafeObject = Object.create(null) as SafeObject |
| for (const [key, group] of Object.entries(groups)) { |
| if (!isBlockedMember(key)) safeGroups[key] = group |
| } |
| callbackArgs[callbackArgs.length - 1] = safeGroups |
| } |
| matches.push({ match, offset, args: callbackArgs }) |
| return match |
| } |
|
|
| const pattern = args[0] |
| if (pattern instanceof SandboxRegExp) { |
| if (name === "replaceAll" && !pattern.regex.global) { |
| throw new InterpreterRuntimeError( |
| `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.`, |
| node, |
| ) |
| } |
| if (name === "replace") value.replace(pattern.regex, collect) |
| else value.replaceAll(pattern.regex, collect) |
| } else { |
| if (typeof pattern !== "string") { |
| throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node) |
| } |
| if (name === "replace") value.replace(pattern, collect) |
| else value.replaceAll(pattern, collect) |
| } |
|
|
| return Effect.gen(function* () { |
| const output: Array<string> = [] |
| let end = 0 |
| for (const match of matches) { |
| output.push( |
| value.slice(end, match.offset), |
| coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)), |
| ) |
| end = match.offset + match.match.length |
| } |
| output.push(value.slice(end)) |
| return boundedData(output.join(""), `String.${name} result`) |
| }) |
| } |
|
|
| |
| |
| private applyCollectionCallback( |
| callback: unknown, |
| name: string, |
| node: AstNode, |
| ): (args: Array<unknown>) => Effect.Effect<unknown, unknown, R> { |
| if ( |
| !(callback instanceof CodeModeFunction) && |
| !(callback instanceof CoercionFunction) && |
| !(callback instanceof UriFunction) |
| ) { |
| throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) |
| } |
| return (callbackArgs) => |
| callback instanceof CoercionFunction |
| ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) |
| : callback instanceof UriFunction |
| ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) |
| : this.invokeFunction(callback, callbackArgs) |
| } |
|
|
| private invokeMapMethod( |
| target: SandboxMap, |
| name: string, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| switch (name) { |
| case "get": |
| return Effect.succeed(target.map.get(args[0])) |
| case "has": |
| return Effect.succeed(target.map.has(args[0])) |
| case "set": |
| return Effect.sync(() => { |
| target.map.set(args[0], args[1]) |
| return target |
| }) |
| case "delete": |
| return Effect.sync(() => target.map.delete(args[0])) |
| case "clear": |
| return Effect.sync(() => { |
| target.map.clear() |
| return undefined |
| }) |
| case "keys": |
| return Effect.sync(() => Array.from(target.map.keys())) |
| case "values": |
| return Effect.sync(() => Array.from(target.map.values())) |
| case "entries": |
| return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item])) |
| case "forEach": { |
| const apply = this.applyCollectionCallback(args[0], "Map.forEach", node) |
| return Effect.gen(function* () { |
| |
| for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) |
| return undefined |
| }) |
| } |
| default: |
| throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node) |
| } |
| } |
|
|
| private invokeSetMethod( |
| target: SandboxSet, |
| name: string, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| switch (name) { |
| case "has": |
| return Effect.succeed(target.set.has(args[0])) |
| case "add": |
| return Effect.sync(() => { |
| target.set.add(args[0]) |
| return target |
| }) |
| case "delete": |
| return Effect.sync(() => target.set.delete(args[0])) |
| case "clear": |
| return Effect.sync(() => { |
| target.set.clear() |
| return undefined |
| }) |
| case "keys": |
| case "values": |
| return Effect.sync(() => Array.from(target.set.values())) |
| case "entries": |
| return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item])) |
| case "forEach": { |
| const apply = this.applyCollectionCallback(args[0], "Set.forEach", node) |
| return Effect.gen(function* () { |
| for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) |
| return undefined |
| }) |
| } |
| default: |
| throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) |
| } |
| } |
|
|
| private invokeURLSearchParamsMethod( |
| target: SandboxURLSearchParams, |
| name: string, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) |
| const requireArgs = (count: number): void => { |
| if (args.length < count) { |
| throw new InterpreterRuntimeError( |
| `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, |
| node, |
| ).as("TypeError") |
| } |
| } |
| switch (name) { |
| case "append": { |
| requireArgs(2) |
| return Effect.sync(() => { |
| target.params.append(arg(0), arg(1)) |
| return undefined |
| }) |
| } |
| case "delete": { |
| requireArgs(1) |
| return Effect.sync(() => { |
| if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) |
| else target.params.delete(arg(0)) |
| return undefined |
| }) |
| } |
| case "get": |
| requireArgs(1) |
| return Effect.sync(() => target.params.get(arg(0))) |
| case "getAll": |
| requireArgs(1) |
| return Effect.sync(() => target.params.getAll(arg(0))) |
| case "has": |
| requireArgs(1) |
| return Effect.sync(() => |
| args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)), |
| ) |
| case "set": { |
| requireArgs(2) |
| return Effect.sync(() => { |
| target.params.set(arg(0), arg(1)) |
| return undefined |
| }) |
| } |
| case "sort": |
| return Effect.sync(() => { |
| target.params.sort() |
| return undefined |
| }) |
| case "keys": |
| return Effect.sync(() => Array.from(target.params.keys())) |
| case "values": |
| return Effect.sync(() => Array.from(target.params.values())) |
| case "entries": |
| return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value])) |
| case "toString": |
| return Effect.sync(() => target.params.toString()) |
| case "forEach": { |
| requireArgs(1) |
| const apply = this.applyCollectionCallback(args[0], "URLSearchParams.forEach", node) |
| return Effect.gen(function* () { |
| for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) |
| return undefined |
| }) |
| } |
| default: |
| throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node) |
| } |
| } |
|
|
| private invokeArrayMethod( |
| target: Array<unknown>, |
| name: string, |
| args: Array<unknown>, |
| node: AstNode, |
| ): Effect.Effect<unknown, unknown, R> { |
| const optNumber = (value: unknown, label: string): number | undefined => { |
| if (value === undefined) return undefined |
| if (typeof value !== "number") |
| throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) |
| return value |
| } |
| switch (name) { |
| case "join": { |
| if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { |
| throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) |
| } |
| const input = boundedData(target, "Array.join input") as Array<unknown> |
| return Effect.succeed( |
| input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), |
| ) |
| } |
| case "includes": |
| if (args.length === 0 || args.length > 2) |
| throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) |
| return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) |
| case "indexOf": |
| return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) |
| case "lastIndexOf": |
| return Effect.succeed( |
| args[1] === undefined |
| ? target.lastIndexOf(args[0]) |
| : target.lastIndexOf(args[0], optNumber(args[1], "start index")), |
| ) |
| case "at": |
| return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) |
| case "slice": |
| return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) |
| case "concat": |
| return Effect.succeed(target.concat(...args)) |
| case "flat": |
| return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1)) |
| case "reverse": |
| return Effect.succeed([...target].reverse()) |
| case "sort": |
| case "toSorted": |
| return this.sortArray(target, args[0], node) |
| case "toReversed": |
| return Effect.succeed([...target].reverse()) |
| case "with": { |
| const index = optNumber(args[0], "index") ?? 0 |
| const resolved = index < 0 ? target.length + index : index |
| if (resolved < 0 || resolved >= target.length) { |
| throw new InterpreterRuntimeError("Array.with index is out of range.", node) |
| } |
| const copied = [...target] |
| copied[resolved] = args[1] |
| return Effect.succeed(copied) |
| } |
| case "push": { |
| |
| |
| for (const item of args) this.rejectCircularInsertion(target, item, "Array.push result", node) |
| target.push(...args) |
| return Effect.succeed(target.length) |
| } |
| case "unshift": { |
| for (const item of args) this.rejectCircularInsertion(target, item, "Array.unshift result", node) |
| target.unshift(...args) |
| return Effect.succeed(target.length) |
| } |
| case "pop": |
| return Effect.succeed(target.pop()) |
| case "shift": |
| return Effect.succeed(target.shift()) |
| case "splice": { |
| |
| |
| if (args.length === 0) return Effect.succeed(target.splice(0, 0)) |
| const start = optNumber(args[0], "start") ?? 0 |
| if (args.length === 1) return Effect.succeed(target.splice(start)) |
| const deleteCount = optNumber(args[1], "delete count") ?? 0 |
| const inserted = args.slice(2) |
| for (const item of inserted) this.rejectCircularInsertion(target, item, "Array.splice result", node) |
| return Effect.succeed(target.splice(start, deleteCount, ...inserted)) |
| } |
| case "fill": { |
| this.rejectCircularInsertion(target, args[0], "Array.fill result", node) |
| return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))) |
| } |
| case "copyWithin": |
| return Effect.succeed( |
| target.copyWithin( |
| optNumber(args[0], "target index") ?? 0, |
| optNumber(args[1], "start") ?? 0, |
| optNumber(args[2], "end"), |
| ), |
| ) |
| |
| |
| case "keys": |
| return Effect.succeed(Array.from(target.keys())) |
| case "values": |
| return Effect.succeed([...target]) |
| case "entries": |
| return Effect.succeed(Array.from(target.entries(), ([index, item]): Array<unknown> => [index, item])) |
| } |
|
|
| const callback = args[0] |
| if ( |
| !(callback instanceof CodeModeFunction) && |
| !(callback instanceof CoercionFunction) && |
| !(callback instanceof UriFunction) |
| ) { |
| throw new InterpreterRuntimeError(`Array.${name} expects a function callback.`, node) |
| } |
| const self = this |
| |
| |
| |
| const apply = (callbackArgs: Array<unknown>): Effect.Effect<unknown, unknown, R> => |
| callback instanceof CoercionFunction |
| ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) |
| : callback instanceof UriFunction |
| ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) |
| : self.invokeFunction(callback, callbackArgs) |
| return Effect.gen(function* () { |
| |
| |
| const items = target.slice() |
| switch (name) { |
| case "map": { |
| const values: Array<unknown> = [] |
| for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items])) |
| return values |
| } |
| case "flatMap": { |
| const values: Array<unknown> = [] |
| for (const [index, item] of items.entries()) { |
| const mapped = yield* apply([item, index, items]) |
| if (Array.isArray(mapped)) values.push(...mapped) |
| else values.push(mapped) |
| } |
| return values |
| } |
| case "filter": { |
| const values: Array<unknown> = [] |
| for (const [index, item] of items.entries()) { |
| if (yield* apply([item, index, items])) values.push(item) |
| } |
| return values |
| } |
| case "find": |
| for (const [index, item] of items.entries()) { |
| if (yield* apply([item, index, items])) return item |
| } |
| return undefined |
| case "findIndex": |
| for (const [index, item] of items.entries()) { |
| if (yield* apply([item, index, items])) return index |
| } |
| return -1 |
| case "some": |
| for (const [index, item] of items.entries()) { |
| if (yield* apply([item, index, items])) return true |
| } |
| return false |
| case "every": |
| for (const [index, item] of items.entries()) { |
| if (!(yield* apply([item, index, items]))) return false |
| } |
| return true |
| case "forEach": |
| for (const [index, item] of items.entries()) yield* apply([item, index, items]) |
| return undefined |
| case "reduce": { |
| let accumulator: unknown |
| let start: number |
| if (args.length >= 2) { |
| accumulator = args[1] |
| start = 0 |
| } else { |
| if (items.length === 0) |
| throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) |
| accumulator = items[0] |
| start = 1 |
| } |
| for (let index = start; index < items.length; index += 1) { |
| accumulator = yield* apply([accumulator, items[index], index, items]) |
| } |
| return accumulator |
| } |
| case "reduceRight": { |
| let accumulator: unknown |
| let start: number |
| if (args.length >= 2) { |
| accumulator = args[1] |
| start = items.length - 1 |
| } else { |
| if (items.length === 0) |
| throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) |
| accumulator = items[items.length - 1] |
| start = items.length - 2 |
| } |
| for (let index = start; index >= 0; index -= 1) { |
| accumulator = yield* apply([accumulator, items[index], index, items]) |
| } |
| return accumulator |
| } |
| case "findLast": |
| for (let index = items.length - 1; index >= 0; index -= 1) { |
| if (yield* apply([items[index], index, items])) return items[index] |
| } |
| return undefined |
| case "findLastIndex": |
| for (let index = items.length - 1; index >= 0; index -= 1) { |
| if (yield* apply([items[index], index, items])) return index |
| } |
| return -1 |
| } |
| throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node) |
| }) |
| } |
|
|
| private sortArray( |
| target: Array<unknown>, |
| comparator: unknown, |
| node: AstNode, |
| ): Effect.Effect<Array<unknown>, unknown, R> { |
| if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) { |
| throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node) |
| } |
| if (!(comparator instanceof CodeModeFunction)) { |
| return Effect.sync(() => |
| [...target].sort((a, b) => { |
| const left = coerceToString(a) |
| const right = coerceToString(b) |
| return left < right ? -1 : left > right ? 1 : 0 |
| }), |
| ) |
| } |
| const self = this |
| const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => { |
| if (items.length <= 1) return Effect.succeed(items) |
| const midpoint = Math.floor(items.length / 2) |
| return Effect.gen(function* () { |
| const left = yield* mergeSort(items.slice(0, midpoint)) |
| const right = yield* mergeSort(items.slice(midpoint)) |
| const merged: Array<unknown> = [] |
| let leftIndex = 0 |
| let rightIndex = 0 |
| while (leftIndex < left.length && rightIndex < right.length) { |
| |
| |
| const order = coerceToNumber(yield* self.invokeFunction(comparator, [left[leftIndex], right[rightIndex]])) |
| if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) |
| else merged.push(right[rightIndex++]) |
| } |
| return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] |
| }) |
| } |
| |
| const defined = target.filter((item) => item !== undefined) |
| const undefinedCount = target.length - defined.length |
| return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) |
| } |
|
|
| private evaluateObjectExpression(node: AstNode): Effect.Effect<Record<string, unknown>, unknown, R> { |
| const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown> |
| const properties = getArray(node, "properties") |
| const self = this |
| return Effect.gen(function* () { |
| for (const propertyValue of properties) { |
| const property = asNode(propertyValue, "properties") |
|
|
| if (property.type === "SpreadElement") { |
| const spread = yield* self.evaluateExpression(getNode(property, "argument")) |
| |
| |
| |
| if (spread === null || spread === undefined || isSandboxValue(spread)) continue |
| if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { |
| throw new InterpreterRuntimeError( |
| "Object spread requires a data object in CodeMode.", |
| property, |
| "InvalidDataValue", |
| ) |
| } |
| for (const [key, value] of Object.entries(spread)) { |
| if (isBlockedMember(key)) |
| throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, property) |
| objectValue[key] = value |
| } |
| continue |
| } |
|
|
| if (property.type !== "Property") { |
| throw new InterpreterRuntimeError("Only standard object properties are supported.", property) |
| } |
|
|
| if (getString(property, "kind") !== "init") { |
| throw new InterpreterRuntimeError("Only init object properties are supported.", property) |
| } |
|
|
| const keyNode = getNode(property, "key") |
| const valueNode = getNode(property, "value") |
| const computed = getBoolean(property, "computed") |
|
|
| let key: PropertyKey |
|
|
| if (computed) { |
| key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode) |
| } else if (keyNode.type === "Identifier") { |
| key = getString(keyNode, "name") |
| } else if (keyNode.type === "Literal") { |
| key = self.toPropertyKey(keyNode.value, keyNode) |
| } else { |
| throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode) |
| } |
|
|
| if (isBlockedMember(String(key))) { |
| throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, keyNode) |
| } |
| objectValue[String(key)] = yield* self.evaluateExpression(valueNode) |
| } |
|
|
| return objectValue |
| }) |
| } |
|
|
| private evaluateArrayExpression(node: AstNode): Effect.Effect<Array<unknown>, unknown, R> { |
| const elements = getArray(node, "elements") |
| const values: Array<unknown> = [] |
|
|
| const self = this |
| return Effect.gen(function* () { |
| for (const elementValue of elements) { |
| if (elementValue === null) { |
| values.push(undefined) |
| continue |
| } |
| const element = asNode(elementValue, "elements") |
| if (element.type === "SpreadElement") { |
| const spread = yield* self.evaluateExpression(getNode(element, "argument")) |
| const items = spreadItems(spread) |
| if (items === undefined) |
| throw new InterpreterRuntimeError( |
| "Array spread requires an array, string, Map, or Set in CodeMode.", |
| element, |
| ) |
| values.push(...items) |
| } else { |
| values.push(yield* self.evaluateExpression(element)) |
| } |
| } |
| return values |
| }) |
| } |
|
|
| private evaluateTemplateLiteral(node: AstNode): Effect.Effect<string, unknown, R> { |
| const quasis = getArray(node, "quasis") |
| const expressions = getArray(node, "expressions") |
|
|
| let output = "" |
|
|
| const self = this |
| return Effect.gen(function* () { |
| for (let index = 0; index < quasis.length; index += 1) { |
| const quasi = asNode(quasis[index], "quasis") |
| const rawValue = quasi.value |
|
|
| if (!isRecord(rawValue) || typeof rawValue.cooked !== "string") { |
| throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi) |
| } |
|
|
| output += rawValue.cooked |
|
|
| if (index < expressions.length) { |
| const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions")) |
| |
| |
| output += coerceToString(boundedData(raw, "Template interpolation")) |
| } |
| } |
|
|
| return output |
| }) |
| } |
|
|
| private evaluateConditionalExpression(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| return Effect.flatMap(this.evaluateExpression(getNode(node, "test")), (test) => |
| this.evaluateExpression(getNode(node, test ? "consequent" : "alternate")), |
| ) |
| } |
|
|
| private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown { |
| |
| |
| |
| |
| if (!compoundOperators.has(operator)) { |
| throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node) |
| } |
| return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node) |
| } |
|
|
| private getMemberReference( |
| node: AstNode, |
| ): Effect.Effect< |
| | MemberReference |
| | ToolReference |
| | PromiseMethodReference |
| | IntrinsicReference |
| | GlobalMethodReference |
| | ComputedValue |
| | typeof OptionalShortCircuit |
| | undefined, |
| unknown, |
| R |
| > { |
| const objectNode = getNode(node, "object") |
| const propertyNode = getNode(node, "property") |
| const computed = getBoolean(node, "computed") |
| const optional = node.optional === true |
| const self = this |
| return Effect.gen(function* () { |
| const objectValue = yield* self.evaluateExpression(objectNode) |
| if (objectValue === OptionalShortCircuit) return OptionalShortCircuit |
| if ((objectValue === null || objectValue === undefined) && optional) return OptionalShortCircuit |
|
|
| const key = computed |
| ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) |
| : propertyNode.type === "Identifier" |
| ? getString(propertyNode, "name") |
| : self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) |
|
|
| if (objectValue instanceof ToolReference) { |
| if (typeof key !== "string" || isBlockedMember(key)) { |
| throw new InterpreterRuntimeError("Tool paths must use safe string property names.", propertyNode) |
| } |
| return new ToolReference([...objectValue.path, key]) |
| } |
|
|
| if (objectValue instanceof PromiseNamespace) { |
| if (typeof key === "string" && promiseStatics.has(key as PromiseMethodName)) { |
| return new PromiseMethodReference(key as PromiseMethodName) |
| } |
| throw new InterpreterRuntimeError( |
| `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`, |
| propertyNode, |
| ) |
| } |
|
|
| if (objectValue instanceof GlobalNamespace) { |
| if (typeof key !== "string" || isBlockedMember(key)) { |
| throw new InterpreterRuntimeError( |
| `${objectValue.name}.${String(key)} is not available in CodeMode.`, |
| propertyNode, |
| ) |
| } |
| if (objectValue.name === "Math" && mathConstants.has(key)) { |
| return new ComputedValue((Math as unknown as Record<string, number>)[key]) |
| } |
| return new GlobalMethodReference(objectValue.name, key) |
| } |
|
|
| if (typeof objectValue === "string") { |
| if (key === "length") return new ComputedValue(objectValue.length) |
| if (typeof key === "number") return new ComputedValue(objectValue[key]) |
| if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)]) |
| if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| |
| |
| |
| |
| return new ComputedValue(undefined) |
| } |
|
|
| if (typeof objectValue === "number") { |
| if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| |
| return new ComputedValue(undefined) |
| } |
|
|
| |
| if (objectValue instanceof CoercionFunction && typeof key === "string" && !isBlockedMember(key)) { |
| if (objectValue.name === "Number" && numberConstants.has(key)) { |
| return new ComputedValue((Number as unknown as Record<string, number>)[key]) |
| } |
| if (objectValue.name === "Number" && numberStatics.has(key)) return new GlobalMethodReference("Number", key) |
| if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key) |
| } |
|
|
| |
| |
| if (objectValue instanceof SandboxDate) { |
| if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| return new ComputedValue(undefined) |
| } |
| if (objectValue instanceof SandboxRegExp) { |
| if (typeof key === "string" && regexpProperties.has(key)) { |
| return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key]) |
| } |
| if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| return new ComputedValue(undefined) |
| } |
| if (objectValue instanceof SandboxMap) { |
| if (key === "size") return new ComputedValue(objectValue.map.size) |
| if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| return new ComputedValue(undefined) |
| } |
| if (objectValue instanceof SandboxSet) { |
| if (key === "size") return new ComputedValue(objectValue.set.size) |
| if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| return new ComputedValue(undefined) |
| } |
| if (objectValue instanceof SandboxURL) { |
| if (key === "searchParams") { |
| return new ComputedValue(objectValue.searchParams) |
| } |
| if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key) |
| if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } |
| return new ComputedValue(undefined) |
| } |
| if (objectValue instanceof SandboxURLSearchParams) { |
| if (key === "size") return new ComputedValue(objectValue.params.size) |
| if (typeof key === "string" && urlSearchParamsMethods.has(key)) { |
| return new IntrinsicReference(objectValue, key) |
| } |
| return new ComputedValue(undefined) |
| } |
|
|
| |
| |
| |
| if (objectValue instanceof SandboxPromise) { |
| if (key === "then" || key === "catch" || key === "finally") { |
| throw new InterpreterRuntimeError( |
| `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(...)\`.`, |
| propertyNode, |
| "UnsupportedSyntax", |
| [supportedSyntaxMessage], |
| ) |
| } |
| throw new InterpreterRuntimeError( |
| "This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.", |
| objectNode, |
| "InvalidDataValue", |
| ) |
| } |
|
|
| if (isRuntimeReference(objectValue)) { |
| throw new InterpreterRuntimeError( |
| "CodeMode runtime references are opaque and do not expose properties.", |
| objectNode, |
| "InvalidDataValue", |
| ) |
| } |
|
|
| if (typeof objectValue !== "object" || objectValue === null) { |
| throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode) |
| } |
|
|
| if (typeof key === "string" && isBlockedMember(key)) { |
| throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, propertyNode) |
| } |
|
|
| if (Array.isArray(objectValue)) { |
| if ( |
| key !== "length" && |
| !(typeof key === "string" && arrayMethods.has(key)) && |
| typeof key !== "number" && |
| !/^\d+$/.test(key) |
| ) { |
| |
| |
| if (typeof key === "string" && Object.hasOwn(objectValue, key)) { |
| return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key]) |
| } |
| |
| |
| return new ComputedValue(undefined) |
| } |
| return { target: objectValue, key } |
| } |
|
|
| return { target: objectValue as SafeObject, key } |
| }) |
| } |
|
|
| private readMember(node: AstNode): Effect.Effect<unknown, unknown, R> { |
| return Effect.map(this.getMemberReference(node), (reference) => { |
| if (reference === OptionalShortCircuit) return OptionalShortCircuit |
| if (reference instanceof ComputedValue) return reference.value |
| if ( |
| reference === undefined || |
| reference instanceof ToolReference || |
| reference instanceof PromiseMethodReference || |
| reference instanceof IntrinsicReference || |
| reference instanceof GlobalMethodReference |
| ) |
| return reference |
| if (Array.isArray(reference.target)) { |
| if (typeof reference.key === "string" && arrayMethods.has(reference.key)) { |
| return new IntrinsicReference(reference.target, reference.key) |
| } |
| return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)] |
| } |
| if (reference.target instanceof SandboxURL) { |
| return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)] |
| } |
| return reference.target[String(reference.key)] |
| }) |
| } |
|
|
| private writeMember(node: AstNode, value: unknown): Effect.Effect<unknown, unknown, R> { |
| return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value })) |
| } |
|
|
| |
| |
| |
| private modifyMember( |
| node: AstNode, |
| compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>, |
| ): Effect.Effect<unknown, unknown, R> { |
| const self = this |
| return Effect.gen(function* () { |
| const reference = yield* self.getMemberReference(node) |
| if ( |
| reference === OptionalShortCircuit || |
| reference instanceof ComputedValue || |
| reference === undefined || |
| reference instanceof ToolReference || |
| reference instanceof PromiseMethodReference || |
| reference instanceof IntrinsicReference || |
| reference instanceof GlobalMethodReference |
| ) { |
| throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node) |
| } |
| if (Array.isArray(reference.target)) { |
| if (reference.key === "length") |
| throw new InterpreterRuntimeError("Array length cannot be assigned in CodeMode.", node) |
| if (typeof reference.key === "string" && arrayMethods.has(reference.key)) { |
| throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node) |
| } |
| } |
| const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key) |
| const current = |
| reference.target instanceof SandboxURL |
| ? (reference.target.url as unknown as Record<string, unknown>)[key] |
| : (reference.target as Record<PropertyKey, unknown>)[key] |
| const { write, next, result } = yield* compute(current) |
| if (write) self.assignToReference(reference, key, next, node) |
| return result |
| }) |
| } |
|
|
| |
| |
| private rejectCircularInsertion( |
| container: object, |
| value: unknown, |
| label: string, |
| node: AstNode, |
| seen = new Set<object>(), |
| ): void { |
| if (value === container) |
| throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") |
| if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return |
| seen.add(value) |
| const items = Array.isArray(value) ? value : Object.values(value) |
| for (const item of items) this.rejectCircularInsertion(container, item, label, node, seen) |
| seen.delete(value) |
| } |
|
|
| private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void { |
| if (Array.isArray(reference.target)) { |
| const target = reference.target |
| const index = key as number |
| if (!Number.isInteger(index) || index < 0) { |
| throw new InterpreterRuntimeError( |
| "Array assignment index must be a non-negative integer.", |
| node, |
| "InvalidDataValue", |
| ) |
| } |
| this.rejectCircularInsertion(target, next, "Array assignment result", node) |
| target[index] = next |
| return |
| } |
| if (reference.target instanceof SandboxURL) { |
| const property = key as string |
| if (!urlWritableProperties.has(property)) { |
| throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") |
| } |
| try { |
| const url = reference.target.url as unknown as Record<string, string> |
| url[property] = uriArgument(next, `URL.${property} value`) |
| return |
| } catch (error) { |
| if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error |
| throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError") |
| } |
| } |
| const target = reference.target as SafeObject |
| const objectKey = key as string |
| this.rejectCircularInsertion(target, next, "Object assignment result", node) |
| target[objectKey] = next |
| } |
|
|
| private toPropertyKey(value: unknown, node: AstNode): string | number { |
| if (typeof value === "string" || typeof value === "number") { |
| return value |
| } |
|
|
| throw new InterpreterRuntimeError("Property key must be a string or number.", node) |
| } |
|
|
| private declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { |
| const scope = this.currentScope() |
|
|
| |
| |
| const existing = scope.get(name) |
| if (existing && existing.initialized !== false) { |
| throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) |
| } |
|
|
| scope.set(name, { mutable, value, initialized: true }) |
| } |
|
|
| private getIdentifierValue(name: string, node: AstNode): unknown { |
| const binding = this.resolveBinding(name) |
|
|
| if (!binding) { |
| throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") |
| } |
|
|
| |
| if (binding.initialized === false) { |
| throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") |
| } |
|
|
| return binding.value |
| } |
|
|
| private setIdentifierValue(name: string, value: unknown, node: AstNode): unknown { |
| const binding = this.resolveBinding(name) |
|
|
| if (!binding) { |
| throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") |
| } |
|
|
| if (!binding.mutable) { |
| throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") |
| } |
|
|
| binding.value = value |
| return value |
| } |
|
|
| private resolveBinding(name: string): Binding | undefined { |
| for (let index = this.scopes.length - 1; index >= 0; index -= 1) { |
| const scope = this.scopes[index] |
| const binding = scope?.get(name) |
|
|
| if (binding) { |
| return binding |
| } |
| } |
|
|
| return undefined |
| } |
|
|
| private currentScope(): Map<string, Binding> { |
| const scope = this.scopes[this.scopes.length - 1] |
|
|
| if (!scope) { |
| throw new InterpreterRuntimeError("Interpreter scope stack is empty.") |
| } |
|
|
| return scope |
| } |
|
|
| private pushScope(): void { |
| this.scopes.push(new Map()) |
| } |
|
|
| private popScope(): void { |
| this.scopes.pop() |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const executeWithLimits = <const Tools extends Record<string, unknown>>( |
| options: ExecuteOptions<Tools>, |
| limits: ResolvedExecutionLimits, |
| searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], |
| ): Effect.Effect<Result, never, Services<Tools>> => { |
| const hooks = { |
| ...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }), |
| ...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }), |
| } |
| const tools = ToolRuntime.make( |
| (options.tools ?? {}) as HostTools<Services<Tools>>, |
| limits.maxToolCalls, |
| searchIndex, |
| hooks, |
| ) |
| const logs: Array<string> = [] |
| const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) |
|
|
| if (options.code.trim().length === 0) { |
| return Effect.succeed({ |
| ok: false, |
| error: { kind: "ParseError", message: "Code cannot be empty." }, |
| toolCalls: tools.calls, |
| }) |
| } |
|
|
| const operation = Effect.gen(function* () { |
| const program = parseProgram(options.code) |
| const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.keys, logs) |
| const value = yield* interpreter.run(program) |
| const result = copyOut(copyIn(value, "Execution result"), true) as DataValue |
| return { |
| ok: true, |
| value: result, |
| ...logged(), |
| toolCalls: tools.calls, |
| } satisfies Result |
| }).pipe((program) => { |
| const timeoutMs = limits.timeoutMs |
| if (timeoutMs === undefined) return program |
| return program.pipe( |
| Effect.timeoutOrElse({ |
| duration: timeoutMs, |
| orElse: () => |
| Effect.succeed({ |
| ok: false, |
| error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, |
| ...logged(), |
| toolCalls: tools.calls, |
| } satisfies Result), |
| }), |
| ) |
| }) |
|
|
| return operation.pipe( |
| Effect.catchCause((cause) => |
| Cause.hasInterruptsOnly(cause) |
| ? Effect.interrupt |
| : Effect.succeed({ |
| ok: false, |
| error: normalizeError(Cause.squash(cause)), |
| ...logged(), |
| toolCalls: tools.calls, |
| } satisfies Result), |
| ), |
| Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))), |
| ) |
| } |
|
|
| const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength |
|
|
| // Truncates to a UTF-8 byte budget without splitting a code point (a split multi-byte |
| // sequence decodes to a replacement character, which is dropped). |
| const utf8Truncate = (value: string, maxBytes: number): string => { |
| const bytes = new TextEncoder().encode(value) |
| if (bytes.byteLength <= maxBytes) return value |
| const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes))) |
| return text.endsWith("\uFFFD") ? text.slice(0, -1) : text |
| } |
|
|
| /** |
| * Bounds the model-facing output (serialized result value plus logs) to `maxOutputBytes`. |
| * Oversized values are replaced by their truncated serialized text with an explanatory marker, |
| * and logs are kept from the start until the remaining budget is exhausted. Truncation never |
| * fails the execution; `truncated: true` marks affected results. Only runs when the host set |
| * `maxOutputBytes` - with the limit absent, output passes through unbounded. |
| */ |
| const boundOutput = (result: Result, maxOutputBytes: number): Result => { |
| let truncated = false |
|
|
| let value: DataValue = null |
| let valueBytes = 0 |
| if (result.ok) { |
| const serialized = JSON.stringify(result.value) ?? "null" |
| const bytes = utf8ByteLength(serialized) |
| if (bytes > maxOutputBytes) { |
| truncated = true |
| value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]` |
| valueBytes = maxOutputBytes |
| } else { |
| value = result.value |
| valueBytes = bytes |
| } |
| } |
|
|
| const logs = result.logs ?? [] |
| const kept: Array<string> = [] |
| const logBudget = Math.max(0, maxOutputBytes - valueBytes) |
| let logBytes = 0 |
| for (const line of logs) { |
| const lineBytes = utf8ByteLength(line) + 1 |
| if (logBytes + lineBytes > logBudget) break |
| logBytes += lineBytes |
| kept.push(line) |
| } |
| if (kept.length < logs.length) { |
| truncated = true |
| kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`) |
| } |
|
|
| if (!truncated) return result |
| const logsPart = kept.length > 0 ? { logs: kept } : {} |
| return result.ok |
| ? { ok: true, value, ...logsPart, truncated: true, toolCalls: result.toolCalls } |
| : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls } |
| } |
|
|