OpenCode
Last updated Oct 22, 2026
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 models, AI Gateway catalog models, and dynamic routes through your AI binding.







Copy promptPrompt copied!
How it works
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["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 and AI Gateway 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
Install the Agents SDK and OpenCode packages:
npmyarnpnpmbun
npm i agents@latest @opencode/sdk@2 @opencode/plugin@2
yarn add agents@latest @opencode/sdk@2 @opencode/plugin@2
pnpm add agents@latest @opencode/sdk@2 @opencode/plugin@2
bun add agents@latest @opencode/sdk@2 @opencode/plugin@2
Use the latest agents release. The OpenCode packages are optional peer dependencies, required by this integration, with a supported range of ^2.0.24.
Add the 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-27",
"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-27"
compatibility_flags = [ "nodejs_compat" ]
[ai]
binding = "AI"
remote = true
[[durable_objects.bindings]]
name = "Assistant"
class_name = "Assistant"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Assistant" ]
Use it in a Durable Object
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:
src/worker-tools.jsjs
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() };
},
});
});
},
});
src/worker-tools.tsts
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:
src/index.jsjs
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?"));
},
};
src/index.tsts
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 add a provider named "Cloudflare" (id cloudflare) to OpenCode. models: { fetch: false } skips OpenCode's external model catalog refresh. The provider registers the 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.
Choose a model
this.ai() takes the same model ids as the AI SDK provider, and accepts its model options:
this.ai("@cf/moonshotai/kimi-k2.7-code"); // Workers AI
this.ai("anthropic/claude-opus-5.5"); // AI Gateway catalog
this.ai("dynamic/support"); // AI Gateway dynamic route
A catalog id uses the model catalog's spelling, such as anthropic/claude-opus-5.5. A dynamic route belongs to one gateway, so pass that gateway's id to createAI. An id that is not one of these three forms throws a TypeError on the line that names it. Streams from catalog models and dynamic routes resume after a dropped connection.
Each call returns OpenCode's model reference, { providerID, id }. Pass it to defaults.model or session.setModel(). To set how OpenCode budgets a model's context, pass limit: { context, output }. The default is 128,000 context tokens and 8,192 output tokens.
Send prompts
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.
