The agent loop: read, plan, act, observe — and where each step fails
Read, plan, act, observe: a frame for working out which step actually failed. The observation step is the one that quietly decides what ships.
A while-loop, not a mind
We don't really think you should be talking about agents, because the word makes people picture sth that thinks about the codebase and forms an intent of its own. What actually runs is simpler than that: the model sends a well-structured request, your harness runs it and returns the result to the conversation as a message, and then the model gets prompted again.
Anthropic's docs put the canonical shape as a while loop keyed on stop_reason — as long as the
tool requests keep coming it runs, and it stops when they don't anymore.
Here's a transcript of such a headless session, on a small Rust project where we asked it to replace one string literal with another. Only the paths are removed; the rest is exactly as it came back:
assistant text "I'll read the file first."
assistant tool_use Read {"file_path": "…/main.rs"}
user tool_result "1\tfn main() {\n2\t println!(\"hello\");\n3\t}\n4\t"
assistant tool_use Edit {"file_path": "…/main.rs",
"old_string": " println!(\"hello\");",
"new_string": " println!(\"goodbye\");",
"replace_all": false}
user tool_result "The file …/main.rs has been updated successfully. (file state is
current in your context — no need to Read it back)"
assistant text "Done — `main.rs:2` now prints `goodbye`."As you can see — three turns, no sorcery. The assistant lines are the model's output, the user
lines are what the harness says back, and the tool results are delivered as if the user were saying
them. So whatever the model believes about your repo went through a channel the harness controls.
We divide that loop into four phases:
Read — the model operates on what it was given
Plan — it comes up with something logically consistent, which may not make sense because its basis was the read
Act — it acts on the plan, modifying the disk
Observe — from what it sees, it decides whether everything's alright and whether there's more to do
This is an educational split, not something officially used by any vendor. Anthropic describes it as: gathers context, acts, verifies the work, repeats if needed, and responds. We find four phases more useful, because it helps you see what actually went wrong — plan and act break in different ways. In the plan phase things can be sensible and still not reflect reality (a great plan for a file that doesn't exist), whereas in the act phase the model does something reasonable in the wrong place (it edits a file that has since moved).
Read: it acts on what it was shown
The model doesn't have access to the repo. It only knows about the project from what came back in
the tool_result, and that's where the first type of problem comes from. They all look the same
from outside: a well-meant solution built on an incorrect assumption.
It didn't read anything. Nothing in the loop forces a read, the model decides. So when it comes back with a very confident solution, check whether it opened anything first.
It read a rendering, not the file. In the tool result above, the output has line numbers, and it says there's a line 4 — while on disk there are only three lines, the last one being there because the file ends with a newline. That's how it generally works: the model thinks about the rendering of the file rather than the file.
What it read is out of date. You've made a change, a test run overwrote a snapshot, or a colleague pushed sth. Harnesses now track this per file — you can see the note about it in the edit result above — which is a decent sign the problem is real enough to build against.
Also, every request carries the entire chat history: the documented loop sends all the previous messages, the assistant's response and the new results, each time. In the run above that came out as 6 new tokens against 78k tokens of chat history. Caching changes how much it costs, not that it happens. The read is a cost per turn, not a one-off.
Plan: coherent, and wrong
The model's plan is usually the most satisfying and least helpful thing it can say. It's sound in the sense that it's consistent with what the model read, and it can still be pointless, because it may be describing steps for a file that doesn't exist.
That's why so many session recaps open with "the plan looked good" — people were reviewing the wrong layer. When you see a plan, don't ask whether the steps are sensible. Ask what it assumes:
that there's a function with this name
that this config key is called that
that the tests currently pass
These take seconds to check, and that's usually where the problem already is.
The other big issue is that the model aims too high. Anthropic's engineering write-up on long-running agents names it: the agents attempted to "one-shot the app" rather than work incrementally, often running out of context mid-implementation and leaving the next session confused about incomplete features. Which is what we mean when we say an eleven-point plan is eleven opportunities to veer off with no checkpoint in between.
Act: the only step with side effects
This is the only phase that modifies the disk. Read, plan and observe all emit text; act alters the
contents of files. That's why the gates cluster here — permissions, approvals, sandboxes, hooks —
and why the ones you reach for first are the ones sitting in front of a write. There are read-side
gates too, and blocking access to .env is the obvious one, but writing is where the expensive
mistakes live.
What usually breaks is that it makes the right change in the wrong place: you moved a file so it edited the old location, or a command ran from the wrong directory. The reason is structural — the model operates on a snapshot and the disk isn't one.
Some harnesses do something about that gap. In the run above, the edit result carries a note saying the file was current in its context and it doesn't need to read it again — the tool tracks per-file freshness and reports it.
What matters is that this isn't protection, it's information. The same harness let us do the opposite: in a session where we didn't even allow it to read anything, we asked it to edit a file it had never seen. It worked. The file got modified. Freshness tracking tells the model what it knows; it doesn't sit between your bad assumption and the disk. Spend ten minutes finding out which of the two you've got, on your machine, with your configuration.
Observe: the step that decides what ships
This is the crucial one. In our experience it's behind the majority of the "that worked perfectly" sessions. The earlier phases are reasonably well documented and have protections in place; this is where sth gets marked as done even when it isn't.
The model receives part of the command's output and an exit code, and neither can be trusted.
Zero doesn't mean it worked. We measured three:
cargo teston a crate without tests returns 0 and saystest result: ok. 0 passedgo test ./...in a module with no test files returns 0 and prints[no test files]a shell script whose real work sits behind an always-false
ifreturns 0 and outputs nothing
All three tell the truth in their output and report success in their exit code.
It might also cut off the crucial part. We ran a command that produced 1.2MB of output, and the model got back just 2KB of the beginning — around 525 lines — with the rest written to disk and the path handed over. Test runners and build tools put their conclusion at the end, so a cut like that takes off exactly the part telling you whether it passed. And nothing obliges the model to open the file it was given.
Most importantly, it might not have run the thing at all. The tests that passed did pass, and the feature is still broken, because nothing exercised it end to end.
These are the behaviours Anthropic described in their own long-running agent work: the agents were looking around, seeing that progress had been made, and declaring the job done. Claude tended to make code changes and then fail to recognise that a feature didn't work end-to-end, marking incomplete features as passing. This isn't jailbreaking or some edge case — it's how any system works when what you give it to decide on is a string.
The fix is in the observation stage, not the prompt:
Make it able to return a failure. If you can't think of an input that would make your verification step fail, it isn't verification. Something that has never once returned a failure is untested.
Use numbers instead of narrative. "12 passed, 0 failed" is an observation. "Tests look good" is the model's opinion of itself.
Put the check somewhere it can't be ignored. A hook that runs the tests runs. Telling the agent to run the tests is a request.
Where the loop stops
The loop ends when the model stops requesting tools. stop_reason comes back as end_turn instead
of tool_use, the harness stops iterating and delivers the final message. It might also stop for
its own reasons — the model hitting its output limit, or you interrupting it.
None of those is an arbiter of completion. The model decides the session is over, and it decides it from the same information that might be incomplete or out of date. So if you want done to mean something inside this loop, you have to define it yourself and make it something the loop has to ask about.
Tools name the parts differently — Claude Code reports num_turns in its session result, and in
Codex CLI a run is a thread with turns inside it — but we haven't seen a harness whose underlying
shape is different, which is why there's no per-tool section here. Learn the loop once.
What to actually change
When sth doesn't work we tend to improve the prompt. The prompt is the plan phase: the most visible lever and at the same time one of the weakest, because a better prompt won't give you a good plan on top of a bad read.
In terms of payback, we'd say:
Repair the observation. Make it honest, make it able to fail. Everything else is built on that foundation.
Repair the read. Paths instead of descriptions, and treat an edit with nothing read as a reason to stop.
Gate the act. Not to slow things down, but to make a mistake survivable.
Then work on the prompt.
The loop isn't intelligent and it isn't malicious — it's just really fast. It reflects your input back at you quicker than you can read it. So give it something true.