mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 18:26:09 +00:00
docs(codemode): streamline README (#37769)
This commit is contained in:
+97
-136
@@ -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<unknown>`.
|
||||
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<unknown>`.
|
||||
|
||||
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<CodeMode.Result, never, ToolServices>
|
||||
```
|
||||
|
||||
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<CodeMode.Diagnostic>
|
||||
readonly logs?: ReadonlyArray<string>
|
||||
readonly truncated?: boolean
|
||||
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
|
||||
}
|
||||
|
||||
interface Failure {
|
||||
readonly ok: false
|
||||
readonly error: CodeMode.Diagnostic
|
||||
readonly logs?: ReadonlyArray<string>
|
||||
readonly truncated?: boolean
|
||||
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
|
||||
}
|
||||
type Result =
|
||||
| {
|
||||
readonly ok: true
|
||||
readonly value: CodeMode.DataValue
|
||||
readonly warnings?: ReadonlyArray<CodeMode.Diagnostic>
|
||||
readonly logs?: ReadonlyArray<string>
|
||||
readonly truncated?: boolean
|
||||
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
|
||||
}
|
||||
| {
|
||||
readonly ok: false
|
||||
readonly error: CodeMode.Diagnostic
|
||||
readonly logs?: ReadonlyArray<string>
|
||||
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,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user