From d5669ca934957785fe7938bef2c5190862f09137 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:18:24 -0500 Subject: [PATCH] docs(codemode): streamline README (#37769) --- packages/codemode/README.md | 233 +++++++++++++++--------------------- 1 file changed, 97 insertions(+), 136 deletions(-) diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 622fcaa574..09ba57346e 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -1,41 +1,30 @@ # @opencode-ai/codemode -This is our take on code mode. Programs are written in a lightweight, JavaScript-like DSL and run in the package's -own interpreter. They never execute as actual JavaScript, so there is no runtime to escape into. The interpreter -itself can reach nothing; every effect a program has goes through a tool you explicitly supplied. The tradeoff is a -bounded language rather than full JavaScript: the [interpreter support checklist](./interpreter-support.md) documents -exactly what is supported. +This is our take on code mode: a lightweight, pure interpreter for a JavaScript-like language built around calling +tools. It supports familiar JavaScript syntax with a few key differences and limitations. See the +[interpreter support checklist](./interpreter-support.md) for more details. -[Cloudflare's post](https://blog.cloudflare.com/code-mode/) introduced the idea. Their implementation executes -generated code in isolate sandboxes. We took a lighter route: a pure interpreter that runs wherever your application -runs, no sandbox required. +Rather than trying to sandbox arbitrary JavaScript, CodeMode only runs the language features we implement. Programs +cannot directly access the network, filesystem, processes, or application APIs. They can interact with the outside +world only through tools provided by the host, which can also limit execution time, tool calls, output size, and data. + +The idea of code mode was originally introduced by Cloudflare. See +[their post](https://blog.cloudflare.com/code-mode/) to learn more about the concept and their isolate-based approach. ## How it differs from JavaScript -The deliberate differences: +- **Only supported APIs are available.** Programs can use the provided tools and supported JavaScript built-ins. APIs + such as `fetch`, timers, `process`, filesystem access, imports, and modules are unavailable. +- **Unfinished work is interrupted.** Tool calls and async functions start when called. When the program finishes, + anything still running is interrupted. Unhandled rejections from un-awaited promises are returned as warnings. +- **REPL-style results.** Without an explicit `return`, the final top-level expression becomes the result. `undefined` + becomes `null`. -- **No ambient authority.** No `fetch`, `process`, filesystem, timers, or host globals - only the allowlisted standard - library and supplied `tools`. -- **No dynamic code.** No `eval`, `Function`, or module loading. -- **Plain-data boundaries.** Tool arguments and program results are JSON-like data. Dates become ISO strings, RegExp, - Map, and Set serialize as `{}`, and promises, functions, and runtime references cannot cross the boundary. -- **Eager, supervised promises.** Tool calls and async functions start immediately when called. Whatever is still - running when the program returns is interrupted - race losers and fire-and-forget calls alike - so a program must - await every call whose completion matters. Rejections that settle un-awaited become `warnings` on the result instead - of crashing the run. -- **REPL-style results.** An omitted `return` yields the final top-level expression; `undefined` normalizes to `null`. - -Beyond these, the language is a growing subset rather than a divergent one: unsupported syntax returns an -`UnsupportedSyntax` diagnostic with a source location, and current gaps (for example thenable assimilation, classes, -generators, and full sparse-array parity) are tracked as unchecked items in the +Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location. Current gaps are tracked in the [interpreter support checklist](./interpreter-support.md). ## Quick Start -The package is workspace-private (`"@opencode-ai/codemode": "workspace:*"`). Hosts interact with it through `effect` -and should depend on `effect` themselves. Define tools with Effect Schema, then expose them to programs through -`tools`: - ```ts import { CodeMode, Tool } from "@opencode-ai/codemode" import { Effect, Schema } from "effect" @@ -48,161 +37,133 @@ const lookupOrder = Tool.make({ }) const runtime = CodeMode.make({ - tools: { - orders: { - lookup: lookupOrder, - }, - }, + tools: { orders: { lookup: lookupOrder } }, }) -const result = - yield * +const result = await Effect.runPromise( runtime.execute(` - const order = await tools.orders.lookup({ id: "order_42" }) - return { id: order.id, needsAttention: order.status !== "complete" } -`) + const order = await tools.orders.lookup({ id: "order_42" }) + return { id: order.id, needsAttention: order.status !== "complete" } + `), +) ``` -`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics -rather than failing the Effect; host interruption remains interruption. +`result` is always a [`CodeMode.Result`](#results). ## API ### `Tool.make` -`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document. Effect Schema input -is decoded before `run` is invoked; an Effect Schema `output` is decoded and copied before the program sees it. JSON -Schemas only shape the model-visible signature. Without `output` the signature advertises `Promise`. -Descriptions and schemas are model-visible contract; keep authorization in `run`. +`input` and `output` accept either an Effect Schema or a render-only JSON Schema document. Effect Schema input is +decoded before `run`; Effect Schema output is decoded and safely copied before the program sees it. JSON Schemas only +shape the model-visible signature. Without `output`, the signature uses `Promise`. -Dots in tool names are namespace separators: `{ "issues.list": tool }` exposes `tools.issues.list(...)`, exactly like -`{ issues: { list: tool } }`. Other non-identifier characters render with bracket notation, e.g. +Descriptions and schemas are model-visible contracts. Authorization belongs in `run`. + +Dots in tool names create namespaces: `{ "issues.list": tool }` and `{ issues: { list: tool } }` both expose +`tools.issues.list(...)`. Other characters use bracket notation, such as `tools.context7["resolve-library-id"](...)`. ### `CodeMode.execute` and `CodeMode.make` -`CodeMode.execute({ ...options, code })` runs once and is equivalent to `CodeMode.make(options).execute(code)`. A -runtime from `make` reuses the tool set and policy: +`CodeMode.execute({ ...options, code })` runs once. `CodeMode.make(options)` creates a reusable runtime: ```ts const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } }) runtime.catalog() // structured tool descriptions runtime.instructions() // model-facing syntax and tool guide -runtime.execute(source) // CodeMode.Result +runtime.execute(source) // Effect ``` -The Effect environment is inferred from the supplied tools; service requirements are not erased. Optional -`onToolCallStart` / `onToolCallEnd` hooks observe admitted calls with decoded input, outcome, and duration; both are -Effect-returning and must not fail. +The Effect environment is inferred from the supplied tools. `onToolCallStart` observes admitted calls with decoded +input; `onToolCallEnd` observes settled outcomes and duration. Both hooks return Effects and must not fail. ### OpenAPI tools -`OpenAPI.fromSpec` turns an OpenAPI 3.x document into namespaced tools - one tool per operation, using dotted -`operationId` segments as namespaces: +`OpenAPI.fromSpec` converts an OpenAPI 3.x document into one tool per supported operation. Dotted `operationId` values +create namespaces: ```ts const api = OpenAPI.fromSpec({ spec, auth: { resolve } }) const runtime = CodeMode.make({ tools: { opencode: api.tools } }) ``` -It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary -responses, or streaming land in `skipped` instead of producing broken tools. Auth is resolved host-side and never -model-visible; generated tools require `HttpClient.HttpClient` in the environment. `readOnly` properties are omitted -from request signatures and `writeOnly` properties from response signatures. These JSON Schemas are model-facing, not -runtime filters: nested value bodies and server responses pass through unchanged. See the option docstrings in -`src/openapi/types.ts` for full semantics. +The synchronous result is `{ tools, skipped }`. Operations with unsupported parameter encodings, request bodies +without JSON content, WebSocket or SSE semantics, or binary responses are reported in `skipped`. -## Outputs +Authentication is resolved by the host and never shown to the model. Generated tools require `HttpClient.HttpClient`. +Request signatures omit `readOnly` properties; response signatures omit `writeOnly` properties. These JSON Schemas +shape model-visible signatures but do not filter runtime values: nested JSON body properties and decoded server +responses pass through unchanged. See `src/openapi/types.ts` for option details. -Every execution returns a `CodeMode.Result`: +## Results + +Every execution returns: ```ts -type Result = Success | Failure - -interface Success { - readonly ok: true - readonly value: CodeMode.DataValue - readonly warnings?: ReadonlyArray - readonly logs?: ReadonlyArray - readonly truncated?: boolean - readonly toolCalls: ReadonlyArray -} - -interface Failure { - readonly ok: false - readonly error: CodeMode.Diagnostic - readonly logs?: ReadonlyArray - readonly truncated?: boolean - readonly toolCalls: ReadonlyArray -} +type Result = + | { + readonly ok: true + readonly value: CodeMode.DataValue + readonly warnings?: ReadonlyArray + readonly logs?: ReadonlyArray + readonly truncated?: boolean + readonly toolCalls: ReadonlyArray + } + | { + readonly ok: false + readonly error: CodeMode.Diagnostic + readonly logs?: ReadonlyArray + readonly truncated?: boolean + readonly toolCalls: ReadonlyArray + } ``` -`value` is JSON-safe data. `warnings` are non-fatal diagnostics alongside a valid value (un-awaited rejections, -timeout cleanup after the return). `logs` holds program console output, `truncated` marks any output-budget cut, and -`toolCalls` lists admitted calls in order - retained on failure for auditing. +`value` is JSON-safe. `warnings` are non-fatal diagnostics, `logs` contain program console output, and `truncated` +indicates that retained output was cut by `maxOutputBytes`. `toolCalls` retains admitted calls in order, including after +failure. -Failure `error` and success `warnings` share one diagnostic vocabulary: +Diagnostic kinds: -| Kind | Meaning | -| ----------------------- | --------------------------------------------------------------------------------------------------------- | -| `ParseError` | Source is empty or cannot be parsed. | -| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | -| `UnknownTool` | A program referenced a tool the host did not provide. | -| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | -| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | -| `InvalidDataValue` | Program data violated the plain-data contract (depth, circularity, blocked properties, non-data values). | -| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | -| `TimeoutExceeded` | Execution exceeded `timeoutMs`; as a warning, background work was interrupted after the program returned. | -| `ToolFailure` | A tool refused or failed. | -| `ExecutionFailure` | The program threw or another execution error occurred. | -| `Truncated` | Warning-only marker: additional warnings were omitted by `maxOutputBytes`. | +| Kind | Meaning | +| ----------------------- | ---------------------------------------------------------------------------------------------- | +| `ParseError` | Source is empty or cannot be parsed. | +| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | +| `UnknownTool` | The program referenced an unavailable tool. | +| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | +| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | +| `InvalidDataValue` | Program data violated the plain-data contract. | +| `ToolCallLimitExceeded` | The program exceeded `maxToolCalls`. | +| `TimeoutExceeded` | Execution timed out; as a warning, background work was interrupted after the program returned. | +| `ToolFailure` | A tool refused or failed. | +| `ExecutionFailure` | The program threw or another execution error occurred. | +| `Truncated` | Warning only: additional warnings were omitted by `maxOutputBytes`. | -Unknown host failures, defects, and invalid outputs are sanitized. `toolError("safe message")` is the explicit channel -for a model-visible refusal; its optional cause never crosses the boundary. +Unknown host failures, defects, and invalid outputs are sanitized. `toolError("safe message")` explicitly exposes a +safe refusal to the model; its optional cause remains private. ## Discovery -The generated instructions inline a budgeted catalog (default 2,000 estimated tokens, override with -`discovery: { catalogBudget }`): every namespace is always listed with its tool count, signatures are selected -round-robin so every namespace gets representation, and the instructions state whether the list is complete or -partial. Programs also get a global `search(...)` built-in - always available, advertised when the list is partial: -synchronous, deterministic field-weighted substring matching that returns directly callable paths with full -signatures, supports namespace scoping and pagination, and treats an empty query as browsing and an exact path as -lookup. Search counts as an admitted tool call. +Generated instructions contain a tool catalog with a default budget of 2,000 estimated tokens. Configure it with +`discovery: { catalogBudget }`. Every namespace remains visible, and the instructions say whether the catalog is +complete or partial. + +The synchronous `search(...)` built-in is always available and advertised when the catalog is partial. It supports +exact-path lookup, namespace-scoped search, empty-query browsing, and pagination, and returns callable paths with full +signatures. Search counts toward `maxToolCalls`. ## Execution Limits -| Limit | Default | Bounds | -| ---------------- | -------------------: | ---------------------------------------------------- | -| `timeoutMs` | none - no timeout | Wall-clock execution time. | -| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | -| `maxOutputBytes` | none - no truncation | Retained result value and logs; warnings separately. | +| Limit | Default | Controls | +| ---------------- | --------- | ------------------------------- | +| `timeoutMs` | unlimited | Total execution time. | +| `maxToolCalls` | unlimited | Admitted tool calls. | +| `maxOutputBytes` | unlimited | Retained result value and logs. | -No limit has a default, on purpose: execution budgets are host policy. A host without its own truncation or -interruption should set `maxOutputBytes` and `timeoutMs`. Limits are safe integers; invalid configuration throws a -`RangeError` at construction. Exceeding `maxOutputBytes` never fails the execution - oversized output is truncated -with an in-band marker. The timeout interrupts in-flight tool fibers and pure busy loops alike; a value the program -already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. CodeMode does not limit -tool-call concurrency. Data nesting at boundaries is limited to 32 levels. +Execution limits have no default values. -## Boundaries and Non-Goals - -The host owns authentication, authorization, tool selection, credentials, persistence, approval, and logging policy. -CodeMode owns interpretation, schema and plain-data boundaries, resource limits, diagnostics, and discovery. A program -can only exercise authority already present in the supplied tools - do not expose a broad tool and expect the prompt -to restrict it. - -Non-goals: permission prompts and approval workflows, durable pause/resume or replay, exactly-once side effects, -application authorization policy, sandboxing arbitrary JavaScript, and compatibility with the full language or npm -ecosystem. Applications that need approval or durable consequences should model those above CodeMode and expose only -the currently authorized tools. - -## Testing - -From the package directory: - -```sh -bun test -bun run typecheck -``` +Invalid limit configuration throws `RangeError`. Warnings receive a separate budget equal to `maxOutputBytes`. +Truncation does not fail execution; an oversized value becomes a string with an in-band marker. Timeouts interrupt +tool calls and busy loops, while a result returned before cleanup times out remains successful with a +`TimeoutExceeded` warning. Tool-call concurrency is unrestricted. Boundary data is limited to 32 nested levels.