← Blog

2026.10.07 · 11 min · ai · agents · tooling

A coding agent is just a loop.

Strip a coding agent down and you find a while loop, a few tools and a chat that keeps growing. A tour of that loop, using Pi, the open-source agent that ships with four tools and a prompt under 1,000 tokens, as the specimen.

Claude Code, Cursor and Codex feel like something new: you describe a bug, and a few minutes later files have changed, tests have run and the bug is gone. It is easy to treat that as magic. It isn't. Underneath every one of them is a loop you could write in ten lines: send the chat to a model, run whatever tools it asks for, append the results, and go again until it stops asking.

The cleanest proof I know is Pi, the open-source coding agent by Mario Zechner, now published by Earendil under the MIT licence. By default it gives the model four tools (read, write, edit, bash) and a system prompt that, together with the tool definitions, fits in under 1,000 tokens. Hundreds of thousands of people use it every week. Pi 1.0 shipped on 2026.10.01. This post uses it as the specimen to show what a coding agent actually is.

Watch the 60-second version on Instagram.

I went looking for this loop when I was building a WhatsApp agent with LangGraph and needed to customise it. Once you have had to change the loop, you stop seeing the agent as a product and start seeing it as a while statement.

The model never runs anything

This is the part most people get wrong. A language model takes text in and produces text out. It can't open a file, start a process or touch your disk. What it can do is write a structured request, and the APIs every provider ships today have a field for exactly that: a tool call.

When you hand the model a list of tools (a name, a description and a JSON schema for the arguments), its reply can contain, next to or instead of prose, something like this:

assistant reply (tool call)
{ "type": "toolCall", "name": "read", "arguments": { "path": "src/app.js" } }

That is still just text the model generated. Something else has to see it, actually read src/app.js, and send the file's contents back as a new message. That something is the harness: the program around the model. When people say "the agent", they usually mean the model. The agent is the harness plus the model, and the harness is the boring half that does all the work.

The loop

Here is the whole harness, simplified:

agent.js (simplified pseudo-code)
while (true) {
  const reply = await model(messages, tools);
  messages.push(reply);
  if (!reply.toolCalls.length) break; // done
  for (const call of reply.toolCalls) {
    const result = await tools[call.name](call.args);
    messages.push({ role: "toolResult", callId: call.id, result });
  }
}

Nine lines. The model is called with the whole conversation so far and the tool list. Its reply is appended. If the reply asks for no tools, the run is over: that reply is the answer you see. Otherwise every requested tool runs, every result is appended, and the model is called again with the longer conversation.

MODELTOOLSCHATDONEFIG. 01 / THE LOOP

The figure is that loop as a machine. The turn goes from the model to the tools and back. Each time it leaves the model, a reply lands on the chat; each time it passes the tools, a tool result lands, and results are usually the bulky messages. On the third visit the reply asks for nothing, and the turn leaves the loop.

The highlighted line is the only exit. There is no "task complete" tool and no planner deciding when to stop. Pi's own loop, in its pi-agent-core package, works exactly this way: a turn is one model call plus the tool calls it made, and the run ends when an assistant message comes back with no tool calls (or an extension says to stop). Zechner is explicit that there is no step limit either: no max-steps knob, it simply loops until the model is done.

Two details the pseudo-code hides:

  • Tools run in parallel. If a reply asks for three reads, Pi validates the calls one by one, then runs them concurrently and appends the results in the order they were requested. A tool can opt into sequential mode when order matters.
  • Every turn sends everything. The model has no memory between calls. messages is the memory, and the whole array goes over the wire on every lap.

One task, lap by lap

Ask an agent to fix a failing test and the conversation grows like this. The messages are illustrative, but the shape is what every coding agent produces:

LapModel replies withHarness appends
1read("src/cart.test.js")The test file, 142 lines
2bash("npm test")Test output: 1 failed, with the assertion
3read("src/cart.js")The source file
4edit("src/cart.js", oldText, newText)"Edited"
5bash("npm test")Test output: all passing
6"Fixed: the discount was applied twice."Nothing. No tool calls, so the loop ends.

Notice what the model is doing: it is not "fixing the bug" in one leap. It is choosing the next command, one lap at a time, from what is already in the chat. Everything that looks like intelligence in a coding agent is the model making a good next choice, over and over, from an ever-longer transcript.

It also explains a habit you have probably seen: agents read a file before they edit it. They have to. Pi's edit tool replaces an exact piece of old text with new text, and fails if the old text doesn't match. The model can only produce an exact match for text that is in its context, so the read comes first.

Pi: four tools and a short prompt

Most agents compete on features. Pi competes on how little it needs. The default tool set in its source is exactly four tools:

ToolWhat it does
readReturns a file's contents (the first 2,000 lines by default, with offset and limit for larger files). Reads images too.
writeCreates or overwrites a file, making parent directories as needed.
editReplaces an exact span of old text with new text. Not a diff or a patch: the old text has to match.
bashRuns a shell command, with an optional timeout, and returns stdout and stderr.

Read-only grep, find and ls exist as optional extras, off unless you enable them. The system prompt opens by telling the model it is an expert coding assistant working inside Pi, then lists the tools and a few guidelines. In Zechner's original write-up the prompt and tool definitions together came to under 1,000 tokens. For comparison, he notes that Claude Code's system prompt runs to several thousand tokens.

Why four is enough: bash is every CLI

Four tools sounds like a toy until you look at the fourth. bash is not one tool. It is every command-line program on the machine:

what one tool covers
git diff --stat
npm test -- cart
grep -rn "applyDiscount" src
curl -s localhost:3000/api/health

Version control, tests, search, HTTP, package managers, linters, database clients: if a person can type it, the model can ask for it. As Zechner puts it, models already know how to use bash, and have been trained on read, write and edit tools with similar schemas. A dedicated git tool or search tool mostly re-describes something the model already knows.

And re-describing costs something. A tool definition is text, and it goes into the context window on every single call, used or not. Zechner measured two popular MCP servers:

0K5K10K15K20KTOKENS BEFORE THE FIRST MESSAGE →PI · PROMPT + 4 TOOLS< 1KPLAYWRIGHT MCP · 21 TOOLS13.7KCHROME DEVTOOLS MCP · 26 TOOLS18KFIG. 02 / TOKENS BEFORE THE FIRST MESSAGE

Playwright's MCP server adds 21 tools and 13,700 tokens; Chrome DevTools' adds 26 tools and 18,000. By his count that is 7 to 9% of the context window gone before you type anything, most of it for tools a given task never touches. Pi's whole prompt plus its four tools is a sliver of that.

My own tool list is large, and it works for me, but only because I prune it often: skills and tools I stop using get removed rather than left to sit in every session's context.

Context is the real budget

Once you see the loop, you see the constraint. Every lap appends at least two messages, and tool results are the heavy ones: a 2,000-line file read, a test run with a stack trace, a grep that matched more than you expected. The context window is the only working memory the agent has, and it only ever grows within a run.

That one fact explains most agent behaviour people find mysterious:

  • Long sessions get worse. The model is choosing its next step from a longer, noisier transcript, with the original request far behind it.
  • Small tool outputs matter. A read that defaults to 2,000 lines and a bash that returns only what the command printed are design decisions about context, not just convenience.
  • Fewer tool definitions leave more room to work. Every token of tool description is a token that can't hold your code.

This is the real argument for a minimal agent. Not that features are bad, but that each one has a price paid in the scarcest thing the loop has.

What Pi leaves out, on purpose

Pi skips features that most agents now treat as standard, and offers a plain alternative for each:

FeaturePi's alternative
Sub-agentsSpawn another Pi through bash or tmux, so you can see what it does.
Plan modeWrite the plan to a file (PLAN.md) and have the agent read it.
Built-in to-dosA TODO.md the agent edits like any other file.
Background bashRun long processes in tmux.
Permission pop-upsNone. Run Pi in a container if you want a boundary.

Zechner's reasons are about visibility and context. A built-in sub-agent runs a whole conversation you never see; a separate Pi in a tmux pane is something you can watch. To-do tools, he found, tend to confuse models more than help them, while a file is state everyone can read. What isn't built in can still be added: Pi has TypeScript extensions, skills, prompt templates and themes, and the repository ships more than 50 example extensions, including sub-agents, plan mode and permission gates.

One correction to an older story: Pi used to be known for having no MCP at all. As of 1.0 it has it, through Codemode: MCP tools are exposed inside a JavaScript sandbox where the agent composes calls in code, rather than every tool definition being dumped into the context. Codemode loads automatically only when MCP is configured, so the default is still four tools. The 1.0 release also brought virtual models, deferred tool loading and cache warming for Anthropic models, and keeps the stance that a feature has to prove itself before it gets in.

The trade-off nobody should skip

Minimal also means no guard rails. Zechner calls it full YOLO mode: unrestricted filesystem access, no prompt before a command runs. bash can do anything your user account can do, including rm -rf on the wrong directory or pushing to the wrong branch. Zechner's position is that once an agent can write and run code, a permission layer buys little real security; the only airtight option is cutting off the network, which makes the agent mostly useless. Pi's own docs point to containers (Docker, or sandboxing extensions such as Gondolin) for anyone who wants a real boundary. If you try it, do that.

My own compromise: anything new or untested runs in a container first, before it gets anywhere near my real filesystem.

Writing your own

The loop is small enough that building one is the best way to understand any agent you use. Pi's pi-agent-core package is that loop as a library: you define tools, it runs the turns. A tool is a name, a description, a schema for its arguments, and a function:

read-tool.ts (shape from the pi-agent-core README)
const readFileTool: AgentTool = {
  name: "read_file",
  description: "Read a file's contents",
  parameters: Type.Object({
    path: Type.String({ description: "File path" }),
  }),
  execute: async (toolCallId, params, signal) => {
    const text = await readFile(params.path, "utf8");
    return { content: [{ type: "text", text }], details: { path: params.path } };
  },
};

The description and parameter schema are what the model sees; execute is what the harness runs when the model asks. Add a bash tool the same way and you have a working coding agent. Everything after that (better prompts, compaction, extensions, a TUI) is refinement on the same nine lines.

The short version

  1. The model only writes requests. The harness runs them.
  2. A coding agent is a loop: call the model, run the tools it asked for, append the results, repeat until a reply asks for nothing.
  3. Every lap grows the context. The context window is the agent's only memory and its real budget.
  4. bash turns one tool into every CLI, which is why Pi gets by with four.
  5. Every tool definition costs tokens on every call. Fewer tools, more room for your code.
  6. No permission prompts means you supply the boundary: run it in a container.

Sources: Earendil, Pi 1.0 · Earendil, You said no MCP · Mario Zechner, What I learned building an opinionated and minimal coding agent · pi.dev · earendil-works/pi on GitHub · Pi's system-prompt.ts · pi-agent-core README