refactor(codemode): clean up package comments
This commit is contained in:
parent
ac7ee99a24
commit
92a50f2397
10 changed files with 389 additions and 444 deletions
|
|
@ -10,6 +10,6 @@
|
|||
- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead.
|
||||
- Improve the sandbox failure taxonomy. Distinguish parse/compile mistakes, unsupported syntax, user-thrown errors, invalid returned data, tool refusal, tool internal failure, timeout, and genuine runtime defects so agents can recover accurately instead of treating everything as a generic execution failure.
|
||||
- Preserve the public/private error split. Tool authors should be able to return a safe model-visible message while retaining a private cause for host diagnostics. Unknown host failures must remain sanitized by default.
|
||||
- Think deliberately about richer binary boundaries before allowing `Blob`, `File`, `ArrayBuffer`, streams, or typed arrays beyond today’s JSON-like values. If CodeMode supports binary tool args/results, use explicit tagged data shapes and clear size limits rather than relying on ambient runtime serialization.
|
||||
- Think deliberately about richer binary boundaries before allowing `Blob`, `File`, `ArrayBuffer`, streams, or typed arrays beyond today's JSON-like values. If CodeMode supports binary tool args/results, use explicit tagged data shapes and clear size limits rather than relying on ambient runtime serialization.
|
||||
- Keep host capabilities explicit. Globals such as `fetch`, `crypto`, filesystem handles, extra modules, or network clients should be opt-in runtime capabilities with obvious policy defaults, not ambient authority. Default to unavailable unless a host deliberately provides the capability.
|
||||
- If `fetch` is added, model it as a host-provided outbound capability with policy controls: allowed origins, methods, headers, response size, timeout, and whether response bodies may be returned, emitted, or only summarized through a tool.
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ interface ExecuteFailure {
|
|||
|
||||
## Discovery
|
||||
|
||||
The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going — so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL — N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`).
|
||||
The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`).
|
||||
|
||||
The default budget is 2,000 estimated tokens (characters / 4, the same heuristic OpenCode uses). Override it when constructing a runtime:
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ const runtime = CodeMode.make({
|
|||
|
||||
The budget must be a non-negative safe integer.
|
||||
|
||||
The runtime search tool is always registered — including when the catalog is fully inlined — so a speculative `tools.$codemode.search` call never fails as an unknown tool. It is only advertised in the instructions when the inlined list is partial:
|
||||
The runtime search tool is always registered - including when the catalog is fully inlined - so a speculative `tools.$codemode.search` call never fails as an unknown tool. It is only advertised in the instructions when the inlined list is partial:
|
||||
|
||||
```ts
|
||||
const matches = await tools.$codemode.search({
|
||||
|
|
@ -173,9 +173,9 @@ const matches = await tools.$codemode.search({
|
|||
})
|
||||
```
|
||||
|
||||
`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches — so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path) and capped at `limit` results (default 10).
|
||||
`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches - so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path) and capped at `limit` results (default 10).
|
||||
|
||||
Each result contains the path, description, and generated TypeScript signature, so no second lookup is needed. The result signature is the pretty, JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** … */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). The inline catalog in the instructions keeps the compact single-line form.
|
||||
Each result contains the path, description, and generated TypeScript signature, so no second lookup is needed. The result signature is the pretty, JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** ... */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). The inline catalog in the instructions keeps the compact single-line form.
|
||||
|
||||
```ts
|
||||
tools.github.list_issues(input: {
|
||||
|
|
@ -193,7 +193,7 @@ tools.github.list_issues(input: {
|
|||
|
||||
Result paths are rendered as JavaScript expressions rooted at `tools` (`tools.orders.lookup`, or `tools.context7["resolve-library-id"]` for non-identifier segments), so each `path` is directly usable as the call site. An empty query browses the catalog alphabetically by path; combined with `namespace` (`{ query: "", namespace: "orders" }`) it lists everything in that namespace. A query that names one tool path exactly (canonical path, `tools.`-prefixed path, or rendered JavaScript expression) is treated as a lookup and returns that tool alone.
|
||||
|
||||
The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; `JSON.parse` string results; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result tools exist inside `tools`; filter and aggregate collections in code; treat `Promise<unknown>` results as shapeless until verified; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace via search when it is advertised), a short `## Syntax` section that assumes standard JavaScript and names only what is unusual (TypeScript annotations stripped; the data-boundary serialization of Date/Map/Set/RegExp) or missing (classes, generators, `for await...of`, `.then`/`.catch`/`.finally`), and the budgeted `## Available tools` catalog. Example call forms use explicit `<namespace>.<tool>`/`<field>` placeholders — never a real or fabricated tool name.
|
||||
The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; `JSON.parse` string results; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result tools exist inside `tools`; filter and aggregate collections in code; treat `Promise<unknown>` results as shapeless until verified; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace via search when it is advertised), a short `## Syntax` section that assumes standard JavaScript and names only what is unusual (TypeScript annotations stripped; the data-boundary serialization of Date/Map/Set/RegExp) or missing (classes, generators, `for await...of`, `.then`/`.catch`/`.finally`), and the budgeted `## Available tools` catalog. Example call forms use explicit `<namespace>.<tool>`/`<field>` placeholders - never a real or fabricated tool name.
|
||||
|
||||
A host cannot define its own `$codemode` top-level namespace.
|
||||
|
||||
|
|
@ -202,19 +202,19 @@ A host cannot define its own `$codemode` top-level namespace.
|
|||
CodeMode executes a deliberately bounded JavaScript subset. It supports:
|
||||
|
||||
- Plain data literals, property access, assignment, and destructuring.
|
||||
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references — anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
|
||||
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
|
||||
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
|
||||
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
|
||||
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces and `Object.keys(tools.ns)` the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
|
||||
- `Date` — `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
|
||||
- Regular expressions — `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. Function replacers are not supported.
|
||||
- `Map` and `Set` — construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
||||
- First-class promises — an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
|
||||
- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure — thrown errors, interpreter runtime errors, and tool failures — is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have — `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.
|
||||
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
|
||||
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. Function replacers are not supported.
|
||||
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
||||
- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
|
||||
- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.
|
||||
|
||||
Inside a program, Date/RegExp/Map/Set values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do the four value types serialize exactly as `JSON.stringify` would: a Date becomes its ISO string (`null` when invalid) and RegExp/Map/Set become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`.
|
||||
|
||||
It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), promise chaining (`.then`/`.catch`/`.finally` — `await` with `try`/`catch` is the supported style), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available.
|
||||
It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), promise chaining (`.then`/`.catch`/`.finally` - `await` with `try`/`catch` is the supported style), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available.
|
||||
|
||||
CodeMode is an orchestration language, not a general JavaScript runtime.
|
||||
|
||||
|
|
@ -224,11 +224,11 @@ The limits are exactly three knobs:
|
|||
|
||||
| Limit | Default | Bounds |
|
||||
| --- | ---: | --- |
|
||||
| `timeoutMs` | none — no timeout | Wall-clock execution time. |
|
||||
| `maxToolCalls` | none — unlimited | Tool calls admitted during the execution. |
|
||||
| `maxOutputBytes` | none — no truncation | Model-facing output: the serialized result value plus captured logs. |
|
||||
| `timeoutMs` | none - no timeout | Wall-clock execution time. |
|
||||
| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. |
|
||||
| `maxOutputBytes` | none - no truncation | Model-facing output: the serialized result value plus captured logs. |
|
||||
|
||||
No limit has a default, on purpose: execution budgets are host policy, not library policy — a host that wants a bound sets one; a host that can interrupt the execution fiber (as OpenCode does on user cancel) may set no timeout, and a host with its own tool-output truncation (as OpenCode has) may leave `maxOutputBytes` unset. A host with neither should set `maxOutputBytes`, or oversized results silently flood model context.
|
||||
No limit has a default, on purpose: execution budgets are host policy, not library policy - a host that wants a bound sets one; a host that can interrupt the execution fiber (as OpenCode does on user cancel) may set no timeout, and a host with its own tool-output truncation (as OpenCode has) may leave `maxOutputBytes` unset. A host with neither should set `maxOutputBytes`, or oversized results silently flood model context.
|
||||
|
||||
Pass only the overrides you need:
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ Limits are safe integers. `timeoutMs` must be at least `1`; the others may be `0
|
|||
|
||||
Exceeding a configured `maxOutputBytes` never fails the execution. An oversized result value is replaced by its truncated serialized text plus an explanatory marker, logs are kept from the start until the remaining budget is exhausted (with a final marker line noting the cut), and the result carries `truncated: true`.
|
||||
|
||||
When configured, the timeout interrupts in-flight tool Effects, including eagerly started calls the program has not awaited (their fibers are supervised by the execution). The interpreter yields cooperatively between steps, so the timeout also interrupts pure busy loops (`while (true) {}`) — no separate work budget exists. Tool implementations remain responsible for making their external operations interruptible or independently bounded.
|
||||
When configured, the timeout interrupts in-flight tool Effects, including eagerly started calls the program has not awaited (their fibers are supervised by the execution). The interpreter yields cooperatively between steps, so the timeout also interrupts pure busy loops (`while (true) {}`) - no separate work budget exists. Tool implementations remain responsible for making their external operations interruptible or independently bounded.
|
||||
|
||||
Two interpreter internals are fixed constants rather than knobs: at most 8 tool calls run concurrently, and values crossing a data boundary may nest at most 32 levels deep (deeper values fail as `InvalidDataValue`, which reads better than a native stack-overflow error). Neither is part of the public contract.
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -65,7 +65,7 @@ export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = {
|
|||
/** Source for one program in the supported JavaScript subset. */
|
||||
code: string
|
||||
/** Explicit tool tree exposed to the program as `tools`. */
|
||||
tools?: Tools & ToolTree<any>
|
||||
tools?: Tools & ToolTree<Services<Tools>>
|
||||
/** Per-execution overrides for the default resource limits. */
|
||||
limits?: ExecutionLimits
|
||||
/** Observes decoded tool input immediately before tool execution. */
|
||||
|
|
@ -224,7 +224,6 @@ class IntrinsicReference {
|
|||
) {}
|
||||
}
|
||||
|
||||
// A read-only computed member (e.g. `str.length`, a character index) — not assignable.
|
||||
class ComputedValue {
|
||||
constructor(readonly value: unknown) {}
|
||||
}
|
||||
|
|
@ -249,7 +248,6 @@ class GlobalMethodReference {
|
|||
constructor(readonly namespace: GlobalNamespaceName | "Number" | "String", readonly name: string) {}
|
||||
}
|
||||
|
||||
// A built-in callable global (`Number`, `String`, `Boolean`, `parseInt`, `parseFloat`).
|
||||
class CoercionFunction {
|
||||
constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat") {}
|
||||
}
|
||||
|
|
@ -258,17 +256,11 @@ class ProgramThrow {
|
|||
constructor(readonly value: unknown) {}
|
||||
}
|
||||
|
||||
// A bound error constructor global (`Error`, `TypeError`, ...): callable with or without
|
||||
// `new`, and the recognized right-hand side of `x instanceof Error`.
|
||||
class ErrorConstructorReference {
|
||||
constructor(readonly name: string) {}
|
||||
}
|
||||
|
||||
// Error values stay plain `{ name, message }` data objects — they stringify/serialize exactly
|
||||
// as before — but carry their constructor name on a non-enumerable symbol key so `instanceof
|
||||
// Error` can recognize them. Object.entries/JSON walks (copyIn/copyOut, spread, stringify)
|
||||
// never see the brand, and losing it on spread/boundary copies matches JS, where a spread
|
||||
// error loses its prototype too.
|
||||
// Non-enumerable so spread/copyOut preserve the plain `{ name, message }` data shape.
|
||||
const ErrorBrand: unique symbol = Symbol("codemode.error")
|
||||
|
||||
const brandError = (errorValue: SafeObject, name: string): SafeObject => {
|
||||
|
|
@ -347,7 +339,7 @@ const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys",
|
|||
const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
|
||||
|
||||
const supportedSyntaxMessage =
|
||||
"Supported orchestration syntax: tools.* calls (they return promises — resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported — use await with try/catch)."
|
||||
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported - use await with try/catch)."
|
||||
|
||||
const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError =>
|
||||
new InterpreterRuntimeError(`Syntax '${kind}' is not supported in CodeMode. ${supportedSyntaxMessage}`, node, "UnsupportedSyntax", [supportedSyntaxMessage])
|
||||
|
|
@ -355,7 +347,7 @@ const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError
|
|||
/** How many eagerly forked tool calls may run at once. Fixed; not a configurable knob. */
|
||||
const TOOL_CALL_CONCURRENCY = 8
|
||||
|
||||
/** Console formatting recursion ceiling; deeper values render as "…". Fixed; not a knob. */
|
||||
/** Console formatting recursion ceiling; deeper values render as "...". Fixed; not a knob. */
|
||||
const MAX_CONSOLE_DEPTH = 32
|
||||
|
||||
const validateLimit = <Value extends number | undefined>(name: keyof ExecutionLimits, value: Value, minimum: number): Value => {
|
||||
|
|
@ -365,7 +357,7 @@ const validateLimit = <Value extends number | undefined>(name: keyof ExecutionLi
|
|||
return value
|
||||
}
|
||||
|
||||
// No limit has a default: absent means no timeout / unlimited calls / no output truncation —
|
||||
// No limit has a default: absent means no timeout / unlimited calls / no output truncation -
|
||||
// budgets are host policy, not library policy. A host without its own output bounding should
|
||||
// pass maxOutputBytes explicitly, or oversized results flood model context.
|
||||
const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimits => ({
|
||||
|
|
@ -379,7 +371,7 @@ class InterpreterRuntimeError extends Error {
|
|||
/**
|
||||
* The constructor name a program observes when it catches this failure (`caught.name`, and
|
||||
* the brand behind `caught instanceof SyntaxError` etc.). "Error" unless the failing
|
||||
* operation names a standard type in real JS — e.g. JSON.parse and invalid regex patterns
|
||||
* operation names a standard type in real JS - e.g. JSON.parse and invalid regex patterns
|
||||
* throw SyntaxError, an unknown identifier is a ReferenceError, a bad normalize form is a
|
||||
* RangeError.
|
||||
*/
|
||||
|
|
@ -569,21 +561,7 @@ const normalizeError = (error: unknown): Diagnostic => {
|
|||
}
|
||||
}
|
||||
|
||||
// The plain value a program observes for a settled failure — shared by `catch` bindings,
|
||||
// `Promise.allSettled` rejection reasons, and race-loser diagnostics. A thrown program value
|
||||
// passes through as-is (so `throw new Error(m)` yields its `{ name, message }` object); every
|
||||
// other failure becomes a plain `{ name, message }` object, error-branded so `caught
|
||||
// instanceof Error` is true for interpreter and tool failures too. When a host failure is a
|
||||
// real Error whose constructor name is one of the standard seven (e.g. JSON.parse throwing a
|
||||
// SyntaxError), that name is carried through — both as `caught.name` and as the brand, so
|
||||
// `caught instanceof SyntaxError` matches real JS. Interpreter diagnostics carry the name the
|
||||
// equivalent real-JS failure would have (`errorName`, "Error" unless the throw site says
|
||||
// otherwise); tool failures and internal error classes are plain "Error" — internal class
|
||||
// names never leak.
|
||||
// Interpreter errors use their raw message so the program never sees the transpiled-source
|
||||
// "(line N, col N)" coordinates that normalizeError appends — without disturbing a host/tool
|
||||
// message that legitimately ends that way. Other error kinds carry no appended location, so
|
||||
// normalizeError is used as-is.
|
||||
// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers.
|
||||
const caughtErrorValue = (thrown: unknown): unknown => {
|
||||
if (thrown instanceof ProgramThrow) return thrown.value
|
||||
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
|
||||
|
|
@ -591,17 +569,6 @@ const caughtErrorValue = (thrown: unknown): unknown => {
|
|||
return createErrorValue(name, normalizeError(thrown).message)
|
||||
}
|
||||
|
||||
// ── Built-in method/global implementations ───────────────────────────────────
|
||||
// These mirror the corresponding JavaScript operations over Data Values. They are
|
||||
// pure (string/Object/Math/JSON/coercion) and so live as free functions; array
|
||||
// Methods that run CodeMode callbacks live on the interpreter (they need invokeFunction).
|
||||
|
||||
// The intra-sandbox data checkpoint: copies a value through `copyIn` in preserving mode,
|
||||
// which validates the plain-data contract (depth, circularity, plain objects only, blocked
|
||||
// properties) while keeping sandbox value instances (Date/RegExp/Map/Set) alive — so values
|
||||
// flowing through `Object.*` helpers, coercion inputs, and other in-sandbox checkpoints stay
|
||||
// fully usable. Only the HOST boundary (final result, tool-call arguments, JSON.stringify)
|
||||
// serializes them to JSON forms via the default `copyIn` mode.
|
||||
const boundedData = (value: unknown, label: string): unknown => copyIn(value, label, true)
|
||||
|
||||
const isRuntimeReference = (value: unknown): boolean =>
|
||||
|
|
@ -675,7 +642,7 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean =>
|
|||
}
|
||||
if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise
|
||||
// Number/String/Boolean wrap primitives in JS; no boxed values exist in CodeMode, so
|
||||
// `x instanceof Number` is always false — exactly what it is for primitives in JS.
|
||||
// `x instanceof Number` is always false - exactly what it is for primitives in JS.
|
||||
if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -718,7 +685,7 @@ const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFlags = "
|
|||
|
||||
// A host match result as a sandbox value: a plain array of the full match and captures, with
|
||||
// `index` and named `groups` attached as own array properties (readable, and dropped at data
|
||||
// boundaries exactly like JSON.stringify drops them in JS). `input` is omitted — it duplicates
|
||||
// boundaries exactly like JSON.stringify drops them in JS). `input` is omitted - it duplicates
|
||||
// the whole subject string per match.
|
||||
const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
|
||||
const result: Array<unknown> = Array.from(match, (group) => group)
|
||||
|
|
@ -851,7 +818,7 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
|
|||
case "substring": result = value.substring(optNum(0) ?? 0, optNum(1)); break
|
||||
case "substr": result = value.substr(optNum(0) ?? 0, optNum(1)); break
|
||||
// JS charCodeAt returns NaN out of range; NaN flows as an ordinary in-sandbox value
|
||||
// (normalized to null only at the data boundary — see copyOut), so return it as-is.
|
||||
// (normalized to null only at the data boundary - see copyOut), so return it as-is.
|
||||
case "charCodeAt": result = value.charCodeAt(optNum(0) ?? 0); break
|
||||
case "codePointAt": result = value.codePointAt(optNum(0) ?? 0); break
|
||||
case "toString": result = value; break
|
||||
|
|
@ -947,7 +914,7 @@ const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode):
|
|||
const requireObject = (): Record<string, unknown> => {
|
||||
const value = boundedData(args[0], `Object.${name} input`)
|
||||
// Sandbox values (Date/RegExp/Map/Set) have no own enumerable properties in JS, so the
|
||||
// Object.* helpers see them as empty objects — never their interpreter internals.
|
||||
// Object.* helpers see them as empty objects - never their interpreter internals.
|
||||
if (isSandboxValue(value)) return {}
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object.`, node)
|
||||
|
|
@ -961,7 +928,7 @@ const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode):
|
|||
switch (name) {
|
||||
case "keys": {
|
||||
// Object.keys(array) yields index strings (["0", "1", ...]) exactly as in JS; objects
|
||||
// yield their own enumerable keys. (Tool references never reach here — the interpreter
|
||||
// yield their own enumerable keys. (Tool references never reach here - the interpreter
|
||||
// resolves them against the host tool tree first.)
|
||||
const value = boundedData(args[0], "Object.keys input")
|
||||
if (isSandboxValue(value)) return []
|
||||
|
|
@ -1054,7 +1021,7 @@ const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNode): un
|
|||
parsed = JSON.parse(text)
|
||||
} catch (error) {
|
||||
// The engine reason is derived from the program-supplied string (token/position), so
|
||||
// it is safe to surface — and the position is exactly what a model needs to fix it.
|
||||
// it is safe to surface - and the position is exactly what a model needs to fix it.
|
||||
throw new InterpreterRuntimeError(
|
||||
`JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
node,
|
||||
|
|
@ -1289,7 +1256,7 @@ class Interpreter<R> {
|
|||
globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) })
|
||||
}
|
||||
// NaN/Infinity flow as ordinary in-sandbox values (normalized to null only at the data
|
||||
// boundary — see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`.
|
||||
// boundary - see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`.
|
||||
globalScope.set("NaN", { mutable: false, value: NaN })
|
||||
globalScope.set("Infinity", { mutable: false, value: Infinity })
|
||||
}
|
||||
|
|
@ -1324,7 +1291,7 @@ class Interpreter<R> {
|
|||
if (!returned) value = self.lastValue
|
||||
|
||||
// The program body runs inside an implicit async function, so a returned promise
|
||||
// resolves before crossing the data boundary — `return tools.ns.tool(...)` works
|
||||
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
||||
// without an explicit await, exactly as in JS.
|
||||
if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)
|
||||
yield* self.drainPendingSettlements()
|
||||
|
|
@ -1333,7 +1300,7 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
|
||||
// their work completes before the execution ends — mirroring a JS runtime waiting on
|
||||
// their work completes before the execution ends - mirroring a JS runtime waiting on
|
||||
// in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
|
||||
// diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
|
||||
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
||||
|
|
@ -1347,7 +1314,7 @@ class Interpreter<R> {
|
|||
`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."],
|
||||
["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."],
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
@ -1355,8 +1322,8 @@ class Interpreter<R> {
|
|||
|
||||
// Eagerly starts a tool call on a supervised child fiber (so the execution timeout and
|
||||
// scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a
|
||||
// first-class promise value. `startImmediately` makes the runtime admit the call — charging
|
||||
// the tool-call budget and firing onToolCallStart — at the call site, before any await.
|
||||
// first-class promise value. `startImmediately` makes the runtime admit the call - charging
|
||||
// the tool-call budget and firing onToolCallStart - at the call site, before any await.
|
||||
private createToolCallPromise(path: ReadonlyArray<string>, args: Array<unknown>): Effect.Effect<SandboxPromise, never, R> {
|
||||
const self = this
|
||||
return Effect.map(
|
||||
|
|
@ -1738,7 +1705,7 @@ class Interpreter<R> {
|
|||
|
||||
// Own enumerable string keys of a value, shared by `for...in` and `Object.keys` over tool
|
||||
// references: plain data objects enumerate their own keys, arrays their index strings (plus
|
||||
// any own non-index properties, e.g. match results' index/groups — exactly Object.keys in
|
||||
// any own non-index properties, e.g. match results' index/groups - exactly Object.keys in
|
||||
// JS), and a tool reference the namespace/tool names at its path in the host tool tree.
|
||||
// Returns undefined for everything else so callers can raise a contextual error.
|
||||
private enumerableKeys(value: unknown): Array<string> | undefined {
|
||||
|
|
@ -1763,7 +1730,7 @@ class Interpreter<R> {
|
|||
|
||||
// Keys are snapshotted up front (mutation during iteration is safe): plain objects
|
||||
// enumerate their own keys, arrays their index strings, and tool references the
|
||||
// namespace/tool names at that node — the same enumeration Object.keys performs.
|
||||
// namespace/tool names at that node - the same enumeration Object.keys performs.
|
||||
// Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather
|
||||
// than real JS's surprising behavior (indices for strings, zero iterations for
|
||||
// Maps/Sets/null): the hint points at the constructs that do what the program means.
|
||||
|
|
@ -1864,7 +1831,7 @@ class Interpreter<R> {
|
|||
return Effect.failCause(cause)
|
||||
}
|
||||
|
||||
// The program sees a plain { message } error (or the thrown value itself) — see
|
||||
// The program sees a plain { message } error (or the thrown value itself) - see
|
||||
// caughtErrorValue, shared with Promise.allSettled rejection reasons.
|
||||
const caught = caughtErrorValue(Cause.squash(cause))
|
||||
const parameter = getOptionalNode(handler, "param")
|
||||
|
|
@ -1923,7 +1890,7 @@ class Interpreter<R> {
|
|||
return
|
||||
}
|
||||
|
||||
// Default values: `x = expr` / `{ a = 1 }` — the default is evaluated only when the value is undefined.
|
||||
// Default values: `x = expr` / `{ a = 1 }` - the default is evaluated only when the value is undefined.
|
||||
if (pattern.type === "AssignmentPattern") {
|
||||
const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
|
||||
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node)
|
||||
|
|
@ -1939,7 +1906,7 @@ class Interpreter<R> {
|
|||
for (const propertyValue of getArray(pattern, "properties")) {
|
||||
const property = asNode(propertyValue, "properties")
|
||||
|
||||
// Object rest: `{ a, ...others }` — gather the not-yet-consumed own keys.
|
||||
// Object rest: `{ a, ...others }` - gather the not-yet-consumed own keys.
|
||||
if (property.type === "RestElement") {
|
||||
const rest: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, item] of Object.entries(value as SafeObject)) {
|
||||
|
|
@ -1972,7 +1939,7 @@ class Interpreter<R> {
|
|||
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
||||
if (item === null) continue
|
||||
const element = asNode(item, `elements[${index}]`)
|
||||
// Array rest: `[head, ...tail]` — binds the remaining elements (must be last).
|
||||
// Array rest: `[head, ...tail]` - binds the remaining elements (must be last).
|
||||
if (element.type === "RestElement") {
|
||||
yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element)
|
||||
break
|
||||
|
|
@ -2051,7 +2018,7 @@ class Interpreter<R> {
|
|||
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.",
|
||||
"new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
|
||||
node,
|
||||
"UnsupportedSyntax",
|
||||
[supportedSyntaxMessage],
|
||||
|
|
@ -2086,7 +2053,7 @@ class Interpreter<R> {
|
|||
if (typeof arg === "string") return new SandboxDate(Date.parse(arg))
|
||||
return new SandboxDate(Number.NaN)
|
||||
}
|
||||
// new Date(year, month, day?, hours?, ...) — local-time component form.
|
||||
// new Date(year, month, day?, hours?, ...) - local-time component form.
|
||||
const parts = args.map((arg) => coerceToNumber(arg))
|
||||
return new SandboxDate(new Date(...(parts as [number, number])).getTime())
|
||||
}
|
||||
|
|
@ -2161,8 +2128,8 @@ class Interpreter<R> {
|
|||
const operator = getString(node, "operator")
|
||||
const self = this
|
||||
return Effect.gen(function*() {
|
||||
const lhs = (yield* self.evaluateExpression(getNode(node, "left"))) as any
|
||||
const rhs = (yield* self.evaluateExpression(getNode(node, "right"))) as any
|
||||
const lhs = yield* self.evaluateExpression(getNode(node, "left"))
|
||||
const rhs = yield* self.evaluateExpression(getNode(node, "right"))
|
||||
// Like `typeof`, `instanceof` observes any value without coercing it (a promise or
|
||||
// function operand is a legitimate question, not an error), so it is handled before
|
||||
// the data-only operand check.
|
||||
|
|
@ -2176,7 +2143,7 @@ class Interpreter<R> {
|
|||
* semantics. Shared by binary expressions and compound assignment (`x op= y` must behave
|
||||
* exactly like `x = x op y`, coercion included).
|
||||
*/
|
||||
private applyBinaryOperator(operator: string, lhs: any, rhs: any, node: AstNode): unknown {
|
||||
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")
|
||||
}
|
||||
|
|
@ -2184,37 +2151,37 @@ class Interpreter<R> {
|
|||
// "No default value" TypeError when an operator coerces them. Coerce to their JS string
|
||||
// form first (as String(x) / template literals do) so operators behave like JavaScript.
|
||||
// A Date follows its ToPrimitive hints: string for `+` (concatenation), its time value
|
||||
// for arithmetic and ordering — so `end - start` and `a < b` work as in JS.
|
||||
// for arithmetic and ordering - so `end - start` and `a < b` work as in JS.
|
||||
// Identity (=== / !==) and the right operand of `in` keep their raw object value.
|
||||
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) as any
|
||||
const r = coerceOperand(rhs) as any
|
||||
const l = coerceOperand(lhs)
|
||||
const r = coerceOperand(rhs)
|
||||
switch (operator) {
|
||||
case "+": return l + r
|
||||
case "-": return l - r
|
||||
case "*": return l * r
|
||||
case "/": return l / r
|
||||
case "%": return l % r
|
||||
case "**": return l ** r
|
||||
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)
|
||||
// Two objects compare by identity in JS (no ToPrimitive); only object-vs-primitive coerces.
|
||||
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 < r
|
||||
case "<=": return l <= r
|
||||
case ">": return l > r
|
||||
case ">=": return l >= r
|
||||
case "&": return l & r
|
||||
case "|": return l | r
|
||||
case "^": return l ^ r
|
||||
case "<<": return l << r
|
||||
case ">>": return l >> r
|
||||
case ">>>": return l >>> r
|
||||
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)
|
||||
|
|
@ -2245,27 +2212,26 @@ class Interpreter<R> {
|
|||
return Effect.succeed("undefined")
|
||||
}
|
||||
return Effect.map(this.evaluateExpression(argument), (value) => {
|
||||
// `typeof` and `!` never throw in JS — they observe any value (functions and runtime
|
||||
// `typeof` and `!` never throw in JS - they observe any value (functions and runtime
|
||||
// references included) without coercing it, so feature detection and negation work.
|
||||
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 rhs = value as any
|
||||
// Numeric/bitwise unary operators ToPrimitive their operand; a Date yields its time value
|
||||
// (`+date` is the epoch-ms idiom), other null-prototype data objects/arrays coerce to
|
||||
// their JS string form first (see evaluateBinaryExpression).
|
||||
const operand = rhs instanceof SandboxDate
|
||||
? (rhs.time as any)
|
||||
: rhs !== null && typeof rhs === "object"
|
||||
? (coerceToString(rhs) as any)
|
||||
: rhs
|
||||
const operand = value instanceof SandboxDate
|
||||
? value.time
|
||||
: value !== null && typeof value === "object"
|
||||
? coerceToString(value)
|
||||
: value
|
||||
let result: unknown
|
||||
switch (operator) {
|
||||
case "+": result = +operand; break
|
||||
case "-": result = -operand; break
|
||||
case "~": result = ~operand; break
|
||||
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")
|
||||
|
|
@ -2398,7 +2364,7 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate
|
||||
// namespace/tool names from the host tool tree — the discovery idiom a model reaches for
|
||||
// namespace/tool names from the host tool tree - the discovery idiom a model reaches for
|
||||
// first. Every other Object helper cannot produce data from a tool reference, so it fails
|
||||
// with a pointer at the working idioms instead of the generic plain-objects-only message.
|
||||
private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown {
|
||||
|
|
@ -2426,11 +2392,11 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Console arguments format deeply and totally: values render as a debugger would show them
|
||||
// rather than as boundary JSON — numbers keep NaN/Infinity (JSON would say null), sandbox
|
||||
// rather than as boundary JSON - numbers keep NaN/Infinity (JSON would say null), sandbox
|
||||
// values keep their friendly forms at ANY depth (ISO date, /regex/flags, Map(n) [...],
|
||||
// Set(n) [...]), opaque runtime references become "[CodeMode reference]" markers in place,
|
||||
// and plain objects/arrays render JSON-style. Formatting never fails the program: cycles
|
||||
// render "[Circular]" and extreme depth degrades to "…".
|
||||
// render "[Circular]" and extreme depth degrades to "...".
|
||||
private formatConsoleArgument(value: unknown): string {
|
||||
if (value === undefined) return "undefined"
|
||||
// A top-level string prints bare; nested strings are JSON-quoted (see formatConsoleValue).
|
||||
|
|
@ -2448,7 +2414,7 @@ class Interpreter<R> {
|
|||
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 (depth > MAX_CONSOLE_DEPTH) return "…"
|
||||
if (depth > MAX_CONSOLE_DEPTH) return "..."
|
||||
if (seen.has(value)) return "[Circular]"
|
||||
if (value instanceof SandboxMap) {
|
||||
seen.add(value)
|
||||
|
|
@ -2544,8 +2510,8 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Promise.* over ordinary runtime values. Combinators accept ANY array (or spreadable
|
||||
// collection) mixing promise values and plain data — built inline, beforehand, via spread,
|
||||
// whatever — because tool calls already run eagerly on their own fibers; the combinators
|
||||
// collection) mixing promise values and plain data - built inline, beforehand, via spread,
|
||||
// whatever - because tool calls already run eagerly on their own fibers; the combinators
|
||||
// only observe settlements. Joining is therefore sequential (no extra fibers) without
|
||||
// costing parallelism, and the concurrency cap stays where the work is: the fork semaphore.
|
||||
private invokePromiseMethod(ref: PromiseMethodReference, args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
|
|
@ -2638,7 +2604,7 @@ class Interpreter<R> {
|
|||
const run = Effect.gen(function*() {
|
||||
// Seed every parameter name into the scope as a TDZ slot first, so a default that
|
||||
// references another parameter resolves to that (uninitialized) param rather than
|
||||
// silently falling through to an outer binding of the same name — matching JS.
|
||||
// silently falling through to an outer binding of the same name - matching JS.
|
||||
const paramScope = self.currentScope()
|
||||
for (const parameter of fn.parameters) {
|
||||
for (const name of collectPatternNames(parameter)) {
|
||||
|
|
@ -2858,7 +2824,7 @@ class Interpreter<R> {
|
|||
: self.invokeFunction(callback, callbackArgs)
|
||||
return Effect.gen(function*() {
|
||||
// Iterate a snapshot taken at call time so a callback that mutates the array can't
|
||||
// self-extend the loop — matching JS, where elements appended during iteration are not visited.
|
||||
// self-extend the loop - matching JS, where elements appended during iteration are not visited.
|
||||
const items = target.slice()
|
||||
switch (name) {
|
||||
case "map": {
|
||||
|
|
@ -2976,7 +2942,7 @@ class Interpreter<R> {
|
|||
let rightIndex = 0
|
||||
while (leftIndex < left.length && rightIndex < right.length) {
|
||||
// Coerce the comparator's result like JS ToNumber (data objects -> NaN, never a host
|
||||
// crash) and treat NaN as 0 — the spec's "no consistent order" → keep the left element.
|
||||
// crash) and treat NaN as 0 - the spec's "no consistent order" -> keep the left element.
|
||||
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++])
|
||||
|
|
@ -3174,7 +3140,7 @@ class Interpreter<R> {
|
|||
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)
|
||||
// Unknown property on a string reads as `undefined`, matching JS (`"x".foo === undefined`),
|
||||
// instead of throwing — so defensive access like `result?.login ?? result` on a JSON-string
|
||||
// instead of throwing - so defensive access like `result?.login ?? result` on a JSON-string
|
||||
// tool result doesn't crash. (Optional chaining only guards null/undefined receivers, so a
|
||||
// real string still reaches here.) Only the method allowlist above yields callables.
|
||||
return new ComputedValue(undefined)
|
||||
|
|
@ -3225,14 +3191,14 @@ class Interpreter<R> {
|
|||
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(...)\`.`,
|
||||
`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(...)`.",
|
||||
"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",
|
||||
)
|
||||
|
|
@ -3258,7 +3224,7 @@ class Interpreter<R> {
|
|||
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
|
||||
}
|
||||
// Unknown property on an array reads as `undefined`, matching JS (`[1,2].foo === undefined`),
|
||||
// instead of throwing — so defensive access under optional chaining behaves as expected.
|
||||
// instead of throwing - so defensive access under optional chaining behaves as expected.
|
||||
return new ComputedValue(undefined)
|
||||
}
|
||||
return { target: objectValue, key }
|
||||
|
|
@ -3294,7 +3260,7 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression
|
||||
// runs once), then lets `compute` decide whether to write — enabling compound assignment,
|
||||
// runs once), then lets `compute` decide whether to write - enabling compound assignment,
|
||||
// updates, plain writes, and short-circuiting logical assignment to share one safe path.
|
||||
private modifyMember(
|
||||
node: AstNode,
|
||||
|
|
@ -3329,7 +3295,7 @@ class Interpreter<R> {
|
|||
}
|
||||
|
||||
// Rejects inserting a value that (transitively) contains the container it is being inserted
|
||||
// into — the mutation that would create a circular structure no later walk could survive.
|
||||
// into - the mutation that would create a circular structure no later walk could survive.
|
||||
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
|
||||
|
|
@ -3384,7 +3350,7 @@ class Interpreter<R> {
|
|||
throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
|
||||
}
|
||||
|
||||
// A parameter default that forward-references a later (not-yet-bound) parameter — JS TDZ.
|
||||
// A parameter default that forward-references a later (not-yet-bound) parameter - JS TDZ.
|
||||
if (binding.initialized === false) {
|
||||
throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError")
|
||||
}
|
||||
|
|
@ -3532,7 +3498,7 @@ const utf8Truncate = (value: string, maxBytes: number): string => {
|
|||
* 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.
|
||||
* `maxOutputBytes` - with the limit absent, output passes through unbounded.
|
||||
*/
|
||||
const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResult => {
|
||||
let truncated = false
|
||||
|
|
|
|||
|
|
@ -118,9 +118,9 @@ export const isBlockedMember = (name: string): boolean => blockedMemberNames.has
|
|||
* objects only, blocked properties, data-only leaves).
|
||||
*
|
||||
* Two modes share the walk:
|
||||
* - **Boundary** (`preserveSandboxValues` false, the default): the host↔sandbox boundary —
|
||||
* - **Boundary** (`preserveSandboxValues` false, the default): the host<->sandbox boundary -
|
||||
* final results, tool-call arguments, `JSON.stringify`. Sandbox value types serialize
|
||||
* exactly as JSON.stringify would: Date → ISO string (invalid → null), RegExp/Map/Set → {}.
|
||||
* exactly as JSON.stringify would: Date -> ISO string (invalid -> null), RegExp/Map/Set -> {}.
|
||||
* - **Intra-sandbox checkpoint** (`preserveSandboxValues` true; see `boundedData` in
|
||||
* codemode.ts): Date/RegExp/Map/Set instances pass through untouched (treated as leaves,
|
||||
* contents not walked), so values flowing through `Object.*` helpers, coercion inputs, and
|
||||
|
|
@ -142,7 +142,7 @@ const copyBounded = (value: unknown, label: string, depth: number, seen: Set<obj
|
|||
typeof value === "boolean" ||
|
||||
// NaN/Infinity are allowed to exist as in-sandbox intermediates (matching real JS and a real
|
||||
// engine) so defensive guards like `Number.isNaN(x)` / `parseInt(x) || 0` can run. They are
|
||||
// normalized to `null` when the value leaves the sandbox — see copyOut — exactly as
|
||||
// normalized to `null` when the value leaves the sandbox - see copyOut - exactly as
|
||||
// JSON.stringify already does at any tool boundary.
|
||||
typeof value === "number"
|
||||
) {
|
||||
|
|
@ -234,7 +234,7 @@ const copyBounded = (value: unknown, label: string, depth: number, seen: Set<obj
|
|||
export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
|
||||
if (value === undefined && undefinedAsNull) return null
|
||||
// Normalize non-finite numbers to null as the value crosses out of the sandbox (final return
|
||||
// and tool-call arguments both funnel through here), matching JSON semantics — NaN/Infinity
|
||||
// and tool-call arguments both funnel through here), matching JSON semantics - NaN/Infinity
|
||||
// have no JSON representation, so JSON.stringify would produce null anyway.
|
||||
if (typeof value === "number" && !Number.isFinite(value)) {
|
||||
return null
|
||||
|
|
@ -311,7 +311,7 @@ const tokenize = (query: string): Array<string> =>
|
|||
* A term plus its naive singular variants (trailing "s"/"es" stripped), so a plural
|
||||
* query term ("issues") still matches indexed text that only carries the singular
|
||||
* ("issue"). Matching is one-directional substring containment, so the variants are
|
||||
* needed only on the query side; scoring weights are unchanged — each field check
|
||||
* needed only on the query side; scoring weights are unchanged - each field check
|
||||
* passes when ANY form matches.
|
||||
*/
|
||||
const termForms = (term: string): Array<string> => {
|
||||
|
|
@ -326,7 +326,7 @@ const firstLine = (text: string) => text.split("\n", 1)[0]!.trim()
|
|||
/** One-line description used on inline catalog lines; the full text stays in search results. */
|
||||
const brief = (text: string, max = 120) => {
|
||||
const line = firstLine(text)
|
||||
return line.length > max ? line.slice(0, max - 1) + "…" : line
|
||||
return line.length > max ? line.slice(0, max - 1) + "..." : line
|
||||
}
|
||||
|
||||
const catalogLine = (tool: ToolDescription) => {
|
||||
|
|
@ -359,11 +359,11 @@ export const assertValidTools = <R>(tools: HostTools<R>): void => {
|
|||
/**
|
||||
* Budgeted catalog: every namespace is always listed with its tool count; full call
|
||||
* signatures are inlined against the `maxInlineCatalogTokens` budget (estimated tokens,
|
||||
* chars/4) round-robin across namespaces — in each round (namespaces alphabetical), every
|
||||
* chars/4) round-robin across namespaces - in each round (namespaces alphabetical), every
|
||||
* namespace still holding un-inlined tools attempts to place its next-cheapest line, and
|
||||
* a namespace whose next line does not fit is done while the others keep going — so every
|
||||
* a namespace whose next line does not fit is done while the others keep going - so every
|
||||
* namespace gets some representation before any namespace gets everything. The section
|
||||
* states exactly how comprehensive it is — overall (COMPLETE vs PARTIAL) and per
|
||||
* states exactly how comprehensive it is - overall (COMPLETE vs PARTIAL) and per
|
||||
* namespace. Namespace stub lines are never budgeted: every namespace appears with its
|
||||
* tool count even at budget 0.
|
||||
*/
|
||||
|
|
@ -390,7 +390,7 @@ export const discoveryPlan = <R>(
|
|||
// exactly how comprehensive it is. Round-robin fairness: in each round (namespaces
|
||||
// alphabetical), every namespace still holding un-inlined tools tries to place its
|
||||
// next-cheapest line against the shared budget; a namespace whose next line does not
|
||||
// fit is done — the others keep going — so every namespace gets some representation
|
||||
// fit is done - the others keep going - so every namespace gets some representation
|
||||
// before any namespace gets everything.
|
||||
const selections = ordered.map(([namespace, group]) => ({
|
||||
namespace,
|
||||
|
|
@ -424,7 +424,7 @@ export const discoveryPlan = <R>(
|
|||
|
||||
// Section order is deliberate: workflow first (the top is the least likely part of a long
|
||||
// description to be truncated or skimmed away), then rules, then syntax, with the budgeted
|
||||
// catalog at the bottom. Example call forms use explicit `<namespace>.<tool>` placeholders —
|
||||
// catalog at the bottom. Example call forms use explicit `<namespace>.<tool>` placeholders -
|
||||
// never a real or fabricated tool name.
|
||||
const intro = [
|
||||
"Write a CodeMode program to answer the request. Return code only.",
|
||||
|
|
@ -445,17 +445,17 @@ export const discoveryPlan = <R>(
|
|||
"",
|
||||
...(complete
|
||||
? [
|
||||
"1. Pick a tool from the list under `## Available tools` — each line is the exact call signature; use it as-is rather than guessing segments.",
|
||||
"2. Call it using the exact signature shown: `const res = await tools.<namespace>.<tool>(input)` — bracket notation may appear for names that are not JavaScript identifiers.",
|
||||
'3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string.',
|
||||
"4. Return only the fields you need: `return { <field>: data.<field> }` — raw payloads get truncated and waste context.",
|
||||
"1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
|
||||
"2. Call it using the exact signature shown: `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
|
||||
'3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
||||
"4. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
||||
]
|
||||
: [
|
||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` — short phrases like "list issues" work best.',
|
||||
"2. Read the matches: each item is `{ path, description, signature }` — read the description before using an unfamiliar tool.",
|
||||
"3. Call it with the result's `path` as-is (never guess segments): `const res = await tools.<namespace>.<tool>(input)` — bracket notation may appear for names that are not JavaScript identifiers.",
|
||||
'4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string.',
|
||||
"5. Return only the fields you need: `return { <field>: data.<field> }` — raw payloads get truncated and waste context.",
|
||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
||||
"2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.",
|
||||
"3. Call it with the result's `path` as-is (never guess segments): `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
|
||||
'4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
||||
"5. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
||||
]),
|
||||
]
|
||||
|
||||
|
|
@ -468,8 +468,8 @@ export const discoveryPlan = <R>(
|
|||
complete
|
||||
? "- Only tools listed here are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed."
|
||||
: "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.",
|
||||
"- Filter, aggregate, and transform collections in code — never return them raw or call a tool per item across messages.",
|
||||
"- A result typed `Promise<unknown>` has no guaranteed shape — verify what actually came back before relying on its fields.",
|
||||
"- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
|
||||
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
|
||||
"- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
|
||||
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
||||
...(complete ? [] : ['- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.']),
|
||||
|
|
@ -491,8 +491,8 @@ export const discoveryPlan = <R>(
|
|||
} else {
|
||||
toolSection.push(
|
||||
complete
|
||||
? "## Available tools (COMPLETE list — every tool is shown below with its full call signature)"
|
||||
: `## Available tools (PARTIAL — ${totalShown} of ${described.length} shown; find the rest with tools.$codemode.search)`,
|
||||
? "## Available tools (COMPLETE list - every tool is shown below with its full call signature)"
|
||||
: `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with tools.$codemode.search)`,
|
||||
"",
|
||||
)
|
||||
for (const [namespace, group] of ordered) {
|
||||
|
|
@ -528,8 +528,8 @@ export const discoveryPlan = <R>(
|
|||
}
|
||||
|
||||
/**
|
||||
* The enumerable names at one node of the host tool tree — namespace names at the root,
|
||||
* tool/namespace names below — powering `Object.keys(tools)` and `for...in` over tool
|
||||
* The enumerable names at one node of the host tool tree - namespace names at the root,
|
||||
* tool/namespace names below - powering `Object.keys(tools)` and `for...in` over tool
|
||||
* references. A callable tool is a leaf and enumerates as `[]` (like `Object.keys` of a
|
||||
* function in JS). An unknown path is an `UnknownTool` error pointing at the working
|
||||
* discovery idioms, mirroring how calling an unknown tool fails.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Effect, Schema } from "effect"
|
|||
* JSON Schema subset accepted for render-only tool schemas.
|
||||
*
|
||||
* A JSON-Schema-described side of a tool is used to generate the model-visible TypeScript
|
||||
* signature only — CodeMode performs no validation against it. This is the natural shape for
|
||||
* signature only - CodeMode performs no validation against it. This is the natural shape for
|
||||
* adapter-provided tools (e.g. MCP definitions) whose schemas arrive as JSON Schema documents.
|
||||
*/
|
||||
export type JsonSchema = {
|
||||
|
|
@ -63,7 +63,7 @@ const isEffectSchema = (schema: ToolSchema): schema is Schema.Decoder<unknown> &
|
|||
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
|
||||
|
||||
/**
|
||||
* Bare TypeScript identifier — usable unquoted as an object key (and, in the tool runtime,
|
||||
* Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime,
|
||||
* with dot access as a tool-path segment). Anything else must be quoted/bracketed.
|
||||
*/
|
||||
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
||||
|
|
@ -77,7 +77,7 @@ const effectNumberSentinel = (schema: JsonSchema) =>
|
|||
|
||||
/**
|
||||
* Recursion ceiling for schema rendering. Object, array, and union recursion all increment
|
||||
* depth, so this bounds every recursion path — pathological or structurally cyclic schemas
|
||||
* depth, so this bounds every recursion path - pathological or structurally cyclic schemas
|
||||
* degrade to `unknown` instead of overflowing the stack (rendering must never throw).
|
||||
*/
|
||||
const MAX_RENDER_DEPTH = 8
|
||||
|
|
@ -111,7 +111,7 @@ const docTags = (schema: JsonSchema): Array<string> => {
|
|||
|
||||
/**
|
||||
* Format a schema `description` plus `tags` as a JSDoc comment at the given indent,
|
||||
* preserving multi-line text (a single line stays `/** … *\/`; multiple lines become a
|
||||
* preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a
|
||||
* `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and
|
||||
* blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so
|
||||
* callers can prepend it directly to the field line.
|
||||
|
|
@ -220,7 +220,7 @@ export type InputProperty = {
|
|||
}
|
||||
|
||||
/**
|
||||
* The property names, descriptions, and required flags of a tool's input schema — the raw
|
||||
* The property names, descriptions, and required flags of a tool's input schema - the raw
|
||||
* material for search text. Best-effort: Effect Schemas go through their
|
||||
* JSON Schema document (the same emission signature rendering uses); JSON Schemas are read
|
||||
* directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present.
|
||||
|
|
@ -299,7 +299,7 @@ export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unkn
|
|||
* document. Effect Schema input is decoded before `run` is invoked, and `run` returns the
|
||||
* encoded representation of an Effect Schema `output`, which CodeMode decodes before returning
|
||||
* it to the program. JSON Schemas only shape the model-visible signature; values pass through
|
||||
* unvalidated. `output` is optional — without it the signature advertises `unknown` and the
|
||||
* unvalidated. `output` is optional - without it the signature advertises `unknown` and the
|
||||
* host result is exposed as-is. The host tool remains responsible for authorization and
|
||||
* durable side-effect handling.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,36 +1,17 @@
|
|||
// Sandbox value types backed by host primitives. They live in their own module so both the
|
||||
// interpreter (codemode.ts) and the data boundary (tool-runtime.ts) can reference them without
|
||||
// a circular import. All four are opaque runtime values inside a program; when a value crosses
|
||||
// the sandbox boundary (final result, tool arguments, JSON.stringify) they serialize exactly as
|
||||
// JSON.stringify would: Date -> ISO string (invalid -> null), RegExp/Map/Set -> {}.
|
||||
|
||||
import type { Effect, Fiber } from "effect"
|
||||
|
||||
/**
|
||||
* A first-class promise value produced by an un-awaited tool call (or by
|
||||
* `Promise.resolve`/`Promise.reject`). Tool-call promises are eager: the call runs on a fiber
|
||||
* forked at call time, and `await` observes that fiber's settlement. Promises are opaque
|
||||
* runtime references — `typeof` reports `"object"` (as in real JS), operators reject them, and
|
||||
* they cannot cross a data boundary un-awaited (the boundary raises an await-hinting
|
||||
* diagnostic instead of serializing `{}`).
|
||||
*/
|
||||
export class SandboxPromise {
|
||||
/** Set when Promise.race interrupts this promise's in-flight call after another entry wins. */
|
||||
interrupted = false
|
||||
constructor(
|
||||
/** Backing fiber for an eagerly started tool call; undefined for resolve/reject promises. */
|
||||
readonly fiber: Fiber.Fiber<unknown, unknown> | undefined,
|
||||
/** Immediate settlement for fiberless promises (Promise.resolve / Promise.reject). */
|
||||
readonly immediate?: Effect.Effect<unknown, unknown>,
|
||||
) {}
|
||||
}
|
||||
|
||||
/** An immutable instant, backed by an epoch-milliseconds time value (NaN = Invalid Date). */
|
||||
export class SandboxDate {
|
||||
constructor(readonly time: number) {}
|
||||
}
|
||||
|
||||
/** A regular expression backed by the host engine; `lastIndex` state lives on the host regex. */
|
||||
export class SandboxRegExp {
|
||||
readonly regex: RegExp
|
||||
constructor(pattern: string, flags: string) {
|
||||
|
|
@ -38,12 +19,10 @@ export class SandboxRegExp {
|
|||
}
|
||||
}
|
||||
|
||||
/** A keyed collection with SameValueZero keys. */
|
||||
export class SandboxMap {
|
||||
readonly map = new Map<unknown, unknown>()
|
||||
}
|
||||
|
||||
/** A unique-value collection. */
|
||||
export class SandboxSet {
|
||||
readonly set = new Set<unknown>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -561,14 +561,14 @@ describe("CodeMode public contract", () => {
|
|||
expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Syntax"))
|
||||
expect(instructions.indexOf("## Syntax")).toBeLessThan(instructions.indexOf("\n## Available tools (COMPLETE list"))
|
||||
// The workflow carries the result-shape guidance; Rules only add content beyond it.
|
||||
expect(instructions).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string')
|
||||
expect(instructions).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string')
|
||||
expect(instructions).toContain("Return only the fields you need")
|
||||
expect(instructions).toContain("raw payloads get truncated and waste context")
|
||||
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
||||
expect(instructions).toContain("surrounding agent tools are not available unless listed here")
|
||||
expect(instructions).toContain("Only tools listed here are available inside `tools`")
|
||||
expect(instructions).toContain("bracket notation may appear for names that are not JavaScript identifiers")
|
||||
// Placeholders use the <namespace>.<tool>/<field> style ONLY — no fabricated tool
|
||||
// Placeholders use the <namespace>.<tool>/<field> style ONLY - no fabricated tool
|
||||
// names, and no real catalog tools cherry-picked into example lines.
|
||||
expect(instructions).toContain("`return { <field>: data.<field> }`")
|
||||
expect(instructions).not.toContain("total_count")
|
||||
|
|
@ -582,7 +582,7 @@ describe("CodeMode public contract", () => {
|
|||
// PARTIAL: the workflow starts with search (with query-style guidance that is clearly
|
||||
// a query string, never a tool name) and the browse-namespace rule appears.
|
||||
expect(partial).toContain(
|
||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` — short phrases like "list issues" work best.',
|
||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
||||
)
|
||||
expect(partial).toContain("Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`")
|
||||
expect(partial).toContain('- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.')
|
||||
|
|
@ -635,7 +635,7 @@ describe("CodeMode public contract", () => {
|
|||
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
||||
discovery: { maxInlineCatalogTokens: 0 },
|
||||
})
|
||||
expect(runtime.instructions()).toContain("Available tools (PARTIAL — 0 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(runtime.instructions()).toContain("- thread (2 tools, none shown)")
|
||||
expect(runtime.instructions()).toContain("- orders (1 tool, none shown)")
|
||||
expect(runtime.instructions()).toMatch(/\$codemode\.search/)
|
||||
|
|
@ -783,7 +783,7 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
const runtime = CodeMode.make({ tools: { files: { upload, other } } })
|
||||
|
||||
// "attachment" appears in neither path nor description — only in the input schema's
|
||||
// "attachment" appears in neither path nor description - only in the input schema's
|
||||
// property names, which the searchable text includes.
|
||||
const byParameter = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "attachment" })`,
|
||||
|
|
@ -817,7 +817,7 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
// Neither path nor description contains "issues" — only the singular "issue".
|
||||
// Neither path nor description contains "issues" - only the singular "issue".
|
||||
tracker: { fetch_all: simple("Fetch every open issue in the project") },
|
||||
github: { list_issues: simple("List issues") },
|
||||
misc: { rename: simple("Rename the workspace") },
|
||||
|
|
@ -888,7 +888,7 @@ describe("CodeMode public contract", () => {
|
|||
run: () => Effect.succeed("ok"),
|
||||
})
|
||||
// Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2
|
||||
// alpha.expensive does not fit, which marks only alpha done — it must NOT prevent
|
||||
// alpha.expensive does not fit, which marks only alpha done - it must NOT prevent
|
||||
// other namespaces from inlining (beta already got its line in the same round).
|
||||
const runtime = CodeMode.make({
|
||||
tools: { alpha: { cheap, expensive }, beta: { cheap } },
|
||||
|
|
@ -896,7 +896,7 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
const instructions = runtime.instructions()
|
||||
expect(instructions).toContain("Available tools (PARTIAL — 2 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(instructions).toContain("- alpha (2 tools, 1 shown)")
|
||||
expect(instructions).toContain(" - tools.alpha.cheap(input: { q: string }): Promise<string> // Cheap")
|
||||
expect(instructions).not.toContain("tools.alpha.expensive(")
|
||||
|
|
@ -972,7 +972,7 @@ describe("CodeMode public contract", () => {
|
|||
|
||||
test("timeoutMs and maxToolCalls have no defaults: absent means unlimited", async () => {
|
||||
// 150 tool calls would have exceeded the old default cap of 100; with no limits
|
||||
// provided, there is no cap and no timeout — budgets are host policy.
|
||||
// provided, there is no cap and no timeout - budgets are host policy.
|
||||
const counter = Tool.make({
|
||||
description: "Count invocations",
|
||||
input: Schema.Struct({}),
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
|
|||
expect(await value(`return parseInt("abc") || 0`)).toBe(0)
|
||||
expect(await value(`const x = Number("abc"); return Number.isNaN(x) ? 0 : x`)).toBe(0)
|
||||
expect(await value(`const o = {}; o.count = (o.count || 0) + 1; return o.count`)).toBe(1)
|
||||
// average of an empty list, guarded — the classic divide-by-zero that used to throw pre-guard
|
||||
// average of an empty list, guarded - the classic divide-by-zero that used to throw pre-guard
|
||||
expect(await value(`const a = []; return a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0`)).toBe(0)
|
||||
})
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
|
|||
expect(await value(`return 5/0`)).toBeNull()
|
||||
expect(await value(`return 0/0`)).toBeNull()
|
||||
expect(await value(`return Math.max()`)).toBeNull()
|
||||
// nested, too — normalization walks the returned structure
|
||||
// nested, too - normalization walks the returned structure
|
||||
expect(await value(`return { a: Number("x"), b: 2, c: [1/0] }`)).toEqual({ a: null, b: 2, c: [null] })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||
)
|
||||
expect(result).toEqual([1, 2, 3, 4])
|
||||
// maxActive counts truly-overlapping live executions, so > 1 proves real
|
||||
// parallelism deterministically — no wall-clock assertion needed.
|
||||
// parallelism deterministically - no wall-clock assertion needed.
|
||||
expect(trace.maxActive).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ describe("timeout interruption of forked calls", () => {
|
|||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
expect(result.error.kind).toBe("TimeoutExceeded")
|
||||
// Both calls started; neither escaped the timeout — the awaited one AND the abandoned one.
|
||||
// Both calls started; neither escaped the timeout - the awaited one AND the abandoned one.
|
||||
expect(trace.starts).toEqual([1, 2])
|
||||
expect(trace.interrupted).toBe(2)
|
||||
expect(trace.completed).toBe(0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue