Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/opencode-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

Add experimental `agents/harness/opencode` and `agents/models/opencode`. `OpenCodeHarness` hosts OpenCode v2 (`@opencode/sdk/workerd`) in a Durable Object behind the same interface as `PiHarness`: OpenCode's tables are kept under an `opencode_` prefix, a Lifecycle wake job per session brings runs back after eviction, and the harness closes OpenCode when it is idle so the object can hibernate. `createAI({ binding })` from `agents/models/opencode` returns an OpenCode plugin that serves Workers AI models through the `AI` binding. Both APIs are experimental and will change.
1 change: 1 addition & 0 deletions examples/next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ lands.
| [`routing`](./routing) | This PR | `RoutedAgents` on a plain `DurableObject` hub: one Agent per chat, a per-user catalog with push-based metadata, forwarded requests and sockets |
| [`dynamic-agents`](./dynamic-agents) | This PR | A supervisor runs user-submitted code as facets: isolated storage, supervised abort, code upgrades over stable state |
| [`harnesses/codex`](./harnesses/codex) | This PR | A static Codex Rust/Wasm loop composed as a Lifecycle capability, using LanguageModelV4 and Shell Workspace |
| [`harnesses/opencode`](./harnesses/opencode) | This PR | Experimental: OpenCode v2 sessions on `agents/harness/opencode`, with Workers AI over the binding from `agents/models/opencode` |
| [`harnesses/pi`](./harnesses/pi) | This PR | Experimental: pi-durable sessions on `agents/harness/pi`, with Workspace tools and a JavaScript `exec` from `@cloudflare/computer` |
| [`harnesses/think`](./harnesses/think) | This PR | Experimental: Think's agent loop on a plain `DurableObject` with `agents/harness/think`, served to `useAgentChat` by `ThinkChat` |
| [`harnesses/container`](./harnesses/container) | This PR | Experimental: Claude Code or Codex in a Cloudflare Container, driven from a Durable Object by `agents/harness/container`, resumed across containers |
Expand Down
93 changes: 93 additions & 0 deletions examples/next/harnesses/opencode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# OpenCode harness

An experimental example that runs [OpenCode v2](https://opencode.ai/v2/docs/build/sdk/cloudflare/)
(`@opencode/sdk/workerd`) inside a Durable Object. `OpenCodeHarness` comes
from `agents/harness/opencode`, and the Workers AI provider from
`agents/models/opencode`. Both entry points are experimental and will change.
This example is the only documentation for them for now.

## What it shows

- **`OpenCodeHarness`**, a Lifecycle capability with the same interface as
`PiHarness`: `harness.prompt()`, `harness.submit()`, `harness.sessions`,
`harness.session(id)`. Each session also has `history()`, `events()` for
OpenCode's live events, and `log()` for its durable ones. A factory opens
OpenCode:

```ts
readonly harness = new OpenCodeHarness({
opencode: ({ storage }) =>
OpenCodeWorkerd.create({ storage, plugins: [this.ai.plugin] }),
defaults: { model: this.ai("@cf/moonshotai/kimi-k2.7-code") }
});
```

- **Storage.** The `storage` the factory gets keeps OpenCode's tables under
an `opencode_` prefix in the object's SQLite database, so they don't
collide with the SDK's or yours. OpenCode has no option for this yet
([anomalyco/opencode#53577](https://github.com/anomalyco/opencode/issues/53577)),
so the harness rewrites OpenCode's SQL.
- **The wake.** Each session gets one Lifecycle job. The job waits while the
session runs and completes once the session is idle. If the object is
evicted or crashes mid-turn, the job's alarm restarts it, OpenCode resumes
the turn, and the job waits again. This works with no client connected.
- **Hibernation.** An open OpenCode runs background timers that keep the
object in memory. The harness closes OpenCode 30 seconds after its last
use, once nothing is running, and reopens it on the next call.
- **`createAI` from `agents/models/opencode`.** Workers AI as an OpenCode
plugin, over the `AI` binding: no account id or API token.
- **App glue that isn't part of the harness.**
- `sockets.ts` puts sessions on `WebSockets`. Each socket gets a snapshot
of the transcript. While a session runs, one watch on `session.events()`
fans OpenCode's events out to every socket on it, and the watch stops
when the session is idle.
- `plugin.ts` removes OpenCode's file and shell tools, which have nothing
to run on in Workers, and adds notes kept in the object's storage.

## Run locally

```sh
pnpm install
pnpm run start
```

The example uses the remote Workers AI binding, which may incur Workers AI
usage. It needs no API key. If your Wrangler login can see more than one
account, set `CLOUDFLARE_ACCOUNT_ID` when starting.

Each browser gets its own Durable Object, named by a random id kept in
`localStorage`. That id is the only thing standing between one browser's
chats and another's: anyone who has it can read and use them. A real app
routes to an object the user is authorized for, and puts quotas on prompts:
this example only caps a prompt at 100,000 characters. OpenCode adds
about 3.7 MB (gzip) to the Worker, so deploying needs the Workers Paid plan.

## What to try

- `Save a note titled 'groceries' with milk, eggs and bread, then list my notes.`
- `Fetch https://example.com and tell me the page's title.`
- While a turn runs, press Enter to queue a follow-up, Steer to join the
running turn, or Stop to interrupt it.
- Reload mid-turn: you get the transcript so far, and the rest streams in.
- Open the same page in two tabs: both follow the session live.
- Create sessions from the sidebar. They run independently and in parallel.

## What OpenCode can't do here

OpenCode's Workers profile has no filesystem, shell or terminal, so those
tools are removed. Add your own through OpenCode plugins, as `plugin.ts`
does. OpenCode wraps plugin tools in its `execute` code-mode tool, so a
tool call shows up as `execute`.

On boot, OpenCode's local provider plugins (Ollama, LM Studio, vLLM) try to
reach `127.0.0.1`. Those requests fail harmlessly.

If a turn is cut off by a restart, OpenCode keeps only the text it had
already committed. It resumes with a note to the model to continue, and the
model sometimes starts over or wraps up early.

## Vite

`vite.config.ts` stubs one file inside `effect`, which OpenCode is built on:
the Scalar API reference UI, which Vite's import analysis can't parse and
OpenCode never serves here. Wrangler builds the Worker without the stub.
14 changes: 14 additions & 0 deletions examples/next/harnesses/opencode/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --include-runtime=false env.d.ts` (hash: 76c5f72967e59df334c7e9f69d3d7d19)
interface __BaseEnv_Env {
AI: Ai;
OpenCodeAgent: DurableObjectNamespace<import("./src/server").OpenCodeAgent>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
durableNamespaces: "OpenCodeAgent";
}
interface Env extends __BaseEnv_Env {}
}
interface Env extends __BaseEnv_Env {}
21 changes: 21 additions & 0 deletions examples/next/harnesses/opencode/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>OpenCode harness</title>
<script>
(() => {
const stored = localStorage.getItem("theme");
const mode = stored || "light";
document.documentElement.setAttribute("data-mode", mode);
document.documentElement.style.colorScheme = mode;
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions examples/next/harnesses/opencode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@cloudflare/agents-next-opencode-harness-example",
"description": "Experimental: OpenCode v2 hosted on a Durable Object with OpenCodeHarness",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"start": "vite dev",
"build": "vite build",
"deploy": "vite build && wrangler deploy",
"typecheck": "tsc --noEmit",
"types": "wrangler types env.d.ts --include-runtime false"
},
"dependencies": {
"@cloudflare/kumo": "^2.6.0",
"@opencode/plugin": "2.0.24",
"@opencode/sdk": "2.0.24",
"@phosphor-icons/react": "^2.1.10",
"@streamdown/code": "^1.1.1",
"agents": "*",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"streamdown": "^2.5.0"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.62.3",
"@cloudflare/workers-types": "^5.20260729.1",
"@tailwindcss/vite": "^4",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"tailwindcss": "^4.3.2",
"typescript": "^6.0.3",
"vite": "^8.1.0",
"wrangler": "^4.145.0"
}
}
Binary file added examples/next/harnesses/opencode/public/favicon.ico
Binary file not shown.
Loading
Loading