High-level agent runtime for @opencode-ai/sdk/v2.
This package wraps OpenCode's lower-level session and SSE APIs with a more agent-oriented model:
- declare agents once
- create reusable sessions
- call
query()/receiveResponse()orrunAgent() - observe an existing session without sending a new prompt
- list sessions with their OpenCode status
- check OpenCode server health
- consume normalized text, tool-call, status, error, and final-result events
It is inspired by the usability level of Claude's agent SDK, but it does not try to be API-compatible 1:1.
This SDK follows OpenCode's normal config resolution and adds programmatic overrides on top.
- OpenCode still loads its normal config chain such as global config, project config,
.opencode/, and managed config - if
OPENCODE_CONFIG_CONTENTis already present in the parent process, this SDK inherits and merges it by default - options passed to
createAgentRuntime()are applied as inline runtime overrides on top of the inherited config
In practice, the resolved precedence for config passed through this SDK is:
- existing
OPENCODE_CONFIG_CONTENTfrom the parent environment options.config- SDK-managed overrides such as
agents,mcp,permission, andmodel
Pass rawConfigContent if you want to override the inherited inline config content explicitly.
@opencode-ai/sdk is a solid transport/client layer, but most applications still need to rebuild the same higher-level pieces:
- session lifecycle helpers
- per-turn completion handling
- stream filtering by session
- message delta vs snapshot reconciliation
- tool call lifecycle normalization
- final assistant message lookup
@liontree/opencode-agent-sdk packages those concerns into a reusable runtime.
npm install @liontree/opencode-agent-sdkThis package declares @opencode-ai/sdk and opencode-ai as dependencies, so you do not need to install them separately.
- a working OpenCode provider/auth setup available through normal OpenCode config resolution or inline config passed to this SDK
npm install @liontree/opencode-agent-sdkSee examples/subagents.ts for a complete subagent lineage and resumeAgent() example, examples/attach.ts for attaching to an existing OpenCode server, and examples/observe.ts for monitoring an existing session.
import { createAgentRuntime } from "@liontree/opencode-agent-sdk"
const runtime = await createAgentRuntime({
directory: "/app",
model: "openai/gpt-5.4",
mcp: {
terminal: {
type: "remote",
url: "http://127.0.0.1:8000/mcp",
oauth: false,
timeout: 3600000,
},
},
agents: {
fuzzer: {
description: "Expert in AFL++ fuzzing workflows.",
prompt: "You are the fuzzing agent...",
},
},
})
const session = await runtime.createSession({ agent: "fuzzer" })
await session.query("Start fuzzing target libpng")
for await (const event of session.receiveResponse()) {
switch (event.type) {
case "status":
console.log("status:", event.status)
break
case "text":
process.stdout.write(event.text)
break
case "tool_call":
console.log("tool:", event.toolName, event.status)
break
case "error":
console.error("error:", event.error)
break
case "result":
console.log("final text:\n", event.result.text)
break
}
}
await runtime.dispose()This example assumes your OpenCode provider/auth configuration is already available through OpenCode's normal config resolution, for example:
~/.config/opencode/opencode.jsonopencode.jsonin the project- environment variables consumed by your provider config
- existing
OPENCODE_CONFIG_CONTENT
If you want to provide inline config explicitly, pass rawConfigContent or config.
const runtime = await createAgentRuntime({
directory: "/app",
rawConfigContent: JSON.stringify({
provider: {
openai: {
options: {
apiKey: "{env:OPENAI_API_KEY}",
},
},
},
}),
agents: {
researcher: {
prompt: "You are a focused research agent.",
},
},
})If you already have an OpenCode server running, attach to it with serverUrl:
const runtime = await createAgentRuntime({
directory: "/app",
serverUrl: "http://127.0.0.1:40937",
model: "openai/gpt-5.4",
})In attach mode, model still works as a local default and is sent with each prompt. Per-turn variant overrides are also sent with the prompt. Other runtime config such as config, mcp, permission, and rawConfigContent is not pushed into the existing server.
If you only need the final answer instead of a streamed event loop, use runAgent():
const result = await session.runAgent("Summarize the current repository")
console.log(result.text)To switch model variants for a single turn, pass variant in the per-call options:
const result = await runtime.runAgent({
agent: "general",
model: "openai/gpt-5.4",
variant: "high",
prompt: "Analyze the repository's test strategy",
})To stream descendant subagent activity in the same turn, pass includeSubagents: true and read event.source.chainText:
await session.query("Investigate the auth flow", { includeSubagents: true })
for await (const event of session.receiveResponse()) {
if (event.type === "tool_call") {
console.log(`[${event.source.chainText}]`, event.toolName, event.status)
}
}You can also reopen an existing session and optionally continue it with another turn:
const reopened = await runtime.resumeAgent({
sessionID: "sess_123",
prompt: "Continue from the last auth findings.",
})
for await (const event of reopened.receiveResponse()) {
if (event.type === "text") {
process.stdout.write(event.text)
}
}If you want to monitor an existing session without sending a new prompt, use observeSession():
const observer = await runtime.observeSession({
sessionID: "sess_123",
includeSubagents: true,
resolveFinalResult: true,
untilIdle: true,
})
for await (const event of observer.receiveResponse()) {
if (event.type === "tool_call") {
console.log(`[${event.source.chainText}]`, event.toolName, event.status)
}
}
const result = await observer.result()
console.log(result?.text ?? "no final assistant message observed")If you want a subagent to be able to launch another subagent via the task tool, you must allow it in that agent's permissions. OpenCode controls task separately from normal read/edit/bash permissions.
const runtime = await createAgentRuntime({
directory: "/app",
model: "openai/gpt-5.4",
config: {
agent: {
general: {
permission: {
task: {
"*": "allow",
},
},
},
},
},
})Use config.agent.<name>.permission.task when you want to override a built-in agent such as general. For custom agents declared in options.agents, you can also set permission directly on the agent definition.
Creates an OpenCode runtime in one of two modes:
- managed mode: starts and manages an OpenCode server, then injects agent prompts, optional default model, optional MCP config, optional permission config, and optional extra config merged with inherited inline config
- attach mode: connects to an existing OpenCode server with
serverUrl
Attach mode does not push config, mcp, permission, or rawConfigContent into the existing server. model still works as a local default, and per-turn variant overrides are sent with each prompt. serverUrl cannot be combined with hostname, port, or timeoutMs.
Creates a reusable OpenCode session and returns an OpencodeAgentSession.
Opens an existing OpenCode session and returns an OpencodeAgentSession handle.
Lists the agents currently available from the OpenCode runtime, including built-in subagents such as general and explore.
Creates a fresh session, runs one turn, and resolves the final result.
Reopens an existing session and optionally starts another turn on it.
Starts observing an existing session without sending a prompt.
- by default it only streams future events and stays attached until you call
observer.stop() - pass
untilIdle: trueto stop automatically when the observed session tree becomes idle - pass
resolveFinalResult: trueonly together withuntilIdle: truewhen you wantobserver.result()to resolve the final assistant message for the observed turn
Convenience helper that observes an existing session and resolves once it becomes idle.
Checks whether the OpenCode server is reachable. Returns the server health payload on success and throws if the server is unhealthy or unreachable.
Returns a SessionListEntry[] for every session under the runtime's directory by default. Pass limit to cap how many entries are returned. Each entry includes:
sessionID— OpenCode session identifierstatus— current status type ("busy","idle","retry","unknown", etc.)agentType— detected agent type when available (nullotherwise)title,createTime,directory,parentID
Starts one turn on the session.
- pass
includeSubagents: trueto receive descendant subagent-session events in the same stream - pass
variantto choose a model variant for that turn, such as"low"or"high" - to let a subagent launch more subagents, configure that agent's
permission.task
Consumes the active turn as an async stream of normalized events:
statustexttool_callerrorresult
receiveResponse() is single-consumer per turn.
Convenience helper that internally calls query() and consumes the response stream until a final result is available. It accepts the same per-turn options as query(), including variant.
Consumes an observer as an async stream of normalized events.
receiveResponse() is single-consumer per observation.
Waits for the observer to finish and returns the resolved final assistant message when resolveFinalResult was enabled.
Resolves when the observed session tree becomes idle.
Stops local observation without interrupting the remote OpenCode session.
Both currently map to OpenCode's session abort behavior.
Emitted on deduplicated session status transitions such as busy and idle.
Emitted for assistant text updates.
format: "delta"means incremental textformat: "snapshot"means the full part text was emitted to recover from a missing delta or stream correction
Emitted when a tool part changes lifecycle state.
Emitted for prompt failures, session errors, SSE errors, or shutdown problems.
Emitted exactly once when the final assistant message can be resolved.
Every normalized event now includes a source object describing where it came from.
source.agentType: raw agent name such asbuild,general, orexploresource.agentLabel: display label with sibling ordinal when needed, such asgeneral#2source.chainText: readable lineage such asbuild -> general#2 -> explore#1source.sessionID: the session that produced the eventsource.parentSessionID: parent session ID when the event came from a descendant sessionsource.rootSessionID: root session for the current lineagesource.taskID: session ID when the event came from a subagent task, otherwisenullsource.sourceToolCallID: originatingtasktool call when known
event.agent and result.agent are preserved for compatibility and match source.agentType.
- This SDK is higher-level than raw OpenCode, not a workflow engine.
- It exposes OpenCode subagent lineage metadata, but it does not implement orchestration policy for you.
- It does not aim for complete Claude SDK compatibility.
variantis a per-turn override only. Supported variant names depend on the selected model/provider and OpenCode configuration.- Model resolution order is: per-call override -> session default -> agent default -> runtime default -> OpenCode config.
observeSession()does not replay historical events. It streams future changes after the observer attaches, while priming enough current session state to keep text/tool updates and lineage tracking coherent.