π Pi, Explained
Home / Chapter 2

The Loop — how Pi keeps the model moving

The 30-second version: an agent is just a while loop with good manners. Call the model → if it asked for tools, run them and show the results → call it again → repeat until it stops asking. Everything else in Pi is decoration on this one skeleton.


Why a loop at all?

A normal LLM call is one-shot: question in, answer out, done. That's fine for translations and trivia.

But real work is iterative. "Fix the failing test" means: read the test → read the code it tests → form a hypothesis → edit → run the test → look at the new error → edit again. No single prompt can plan all of that in advance, because each step depends on what the last one discovered.

So agents flip the arrangement around: instead of your code deciding the steps, the model decides — and your code just keeps the wheel turning.

Your code's job:                The model's job:
─────────────────               ─────────────────
run the loop                    decide what to do next
execute tool calls              decide which tool, with which arguments
feed results back               decide when the job is done

That division of labor is the entire philosophy of agent frameworks. Pi's implementation is refreshing because it makes almost no other decisions for you.

Anatomy of one turn

Pi vocabulary time, because two words do a lot of work here:

  • A trace is one complete run: from your prompt until the agent fully stops.
  • A turn is one model call plus all the tool executions that call requested.

Watch what happens when you type "read config.json and explain it":

you press Enter
│
├── turn 1
│   ├── model call → "I'll need to read the file" + toolCall(read, "config.json")
│   ├── tool runs → file contents come back
│   └── (results get appended to the conversation)
│
├── turn 2
│   ├── model call → "This config sets X, Y, Z…" (no tool calls this time)
│   └── nothing to execute
│
└── done

Two turns, one trace. The rhythm is always the same: think → act → observe → think…

If you ever want to see this with your own eyes, Pi's core emits an event for every beat — agent_start, turn_start, message_start, message_update (that's the streaming text appearing token by token), tool_execution_start/update/end, turn_end, agent_end. The terminal UI you're staring at is just one subscriber to that firehose. We'll come back to events later; for now, note that everything Pi does is observable, because everything is an event.

The most important question: when does it stop?

Here's the part that surprises people: the model never announces "I'm finished." It can't — it's a text predictor, not a project manager.

The rule Pi uses is a human-defined convention, and it's beautifully simple:

If a model response contains no tool calls, the round is over.

That's it. "Finished" is inferred from silence. The model wanted a tool? Keep going. The model just talked? Done.

This is also why a misbehaving model that keeps calling tools forever is a real failure mode, and why Pi gives you escape hatches:

  • You interrupt — Ctrl+C aborts the run immediately (the loop treats aborts and hard errors as "stop now, don't even check the queues").
  • shouldStopAfterTurn — a hook Pi itself uses as a safety valve: after each turn it can ask "is the context window nearly full? have we done 200 turns?" and end the run gracefully.
  • Tools can say "enough" — a tool result may set terminate: true. One nuance worth knowing: Pi only stops if every tool in the batch agrees. One dissenting tool keeps the loop alive. Conservative, but predictable.

"Wait, can I talk to it while it's working?"

Yes — and this is one of Pi's nicest interaction ideas. Two queues, two vibes:

steer() — tap it on the shoulder. While the agent is mid-task, you can inject a message that cuts in at the next turn boundary. "Oh, and check the tests folder too." It lands between turns, before the next model call.

followUp() — leave a note for later. This one waits until the current run fully finishes, then kicks off another round with your message. "When you're done, run the linter."

In the SDK they're literally two methods on the session: session.steer(text) and session.followUp(text). In the terminal, you're just typing while it works. Same mechanism, friendlier costume.

Running tools: parallel, but polite

Models often request several tools in one breath — "read these three files." Pi could run them one by one, but that's slow, so by default it runs batches in parallel… with three etiquette rules:

  1. Preflight is sequential. Argument validation and beforeToolCall permission checks happen one at a time — because a check might block a call or touch shared state, and racing those is how bugs are born.
  2. Execution is parallel. The actual execute() calls run concurrently. This is the part that saves wall-clock time.
  3. Results are reported in order. Whichever tool finishes first shouts first (tool_execution_end follows completion order), but the result messages handed back to the model follow the order the model asked for them. Models are picky about narrative order; Pi respects it.

And the override is one line: a tool can declare executionMode: "sequential", and if any tool in a batch says so, the whole batch walks single-file. Editing files is exactly the kind of work that wants this — nobody wants two edits racing on the same file.

Errors don't crash the loop — they feed it

Quick preview of a design idea we'll unpack in the tools chapter, because it shapes how the loop feels:

When a tool fails — file missing, command exits nonzero, network hiccup — Pi doesn't throw an exception that kills the run. It packages the error into a normal tool-result message (flagged isError: true) and hands it to the model. The model then decides: retry, try another path, or explain the problem to you.

A loop that survives its own failures is the difference between a demo and a tool you trust.

The loop, distilled

prompt ──▶ call model ──▶ tool calls? ──no──▶ done (agent_end)
              ▲                 │yes
              │                 ▼
              │          run tools (parallel, polite)
              │                 │
              └──── append results to the conversation
                     (steering messages can cut in here;
                      follow-ups restart the cycle later)

A dozen lines of pseudocode describe the skeleton. Pi's real implementation adds streaming, hooks, event emission, and queue handling — but the skeleton never changes, and you can always see it underneath.

The takeaway: next time someone's agent framework intimidates you with its feature list, ask the question that actually matters — what's its loop, and who decides when it stops? In Pi, the answer fits on an index card. That's on purpose.


Sources & further reading: pi-agent-core README — event sequences, hook semantics, and execution modes, documented by the project itself.