opencode
v0.1.23OpenCode as an iii worker — opencode::* functions run headless OpenCode turns, deliver raw JSON events on the opencode::raw-event trigger type and AgentEvent frames on opencode::agent-event.
Install
iii trigger compose::add worker=opencode@nextRun in your project with the iii engine running. Exact versions are immutable and digest-pinned.
Full markdown
/workers/opencode.md?version=next. Paste it into an LLM prompt or pipe it through curl from a worker.Install
iii trigger compose::add worker=opencode@0.1.23Configuration
- defaults:
agent:
cwd:
model:
engine_url: ws://127.0.0.1:49134
iii_context: true
opencode_executable:Dependencies
Readme
opencode
OpenCode as an iii worker: the OpenCode API exposed as functions and trigger types on the iii bus, nothing else. The worker spawns the same opencode CLI the user runs in their terminal, with the same login, filesystem, and tools. opencode::run executes one headless turn (opencode run --format json) and returns the result with token usage and cost; the raw JSON events are delivered verbatim on the opencode::raw-event trigger type, and a translated AgentEvent view on opencode::agent-event, so the acp worker and any sibling worker that binds them observe an OpenCode run exactly like a native harness turn. The worker also registers run::start_and_wait, the same entrypoint the console and acp worker drive.
Install
iii trigger compose::add worker=opencodeRequires the opencode CLI on the host (npm i -g opencode-ai or the official installer) and an API key for the LLM provider you use — ANTHROPIC_API_KEY (or any provider key) in the worker environment, or an existing opencode auth login.
Skills
npx skills add iii-hq/workers --skill opencodeQuickstart
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii trigger compose::add worker=opencode
iii # starts the engine + workerThen drive it from the console chat, iii trigger, or any SDK:
import { registerWorker } from 'iii-sdk';
const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' });
const res = await iii.trigger({
function_id: 'opencode::run',
payload: {
prompt: 'Add a /health endpoint to server.ts and run the tests',
cwd: '/path/to/repo',
model: 'anthropic/claude-sonnet-4-5',
},
timeout_ms: 600_000,
});
// { session_id, opencode_session_id, result, stop_reason, usage, total_cost_usd }Or from the terminal:
# one full turn (raise the timeout; the default 30s is too short for agent turns)
iii trigger opencode::run --timeout-ms 600000 \
--json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}'
# quick reads
iii trigger opencode::sessions::list
iii trigger opencode::status session_id=<session_id>
# background turn + control
iii trigger opencode::start --json '{"prompt":"...","cwd":"/path/to/repo"}'
iii trigger opencode::stop session_id=<session_id>
iii trigger opencode::run --helpA turn from the CLI returns the result with token usage and cost, and opencode::run --help prints the published request schema as a parameter table:


Resume uses the iii session_id, not opencode_session_id. Two ids come back from every run: session_id (a UUID) is the iii key for resume / status / stop and the session_id event feeds are bound with; opencode_session_id (ses_...) is OpenCode's own id, returned for reference only. To continue a conversation, pass the same iii session_id again. The worker looks up the stored OpenCode session for that key and resumes with --session:

Long turns: use opencode::start to return immediately, then bind opencode::agent-event with { session_id } (see Event feeds) for message_complete, function_execution_start/end, and turn_end. opencode::stop interrupts a live run.
Functions
| Function | Purpose |
|---|---|
opencode::run |
Run one turn, wait, return the final result + usage + cost |
opencode::start |
Fire-and-forget turn; progress arrives on opencode::agent-event |
opencode::stop |
Interrupt a live run |
opencode::status |
Session state, live flag, usage, cost |
opencode::sessions::list |
All sessions this worker has run |
run::start_and_wait |
Alias for opencode::run under the shared agent entrypoint |
opencode::run accepts a bare prompt or a messages array ([{ role: 'user', content: [{ type: 'text', text }] }]), plus model (provider/model), cwd, agent, and iii_context overrides.
Events, usage, and cost
Every line OpenCode emits under --format json is delivered verbatim on opencode::raw-event, and translated onto opencode::agent-event:
| OpenCode event | opencode::agent-event |
|---|---|
text |
message_complete |
tool_use (completed) |
function_execution_start + function_execution_end |
step_finish |
token usage + cost, surfaced on agent_end and the return value |
| stream end | turn_end + agent_end |
OpenCode reports per-step tokens (input / output / reasoning / cache read+write) and cost inline in the event stream, so opencode::run returns usage + total_cost_usd with no extra bookkeeping.
Event feeds
The worker owns two trigger types. Bind either one to a function of yours with the session you want to follow; the worker calls that function once per frame (fire-and-forget).
| Trigger type | event in the payload |
|---|---|
opencode::agent-event |
one translated AgentEvent frame (message_complete, function_execution_start/end, turn_end, agent_end) |
opencode::raw-event |
one raw OpenCode JSON event, verbatim (step_start, text, tool_use, step_finish) |
Binding config: { "session_id": ". session_id is required (non-empty, at most 512 characters); metadata is optional and is handed to your function (it wins over the binding's own metadata). Unknown keys are rejected, and each trigger type accepts at most 256 bindings.
Payload your function receives:
{
"session_id": "<iii session_id>",
"event_id": "<session_id>-<epoch>-00000007", // identity, for dedup
"seq": 7, // contiguous per (feed, session_id, epoch), from 0
"epoch": "<uuid>", // per worker process; a new epoch restarts seq at 0
"source": "opencode",
"event": { "type": "message_complete", "...": "..." }
}The two feeds number their frames independently. Deliveries are not guaranteed to arrive in order, so order by (epoch, seq) and drop repeated event_ids. Frames are ephemeral: nothing is stored or replayed, and a consumer bound after a frame was emitted never sees it. The durable record of a turn is the value opencode::run returns and the session record behind opencode::status / opencode::sessions::list. These feeds replace the former agent::events / opencode::events iii-stream streams (see the guide "Migrate from iii-stream and pubsub").
The agent on the bus
By default every turn's prompt carries the iii runtime context: the engine-grounded discovery rules retargeted to the iii CLI, which OpenCode reaches through its own shell tool. The agent discovers capabilities from the live engine (engine::functions::list, iii trigger , the registry flow) instead of memory. Local file edits stay on OpenCode's native tools; backend actions go through registered functions. The context is prepended on a fresh session and skipped on resume; turn it off with "iii_context": false per call or in config.yaml.
OpenCode answers by querying the live engine itself, grouping every connected worker by runtime:

Configuration
engine_url: ws://127.0.0.1:49134
defaults:
model: "" # empty = OpenCode default; else "provider/model"
cwd: "" # default working directory for runs
agent: "" # OpenCode agent; empty = default
iii_context: true # prepend the iii runtime context on a fresh session
opencode_executable: "" # path to the opencode CLI; empty = resolve on PATHconfig.yaml is the seed: on first boot the worker registers it with the built-in configuration worker as the initial value, then reads the live value back and hot-reloads on configuration:updated. engine_url stays on the local seed (bootstrap). The event feeds are fixed trigger types, not configuration; the former events_stream / raw_events_stream keys are accepted and ignored so stored configs keep loading.
How it maps
| OpenCode | iii |
|---|---|
opencode run --format json turn |
opencode::run invocation |
| each JSON event, verbatim | opencode::raw-event delivery |
text part |
message_complete frame on opencode::agent-event |
tool_use part |
function_execution_start / function_execution_end frames |
step_finish tokens + cost |
usage + total_cost_usd on the result + agent_end |
session resume (--session ses_...) |
engine state scope opencode_sessions, keyed by iii session_id |
| extra capability | another iii worker on the bus (shell, database, storage, ...) |
Skills
API reference (JSON)
{
"functions": [
{
"description": "Internal: reload opencode configuration when it changes.",
"metadata": {
"internal": true
},
"name": "opencode::on-config-change",
"request_schema": {
"properties": {},
"type": "object"
},
"response_schema": {
"type": "null"
}
},
{
"description": "Run one OpenCode coding-agent turn and wait for the result. Accepts `prompt` or a `messages` array; delivers raw events on the opencode::raw-event trigger and AgentEvent frames on opencode::agent-event (bind with { session_id }), and returns {session_id, result, usage, total_cost_usd}.",
"metadata": {},
"name": "opencode::run",
"request_schema": {
"additionalProperties": false,
"properties": {
"agent": {
"description": "OpenCode agent to use",
"type": "string"
},
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/model\"; empty = OpenCode default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same OpenCode conversation",
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"busy": {
"type": "boolean"
},
"is_error": {
"type": "boolean"
},
"num_turns": {
"type": "number"
},
"opencode_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reason": {
"type": "string"
},
"result": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stop_reason": {
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
},
"reasoning_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id"
],
"type": "object"
}
},
{
"description": "List every OpenCode session this worker has run.",
"metadata": {},
"name": "opencode::sessions::list",
"request_schema": {
"properties": {},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"sessions": {
"items": {
"additionalProperties": false,
"properties": {
"cwd": {
"type": "string"
},
"model": {
"type": "string"
},
"opencode_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
},
"status": {
"enum": [
"working",
"done",
"error"
],
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"turns": {
"type": "number"
},
"updated_at_ms": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
},
"reasoning_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id",
"opencode_session_id",
"cwd",
"model",
"status",
"turns",
"total_cost_usd",
"usage",
"updated_at_ms"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"sessions"
],
"type": "object"
}
},
{
"description": "Start an OpenCode turn and return immediately; bind opencode::agent-event with { session_id } for progress and turn_end.",
"metadata": {},
"name": "opencode::start",
"request_schema": {
"additionalProperties": false,
"properties": {
"agent": {
"description": "OpenCode agent to use",
"type": "string"
},
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/model\"; empty = OpenCode default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same OpenCode conversation",
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"session_id": {
"type": "string"
},
"started": {
"type": "boolean"
}
},
"required": [
"session_id",
"started"
],
"type": "object"
}
},
{
"description": "Check the status of an OpenCode session.",
"metadata": {},
"name": "opencode::status",
"request_schema": {
"additionalProperties": false,
"properties": {
"session_id": {
"description": "iii session id returned by opencode::run / opencode::start",
"type": "string"
}
},
"required": [
"session_id"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"live": {
"type": "boolean"
},
"record": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cwd": {
"type": "string"
},
"model": {
"type": "string"
},
"opencode_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
},
"status": {
"enum": [
"working",
"done",
"error"
],
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"turns": {
"type": "number"
},
"updated_at_ms": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
},
"reasoning_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id",
"opencode_session_id",
"cwd",
"model",
"status",
"turns",
"total_cost_usd",
"usage",
"updated_at_ms"
],
"type": "object"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
}
},
"required": [
"session_id",
"live",
"record"
],
"type": "object"
}
},
{
"description": "Stop a live OpenCode run for a session.",
"metadata": {},
"name": "opencode::stop",
"request_schema": {
"additionalProperties": false,
"properties": {
"session_id": {
"description": "iii session id returned by opencode::run / opencode::start",
"type": "string"
}
},
"required": [
"session_id"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"reason": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stopped": {
"type": "boolean"
}
},
"required": [
"session_id",
"stopped"
],
"type": "object"
}
},
{
"description": "Alias for opencode::run under the shared agent entrypoint: run a turn for {session_id, messages} and return when it ends.",
"metadata": {},
"name": "run::start_and_wait",
"request_schema": {
"additionalProperties": false,
"properties": {
"agent": {
"description": "OpenCode agent to use",
"type": "string"
},
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/model\"; empty = OpenCode default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same OpenCode conversation",
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"busy": {
"type": "boolean"
},
"is_error": {
"type": "boolean"
},
"num_turns": {
"type": "number"
},
"opencode_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reason": {
"type": "string"
},
"result": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stop_reason": {
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
},
"reasoning_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id"
],
"type": "object"
}
}
],
"triggers": [
{
"description": "Translated AgentEvent frames of one OpenCode session. Config: { session_id, metadata? }. Payload: { session_id, event_id, seq, epoch, source, event }. Ephemeral: not stored or replayed.",
"invocation_schema": {},
"metadata": {},
"name": "opencode::agent-event",
"return_schema": {}
},
{
"description": "Raw OpenCode JSON events (verbatim `opencode run --format json` lines) of one session. Config: { session_id, metadata? }. Payload: { session_id, event_id, seq, epoch, source, event }. Ephemeral: not stored or replayed.",
"invocation_schema": {},
"metadata": {},
"name": "opencode::raw-event",
"return_schema": {}
}
]
}