durable-harness
v0.0.2
Published
Durable coding-agent harnesses (Claude Code, Codex, OpenCode, Pi) on a Workflow SDK World: the session is a run, its event log the source of truth, and a crash resumes the turn from the log on another machine.
Maintainers
Readme
durable-harness runs coding-agent harnesses (Pi, OpenCode, libfx's fx, Claude Code and Codex) as durable agents on a Workflow SDK World. Each session is a workflow run: its conversation, the turn in flight and every model and tool call are in the run's event log, so a crash anywhere resumes the turn from the log on another machine, and a tool call that isn't safe to repeat is never run twice.
[!NOTE] durable-harness is experimental: it's on
0.0.x, and its API may change between releases.
Quick start
Install it with a harness, here Pi with pi-durable, in a Next.js app:
npm install durable-harness @earendil-works/pi-durable @earendil-works/chord @earendil-works/pi-ai just-bashOpen Pi's own durable Harness on a World-backed storage. Agents are
tool-less by default; Pi's coding tools go on a virtual workspace, whose files
live in the session's log:
// lib/agent.ts
import { BACKGROUND_CONTEXT as ctx } from '@earendil-works/chord/context';
import { createRegistry, Harness, type Storage } from '@earendil-works/pi-durable';
import { CodingTools, GATEWAY_PROVIDER, gatewayModels, resumeWith, virtualEnv } from 'durable-harness/pi-durable';
const models = gatewayModels();
const registry = createRegistry();
registry.install(CodingTools);
export const agent = { model: { provider: GATEWAY_PROVIDER, modelId: 'anthropic/claude-sonnet-4.5' } };
export const open = resumeWith((storage: Storage) => Harness.open(storage, { models, registry, env: virtualEnv() }, ctx));Serve it from a chat route, and configure the integration:
// app/api/chat/route.ts
import { chatRoute } from 'durable-harness/pi-durable';
import { agent, open } from '@/lib/agent';
export const { GET, POST } = chatRoute({ open, agent, authorize: async (request) => { /* your auth */ } });// next.config.ts
import type { NextConfig } from 'next';
import { withDurableHarness } from 'durable-harness/next';
const nextConfig: NextConfig = {
// your existing config
};
export default withDurableHarness(nextConfig);withDurableHarness discovers modules that export agents created at module
scope with durableAgent, claudeCode, codex, opencode or pi, and modules
that register a pi-durable opener with resumeWith. A script that creates an
agent for itself, an exported factory that creates one later, and code samples
in strings or comments are excluded. Import aliases, default exports, export
lists, and containers or accessors for an existing agent are supported.
For custom factories or effect-only agent registration, list the modules
explicitly: withDurableHarness(nextConfig, { agents: ['lib/agent.ts'] }).
Choose your harness and framework in the agent harnesses docs.
Harnesses
- Pi:
durable-harness/pi-durable(recommended: Pi's own durableHarnesson a World), ordurable-harness/pi. - OpenCode:
durable-harness/opencode. - fx:
durable-harness/fx, libfx's kernel tools for its own durable agents. - Claude Code and
Codex:
durable-harness/claude-codeanddurable-harness/codex. They run only in the session's own Vercel Sandbox, never in a shared Function (why).
Model-chosen commands and files run on a virtual workspace (virtual()) or
in the session's own Vercel Sandbox (sandbox()), never on the machine that
drives the session. See tools
and security.
Sending messages
A message goes to a session as one of three kinds: prompt (the default),
followUp or steer (sessions.send(sessionId, text, kind), or a chat
route's POST kind). On an idle session, each kind starts a turn. When a
message arrives while a turn is running, what happens depends on the harness:
| Harness | steer during a turn | prompt or followUp during a turn |
| --- | --- | --- |
| Pi (durable-harness/pi-durable and durable-harness/pi) | Goes into the running turn after its current tool calls (Pi's steer). | Joins the running turn while Pi is still working on it (Pi's followUp). Pi answers it after the turn's own answer, before the turn ends, so the turn's stream carries both answers and ends once. One that arrives as Pi is finishing gets a turn of its own. |
| OpenCode | Goes into the running turn at OpenCode's next step. | Waits for the turn to end, then starts its own turn. |
| fx | Goes into the running turn at its next model request. | Waits for the turn to end. Messages that waited together then start one turn. |
| Claude Code | Goes into the running turn, at priority next. | Waits for the turn to end, then starts its own turn. |
| Codex | Goes into the running turn (turn/steer). If Codex has just ended its turn, the steer runs next, in the same turn. | Waits for the turn to end, then starts its own turn. |
On every harness, prompts and follow-ups are answered in the order they were sent, after the turn that was running. The difference is whether a prompt sent during a turn becomes part of that turn, as on Pi, or gets a turn of its own. Pi's agent can only queue mid-turn input as a steer or a follow-up, and Claude Code, Codex and OpenCode take only steers into a running turn.
Run anywhere
Local development uses the Workflow SDK's local World with no configuration. Deploy to Vercel for managed storage, queuing and scaling, with sessions in Agent Runs. To self-host, use the Postgres World or any other World: see self-hosting.
Contributing
Contributions are welcome. Use issues to collaborate with the team, and read CONTRIBUTING.md before opening a pull request.
Security
If you find a security vulnerability in durable-harness, disclose it responsibly instead of opening a public issue.
To participate in our Open Source Software Bug Bounty program, please email [email protected]. We will add you to the program and provide further instructions for submitting your report.
