opencode/packages/codemode/codemode.md
Aiden Cline 6273304f2a Merge remote-tracking branch 'origin/v2' into promise-chaining
# Conflicts:
#	packages/codemode/src/interpreter/runtime.ts
#	packages/codemode/src/tool-runtime.ts
2026-07-08 17:33:50 -05:00

14 KiB

CodeMode Design and Status

This is the living design and status document for @opencode-ai/codemode and its existing V2 OpenCode adapter. It records current behavior, intentional boundaries, durable rationale, and material remaining work.

Completed implementation history, branch names, test counts, and closed findings belong in git, not here. Remove completed work instead of preserving checked-off chronology.

Detailed package API documentation lives in README.md. OpenAPI-specific follow-ups live in src/openapi/TODO.md.

How CodeMode Works

Purpose

CodeMode gives a model one execute tool backed by a confined JavaScript interpreter. Inside the program, the model can call an explicit tree of schema-described tools, sequence dependent work, run independent calls concurrently, and filter or aggregate results before returning them to the agent loop.

The goals are:

  • Reduce model context consumed by large tool catalogs.
  • Avoid an agent round-trip between every dependent tool call.
  • Keep large intermediate results inside the program instead of sending them through model context.
  • Give generated code only the authority explicitly supplied by the host.

CodeMode is an orchestration language, not a general JavaScript runtime or an application authorization system.

Runtime

The generic runtime lives in packages/codemode and is host-neutral:

  1. The host builds a tree of Tool.make(...) definitions and calls CodeMode.make(...) or CodeMode.execute(...).
  2. CodeMode generates model instructions, a budgeted inline catalog, and the internal $codemode.search tool.
  3. TypeScript syntax is transpiled away, Acorn parses the resulting JavaScript, and an owned tree-walking interpreter executes it without eval.
  4. Tool inputs and outputs cross schema and plain-data boundaries before they become visible on either side.
  5. Execution returns CodeMode.Result. Expected program and tool failures are diagnostic data; host interruption remains Effect interruption.

Effect Schemas validate and transform tool inputs and outputs. JSON Schemas render model-facing signatures but do not validate values; adapter-provided values still cross the plain-data boundary. A tool without an output schema is advertised as Promise<unknown>.

Discovery and model workflow

The model sees a token-budgeted catalog. Every namespace remains visible, and complete signatures are selected round-robin across namespaces so one large namespace cannot starve the others. $codemode.search is always callable and is advertised when the inline catalog is partial.

The intended workflow is:

  1. Pick an exact signature from the inline catalog, or return $codemode.search(...) results and use a selected path in the next execution.
  2. Call the exact returned path without guessing or normalizing segments.
  3. Narrow Promise<unknown> results before reading fields.
  4. Start independent calls together and await them with Promise.all.
  5. Filter and aggregate inside the program, then return only the data needed by the model.

Search returns directly usable JavaScript paths, descriptions, and complete TypeScript signatures. It supports exact 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, 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 concurrency and data nesting depth.

Data, files, and failures

Program results and tool arguments are JSON-like data. Dates become ISO strings at host boundaries; RegExp, Map, and Set values become {} as they do under JSON serialization. Promise and runtime reference values cannot cross the boundary.

Unknown host failures and invalid outputs are sanitized. ToolError is the explicit channel for a safe message that a tool wants the model to see. Diagnostic categories distinguish parsing, unsupported syntax, unknown tools, invalid data, tool failures, limits, timeouts, and execution failures.

Files and other attachment content stay outside the interpreter. A host may collect them while child tools execute and attach them to the outer result, but the program receives only the structured tool output.

V2 OpenCode adapter

CodeMode is integrated into V2 through packages/core/src/tool/registry.ts and packages/core/src/tool/execute.ts:

  • Core has one canonical Tool representation. Location-scoped producers register direct or deferred tools through Tools.Service.
  • Each model step snapshots effective registrations, applies catalog visibility filtering, and exposes direct tools normally.
  • When visible deferred tools exist, Core reserves and materializes one execute tool. Grouped deferred tools become CodeMode namespaces instead of flattened model-facing names.
  • Each nested call checks that its captured registration is still current before dispatching it.
  • Authorization and side-effect ordering remain responsibilities of the leaf tool. Catalog visibility is not execution authorization.
  • Structured child output enters the interpreter. File parts are collected host-side and attached to the outer result.
  • Nested call statuses are returned as final execute metadata for the TUI.
  • execute is the one model-facing tool invocation. Nested calls reuse its invocation context and do not independently run registry hooks or model-output bounding; this keeps complete intermediate structured values available for in-program filtering. The outer execute settlement is the single model-output bounding boundary.
  • Core supplies no CodeMode timeout or tool-call limit. User cancellation interrupts the outer invocation and its supervised children; the outer settlement applies Core's normal output-retention policy.

MCP tools use this canonical path: they register as grouped tools and are deferred while CodeMode is enabled. Existing 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:

  • Ambient filesystem, process, environment, network, credential, or application access. External work must go through supplied tools.
  • Modules, imports, dynamic imports, eval, arbitrary host globals, npm packages, and prototype mutation.
  • Generic permission prompts, authorization policy, durable pause/resume, replay, storage, or exactly-once external side effects. Hosts and tools own those concerns.
  • Heuristic parsing of text tool results as JSON. A result should not silently change type based on its contents.

The OpenAPI adapter may gain more transports and encodings, but it must continue skipping operations it cannot represent accurately rather than guessing semantics.

Decisions and Rationale

Decision Rationale
Keep an owned tree-walking interpreter. The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit.
Treat schemas as the model-facing interface. Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability.
Keep authority host-owned. CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy.
Use progressive catalog disclosure plus search. Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid.
Start tool promises eagerly and supervise them. This preserves normal call-time parallelism while giving each call run-once settlement and interruption safety.
Keep files outside the sandbox value space. Models should compose structured data without routing binary payloads through generated code or context.
Treat execute as the model-facing invocation boundary. Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities.
Return expected failures as data. Models need actionable diagnostics without exposing private host causes; host interruption and defects must still propagate correctly.
Leave execution-limit defaults to hosts. Appropriate budgets depend on the surrounding product and its own cancellation, retention, and output-bounding policies.
Skip unsupported OpenAPI operations. Incorrect parameter encoding, authentication, or transport behavior is worse than a precise skipped reason.

Remaining Work

Keep only material unresolved work here. Small isolated defects should be GitHub issues; adapter-only work belongs in the adapter TODO. Delete entries when completed.

DSL expansion

The supported JavaScript subset should grow when common model-generated code improves tool orchestration. These are current omissions to implement, not intentional product boundaries.

  • 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 Array.from(...) and replacers for JSON.stringify(...), including Effect-aware callbacks where needed.
  • Add Object.is after runtime method and tool references have stable identity semantics.
  • Add deterministic modern collection conveniences where they improve orchestration: Object.groupBy, Set composition methods, and Array.prototype.toSpliced.
  • Decide whether iterable Math.sumPrecise belongs in the runtime.
  • Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter defects are distinguishable without leaking private causes.

Tool and result contracts

  • Design explicit tagged representations and size rules before allowing Blob, File, ArrayBuffer, typed arrays, or host streams to cross the sandbox boundary.
  • Define one consistent policy for tool path segments named __proto__, constructor, or prototype. They must either be safely callable, rejected before catalog generation, or use one documented escaping rule.