docs(codemode): streamline README (#37769)

This commit is contained in:
Aiden Cline
2026-07-19 10:18:24 -05:00
committed by GitHub
parent 04f0a771a3
commit d5669ca934
+97 -136
View File
@@ -1,41 +1,30 @@
# @opencode-ai/codemode # @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 This is our take on code mode: a lightweight, pure interpreter for a JavaScript-like language built around calling
own interpreter. They never execute as actual JavaScript, so there is no runtime to escape into. The interpreter tools. It supports familiar JavaScript syntax with a few key differences and limitations. See the
itself can reach nothing; every effect a program has goes through a tool you explicitly supplied. The tradeoff is a [interpreter support checklist](./interpreter-support.md) for more details.
bounded language rather than full JavaScript: the [interpreter support checklist](./interpreter-support.md) documents
exactly what is supported.
[Cloudflare's post](https://blog.cloudflare.com/code-mode/) introduced the idea. Their implementation executes Rather than trying to sandbox arbitrary JavaScript, CodeMode only runs the language features we implement. Programs
generated code in isolate sandboxes. We took a lighter route: a pure interpreter that runs wherever your application cannot directly access the network, filesystem, processes, or application APIs. They can interact with the outside
runs, no sandbox required. 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 ## 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 Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location. Current gaps are tracked in the
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
[interpreter support checklist](./interpreter-support.md). [interpreter support checklist](./interpreter-support.md).
## Quick Start ## 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 ```ts
import { CodeMode, Tool } from "@opencode-ai/codemode" import { CodeMode, Tool } from "@opencode-ai/codemode"
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
@@ -48,161 +37,133 @@ const lookupOrder = Tool.make({
}) })
const runtime = CodeMode.make({ const runtime = CodeMode.make({
tools: { tools: { orders: { lookup: lookupOrder } },
orders: {
lookup: lookupOrder,
},
},
}) })
const result = const result = await Effect.runPromise(
yield *
runtime.execute(` runtime.execute(`
const order = await tools.orders.lookup({ id: "order_42" }) const order = await tools.orders.lookup({ id: "order_42" })
return { id: order.id, needsAttention: order.status !== "complete" } return { id: order.id, needsAttention: order.status !== "complete" }
`) `),
)
``` ```
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics `result` is always a [`CodeMode.Result`](#results).
rather than failing the Effect; host interruption remains interruption.
## API ## API
### `Tool.make` ### `Tool.make`
`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document. Effect Schema input `input` and `output` accept either an Effect Schema or a render-only JSON Schema document. Effect Schema input is
is decoded before `run` is invoked; an Effect Schema `output` is decoded and copied before the program sees it. JSON decoded before `run`; Effect Schema output is decoded and safely copied before the program sees it. JSON Schemas only
Schemas only shape the model-visible signature. Without `output` the signature advertises `Promise<unknown>`. shape the model-visible signature. Without `output`, the signature uses `Promise<unknown>`.
Descriptions and schemas are model-visible contract; keep authorization in `run`.
Dots in tool names are namespace separators: `{ "issues.list": tool }` exposes `tools.issues.list(...)`, exactly like Descriptions and schemas are model-visible contracts. Authorization belongs in `run`.
`{ issues: { list: tool } }`. Other non-identifier characters render with bracket notation, e.g.
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"](...)`. `tools.context7["resolve-library-id"](...)`.
### `CodeMode.execute` and `CodeMode.make` ### `CodeMode.execute` and `CodeMode.make`
`CodeMode.execute({ ...options, code })` runs once and is equivalent to `CodeMode.make(options).execute(code)`. A `CodeMode.execute({ ...options, code })` runs once. `CodeMode.make(options)` creates a reusable runtime:
runtime from `make` reuses the tool set and policy:
```ts ```ts
const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } }) const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } })
runtime.catalog() // structured tool descriptions runtime.catalog() // structured tool descriptions
runtime.instructions() // model-facing syntax and tool guide runtime.instructions() // model-facing syntax and tool guide
runtime.execute(source) // CodeMode.Result runtime.execute(source) // Effect<CodeMode.Result, never, ToolServices>
``` ```
The Effect environment is inferred from the supplied tools; service requirements are not erased. Optional The Effect environment is inferred from the supplied tools. `onToolCallStart` observes admitted calls with decoded
`onToolCallStart` / `onToolCallEnd` hooks observe admitted calls with decoded input, outcome, and duration; both are input; `onToolCallEnd` observes settled outcomes and duration. Both hooks return Effects and must not fail.
Effect-returning and must not fail.
### OpenAPI tools ### OpenAPI tools
`OpenAPI.fromSpec` turns an OpenAPI 3.x document into namespaced tools - one tool per operation, using dotted `OpenAPI.fromSpec` converts an OpenAPI 3.x document into one tool per supported operation. Dotted `operationId` values
`operationId` segments as namespaces: create namespaces:
```ts ```ts
const api = OpenAPI.fromSpec({ spec, auth: { resolve } }) const api = OpenAPI.fromSpec({ spec, auth: { resolve } })
const runtime = CodeMode.make({ tools: { opencode: api.tools } }) const runtime = CodeMode.make({ tools: { opencode: api.tools } })
``` ```
It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary The synchronous result is `{ tools, skipped }`. Operations with unsupported parameter encodings, request bodies
responses, or streaming land in `skipped` instead of producing broken tools. Auth is resolved host-side and never without JSON content, WebSocket or SSE semantics, or binary responses are reported in `skipped`.
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.
## 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 ```ts
type Result = Success | Failure type Result =
| {
interface Success { readonly ok: true
readonly ok: true readonly value: CodeMode.DataValue
readonly value: CodeMode.DataValue readonly warnings?: ReadonlyArray<CodeMode.Diagnostic>
readonly warnings?: ReadonlyArray<CodeMode.Diagnostic> readonly logs?: ReadonlyArray<string>
readonly logs?: ReadonlyArray<string> readonly truncated?: boolean
readonly truncated?: boolean readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall> }
} | {
readonly ok: false
interface Failure { readonly error: CodeMode.Diagnostic
readonly ok: false readonly logs?: ReadonlyArray<string>
readonly error: CodeMode.Diagnostic readonly truncated?: boolean
readonly logs?: ReadonlyArray<string> readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
readonly truncated?: boolean }
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
}
``` ```
`value` is JSON-safe data. `warnings` are non-fatal diagnostics alongside a valid value (un-awaited rejections, `value` is JSON-safe. `warnings` are non-fatal diagnostics, `logs` contain program console output, and `truncated`
timeout cleanup after the return). `logs` holds program console output, `truncated` marks any output-budget cut, and indicates that retained output was cut by `maxOutputBytes`. `toolCalls` retains admitted calls in order, including after
`toolCalls` lists admitted calls in order - retained on failure for auditing. failure.
Failure `error` and success `warnings` share one diagnostic vocabulary: Diagnostic kinds:
| Kind | Meaning | | Kind | Meaning |
| ----------------------- | --------------------------------------------------------------------------------------------------------- | | ----------------------- | ---------------------------------------------------------------------------------------------- |
| `ParseError` | Source is empty or cannot be parsed. | | `ParseError` | Source is empty or cannot be parsed. |
| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | | `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. |
| `UnknownTool` | A program referenced a tool the host did not provide. | | `UnknownTool` | The program referenced an unavailable tool. |
| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | | `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. |
| `InvalidToolOutput` | Tool output 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). | | `InvalidDataValue` | Program data violated the plain-data contract. |
| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | | `ToolCallLimitExceeded` | The program exceeded `maxToolCalls`. |
| `TimeoutExceeded` | Execution exceeded `timeoutMs`; as a warning, background work was interrupted after the program returned. | | `TimeoutExceeded` | Execution timed out; as a warning, background work was interrupted after the program returned. |
| `ToolFailure` | A tool refused or failed. | | `ToolFailure` | A tool refused or failed. |
| `ExecutionFailure` | The program threw or another execution error occurred. | | `ExecutionFailure` | The program threw or another execution error occurred. |
| `Truncated` | Warning-only marker: additional warnings were omitted by `maxOutputBytes`. | | `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 Unknown host failures, defects, and invalid outputs are sanitized. `toolError("safe message")` explicitly exposes a
for a model-visible refusal; its optional cause never crosses the boundary. safe refusal to the model; its optional cause remains private.
## Discovery ## Discovery
The generated instructions inline a budgeted catalog (default 2,000 estimated tokens, override with Generated instructions contain a tool catalog with a default budget of 2,000 estimated tokens. Configure it with
`discovery: { catalogBudget }`): every namespace is always listed with its tool count, signatures are selected `discovery: { catalogBudget }`. Every namespace remains visible, and the instructions say whether the catalog is
round-robin so every namespace gets representation, and the instructions state whether the list is complete or complete or partial.
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 The synchronous `search(...)` built-in is always available and advertised when the catalog is partial. It supports
signatures, supports namespace scoping and pagination, and treats an empty query as browsing and an exact path as exact-path lookup, namespace-scoped search, empty-query browsing, and pagination, and returns callable paths with full
lookup. Search counts as an admitted tool call. signatures. Search counts toward `maxToolCalls`.
## Execution Limits ## Execution Limits
| Limit | Default | Bounds | | Limit | Default | Controls |
| ---------------- | -------------------: | ---------------------------------------------------- | | ---------------- | --------- | ------------------------------- |
| `timeoutMs` | none - no timeout | Wall-clock execution time. | | `timeoutMs` | unlimited | Total execution time. |
| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | | `maxToolCalls` | unlimited | Admitted tool calls. |
| `maxOutputBytes` | none - no truncation | Retained result value and logs; warnings separately. | | `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 Execution limits have no default values.
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.
## Boundaries and Non-Goals 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
The host owns authentication, authorization, tool selection, credentials, persistence, approval, and logging policy. tool calls and busy loops, while a result returned before cleanup times out remains successful with a
CodeMode owns interpretation, schema and plain-data boundaries, resource limits, diagnostics, and discovery. A program `TimeoutExceeded` warning. Tool-call concurrency is unrestricted. Boundary data is limited to 32 nested levels.
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
```