Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/codemode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,12 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports:
- 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). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. 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.
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention.
- 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.
- 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). Promises support `then`/`catch`/`finally`, including returned-promise flattening and rejection recovery. `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve` preserves an existing promise or wraps a plain value, and `Promise.reject` creates a rejected promise. `Promise.allSettled` rejection reasons use the same values a `catch` binding sees, and `Promise.race` lets losing promises continue. 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`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.

Inside a program, standard-library 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 they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams 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`), 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.

Expand Down
59 changes: 54 additions & 5 deletions packages/codemode/codemode.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
### Tool execution

Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be
awaited directly or through the supported `Promise` combinators. At most eight tool calls execute concurrently.
Unfinished calls are drained before successful program completion, and an unhandled call failure becomes a diagnostic.
awaited directly, chained with `then`/`catch`/`finally`, or passed through the supported `Promise` combinators. At most
eight tool calls execute concurrently.
Unfinished tracked promises are drained before successful program completion, and an unhandled rejection becomes a
diagnostic.

The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no
defaults because budgets are host policy. The interpreter also enforces fixed internal boundaries for tool-call
Expand Down Expand Up @@ -109,6 +111,56 @@ MCP tools use this canonical path: they register as grouped tools and are deferr
output schemas are preserved in generated signatures. Direct Core tools remain direct and are not ambient globals
inside CodeMode.

## Promise Status

The runtime currently provides eager, run-once promises for tool calls and async functions; `await`;
`then`/`catch`/`finally`; and chainable `all`/`allSettled`/`race`/`resolve`/`reject`. `Promise.all` rejects promptly while
siblings continue, and `Promise.race` leaves losers running as JavaScript does. Tracked work remains supervised in one
execution scope, at most eight tool calls run concurrently, and ordinary success or failure drains unfinished work before
closing. Timeout and external interruption cancel immediately instead.

### Confirmed defects

- [ ] Return rejected promises for invalid `Promise.all`/`allSettled`/`race` inputs instead of throwing during the call.
- [ ] Align handler callability with the values CodeMode reports as functions, or document the narrower callback
allowlist. For example, unsupported constructor-like callables are currently treated as absent handlers.

### Deliberate deviations and open decisions

- CodeMode drains unfinished work before ordinary success or failure closes. This keeps tool effects supervised, but a
race loser or fail-fast `Promise.all` sibling that never settles can hold execution open indefinitely when the host
supplies no timeout.
- Promise resolution unwraps only `SandboxPromise`; arbitrary `{ then(resolve, reject) }` values remain data. Full
thenable assimilation requires internal callable resolver values, first-settlement arbitration, recursive adoption,
and cycle detection. Decide whether that machinery belongs in the bounded runtime.
- `new Promise`, `Promise.any`, resolver APIs, subclasses/species, and the broader prototype surface are unavailable.
Consider `Promise.any` independently; custom constructors and subclassing are not current goals.
- Combinators currently accept arrays plus CodeMode's spreadable strings, Maps, and Sets, while documentation and
diagnostics describe array inputs. Choose and document one contract.
- `Promise.race([])` raises a clear error instead of creating a permanently pending promise.
- Rejection tracking is execution-scoped and checked at drain time, not an ECMAScript microtask-level unhandled
rejection model.

### Covered regressions

- Nested unreturned tool calls remain alive after an async function or `then` handler settles.
- Abandoned failing tool calls, async functions, and immediate `Promise.reject` values report unhandled rejections.
- Ordinary program failure drains pending work and preserves the original error; a rejecting race winner drains its slow
loser.
- Timeouts interrupt all in-flight promise fibers with parallel teardown, while host interruption propagates instead of
becoming a diagnostic.
- Promise reactions and plain, pending, or settled `await` continuations start in deterministic FIFO order. Nested
reactions preserve enqueue order, while an async reaction can suspend without blocking the next queued reaction.
- Awaiting the same promise twice settles it once.

### Missing coverage

- Nested unreturned work from `catch` and `finally` handlers.
- Abandoned chained and combinator rejections.
- External interruption while handled pending work remains.
- Never-settling race losers and fail-fast `Promise.all` siblings under an explicit timeout.
- Shared or duplicate promises across combinators, discarded inner chains, and detailed reaction ordering.

## Intentionally Unsupported

These are product boundaries rather than DSL backlog:
Expand Down Expand Up @@ -148,9 +200,6 @@ the adapter TODO. Delete entries when completed.
The supported JavaScript subset should grow when common model-generated code improves tool orchestration. These are
current omissions to implement, not intentional product boundaries.

- [ ] Design proper multi-stage promise pipelines. Supporting `.then`, `.catch`, and `.finally` should preserve promise
assimilation, cancellation, failure handling, and concurrent per-item pipelines rather than adding syntax-only
shims. Consider `Promise.any` in the same pass.
- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and
collection values, then extend it to bounded host streams when a stream boundary exists.
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
Expand Down
2 changes: 1 addition & 1 deletion packages/codemode/src/interpreter/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export type DiagnosticKind =
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")

export 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/URL/URLSearchParams, URI encoding helpers, 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 or .then/.catch/.finally), 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/URL/URLSearchParams, URI encoding helpers, 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."

export class InterpreterRuntimeError extends Error {
readonly node?: AstNode
Expand Down
Loading
Loading