Pi Durable: A Durable Harness for Long‑Running, Multi‑User Agentic Applications
Pi Durable delivers durable, multi‑user agentic applications
Pi Durable is an experimental harness shipped with Pi 1.0 that lets large‑language‑model agents run indefinitely, survive process crashes, and be accessed by many users simultaneously. It provides a storage‑backed transcript, durable task checkpoints, and a plug‑in extension system, making it a solid foundation for building always‑on AI workers.
Long‑running agents on any JavaScript runtime
Pi Durable opens a harness over a storage backend (memory, SQLite, or JSONL) and can run wherever a JavaScript runtime exists – Node, Bun, Cloudflare Durable Objects, etc. The storage interface is tiny, so developers can implement it on top of key‑value stores or Postgres. Only the active transcripts, live tasks, and pending submissions are kept in memory; everything else stays on disk until needed. Compaction summarises older messages, so even conversations with tens of thousands of entries fit comfortably in RAM.
import { Harness } from "@earendil-works/pi-durable";
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
const harness = await Harness.open(
await openNodeSqliteStorage("./agent.sqlite"),
{ models, registry, env },
context,
);
Crash‑resilient execution via task checkpoints
Every step—model request, tool call, or compaction—is a task that writes a checkpoint before moving on. If the process dies, a new process re‑opens the same storage, discovers unfinished tasks, and resumes them from the last checkpoint. Model calls are retried, tool calls are re‑executed only when marked replay: "safe", and a requestId guarantees exactly‑once semantics for submissions.
await root.submit({ type: "input", content: "Fix flaky login test", requestId: "job-42" }, ctx);
// process crashes here
const newHarness = await Harness.open(...);
newHarness.resume(); // continues the interrupted run
Parallel conversations and forking
A single harness can host unlimited concurrent conversations. Conversations can fork at any transcript entry, inheriting the parent’s history without copying it. This models Slack‑style channels and threads: the channel runs as one conversation, each thread as a fork that proceeds independently.
const thread = await channel.fork(answer, { ownership: { kind: "ownerless" } }, ctx);
await Promise.all([
thread.submit({ type: "input", content: "@agent roll back?" }, ctx).wait(ctx),
channel.submit({ type: "input", content: "@agent who is on call?" }, ctx).wait(ctx),
]);
Extensible architecture: extensions, tools, hooks, and tasks
Pi Durable treats extensions as named bundles of system‑prompt sections, tools, hooks, and custom tasks. Conversations store only the names of selected extensions, so code can be upgraded without breaking durability. Tools run as durable tasks; a replay flag declares 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) => ({
content: [{ type: "text", text: await tracker.search(args.query) }],
}),
});
Hooks can rewrite requests, block tool calls, or inject decisions. Multiple hooks chain in the order extensions are selected, and each hook can store a memo so that decisions survive restarts.
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." };
},
});
Custom tasks receive the same checkpointing and ownership semantics as built‑in model and tool tasks. The example shop.checkout task demonstrates idempotent payment handling, fail‑fast aborts, and background vs. foreground execution.
Automatic compaction for endless context windows
When a conversation approaches the model’s context limit, Pi Durable launches a background compaction task that summarises older messages. The summary is inserted at the next turn boundary, and the conversation only waits for the summary if the upcoming request would otherwise exceed the limit. Developers can also trigger manual compaction or a full reset() to start a new context while preserving searchable history.
await root.compact("Keep the names of the failing tests", ctx);
Durable application state via typed documents
Application state lives in documents—typed JSON objects stored atomically alongside the transcript. Documents are versioned, rewindable, and inherit values across conversation forks. A Todo list example shows how a tool can mutate a document and a UI can subscribe to live updates.
const Todos = defineDoc<{ items: string[] }>({
kind: "app.todos",
version: 1,
scope: "conversation",
history: "rewindable",
fork: "asOf",
initial: () => ({ items: [] }),
});
Malleability: hot‑swap extensions without downtime
The extension registry can be updated at runtime. Installing an extension under an existing name replaces the previous implementation instantly. Ongoing tool calls finish with the code they started with; subsequent calls use the new version. Because conversations store only extension names, a restart automatically picks up the latest code.
registry.install(await loadExtension("./ops.ts")); // hot‑swap
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 stream incremental updates. Clients can join late, watch the transcript, or steer the agent by submitting new inputs with whenBusy: "steer".
const view = await thread.viewState(ctx);
view.subscribe(v => render(v));
await thread.submit({ type: "input", content: "Check staging logs", whenBusy: "steer" }, ctx);
Community reaction
"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" – lukebuehler【source】
"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【source】
"I like the multi‑user bit the most. Should make it easier to build my remote control tool" – skeledrew【source】
"Brilliant. I wish the durable application state wasn't restricted to just the json documents though. There should be some integrated way of implementing the outbox pattern" – vmg12【source】
These comments highlight that the community sees Pi Durable as a timely, extensible alternative to existing agent frameworks, while also pointing out areas for future improvement such as richer state‑sync mechanisms.
Getting started
Pi Durable is shipped as an experimental package. Install the core libraries and explore the bundled examples:
npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
# run the demo coding agent
node packages/coding-agent/src/experimental/durable/main.ts
# run the vacation‑planning demo
node packages/coding-agent/src/experimental/vacation/main.ts
The repository contains a README, over thirty test‑driven examples, and a full‑featured vacation‑planner TUI that demonstrates concurrent subagents, crash recovery, and live document updates.
Outlook
Pi Durable is positioned as a minimal yet powerful foundation for always‑on AI workers. Its design choices—storage‑backed transcripts, checkpointed tasks, extensible extensions, and multi‑client views—address the core challenges of durability, scalability, and collaboration. As the authors plan further releases, we can expect richer sandboxing, tighter integration with external state stores, and broader language support beyond TypeScript.
Sources
Related
- Dispatch
- Project
- Dispatch
- Project
- Dispatch