pi-durable, from the ground up
20th October 2026
I built a runnable example of pi-durable and tested every lesson in it. The code lives at avparkhi/pi-durable-hello. The example has five scripts and one SQLite file. It uses a free fake model by default. It needs no server, no web UI, and no cloud. This post explains what the library does, and why each piece exists.
The problem
A normal agent loop keeps its work in process memory. The loop calls the model, runs a tool, and calls the model again. If the process dies, the work dies with it.
A chat resume does not solve this. It restores the text of the chat. It does not restore:
- which tool call was running when the process died,
- what that tool already changed on disk,
- what input waited in the queue behind it,
- what your application believed was true at that moment.
So the window that started the agent owns it. Close the laptop, and the agent stops.
The idea
pi-durable commits every step to storage before it shows that step to anyone. The process becomes disposable. If it dies, you open the same storage again. The harness then knows where every piece of work stopped.
Once a run survives its own process, three things become possible:
- The agent can live on a machine, not in a terminal window.
- Many clients can watch and steer the same agent.
- The agent can wait hours for a human and then continue.
That is the whole frontier in one sentence: the agent outlives the client.
The building blocks
Each term below is a real type in the library. You will see all of them in the lessons.
| Block | What it is | Why it exists |
|---|---|---|
| Storage | A SQLite file, a JSONL folder, or memory | The place where durability lives. One process owns a file at a time. |
| Harness | The object that opens one storage and runs agents on it | All writes go through one line of atomic commits. |
| Conversation | A transcript. The first one is the root. | Each agent is one conversation. |
| Entry | One immutable record in the transcript | pi.user, pi.assistant, pi.tool-result, pi.system, pi.reset, pi.compaction |
| Commit | One atomic write of entries, documents, and tasks together | The harness stores all of it, or none of it. |
| Document | Typed JSON stored next to the transcript | The agent config, the inbox, the live stream state, usage, and your own state. |
| Task | A durable state machine with a checkpoint at every phase | A restart continues from the last checkpoint. |
| Submission | Input you give to a conversation, with an optional requestId | The harness admits each requestId exactly once. |
| Extension | A named bundle of tools, prompt sections, hooks, and tasks | The conversation stores the name. The code can change while the agent runs. |
| Watch | A subscription to committed state | A UI is only a watcher. A late joiner gets a snapshot. |
One input flows through these blocks like this:
submit(input) → pi.user entry
pi.generation task → pi.assistant entry (with tool calls)
pi.tool task × n → pi.tool-result entry × n
pi.generation task → pi.assistant entry (the answer)
→ the submission is "done"
Each line above is a commit. Each task saves a checkpoint. That is why a crash at any line is safe.
The one rule that makes crashes safe
Every tool declares a replay policy.
- A tool marked
replay: "safe"runs again after a crash. A tool that reads a file is safe to repeat. - Any other tool returns an interrupted result after a crash.
bashis one of these.
The interrupted result tells the model: “this tool was interrupted and may have partially run.” The model then decides what to do. It does not repeat a side effect by accident.
This is also the honest limit of durability. Durable means checkpointed, not exactly once. A payment or a deploy still needs its own idempotency key.
Run the five lessons
Clone the repo and run the lessons in order. Each lesson takes less than ten seconds. The scripts use a free fake model named faux. It echoes your text. If your text starts with !, it runs one bash tool with the rest of the line. To use a real model, copy .env.example to .env. Then set PROVIDER=openrouter and your key.
git clone https://github.com/avparkhi/pi-durable-hello
cd pi-durable-hello
npm install
npm run hello # lesson 1: one question, one answer, stored in .data/agent.sqlite
npm run hello # run it again and compare
npm run crash # lesson 2: the process kills itself while a tool runs
npm run resume # lesson 3: the same run finishes in a new process
npm run twice # lesson 4: the same requestId twice, the same submission
npm run watch # lesson 5: stream events while a steer joins the run
npm run clean # delete .data/ to start over
Lesson 1: hello
The script opens .data/agent.sqlite, sends one question, waits for the answer, and prints every stored entry.
Run it twice. The second run prints the entries of the first run too. The SQLite file is the conversation.
#7 pi.user What is a durable agent?
#10 pi.system
#11 pi.assistant Echo: What is a durable agent?
The pi.system entry stores the system prompt and the tool list of that request. Later requests store only what changed. This keeps provider prompt caches warm.
Lesson 2: crash
The script sends !sleep 4 && echo FINISHED. The model calls bash. The tool runs for four seconds. After 1.5 seconds the script calls process.exit(1). There is no close and no cleanup.
Nothing is lost. The user entry, the assistant entry with the tool call, and the tool task with its checkpoint are already in SQLite.
Lesson 3: resume
The script opens the same file and calls harness.resume(). The scheduler finds the pending tool task. bash is not replay-safe, so the task writes an interrupted result. The model reads it and answers.
#16 pi.user !sleep 4 && echo FINISHED
#19 pi.assistant Running it. [tool bash {"command":"sleep 4 && echo FINISHED"}]
#21 pi.tool-result <harness> [error] Tool bash was interrupted and may have partially run </harness>
#23 pi.assistant Tool finished: ...
The first process died after it wrote line 19. A new process wrote lines 21 and 23 from the same file.
The script also shows harness.submission(id). You can wait for a submission from a different process than the one that sent it.
Lesson 4: twice
The script sends the same input with requestId: "greeting-1" two times. Both calls return the same submission id.
first.id=25 second.id=25 same=true
A retry after a timeout, or after a restart, never sends the message twice.
Lesson 5: watch
The script opens an event stream with watchEvents(). Then it sends a tool-using input and a steer while the run is busy. The steer joins after the current tool round. You see inbox_update, then turn_start, then the answer to the steer.
The web UI and the terminal UI of Durable Pi use exactly this stream. A UI is a watcher of committed state, nothing more.
Where this leads
Durable Pi is an app built on this library. It puts a small HTTP server around the harness. The server runs on each machine you own. It exposes sessions and agents over /v1 with a bearer token. The mapping from the lessons to the server is direct.
| You learned here | It becomes there |
|---|---|
| One SQLite file | One file per session on a device |
root.submit() with a requestId | POST /v1/sessions/:s/agents/:a/messages |
watchEvents() | An SSE stream: hello, snapshot, deltas |
harness.resume() on reopen | A device restart that resumes every session |
| A steer while busy | The “Steer” button in the web composer |
An interrupted bash result | The same result, shown in any client after a device restart |
Three patterns follow from this:
- Proactive agents. An event, not a person, starts the run. Cheap code decides when. The durable agent does the first hour of work.
- Multi-client agents. You reach one agent from a browser, a terminal, a script, or another agent.
- Human near the loop. An approval is a durable task. The agent waits for hours, and a restart finds the same task still waiting.
Limits to remember
- Durable is not exactly-once. External effects need their own idempotency keys.
- One process owns a storage file at a time. There is no cross-process lock.
- A tool like
bashhas the full power of the user that runs the process. - pi-durable is experimental. The example pins version 1.0.3.
References
More recent articles
- Hermes Agent: The Self-Improving Story - 12th June 2026
- Claude Code Dynamic Workflows: Inside Out - 29th May 2026