DEV Community

Harrison Guo
Harrison Guo

Posted on Originally published at harrisonsec.com

Three Agent Harnesses, One Loop

Claude Code's agent loop is 1,729 lines of TypeScript. Codex's is 983 lines of Rust. Pi's is 794 lines of TypeScript.

Three teams. Three languages. No shared code, no shared lineage, and in Pi's case an explicit design goal of being the small one. The loop lands in the same place anyway.

That convergence is the least interesting thing about these three systems, and almost every comparison I have read stops there. The interesting part is what does not converge.

The reading is legal now

When I took apart Claude Code's query.ts in Part 2 of the deep dive, the only reason it was possible was a leak. That was a real limitation. Line numbers from a leaked bundle are a snapshot of a build nobody can check, and I could not tell you whether any of it survived the next release.

That constraint is gone.

OpenAI published the Codex harness under Apache-2.0 in August 2026, including codex exec, the SDK, and the app-server that hosts the agent core. Pi is MIT. Both are on GitHub with permanent history. Every number in this piece comes from a clone I made on 2026-09-01, and you can check all of them.

So the useful question stopped being "what does a production agent loop look like inside." We can all see. The question now is why three teams solving the same problem produced three architectures that agree about the middle and disagree about the edges.

The middle: everyone wrote the same loop

Strip each implementation to its control flow and you get the same five steps.

Assemble context. Call the model. If the response contains tool calls, execute them. Append the results to the conversation. Decide whether to loop again or stop.

That is it. That is the whole agentic pattern, and it fits in a paragraph.

Here is what it costs each team to actually ship it:

file lines language
Claude Code query.ts 1,729 TypeScript
Codex codex-rs/core/src/codex_thread.rs 983 Rust
Pi packages/agent/src/agent-loop.ts 794 TypeScript

Claude Code's while(true) runs from line 307 to line 1728, so the loop body alone is 1,421 lines. Codex and Pi spread more of the same work across neighbouring modules, so file-to-file is the honest comparison, and file-to-file the spread is 794 to 1,729. Slightly more than a factor of two, across three independent teams.

I want to flag something here, because I got it wrong in print.

That Part 2 article carried a comparison table with a row claiming most open-source agents implement the loop in "~50-200 lines." I believed it when I wrote it. It is false, and Pi is the proof: the most deliberately minimal harness in the field spends 794 lines on its loop. The 50-line version exists in tutorials. It does not exist in anything people run all day. The gap I described as "Claude Code versus open source" was really "production versus demo," and open source has since closed it.

What the extra lines buy

The five-step summary omits every hard part, and each team's loop is mostly the omissions.

A response can stream, so tool calls arrive before the message is finished and you have to decide whether to start executing. Tool calls can run in parallel or must not, depending on what they touch. The user can interrupt mid-turn, which means an abort has to unwind cleanly without corrupting the transcript. Context fills up, so compaction has to fire without losing the thread. Providers fail in ways that are worth retrying and ways that are not. Approvals can suspend the loop indefinitely while a human decides.

None of that is intelligence. All of it is the difference between a demo and a tool, which is the argument I made in The 90% Problem before I could check it against three codebases. Now I can. The loop is 5 steps and 800 to 1,700 lines, and the ratio between those two numbers is the whole point.

The edges: three different boundaries

Here is where they stop agreeing.

Every harness has to answer one question: what is outside the loop, and how does the loop talk to it. Each of these three gave a different answer, and the answer determines nearly everything else about the system.

Claude Code puts the boundary inside the process

The loop, the tools, the permission checks, the compaction and the UI live together. Coordination is function calls and shared state. That buys tight control over things that are painful to coordinate across a wire, and the five-level compression pipeline is the clearest example: it can make decisions using information the loop has not committed to anything yet.

The cost is that everything must be in-process. Another program cannot drive the agent without going through the surface Anthropic chose to expose.

Codex puts the boundary on a protocol

Codex pulled the agent core into app-server and put a documented, bidirectional JSON-RPC 2.0 interface in front of it. The README is explicit about the three primitives:

Thread: A conversation between a user and the Codex agent. Each thread contains multiple turns.
Turn: One turn of the conversation, typically starting with a user message and finishing with an agent message.
Item: Represents user inputs and agent outputs as part of the turn, persisted and used as the context for future conversations.

That is not an implementation detail written down after the fact. It is a contract. thread/start, thread/resume, thread/fork to branch with copied history, turn/start, turn/interrupt, streaming item/started and item/completed notifications, and turn/completed carrying final token usage.

Transports are stdio by default, with a unix socket and an experimental websocket listener. There is even backpressure in the protocol: saturate request ingress and you get JSON-RPC error -32001, "Server overloaded; retry later." A harness that has an overload error code has stopped thinking of itself as a CLI.

The consequence is that the VS Code extension, the terminal, and any third-party client are all peers. None of them is the real one. The agent is a service, and the loop is what runs inside it.

The cost is versioning. Once thread and turn are wire types, changing them is a compatibility event.

Pi puts the boundary at the extension host

Pi ships four tools: read, bash, edit, write. Everything else is a TypeScript extension, loaded from the project, hot-reloadable, and able to register tools, persist state into the session, and render its own terminal components.

That is a genuinely different bet. Codex says the interesting extensibility is other processes driving the agent. Pi says it is the agent growing new tools inside your repository, at runtime, in the same language as the harness. Pi deliberately ships no MCP support at all, which I will come back to in a later piece, because the reasoning is better than the summaries suggest.

The cost is that the boundary is a language boundary. Extensions are TypeScript because the host is TypeScript.

The minimalism that is not

This is the part that surprised me most, and it is the part the coverage keeps getting wrong.

Pi's reputation is radical minimalism. Four tools. A system prompt small enough that people quote its token count. That reputation is earned. I measured the default prompt at 550 tokens, which is genuinely tiny, and I will take that number apart properly in a later piece because the interesting half is what it leaves out.

Now count the codebase.

source lines
Pi, packages/*/src, tests excluded 121,240
Codex, codex-rs/core/src, tests excluded 125,574

The minimal agent and the OpenAI agent core are within four percent of each other.

Inside Pi that is 60,960 lines in the coding agent, 23,668 in the provider layer, 17,000 in the terminal UI, 12,640 in the agent package. This is not bloat and it is not a gotcha. It is what shipping takes.

Both things are true at once, and holding them together is the actual lesson. Pi minimizes what the model has to reason about. It does not minimize what the team has to maintain, and it never claimed to. Those are two different surfaces, and almost every writeup I have seen collapses them into one sentence about how small Pi is.

The same confusion runs the other way with Claude Code. A 510,000-line bundle sounds like the opposite of minimal, until you notice the model still only sees a loop and a tool list.

Model-facing surface and engineering surface are independent axes. You can be small on one and large on the other, and every one of these three is.

Why the boundary is the decision

Once you see the boundary as the design, the rest of each system reads as consequence rather than taste.

Codex has 43,591 lines of sandboxing crates and four platform backends because a service that anyone can drive cannot assume the caller is a trusted human at a terminal. Pi has none of that, and says so in its README, because it assumes exactly that. Neither is careless. They are answers to different questions, and I will take that specific difference apart next, because it is the one with security consequences that people are getting wrong right now.

Codex's thread/fork and Pi's parent-linked session tree exist for the same reason and were reached independently: once a session is a real object rather than a transcript, branching is cheap and re-running from a known point stops costing you the whole context.

This is the same move as the technique boundary. Deciding where determinism ends and judgement begins is the design. Deciding where your process ends and the outside begins is the same kind of decision, one level down.

How to choose

Not by benchmark. Benchmarks of harnesses measure the harness plus the model plus the configuration, which is a trap I will spend a whole piece on shortly.

Choose by boundary.

If other processes need to drive the agent, take Codex. The protocol exists, it is documented, it generates its own TypeScript and JSON Schema, and rebuilding that on top of a library is months of work that ends worse.

If the agent needs to grow tools inside your codebase and you can live in TypeScript, take Pi. The extension host is the shortest path from "the agent should be able to do X here" to the agent doing X, and hot reload means the agent can write and test its own tools.

If neither is true, you may not need a harness. A loop you understand, with the four tools everyone converged on anyway, is 800 lines. That is not a small number, but it is a knowable one, and now you can read three implementations before you write yours.

The loop was never the moat. It was the part that was always going to converge.


Measurements taken 2026-09-01 against github.com/earendil-works/pi and github.com/openai/codex at that day's HEAD. Claude Code figures are from the leaked bundle analysed in Part 2 and have not been re-verified against a current build.

Top comments (0)