A month ago I started building a coding agent from scratch, with one rule: build first, then read. I'd picked pi as my reference precisely because it was described as the smallest serious harness — four packages, four tools, a system prompt under a thousand tokens, and competitive benchmark scores anyway. Build each piece myself, then open pi's version and diff the decisions. Reading first turns study into copying.
Last week I finally opened it. The premise I'd chosen it on was gone.
| pi (v0.84) | mine | |
|---|---|---|
| packages | 10 | |
| 1 | ||
| tools shipped | 8 | |
| 5 | ||
| agent core | ~12,600 lines | ~300 |
| iteration cap | none | |
| 20 | ||
| compaction | yes | |
| none |
The edit tool alone is 443 lines. There's a compaction subsystem, session branching, an extension system, and its own eval suite. My first note to myself was blunt: the minimalism thesis has eroded.
That note was wrong twice over, and both ways are the interesting part.
pi is known for what it leaves out: no MCP, no subagents, no to-do tool, no plan mode, no background bash. Each omission is an argument — background bash means managing processes you can't see; subagents mean decisions made where you can't inspect them.
I grepped for all five. Two came back present. There it is, I thought — the project grew up and quietly took the features back.
Then I looked at what the matches actually were. Both were inside a vendored highlight.min.js: the "MCP" hits were the string
mcpy
in a language list, the "TODO"s were comments in third-party source.All five refusals hold. I had a paragraph half-written arguing the opposite.
The same correction applies to the tool count. pi ships eight tools, but one line decides what the agent actually gets:
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];
The original four. ls
, grep
, find
and truncate
are opt-in. The default context surface — what an ordinary session puts in front of the model — never grew.
That's the principle I keep coming back to: do less, and do more of what's left. Not less effort — fewer things, each taken further. pi is the cleanest example of it I've read.
It didn't grow scope. It grew depth. The edit tool is 443 lines because it handles multiple disjoint edits in one call, matches each against the original file rather than incrementally, detects and restores line endings, and serialises concurrent mutations. Compaction arrived because long sessions genuinely need it. The eval suite arrived because you can't improve a harness you can't measure. Every one of those is the same capability set, done properly — while the list of things it says no to hasn't moved in a year.
So "minimal" was never about line count. A four-tool agent whose tools are shallow isn't minimal, it's unfinished. An eight-tool agent that has said no to twenty others is disciplined. I'd been treating small as the virtue; small was the side effect.
pi ships an eval package. I built one separately, without looking. Both isolate each run in a temp directory, persist the session as an artifact, index runs in a JSONL file, compare configurations against each other, and pin the provider and model into the results. Two people solving unattended agent evaluation independently produced the same five primitives — stronger evidence the design is right than either implementation alone.
One surprise: pi's "judge" isn't an LLM. createJudge()
is a deterministic multi-criteria scorer. For all the talk of LLM-as-judge, a mature harness still grades its agent with code.
pi checks whether the model's output was cut off by the token limit, and if so refuses every tool call in that message — truncated output means possibly-truncated arguments, and half a JSON argument still parses sometimes. My harness records that stop reason in three places and branches on it in none. A real bug I hadn't found, in a family that had already bitten me twice.
(The other: pi's graders assert on tool calls — name, arguments, status, return value — not just the final answer. Mine grades the end state, so it can tell me a task succeeded but not that the agent got there acceptably.)
Build-first paid off in a way I didn't anticipate. Because I'd already made every decision myself, the diff wasn't a list of things pi does — it was a list of places where my reasoning and theirs parted, each with a why attached. Reading first, I'd have copied the structure and learned none of it.
And the most valuable thirty seconds was checking what a grep actually matched before believing it. I've now been wrong that way six times on this project: a metric that reported zero problems, twice; a regex that silently matched nothing; a grader so lenient it reported 100% while quality collapsed to zero. Every time, the number was confident and the number was wrong.
The rule I keep relearning: a measurement that says everything is fine is a hypothesis, not a result.
Built as a learning project on agent harnesses and evaluation. Comparison notes and the harness are in zachzwy/agentloop. I'm Wenyu — github · linkedin.