From 9445497fc5362d5fca34b32d622aa8f3c0ccebd4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 17:50:21 -0500 Subject: [PATCH] docs(codemode): track promise status --- packages/codemode/README.md | 2 +- packages/codemode/codemode.md | 55 ++++++++++++++++++++++++--- packages/codemode/src/tool-runtime.ts | 2 +- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 27358e7807..b80247e889 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -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 `{}`. diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index 973d933e93..a8a03292cb 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -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 diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 4e7ef5fce3..8f76f6c0d7 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -615,7 +615,7 @@ export const prepare = (tools: HostTools, 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 `{}`.", ]