Pi Durable: A Harness for Long‑Running, Multi‑User Agentic Applications
TL;DR – What Pi Durable Is and Why It Matters
Pi Durable is a new experimental harness released with Pi 1.0 that enables agents to run anywhere, survive process crashes, handle infinitely long conversations, and support multiple users and extensions. It provides the storage, task checkpointing, and execution‑environment abstractions needed to build durable, malleable agentic applications.
The Core Problem: Pi 1.0 Is a Single‑User, Terminal‑Bound Agent
Pi 1.0 is designed for a single human to drive a coding agent inside a terminal. If the process dies, the user manually restarts it and tells the agent to continue. This model works for interactive coding but does not support:
- Always‑on agents that run unattended for days or weeks.
- Multi‑user collaboration where many clients watch or steer the same conversation.
- Concurrent conversations such as Slack threads or parallel sub‑agents.
- Robust recovery from container restarts, VM crashes, or out‑of‑memory kills.
Pi Durable addresses these gaps without changing the original Pi coding agent.
Harness Definition – Storage + Execution Machinery
A harness in Pi Durable is the combination of:
- Storage backend – persists transcripts, tasks, and typed JSON documents.
- Execution environment – supplies tools (file access, shell, etc.) to the agent.
- Task engine – runs model requests, tool calls, and compaction as durable tasks.
The harness exposes a simple API (Harness.open, harness.root, conversation.submit, etc.) that agents can understand and manipulate.
Long‑Running Agents Anywhere
Pi Durable ships three storage adapters out of the box:
- Memory – pure‑in‑memory for testing.
- SQLite – on‑disk relational store.
- JSONL – line‑delimited JSON log.
All adapters expose the same minimal interface, making it trivial to implement a custom backend (e.g., Postgres, DynamoDB, or Cloudflare Durable Objects). Only one process owns a storage instance at a time; other clients attach as readers/writers.
The harness keeps only the working set (active transcripts, live tasks, pending submissions) in memory. Older messages are compacted into summaries, so even conversations with tens of thousands of messages fit comfortably within a model’s context window.
Example: Opening a SQLite Harness
import { Harness, openNodeSqliteStorage } from "@earendil-works/pi-durable";
import { createModels, openaiProvider } from "@earendil-works/pi-ai";
import { createRegistry, CodingTools } from "@earendil-works/pi-durable/tools";
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
const models = createModels();
models.setProvider(openaiProvider());
const registry = createRegistry();
registry.install(CodingTools);
const env = ({ cwd }: { cwd?: string }) =>
new NodeExecutionEnv({ cwd: cwd ?? process.cwd() });
const harness = await Harness.open(
await openNodeSqliteStorage("./agent.sqlite"),
{ models, registry, env },
BACKGROUND_CONTEXT,
);
const root = await harness.root(BACKGROUND_CONTEXT, {
agent: { model: { provider: "openai", modelId: "gpt-6.1-sol" }, cwd: "/work/repo" },
});
Crash‑Resilient Execution
Every step—model request, tool call, or compaction—is a task that checkpoints its input and output before moving forward. If the process dies, a new process re‑opens the same storage, discovers unfinished tasks, and resumes them from the last checkpoint.
- Model calls that were interrupted are re‑sent; the partial answer is marked aborted in the transcript.
- Tool calls declare a
replaypolicy ("safe"or omitted). Safe calls are automatically retried; unsafe calls inform the model that they were interrupted. - A
requestIdguarantees exact‑once semantics for submissions: retries after a crash return the original result instead of issuing a duplicate request.
Crash‑Recovery Example
const job = { type: "input", content: "Fix the flaky login test", requestId: "job-42" } as const;
await root.submit(job, ctx); // process dies during tool call
// New process resumes
const harness2 = await Harness.open(await openNodeSqliteStorage("./agent.sqlite"), { models, registry, env }, ctx);
harness2.resume();
const root2 = await harness2.root(ctx);
const settled = await (await root2.submit(job, ctx)).wait(ctx); // returns the same answer
Concurrent Conversations and Forking
A single harness can host many independent conversations that run in parallel without blocking each other. Conversations can fork at any transcript entry, inheriting the parent’s history up to the fork point but diverging thereafter.
Forking models Slack channels and threads: the main channel is one conversation; each thread is a fork that runs concurrently.
Forking Example
const channel = await harness.root(ctx);
const q1 = await channel.submit({ type: "input", content: "@agent why did the deploy fail?" }, ctx);
const answered = await q1.wait(ctx);
const thread = await channel.fork(answered.answer!, { ownership: { kind: "ownerless" } }, ctx);
await Promise.all([
thread.submit({ type: "input", content: "@agent can we roll it back?" }, ctx).then(t => t.wait(ctx)),
channel.submit({ type: "input", content: "@agent who is on call today?" }, ctx).then(t => t.wait(ctx)),
]);
Each conversation stores its own agent configuration (model, tools, working directory), enabling cheap reviewers or specialized sub‑agents.
Extensible Architecture – Extensions, Tools, Hooks, and Tasks
Extensions
An extension bundles:
- System‑prompt sections (re‑rendered before each model request).
- Tools (functions the model can invoke).
- Hooks (intercept or modify task execution).
- Custom tasks (user‑defined durable workflows).
Conversations select extensions by name; only the names are persisted, so after a restart they automatically load the latest code.
System‑Prompt Sections
Sections are functions that read from the conversation’s execution environment and return text inserted into the system prompt. Changes are versioned in the transcript, so a restart sees exactly what the model saw.
import { defineExtension, section } from "@earendil-works/pi-durable";
const ProjectContext = defineExtension({
name: "project-context",
sections: [
section("agents_md", (input) => agentsMd.latest(input.env)),
section("skills", (input) => skills.latest(input.env)),
],
});
Tools and Replay Semantics
Tools are durable tasks. The replay flag tells Pi Durable whether a tool is safe to rerun after a crash.
const searchIssues = defineTool({
name: "search_issues",
description: "Search the issue tracker",
parameters: Type.Object({ query: Type.String() }),
replay: "safe",
execute: async (args, api) => {
api.output(`searching for ${args.query}\n`);
return { content: [{ type: "text", text: await tracker.search(args.query) }] };
},
});
A non‑safe tool like deploy omits replay; if interrupted, the model is notified and the operation is not repeated.
Hooks – Intercepting Tasks
Hooks can modify or block model requests, tool calls, or compaction. They run in a chain ordered by the conversation’s selected extensions.
import { hook, ToolTask } from "@earendil-works/pi-durable";
const Approval = defineExtension({
name: "approval",
hooks: [
hook(ToolTask, {
beforeTool: async (call, api, ctx) => {
if (call.name !== "deploy") return undefined;
let approved = await api.memo<boolean>("approval:deploy", ctx);
approved ??= await api.memo("approval:deploy", await askInSlack(call), ctx);
return approved ? undefined : { block: "Nobody approved the deploy." };
},
}),
],
});
Hooks survive crashes because they store decisions in a memo attached to the task.
Custom Tasks – Durable Workflows
Tasks are first‑class durable units with explicit phases, checkpoints, and abort logic. They can own other tasks, forming an ownership tree that guarantees clean abort propagation.
const Payment = defineTask<{ card: string }, { phase: "charge" }, string>({
name: "shop.payment",
version: 1,
initial: () => ({ phase: "charge" }),
phases: {
charge: async (task, rt, ctx) => {
const charge = await bank.charge(task.input.card, `payment-${task.id}`);
await rt.commit(() => ({
status: "terminal",
outcome: charge.ok ? { status: "completed", result: charge.receipt }
: { status: "failed", error: { message: charge.error } },
}), ctx);
},
},
abort: async (task, rt, ctx) => {
await bank.refund(`payment-${task.id}`);
await rt.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), ctx);
},
});
A higher‑level Checkout task coordinates multiple Payment tasks, demonstrating fail‑fast orchestration and automatic refunds on abort.
Automatic Compaction for Infinite Context
When a conversation approaches the model’s context limit, Pi Durable runs a background compaction task that summarizes older messages. The summary is inserted at the next turn boundary, keeping the active window within reserveTokens (default 16 384). If the provider still rejects the request, the harness compacts once more and retries.
await harness.root(ctx).compact("Keep the names of the failing tests", ctx);
The reset() operation can start a fresh context while preserving older messages for later search, enabling hand‑off patterns.
Durable Application State via Typed Documents
Application state (e.g., a todo list) lives in documents—typed JSON objects stored atomically with the transcript. Documents support:
- Scope (
conversationorglobal). - History mode (
rewindablefor versioned reads). - Fork behavior (
asOfto inherit parent state at fork point).
const Todos = defineDoc<{ items: string[] }>({
kind: "app.todos",
version: 1,
scope: "conversation",
history: "rewindable",
fork: "asOf",
initial: () => ({ items: [] }),
});
Tools can read/write documents inside a transaction, guaranteeing that the transcript and state never diverge.
Malleability – Hot‑Swapping Extensions
The registry can be updated at runtime. Installing an extension under an existing name replaces it instantly. Ongoing tool calls finish with the code they started with; subsequent calls use the new implementation. Because conversations store only names, a restart automatically picks up the latest version.
registry.install(await loadExtension("./ops.ts")); // replaces previous "ops" extension
Multiplayer Collaboration
All UI state is derived from committed storage, so any number of clients can attach to a conversation, receive the current view, and then subscribe to incremental updates.
const view = await thread.viewState(ctx);
view.subscribe(v => render(v));
await thread.submit({ type: "input", content: "Check the staging logs first", whenBusy: "steer" }, ctx);
Clients can also watch low‑level commit streams (thread.watch()) or higher‑level event streams (watchEvents()).
Community Reaction (Selected HN Comments)
"Very cool to see Pi build a durable agent harness too. I've been building in this space for quite some time myself… major players are building products in this space: LangChain Deep Agents, Vercel Eve, OpenAI Agents API, Anthropic Managed Agents, etc." – lukebuehler
"The entire source code, without tests, is about 15,000 lines, which comes out to about 150,000 tokens with GPT and about 250,000 with Claude." – ireadmevs (highlights the size of the code‑base and token‑count differences).
"I like the multi‑user bit the most. Should make it easier to build my remote control tool…" – skeledrew
"Brilliant. I wish the durable application state wasn't restricted to just the json documents though. There should be some sort of integrated way of implementing the outbox pattern…" – vmg12
"Cross‑post from the 1.0 thread. I'm building a harness for Slack… dealing with JSONL session files and pod interruptions adds complexity. I'm using DBOS for that now." – azuanrb
These comments underline the community’s appetite for durability, multi‑user support, and flexible storage back‑ends.
Getting Started
Pi Durable is experimental; the API may evolve. To try it locally:
npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
# Clone the Pi repo and run the examples
npm install && npm run build
node packages/coding-agent/src/experimental/durable/main.ts
node packages/coding-agent/src/experimental/vacation/main.ts
Explore the over‑30 example scripts in packages/durable/test/examples and the vacation‑planning TUI for a concrete use‑case.
Outlook
Pi Durable provides a solid, extensible foundation for building always‑on, collaborative agents. By separating the harness from the coding agent, Earendil can iterate on durability, storage, and multi‑user features without disrupting the core Pi experience. The upcoming weeks will see more production‑grade extensions (e.g., Slack bots, GitHub triage agents) and deeper integrations with external state stores.
Sources
Related
- Dispatch
- Project
- Project
- Project
- Dispatch