MMNTM logo
Technical Deep Dive

What the Pi Harness Teaches About Building Agents

Six design patterns read out of the Pi agent harness at v0.84.3: seven tools, a deleted feature, no error field, four compaction guards, and a leak the host paid for.

Prime
11 min read
#Pi#Agent Harness#Agent Architecture#Software Design#Open Source
What the Pi Harness Teaches About Building Agents

On 28 August 2026, the top commit in the Pi repository was fix(coding-agent): compact before post-tool model requests. It closed an issue about a session blowing past its context window during a long chain of tool calls. It is the fourth guard added to a subsystem whose core logic is one line of arithmetic.

That commit is the whole briefing. Pi is a twelve-month-old agent harness with 5,820 commits, ten packages, and a tool surface that has not grown since we first read it. The interesting engineering is not in what it added. It is in what it refused to add, and in one feature it built, shipped, and then deleted on purpose.

This is a code-level read of Pi at v0.84.3, and six patterns worth carrying into your own agent.

Built-in Tools

7

Unchanged while packages doubled

Packages

10

The README lists five

Compaction Guards

4

Around one line of arithmetic


1. Seven Tools, Ten Packages

Pi ships seven built-in tools: read, bash, edit, write, grep, find, ls. No todo list. No web fetch. No sub-agent spawn. That inventory has held steady while the repository grew from four packages to ten.

The restraint is the design. Every tool definition is a standing tax on the context window, paid on every single request, whether or not the model uses it. A harness that ships forty tools has decided that its users' context budget is a free resource.

"The set of tools an agent has access to is its tool inventory. Since an agent's tool inventory determines what an agent can do, it's important to think through what and how many tools to give an agent. More tools give an agent more capabilities. However, the more tools there are, the more challenging it is to understand and utilize them well." — Chip Huyen, AI Engineering (p. 541)

Pi's answer is that bash is a universal adapter. Anything you would have built a tool for, the model can already do by writing a command. The seven tools are not a starter set awaiting expansion — they are a claim that seven is enough, and that everything else belongs in an extension the operator opts into.

Count your tool definitions and multiply by every request in a session. That is your standing context tax. If a tool has not been called in a week of real traffic, it is costing you tokens on every turn to do nothing.


2. Pi Built a Steering Feature, Then Deleted It

Earlier versions of Pi polled for queued user messages after every individual tool call. If you typed while the agent was working through a batch of five tool calls, it cancelled the remaining four, marked them as errors with the message Skipped due to queued user message., and got your input in front of the model on the very next turn.

It worked. It was tested. On 16 March 2026 the maintainers deleted it, along with its dedicated test file, in a commit titled defer steering until after tool execution. At HEAD, tool batches always run to completion and steering is checked only at turn boundaries.

Steering Semantics, Before and After

FeatureBefore (≤ v0.55)At HEAD (v0.84.3)
Poll frequencyAfter every tool callAt turn boundaries only
In-flight tool batchCancelled, remainder marked as errorsAlways runs to completion
Interruption latencyNext tool callNext turn
Partial-execution statesPossibleImpossible

Read the trade honestly. They gave up responsiveness and bought a guarantee: a tool batch either ran or it did not. No half-executed batch, no synthetic errors polluting the transcript, no reasoning over a file write that was skipped because someone typed.

An agent that cancels work mid-batch produces transcripts describing things that never happened. The model then plans against that fiction. Deleting the feature removed a whole class of state the rest of the system had to reason about.

Removal is a design decision with the same standing as addition, and it is the one most teams never make. A feature that produces states nothing downstream can interpret is a liability regardless of how well it works.


3. There Is No isError Field

Here is the entire result type a Pi tool returns:

// packages/agent/src/types.ts, line 362
export interface AgentToolResult<T> {
	/** Text or image content returned to the model. */
	content: (TextContent | ImageContent)[];
	/** Arbitrary structured details for logs or UI rendering. */
	details: T;
	/** Usage from the final tool execution itself, if available. */
	usage?: Usage;
	/** Names of tools introduced by this result. */
	addedToolNames?: string[];
}

Four fields. None of them is isError. The only way to mark a tool execution as failed is to throw.

This trips up nearly everyone embedding Pi, because returning an error-shaped object is the obvious move and it fails silently — the model is told, in effect, that the operation succeeded. The docs once implied you could do it; a changelog entry corrected "misleading docs and example."

The design is deliberate, and it is textbook:

"The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence." — John Ousterhout, A Philosophy of Software Design

Pi applies it in the harder direction too. If a session is interrupted between a tool call and its result, replaying that transcript would send an API a dangling tool call and get rejected. Rather than making every caller handle that, Pi's message transform injects a synthetic error result for each orphaned call. The invalid state is repaired on the way out, so it never becomes an error anyone handles.

If your tool interface accepts a returned error object and a thrown exception, you have two failure channels and one of them is silently wrong. Pick one and make the other a type error.


4. Compaction Is Four Guards Around One Line

The trigger for compacting a context window is exactly what you would write on a whiteboard:

// packages/coding-agent/src/core/compaction/compaction.ts, line 235
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
	return contextTokens > contextWindow - settings.reserveTokens;
}

reserveTokens defaults to 16,384. That is the entire idea. Everything that makes it correct in production arrived afterward, one bug at a time:

1

Stale usage retriggered compaction (March 2026)

Token counts from before a compaction were still being checked after it, so the first prompt following a compaction immediately compacted again. Fixed by comparing message timestamps against the latest compaction boundary.

2

Persistent API errors froze the threshold (March 2026)

When a provider returned repeated 529s — or a malformed zero-usage response — the real token count never updated, so the threshold never fired and the session never compacted. Fixed with a fallback estimate whenever the stop reason is an error or usage is zero.

3

The summarizer blew its own budget (March 2026)

The call that writes the summary is itself a model call with its own context limit. Long tool outputs pushed it over. Fixed by truncating each tool result to 2,000 characters before serialization.

4

Mid-run chains escaped the check (August 2026)

The threshold was tested before a new user prompt and after a run ended, but never between turns inside one continuous run. A long tool-calling chain could sail past the window unchecked. Fixed the day this article was researched.

Three of those four are not about summarization at all. They are about when you are allowed to trust your own token count — after a compaction, during an error storm, inside an uninterrupted chain. Any team that has written "compact when the context is full" and moved on has these four bugs ahead of them.

The hard part of context management is not deciding what to summarize. It is knowing when your measurement of the context is stale, and every one of those moments is a boundary in your own control flow.


5. The Renderer Diffs Strings, Not Trees

Pi's terminal UI has no virtual DOM and no cell buffer. The entire component contract is one method that returns an array of strings:

render(width: number): string[]

Every tick, the whole component tree re-stringifies from scratch. The renderer then compares that array against the previous frame line by line, by string equality, finds the first and last changed index, and repaints only that range — wrapped in DEC synchronized output (\x1b[?2026h / \x1b[?2026l) so the terminal composites one frame instead of tearing.

The result feels smoother than agent CLIs built on far more sophisticated renderers. Two details explain why, and both are the kind of thing you only find by reading the source.

Streaming sends the whole message, every token. The update event carries the full accumulated assistant message, not a delta. The message component clears itself and rebuilds all children on every single token.

The markdown cache structurally cannot hit during streaming. Its cache key is exact string equality on the accumulated text. Since that text is different on every token by definition, the lexer re-parses everything received so far, every time.

By the usual metrics this is the slow path — quadratic re-parsing and full-tree rebuilds per token. It reads as fluid anyway, because the only thing that reaches the terminal is a diff of changed lines. The expensive work is invisible; the cheap work is what the user sees.

There is a hard edge, and it is deliberate. If any component emits a line wider than the terminal, Pi dumps its full render state to ~/.pi/agent/pi-crash.log and throws, killing the session rather than corrupting the screen. Worth knowing before you ship it: that dump contains whatever was on screen, it is written with no explicit file permissions, and this is a codebase that sets 0600 on its credentials file.


6. The Leak Gets Priced by the Host

Pi's most-scrutinized consumer is OpenClaw, which embeds it in-process to run chat agents across messaging platforms. Reading that integration tells you more about Pi's interface than reading Pi's own documentation.

To control the system prompt per channel, OpenClaw casts the session object to unknown, overwrites two fields marked private in Pi's own type declarations, and replaces Pi's internal prompt-rebuild method with a function that returns a constant.

That is not a criticism of OpenClaw. It is the correct diagnosis of an interface:

"Information hiding only makes sense when the information being hidden is not needed outside its module. If the information is needed outside the module, then you must not hide it." — John Ousterhout, A Philosophy of Software Design

The system prompt was needed outside the module. Hiding it did not remove the requirement — it relocated the cost onto the host, in the least durable form available. A private-field write is a dependency on an implementation detail with no deprecation policy and no compiler protection.

The same integration carries a second tell. Its tool adapter inspects, at runtime, which argument order a tool's execute() received, because a Pi version bump once reordered that signature. That check is a scar, and it is the most honest available answer to "what breaks when this library upgrades."

When you find a consumer reaching past your interface, you have found a missing feature, not a badly behaved user. Every private-field write in someone else's codebase is a support ticket you have not received yet.


Patterns Worth Stealing

  • Price your tool inventory in tokens. Tool schemas are billed on every request. Seven well-chosen tools plus a shell beat forty specific ones.
  • Delete features that create uninterpretable states. Pi gave up mid-batch interruption to guarantee a batch either ran or did not. That guarantee is worth more than the latency.
  • Give failure exactly one channel. No isError field means no silent success. Repair invalid states inside the module rather than surfacing them to every caller.
  • Instrument when your measurements go stale. Three of four compaction guards protect the token count, not the summary. The bugs live at control-flow boundaries.
  • Optimize what reaches the user, not what the profiler flags. A per-line string diff makes quadratic re-parsing feel instant.
  • Treat private-field writes by your consumers as a design signal. If someone must reach past your interface to do a reasonable thing, the interface is wrong.

Pi is worth reading precisely because it is small enough to read. Ten packages, seven tools, and a set of decisions its authors were willing to reverse in public. Most agent frameworks ask you to trust their abstractions. This one lets you audit them in an afternoon.

See also: For the primitives underneath every harness, start with building an agent from scratch. For the context-allocation problem compaction exists to solve, see context engineering. For the trust boundaries a harness does not enforce for you, see the agent attack surface. For a production host that embeds a harness rather than building one, see the architecture of Clawdbot.

Developer Guides15 min

Build an AI Agent from Scratch: The 80-Line Implementation

Build a working AI agent in 80 lines of Python. No frameworks—just a loop, tools, and memory. The primitives every LangChain abstracts away.

Read
Technical Deep Dive7 min

Context Engineering: From Amnesia to Expertise

Context is 90% of agent performance. How to load domain expertise, develop voice, and accumulate institutional knowledge across 200K+ token windows.

Read
Technical Deep Dive13 min

The Agent Attack Surface: Security Beyond Safety

The shift from chat to agency creates a new threat model. AI Security differs from AI Safety. Prompt injection is unsolved—defense requires architectural containment, not prevention.

Read
Technical Deep Dive14 min

The Architecture of Clawdbot: A Deep Dive into Local-First Personal AI Infrastructure

Technical analysis of the open-source personal AI assistant following Federico Viticci's MacStories coverage. Covers gateway-centric control plane, lane-based concurrency, 29+ channel plugins, multi-agent routing, execution approval gating, and memory architecture.

Read
Technical Deep Dive14 min

Building Agent Evals: From Zero to Production

Why 40% of agent projects fail: the 5-level maturity model for production evals. Move beyond SWE-bench scores to measure task completion, error recovery, and ROI.

Read
Best Practices12 min

The Hard Thing About AI Agents

The demo worked. The pilot impressed the board. Now your agent is hallucinating to customers at 3am. Here are the hard truths about deploying AI agents that nobody wants to tell you.

Read