Skip to content

Course · Pi · The core

One level deeper into the loop

You already know the loop’s shape from the last chapter: the model responds, any tool calls it made get executed, the results go back in, and it responds again - repeat until a response comes back with no more tool calls attached. What that description leaves out is a decision Pi’s author made on purpose: there’s no ceiling on how many times that cycle can run. In his own words, describing the design: “I never found a use case for that.”

That cuts two ways, and the second one is worth sitting with before you get comfortable with the first. On a task that’s genuinely finishable, no ceiling means a five-line fix and a fifty-line migration run the exact same mechanism - nothing artificial stops the loop one call short of done. On a task the model can’t actually solve, that same absence of a ceiling means it keeps calling tools, keeps burning turns, keeps costing real money, with nothing built in that says enough. Pi doesn’t protect you from that case by design. It hands you the means to notice it and stop it yourself, which is what the rest of this lesson is about.

Watch what “no ceiling” looks like in practice by handing stash something bigger than a date fix - the fetch worker’s missing retry logic. Right now, if a site times out or 429s, the fetch just fails and the URL sits unprocessed forever. Ask Pi to fix that:

> the fetch worker has no retry or backoff - a slow site just fails once and
never gets retried. Add retry with exponential backoff, and don't hammer
sites that are genuinely down.
• Read app/worker/fetch.py (198 lines)
• Read app/worker/queue.py (94 lines)
• Edit app/worker/fetch.py
• Bash: python -m pytest tests/test_worker.py -v
[3 failed, 9 passed]
• Read tests/test_worker.py (140 lines)
• Edit app/worker/fetch.py
• Bash: python -m pytest tests/test_worker.py -v
[12 passed, 0 failed]

Laid out as a ledger instead of a scrolling transcript, that run looks like this:

TurnTool callResult
1read app/worker/fetch.py198 lines
2read app/worker/queue.py94 lines
3edit app/worker/fetch.pyapplied
4bash pytest tests/test_worker.py -v3 failed, 9 passed
5read tests/test_worker.py140 lines
6edit app/worker/fetch.pyapplied
7bash pytest tests/test_worker.py -v12 passed, 0 failed

Seven tool calls, two of them because the first attempt broke three existing tests and the model went back to find out why. Nothing about this run required a bigger step budget than the one-line date fix - Pi didn’t pre-allocate a turn count for “small task” versus “medium task.” It just kept looping, tool call after tool call, until turn 7 came back with nothing left to call. A much larger migration later in this course will run the exact same loop for far longer, with no different mechanism underneath it.

One more mechanical detail worth knowing before that happens: tool calls inside a single turn don’t have to run one after another. When a response includes more than one tool call, Pi can execute them in parallel and reorder the results back into the sequence the model expects before feeding them back in - so what looks like a strict queue in a transcript is really “whatever the model asked for this turn,” not a hard-coded chain. The retry-worker run above happened to be sequential because each step depended on the last one’s result; a turn that reads three unrelated files at once wouldn’t wait for each read to finish before starting the next.

A loop with no step limit is only comfortable to use if you can talk to it while it’s running - otherwise “no ceiling” just means “no way to stop a run that’s heading somewhere you didn’t intend.” Pi gives you two distinct ways to interject, and they land at genuinely different points:

  • Steering - press Enter while the agent is mid-run and type a message. It doesn’t interrupt the tool call in flight; it gets queued and delivered the moment the current turn’s tool calls finish, folded into the next turn as though you’d said it right then. Good for a correction that matters now: “stop, that retry count is way too high” while the edit is still being drafted.
  • Follow-up - Alt+Enter queues a message for after the entire run completes, once the loop has already terminated on its own. Good for “and once that’s done, also update the README” - something that belongs after this task, not stitched into the middle of it.

The distinction matters because of where each one lands relative to the loop’s own termination check. A steering message can only be delivered between turns - after a tool call’s result has come back, before the model is asked to respond again - never in the middle of the model composing a response. That’s why steering feels like a correction and follow-up feels like a queue: one rides inside the current loop, the other waits for the loop to end and starts a new one.

Try it on the retry fix. Midway through, before the second Edit app/worker/fetch.py call, you notice the backoff cap it chose is too aggressive for stash’s use case:

[mid-run] > cap retries at 3 attempts, not 5 - this is a personal read-it-later
tool, not a production crawler

That lands as steering - folded into context the moment the current tool call resolves, before the model drafts its next move. The run doesn’t restart; it just receives the correction and keeps going with it in view. Contrast that with queuing a genuinely separate ask for after the fix ships:

[Alt+Enter] > once retry logic is in, also add a `last_fetch_error` column so
the UI can show why a URL is stuck

That one waits. It’s not relevant to whether the retry fix is correct, and folding it into the same turn would just be two unrelated changes tangled together in one diff. Follow-up keeps them separate without you having to remember to type it again later. Steering corrects the turn in front of you; follow-up queues the next one.

The absence of a step ceiling isn’t a performance detail - it’s a statement about what Pi assumes you’ll do instead of relying on a governor. Some agents cap step count as a safety valve against a model looping forever on a task it can’t actually solve; Pi’s answer is that the human is the safety valve, via steering, via Ctrl+C to abort outright, via reviewing the diff before it’s committed. That only works if you’re actually watching - which is exactly the habit this whole module is building.

That’s two costs paid so far this module: a fixed tool schema you never chose, and now open-ended turns with no built-in stop. The next lesson shows you exactly where both of those numbers actually live - along with everything else the model is working from at any given moment, and the prompt sitting underneath all of it. Next: see the full context Pi is working from.