docs(codemode): track promise status

This commit is contained in:
Aiden Cline 2026-07-06 17:50:21 -05:00
commit 9445497fc5
3 changed files with 52 additions and 7 deletions

View file

@ -246,7 +246,7 @@ 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). 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`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data 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.
- 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 `{}`.

View file

@ -65,7 +65,8 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be
awaited directly, chained with `then`/`catch`/`finally`, or passed 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.
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
@ -110,6 +111,54 @@ 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, at most
eight tool calls run concurrently, and successful execution drains unfinished work before closing.
### Confirmed defects
- [ ] Keep nested fire-and-forget work alive until the execution drain. A tool call started but not returned inside an
async function or promise handler is currently a child of that short-lived fiber and may be interrupted when the
parent settles even though the promise remains globally tracked.
- [ ] Track immediate rejected promises. `Promise.reject(value)` does not enter `pendingSettlements`, so abandoning it
produces no unhandled-rejection diagnostic.
- [ ] Drain handled work after ordinary program failure, then preserve the original failure. Today pending work drains
only after success, so a rejecting race winner or a later program throw interrupts race losers and `Promise.all`
siblings. Timeout and external interruption should still cancel immediately rather than drain.
- [ ] Make every `await` continuation asynchronous. Awaiting a plain or already-settled value currently resumes in the
same scheduling turn and can reorder state mutation relative to JavaScript.
- [ ] 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 successful execution 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.
### Required coverage
- Nested unreturned work from async functions and `then`/`catch`/`finally` handlers.
- Abandoned immediate, chained, and combinator rejections.
- Plain-value and already-settled `await` ordering.
- Ordinary program failure versus timeout/external interruption while handled 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 reaction ordering.
## Intentionally Unsupported
These are product boundaries rather than DSL backlog:
@ -149,10 +198,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.
- [ ] Decide whether thenable assimilation belongs in the bounded runtime. Promise resolution currently unwraps only
`SandboxPromise`; arbitrary `{ then(resolve, reject) }` values remain data. Full assimilation requires internal
callable resolver values, first-settlement arbitration, recursive adoption, and cycle detection.
- [ ] Consider `Promise.any`.
- [ ] 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

View file

@ -615,7 +615,7 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
"",
"## Language",
"",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and tool calls. Promises support await, then/catch/finally, and the listed Promise combinators. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and tool calls. Promises support await, then/catch/finally, and Promise.all/allSettled/race/resolve/reject. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
]