cat posts/2026-08-24-the-agent-loop-visually.md
The agent loop, visually
On Wednesday night I gave an agent a dependency upgrade in one of my side projects, closed the laptop, and went to bed. In the morning the session had made 186 tool calls over about fifty minutes, compacted its own context twice, and stopped with a summary that said the upgrade was done, two tests had been changed and why, and one deprecation warning was left that it didn’t think it should fix without asking me. All three claims were true.
The session ran on a small VPS I set up last month as a dev box for exactly this, inside tmux, so it doesn’t care whether my laptop is open, and I can check on it from my phone over my tailnet. That’s not interesting in itself. What’s interesting is what happened in those fifty minutes, because no single model call did anything remarkable. Each one read some text and asked for a tool. The useful behavior is all in the loop around them.
In December I went through the mechanism at the bottom: the model writes a structured request, your code runs it, the result goes back into a conversation that’s resent in full every time. This post is the layer above that. If the December post was about one turn, this one is about the hour.
The shape of it
There’s no planner module in most agents, and no separate checker either. Plan, act, observe and check are all the same model, reading the same growing conversation, and writing whatever comes next. What makes them distinct steps is mostly the prompt and the tools: the harness gives the model a way to write a plan down, tools to act with, and a reason to check before it says it’s done. Strip those away and you have a model that answers once and stops, which is what a chatbot is.
Plan
Claude Code has a todo tool. The model calls it with a list of items and their status, and the harness shows the list to me and puts it back in the conversation. It looks like a UI nicety. I think it’s one of the more important parts of the design.
Remember that the model has no memory between calls except the conversation. On turn 140 of a long session, the original request is a long way back, under a hundred file reads and test runs. A todo list that the model rewrites every few turns keeps a current, short statement of what it’s doing near the end of the conversation, where it’ll be read with the most attention. And it survives compaction better than anything else, because a good summary keeps it. On Wednesday’s run the list started with six items and ended with nine, because three things turned up along the way, and the summary at the end was mostly the final list with notes.
Writing the plan down also gives me something to read. When an agent is going wrong, the todo list usually shows it before the diff does. An item that’s been “in progress” for forty turns, or a new item called something like “work around test failure”, is my cue to look.
Act and observe
Acting is the tool call from December, often several in parallel. Observing is reading what came back, and the quality of what comes back makes a bigger difference than I expected.
A test run that prints four thousand lines of output is a bad observation. It fills the context, it costs tokens on every later turn, and the line that matters is somewhere in the middle. Harnesses truncate long tool output for that reason. In December I said most of my fixes to my own small agents were edits to tool descriptions, and the rest have been on this side: making a tool return the failing test’s name and the assertion, not the whole log, or returning an error message that says what to do differently. “File not found” makes the model guess. “File not found; did you mean app/models/invoice.rb?” makes it right the next time. The model can only be as good as what it’s shown.
Check
This is where agents fail most often, in my experience, and the failure has a particular flavor. The model decides it’s done when it isn’t. It ran the tests, two were failing, it fixed one, and the summary says “all tests pass”, because the last thing it saw was a pass. Or it was asked to fix a bug and fixed the test that caught it.
The fix is to make “done” something the loop can check for itself. In a spec that’s the “done means” section, which I wrote about last month. In the harness it can be enforced. Claude Code has hooks that run when the agent tries to stop, and a hook can refuse, with a message that goes back to the model as if I’d typed it. Mine runs the project’s check script and, if it fails, sends back the failures and “you’re not done”. It’s about fifteen lines, and it’s done more for my overnight runs than any model upgrade this year.
When it stops
A model call ends when the model stops writing. A loop has to be told. These are the ways the loop ends, roughly in the order I’d want them to:
- The model says it’s done, and the check agrees.
- The model stops to ask a question, because the spec said to stop and ask about something.
- A budget runs out: a maximum number of turns, tokens or minutes.
- The harness notices it’s going round in circles.
- I interrupt it.
Number 4 is the interesting one. The classic stuck agent runs the same failing command, reads the same error, makes a small change that doesn’t address it, and runs the command again, and it’ll do that for as long as you let it, each time with a confident explanation. Detecting that is crude and works: if the last few tool calls are nearly identical and the results haven’t changed, stop and say so. A person sees the circle at once. A model, inside the loop, sees only the last few turns, each of which looks like a reasonable next step.
The context, over an hour
Every turn adds to the conversation, so a long session fills the context window, and the harness has to do something. Claude Code’s answer is compaction: when the context gets near the limit, it asks the model to summarize the session so far, and starts a fresh conversation with that summary in place of the history.
It works better than I expected and loses things in ways that are hard to predict. A detail that seemed minor at turn 50, like “this test is flaky, ignore it if it fails once”, can be gone after the summary, and then at turn 120 the agent spends ten minutes fixing a flaky test. The defenses are the same ones as for people with bad memories: write things down. The todo list survives. A notes file in the repo, which my specs ask for on any long run, survives completely, because it’s not in the context at all until the agent reads it again.
Sub-agents, which I wrote about last September, are the other defense. A sub-agent is the whole loop again, nested inside one tool call of the outer loop: its own plan, its own thirty file reads, its own check, and a short summary that comes back as the tool result. The outer context only grows by the summary.
Persistence cuts both ways
The quality that makes the hour-long run work is that the loop doesn’t give up. A failed test is just an observation, and the next turn tries something else. That’s the whole reason agents got useful in the last eighteen months, and most of it is training, models taught to keep going on long tasks, more than any change to the loop itself.
It’s also the quality on show in the OpenAI incident this summer, which we had to deal with as customers. In OpenAI’s Black Hat talk, one of the early steps in the timeline was an agent with a task it couldn’t complete that found it could write to a system it wasn’t meant to. A loop that treats every obstacle as something to route around will treat the edge of its environment that way too, if the environment lets it. Nothing about that needs the model to want anything. It’s the same loop as my dependency upgrade, with a different obstacle.
So the purple box in the diagram is where I spend my attention. The model decides what to try. The harness decides what’s allowed, how long it gets, and what counts as finished, and that’s code I wrote, or at least read, and can test like any other. On Wednesday the most important line in the whole run was the hook that wouldn’t let it stop until the check script passed. It fired twice.