mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-09 11:42:28 +03:00
278 lines
15 KiB
Markdown
278 lines
15 KiB
Markdown
# @opencode/codemode
|
|
|
|
Code Mode runs small programs over tools supplied by the host. OpenCode's agent-facing `execute` tool runs **Lua**.
|
|
The package also supports its existing JavaScript interpreter: `CodeMode.make` and `CodeMode.execute` default to
|
|
JavaScript for compatibility. Set `language: "lua"` for Lua or `language: "javascript"` to pin JavaScript. Neither
|
|
parser retries a failed program in the other language. Stored schedule checks remain JavaScript.
|
|
|
|
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.
|
|
|
|
## Writing a Lua program
|
|
|
|
```lua
|
|
local matches = search({ query = "order lookup" })
|
|
local order = tools.orders["lookup-by-id"]({ id = "order_42" })
|
|
return { id = order.id, needsAttention = order.status ~= "complete" }
|
|
```
|
|
|
|
Use the exact path returned by `search`; the input is a Lua table, not a JavaScript object literal. Calls wait for the
|
|
tool's Effect in the execution's environment, so the next statement sees the result. Calls within one Lua program
|
|
are sequential: there is no `await`, `Promise.all`, detached work, or parallel Lua task API. Independent executions
|
|
still have separate state and can run concurrently.
|
|
|
|
- Use an explicit `return`: only the first value is exported. Falling off the end or returning `nil` yields JSON null.
|
|
- Tables with contiguous integer keys starting at **1** are arrays; tables with string keys are objects. `{}` makes
|
|
an empty object; `json.array()` makes an empty array. Arrays and objects received from a tool keep their shape
|
|
even when empty. Mixed-key, sparse, cyclic and non-data results cannot cross to the host.
|
|
- `nil` removes a table field. Use `json.null` to keep a JSON null in a field or array slot.
|
|
- Use `local ok, result = pcall(tools.ns.lookup, { id = id })` to recover from an ordinary failed call. On failure
|
|
the program sees a message, not a host exception. Interruption and resource exhaustion cannot be caught this way.
|
|
- Only explicit capabilities can enter host code. Tools, search, bindings and extension results cross as plain data,
|
|
never a host object, function, Promise or proxy. Declare Lua globals with `Extension.lua({ name, globals })`:
|
|
each function returns an Effect producing JSON (or `undefined`), not an object with callable methods. In
|
|
OpenCode `fetch(url, init)` returns data (`body` is text), not a JavaScript `Response`.
|
|
|
|
## JavaScript compatibility
|
|
|
|
- **Only supported APIs are available.** Programs can use the provided tools, supported JavaScript built-ins, and the
|
|
globals the host adds through extensions. Timers, `process`, filesystem access, packages, and `export` 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`.
|
|
|
|
Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location. Current gaps are tracked in the
|
|
[interpreter support checklist](./interpreter-support.md).
|
|
|
|
## Quick Start (host API)
|
|
|
|
```ts
|
|
import { CodeMode, Namespace, Tool } from "@opencode/codemode"
|
|
import { Effect, Schema } from "effect"
|
|
|
|
const lookupOrder = Tool.make({
|
|
description: "Look up an order by ID",
|
|
input: Schema.Struct({ id: Schema.String }),
|
|
output: Schema.Struct({ id: Schema.String, status: Schema.String }),
|
|
execute: ({ id }) => Effect.succeed({ id, status: "open" }),
|
|
})
|
|
|
|
const runtime = CodeMode.make({
|
|
language: "lua",
|
|
tools: { orders: { lookup: lookupOrder } },
|
|
})
|
|
|
|
const result = await Effect.runPromise(
|
|
runtime.execute(`
|
|
local order = tools.orders.lookup({ id = "order_42" })
|
|
return { id = order.id, needsAttention = order.status ~= "complete" }
|
|
`),
|
|
)
|
|
```
|
|
|
|
`result` is always a [`CodeMode.Result`](#results).
|
|
|
|
## API
|
|
|
|
### `Tool.make`
|
|
|
|
`input` and `output` accept either an Effect Schema or a render-only JSON Schema document. Effect Schema input is
|
|
decoded before `execute`; 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<void>`.
|
|
|
|
Descriptions and schemas are model-visible contracts. Authorization belongs in `execute`.
|
|
|
|
Nested records are the shorthand for ordinary namespaces. Use `Namespace.make` when a namespace needs a description:
|
|
|
|
```ts
|
|
const runtime = CodeMode.make({
|
|
tools: {
|
|
orders: Namespace.make({
|
|
description: "Purchases, fulfillment, and shipment tracking",
|
|
tools: { lookup: lookupOrder },
|
|
}),
|
|
},
|
|
})
|
|
```
|
|
|
|
Namespace descriptions are optional and participate in search matching for every descendant tool. Names still come
|
|
from record keys, so the wrapper does not repeat `orders`. Dots in keys create nested paths; other characters use
|
|
bracket notation, such as `tools.context7["resolve-library-id"](...)`.
|
|
|
|
### `CodeMode.execute` and `CodeMode.make`
|
|
|
|
`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.execute(source) // Effect<CodeMode.Result, never, ToolServices>
|
|
```
|
|
|
|
The Effect environment is inferred from the supplied tools. `hooks` surround every call the program makes into the
|
|
host: `tool.before`/`tool.after` receive `{ name, input }` with decoded input, `extension.before`/`extension.after`
|
|
receive `{ extension, name, args }`. An `after` hook also receives how the call ended (`success` with its value,
|
|
`failure` with its error, or `interrupted`). A `before` hook runs before the call reaches the host, and a failing one
|
|
denies it: the host function is never called, and the program catches the failure as a thrown error. A denied call
|
|
fails the way the call itself would: a tool call or asynchronous global rejects its promise, and a synchronous global
|
|
throws.
|
|
|
|
### `Extension.make`
|
|
|
|
Extensions are host functions a program calls directly as globals, such as `fetch`. Unlike tools they are not in the
|
|
catalog, not counted against `maxToolCalls`, and not described to the model; the host decides what they mean.
|
|
|
|
```ts
|
|
const web = Extension.make({ name: "web", globals: { fetch: async (url: string) => globalThis.fetch(url) } })
|
|
const runtime = CodeMode.make({ tools, extensions: [web] })
|
|
```
|
|
|
|
In the JavaScript interpreter, a global is synchronous unless it is an `async` function or is marked with `Extension.async`. Mark a plain function
|
|
that returns a Promise or an Effect, so a denied call rejects its promise like any other failure:
|
|
|
|
```ts
|
|
Extension.make({ name: "web", globals: { fetch: Extension.async((url: string) => globalThis.fetch(url)) } })
|
|
```
|
|
|
|
A global may return a value, a Promise, or an Effect. An Effect's requirements join the runtime's environment the same
|
|
way a tool's do, so the host can provide per-execution services such as the calling session:
|
|
|
|
```ts
|
|
const web = Extension.make({
|
|
name: "web",
|
|
globals: {
|
|
fetch: Extension.async((url: string) =>
|
|
Effect.gen(function* () {
|
|
const session = yield* Session
|
|
yield* session.authorize(url)
|
|
return yield* Effect.promise(() => globalThis.fetch(url))
|
|
}),
|
|
),
|
|
},
|
|
})
|
|
const result =
|
|
yield *
|
|
CodeMode.make({ extensions: [web] })
|
|
.execute(code)
|
|
.pipe(Effect.provideService(Session, session))
|
|
```
|
|
|
|
Every value crossing in either direction is converted, never shared: arguments come in as copies, results go out as
|
|
copies, and a function inside a result is callable the same way. A global that shadows a built-in or another
|
|
extension throws at `CodeMode.make`.
|
|
|
|
A global can also be an object of functions, such as `fs`. Every function reachable from it is an entry point like a
|
|
global, so calls fire the `extension` hooks with their path as the name (`fs.readFile`, `fs.promises.readFile`).
|
|
|
|
Programs load globals the way Node code loads modules. `import` and `require` take a global by its Node module name,
|
|
with or without `node:`; `fs/promises`, `url`, `util`, and `buffer` resolve to the globals that provide them. There are
|
|
no packages, so anything else throws.
|
|
|
|
```js
|
|
import fs, { readFile } from "node:fs" // the `fs` global, if the host provides one
|
|
import { URL } from "url"
|
|
const crypto = require("crypto")
|
|
const { default: same } = await import("fs")
|
|
```
|
|
|
|
### OpenAPI tools
|
|
|
|
`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({ language: "lua", tools: { opencode: api.tools } })
|
|
```
|
|
|
|
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`.
|
|
|
|
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.
|
|
|
|
## Results
|
|
|
|
Every execution returns:
|
|
|
|
```ts
|
|
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. `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.
|
|
|
|
Diagnostic kinds:
|
|
|
|
| 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`. |
|
|
|
|
Host failures and defects report their messages and underlying causes. Invalid outputs include the validation or
|
|
copying error. Interruption propagates without becoming an error diagnostic.
|
|
|
|
## Discovery
|
|
|
|
`runtime.catalog` returns structured descriptors for every visible tool (path, description and signature in the
|
|
selected language). For Lua instructions use `CodeMode.luaSearchSignature` and `CodeMode.toolExpression(path, "lua")`;
|
|
`CodeMode.searchSignature` and `CodeMode.toolExpression(path)` retain their JavaScript meanings.
|
|
|
|
The synchronous `search(...)` built-in is always available. It supports exact-path lookup, namespace-scoped search,
|
|
empty-query browsing, and pagination, and returns callable paths with full signatures. Lua sees the result directly.
|
|
In JavaScript it can also be awaited or chained with `then`, `catch`, and `finally`. Search counts toward
|
|
`maxToolCalls`. Search also matches descriptions from enclosing `Namespace` values.
|
|
|
|
## Execution Limits
|
|
|
|
| Limit | Lua default | JavaScript default | Controls |
|
|
| ---------------- | ----------- | ------------------ | --------------------------------------------------------------------------------- |
|
|
| `timeoutMs` | 120,000 | unlimited | Total execution time. |
|
|
| `maxToolCalls` | 128 | unlimited | Lua host call attempts (tools, search and extensions); JavaScript admitted tools. |
|
|
| `maxOutputBytes` | 65,536 | unlimited | Retained result value and logs. |
|
|
|
|
Lua also enforces non-configurable bounds on source and syntax depth, interpreter work and call frames, string and
|
|
table growth, and expanded data copies. These bounds apply even when the host raises its configured limits. The
|
|
production evaluator shares the [Lua interpreter](./lua/README.md)'s frames and call loop but installs only metered
|
|
operations: no prototype libraries, dynamic loading, I/O, metatables, regex/pattern engine or coroutines. Lua strings
|
|
are byte strings; strings crossing JSON boundaries must be valid UTF-8.
|
|
|
|
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. In JavaScript a result returned before cleanup times out remains successful with a
|
|
`TimeoutExceeded` warning; JavaScript tool calls may run concurrently.
|