The Extension-Shaped Harness

how a 7-tool agent turns everything else into a plugin

earendil-works/piView the repository
LANGUAGE
TypeScript · Node/Bun
SOURCE LOC
120,756 (+115k tests)
PACKAGES
10
BUILT-IN TOOLS
7 — 4 on by default
PROVIDERS
40 · 10 wire APIs
OS SANDBOX
none — by design
All 25 blocks

Surfaces & entry

The turn

Tools

Context & memory

Model layer

Safety

Extensibility

State

Isometric plate: the 25 subsystems of PiBlocks are grouped into districts by hatching and tint; block height is a rough proxy for code mass. Lines trace the data flow of a single turn. The block index beside this diagram carries the same information as text.AVWXBJCKLDEMPFNOQRHGYISUT

Hover to read · drag to pan · ⌘/ctrl + scroll to zoom

How to read this plate

Each block is one subsystem of Pi. Its height is a rough proxy for code mass, calibrated the same way across every harness so the plates are comparable. Hatching and tint group blocks into districts — surfaces, the turn, tools, context, the model layer, safety, extensibility and state.

Lines trace the real data flow of a single turn plus the major supporting links. Pick any block for a plain description and the files it was read from, or trace the turn to walk the path a request actually takes.

What this is

Pi is the smallest complete agent of the six. It ships seven tools — read, bash, edit, write, grep, find, ls — and only turns on four of them by default. There is no MCP, no sandbox, no permission prompt, no subagent system, no web UI. That is not an omission; it is the thesis.

Everything a competitor ships as a subsystem, Pi ships as an example extension. Permission gates, sandboxing, subagent delegation, todo lists, plan mode and git checkpoints all live in examples/extensions/ — about 75 worked programs, including a Doom overlay used as a TUI stress test. The product surface is the extension API: 34 lifecycle hooks, including ones that rewrite the entire message list, mutate raw provider payloads and replace Pi's own compaction.

The two places Pi does invest heavily are the provider layer and the session tree. Forty providers across ten wire protocols sit behind one streaming abstraction, with seven OAuth flows and per-call key resolution so refreshed tokens take effect mid-session. Sessions are an append-only tree in one JSONL file where model changes and tool-set changes are entries, so forking replays the exact configuration at any node.

Security is a documented refusal. docs/security.md argues that a partial in-process sandbox "would be easy to misunderstand as a security boundary", so Pi ships none and points at containers instead. The only enforced boundary is project trust: whether repo-local extensions and settings are allowed to load at all.

Implementation

Loop. packages/agent/src/agent-loop.ts — 796 lines. runLoop drains a steering queue, applies transformContext, converts to provider messages, streams, and mutates the last context slot in place with each cumulative partial snapshot. Tool calls run through Promise.all unless a tool declares executionMode: "sequential"; preparation and validation stay strictly ordered so gating is deterministic.

Edits. Exact multi-string replace against the original file — never incrementally. Uniqueness is enforced ("Found N occurrences… must be unique"), overlaps are rejected pairwise, and a fuzzy fallback folds Unicode quotes/dashes and trailing whitespace before re-matching, then overlays only changed line blocks so untouched bytes survive.

Compaction. 969 lines. Anchors token counting on the last real provider usage and estimates only the tail. If the cut point lands mid-turn it generates a separate "turn prefix" summary so no turn is left with orphaned tool calls. It feeds the previous summary back through a dedicated update prompt, carries an accumulated read/edited file set forward, and issues the summarization call with cacheRetention: "none" and a throwaway session id so it never pollutes the prompt cache.

Second runtime. packages/agent/src/harness/ contains AgentHarness — a crash-recoverable operation state machine over lanes, write-once records and registers, with a 700-line spec and a pluggable SQLite backend. It drives the server and evals while the TUI still runs the original loop.