OpenCode ↗︎ is an extensible agent harness. The Agents SDK's OpenCodeHarness runs its Workerd SDK inside a plain Durable Object. Refer to OpenCode's Cloudflare SDK documentation ↗︎ for the embedded host API.
OpenCode owns the agent loop, transcript, tools, and session recovery. OpenCodeHarness supplies SQLite storage and wakes the object after an interrupted run. The agents/models/opencode provider connects OpenCode to Workers AI through your AI binding.
OpenCodeHarness is a Lifecycle capability ↗︎. The Lifecycle starts the capability and provides storage and a durable job queue.
flowchart LR
client["Application or Worker"] -->|HTTP, WebSockets, or RPC| object
subgraph object["Durable Object"]
lifecycle["Lifecycle jobs"] --> harness["OpenCodeHarness"]
harness --> host["OpenCode Workerd host"]
host <--> sqlite[("SQLite: opencode_ tables")]
host <--> plugins["Bundled plugins"]
end
host --> ai["Workers AI binding"]
plugins --> mcp["Remote MCP server"]
plugins --> workspace["External execution environment"]
- Storage. OpenCode keeps its sessions, messages, and durable events in the object's SQLite database. The harness prefixes OpenCode's tables and indexes with
opencode_to avoid collisions. - Wake-up. Each active session has a Lifecycle job. While the session runs, the job keeps a heartbeat. If the object crashes or is evicted, an alarm restarts it, and OpenCode resumes interrupted work.
- Idle close. OpenCode runs background timers while open. The harness closes an unused host after 30 seconds when no work or event subscriptions remain. It reopens the host on the next call, retaining durable state.
| Capability | Support |
|---|---|
| Durable sessions and interrupted-turn recovery | OpenCode persists state; Lifecycle jobs wake the object. |
| Workers AI models | Register the agents/models/opencode plugin. |
| Custom tools, agents, and skills | Bundle OpenCode plugins with your Worker. |
| Remote Model Context Protocol (MCP) servers | Register remote servers through a plugin. |
| Live events and durable event replay | Use session.events() and session.log(). |
Install the Agents SDK and OpenCode packages:
npm i agents@latest @opencode/sdk@2 @opencode/plugin@2yarn add agents@latest @opencode/sdk@2 @opencode/plugin@2pnpm add agents@latest @opencode/sdk@2 @opencode/plugin@2bun add agents@latest @opencode/sdk@2 @opencode/plugin@2Use agents version 0.28.0 or later. The OpenCode packages are optional peer dependencies, required by this integration, with a supported range of ^2.0.24.
Add the Workers AI binding, a SQLite-backed Durable Object, and nodejs_compat to your Wrangler configuration:
{
"name": "opencode-agent",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-10-12",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI", "remote": true },
"durable_objects": {
"bindings": [{ "name": "Assistant", "class_name": "Assistant" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Assistant"] }],
}name = "opencode-agent"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-10-12"
compatibility_flags = [ "nodejs_compat" ]
[ai]
binding = "AI"
remote = true
[[durable_objects.bindings]]
name = "Assistant"
class_name = "Assistant"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Assistant" ]Create a plugin in src/worker-tools.ts to remove OpenCode's unsupported local tools and register a tool that can run inside the Worker:
import { Plugin } from "@opencode/plugin";
export default Plugin.define({
id: "worker-tools",
async setup(ctx) {
await ctx.tool.transform((tools) => {
for (const name of [
"read",
"write",
"edit",
"patch",
"glob",
"grep",
"shell",
]) {
tools.remove(name);
}
tools.add({
name: "current_time",
description: "Return the current UTC time.",
input: { type: "object", properties: {}, additionalProperties: false },
async execute() {
return { content: new Date().toISOString() };
},
});
});
},
});import { Plugin } from "@opencode/plugin";
export default Plugin.define({
id: "worker-tools",
async setup(ctx) {
await ctx.tool.transform((tools) => {
for (const name of [
"read",
"write",
"edit",
"patch",
"glob",
"grep",
"shell",
]) {
tools.remove(name);
}
tools.add({
name: "current_time",
description: "Return the current UTC time.",
input: { type: "object", properties: {}, additionalProperties: false },
async execute() {
return { content: new Date().toISOString() };
},
});
});
},
});Create the harness with an OpenCode factory and install a Lifecycle on the Durable Object:
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { OpenCodeWorkerd } from "@opencode/sdk/workerd";
import { OpenCodeHarness } from "agents/harness/opencode";
import { createAI } from "agents/models/opencode";
import workerTools from "./worker-tools";
export class Assistant extends DurableObject {
ai = createAI({ binding: this.env.AI });
harness = new OpenCodeHarness({
opencode: ({ storage }) =>
OpenCodeWorkerd.create({
storage,
config: { default_agent: "build" },
models: { fetch: false },
plugins: [this.ai.plugin, workerTools],
}),
defaults: {
model: this.ai("@cf/moonshotai/kimi-k2.7-code", {
reasoningEffort: "low",
}),
agent: "build",
},
});
lifecycle = Lifecycle.install(this).use(this.harness);
async ask(prompt) {
const { status, text, reason } = await this.harness.prompt(prompt);
return { status, text, reason };
}
}
export default {
async fetch(_request, env) {
const assistant = env.Assistant.getByName("demo");
return Response.json(await assistant.ask("What is the current UTC time?"));
},
};import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { OpenCodeWorkerd } from "@opencode/sdk/workerd";
import { OpenCodeHarness } from "agents/harness/opencode";
import { createAI } from "agents/models/opencode";
import workerTools from "./worker-tools";
export class Assistant extends DurableObject<Env> {
ai = createAI({ binding: this.env.AI });
harness = new OpenCodeHarness({
opencode: ({ storage }) =>
OpenCodeWorkerd.create({
storage,
config: { default_agent: "build" },
models: { fetch: false },
plugins: [this.ai.plugin, workerTools],
}),
defaults: {
model: this.ai("@cf/moonshotai/kimi-k2.7-code", {
reasoningEffort: "low",
}),
agent: "build",
},
});
lifecycle = Lifecycle.install(this).use(this.harness);
async ask(prompt: string) {
const { status, text, reason } = await this.harness.prompt(prompt);
return { status, text, reason };
}
}
export default {
async fetch(_request: Request, env: Env) {
const assistant = env.Assistant.getByName("demo");
return Response.json(await assistant.ask("What is the current UTC time?"));
},
} satisfies ExportedHandler<Env>;Pass the factory's storage to OpenCodeWorkerd.create() so OpenCode uses the prefixed database. Register this.ai.plugin to make the Workers AI models available. models: { fetch: false } skips OpenCode's external model catalog refresh; the provider registers models you name with this.ai().
The Lifecycle manages host startup. The factory runs again when the harness reopens an idle host, so it must support repeated calls.
The "demo" object name is for this example. In an application, choose an object name based on an authenticated user or conversation.
harness.prompt() submits to the root session and waits for completion. It returns status, optional text or reason, and the active transcript as messages. Check status: "done" means answered; "unanswered" means failed, interrupted, withdrawn, or not found.
For background work, submit and wait separately:
const receipt = await this.harness.submit("Summarize the report", {
operationId: "report-summary-42",
});
// In a later request to the same Durable Object:
const result = await this.harness.wait(receipt.operationId, {
session: receipt.session,
});const receipt = await this.harness.submit("Summarize the report", {
operationId: "report-summary-42",
});
// In a later request to the same Durable Object:
const result = await this.harness.wait(receipt.operationId, {
session: receipt.session,
});An operationId deduplicates submissions within a session. Reusing it returns a receipt with accepted: false. Use letters, digits, ., _, ~, or - in the ID.
Inputs submitted while a session runs queue as follow-ups. Set whenBusy: "steer" or call session.steer() to join the running work at its next step. harness.pending() lists queued and running operations.
The harness creates a root session with ID "ses_root". Use harness.session(id) to address another session:
const session = await this.harness.sessions.create();
await session.prompt("Draft a release note");
const fork = await this.harness.sessions.fork(session.id);
const all = await this.harness.sessions.list();const session = await this.harness.sessions.create();
await session.prompt("Draft a release note");
const fork = await this.harness.sessions.fork(session.id);
const all = await this.harness.sessions.list();| Session method | Description |
|---|---|
submit(input, options) |
Durably submit input and return a receipt. |
prompt(input, options) |
Submit, wait, and return the result and active transcript. |
steer(input, options) |
Submit with whenBusy: "steer". |
wait(operationId, signal) |
Wait for an operation. Aborting the signal stops only the wait. |
abort(operationId) |
Withdraw a queued operation or interrupt the run it joined. Without an ID, abort all work in the session. |
setModel(model) |
Change the model, using a reference such as this.ai("@cf/moonshotai/kimi-k2.7-code"). |
setAgent(agent) |
Change the OpenCode agent, such as "build" or "plan". |
messages() |
Read the active context after the latest compaction. |
history() |
Read the full transcript, including compacted messages. |
events(signal) |
Subscribe to live and durable events for this session. |
log({ after, follow, signal }) |
Replay durable events, optionally following new ones. |
busy() |
Check for running, queued, or interrupted work awaiting recovery. |
sessions.list() includes sessions spawned by OpenCode's subagent tools. Sessions within one object can run independently. Map OpenCode messages to your client's UI format.
session.events() returns an asynchronous iterable of live events, including text deltas. It does not include a transcript snapshot. Read session.messages() or session.history() for the transcript, then forward events over your chosen transport.
For replayable events, use session.log(). Save the event.durable.seq cursor and pass it as after when reconnecting. The durable log does not replay uncommitted live text deltas.
Both subscriptions hold the host open. Stop them with an AbortSignal when no longer needed. Recreate subscriptions after the object restarts. The example's WebSocket adapter ↗︎ shows snapshots, event forwarding, and reattachment after startup.
Bundle plugins with the Worker and pass them in the factory's plugins array alongside this.ai.plugin and workerTools. Do not rely on runtime loading of local plugin files or npm packages.
OpenCode wraps plugin tools in its execute code-mode tool, so tool calls may appear as execute in events. Refer to OpenCode plugin tools ↗︎ for schemas and tool output.
Register a remote MCP server through a plugin. This example connects to the Cloudflare documentation MCP server:
import { Plugin } from "@opencode/plugin";
export default Plugin.define({
id: "cloudflare-docs-mcp",
async setup(ctx) {
await ctx.mcp.transform((servers) => {
servers.set("cloudflare-docs", {
type: "remote",
url: "https://docs.mcp.cloudflare.com/mcp",
});
});
},
});import { Plugin } from "@opencode/plugin";
export default Plugin.define({
id: "cloudflare-docs-mcp",
async setup(ctx) {
await ctx.mcp.transform((servers) => {
servers.set("cloudflare-docs", {
type: "remote",
url: "https://docs.mcp.cloudflare.com/mcp",
});
});
},
});Import this plugin and add it to plugins. Refer to OpenCode MCP transforms ↗︎ for server configuration.
Register skill content through a bundled plugin:
import { Plugin, Skill } from "@opencode/plugin";
export default Plugin.define({
id: "review-skill",
async setup(ctx) {
await ctx.skill.transform((skills) => {
skills.add({
id: Skill.ID.make("review"),
name: Skill.Name.make("Review"),
description:
"Review a proposed change for correctness and missing tests.",
path: Skill.Info.fields.path.make("/skills/review/SKILL.md"),
content:
"Review the proposed change. Report actionable findings with evidence.",
});
});
},
});import { Plugin, Skill } from "@opencode/plugin";
export default Plugin.define({
id: "review-skill",
async setup(ctx) {
await ctx.skill.transform((skills) => {
skills.add({
id: Skill.ID.make("review"),
name: Skill.Name.make("Review"),
description:
"Review a proposed change for correctness and missing tests.",
path: Skill.Info.fields.path.make("/skills/review/SKILL.md"),
content:
"Review the proposed change. Report actionable findings with evidence.",
});
});
},
});Import this plugin and add it to plugins. The content supplies instructions without reading a local file at runtime. Refer to OpenCode skills ↗︎ for registration and Agent Skills for authoring guidance.