← All articles

Loop engineering

Agentic engineering · part 2 of 10 · Previous: Harness engineering

Agent demos all look the same. A task goes in, four or five turns happen, the answer comes out, and everyone in the room is satisfied. Nothing you learn from that demo tells you anything about what the system does at iteration 40, which is the only number that matters.

The loop itself is four steps and they are not the interesting part. Think, act, observe, repeat. Every agent since 2022 is a variation on it. The interesting part is the question the loop asks between steps, which is: should I still be here?

Three ways a loop dies badly

It stops too early. The model says the task is complete. The task is not complete. This is the most common failure and the most annoying, because it fails cheerfully, with a summary, in confident prose. The root cause is that “no more tool calls” means “the model believes it is done”, and a model’s belief about its own completion is one of the least reliable signals in the system. It is generated by the same process that generated the work, so it inherits every misunderstanding in it. If the agent misread the task, it will also believe it finished the task it misread.

It never stops. Two files that each need the other changed. A test that fails for a reason the agent cannot see. A search that returns nothing and gets retried with a synonym, then another synonym, then the first one again. Nothing is on fire. Tokens are burning at a steady rate and the system looks busy, which is the worst possible presentation of being stuck, because a human watching a progress indicator will wait a surprisingly long time before intervening.

It stops for a reason unrelated to the work. Context exhausted at turn 38 of a 45-turn job. Rate limit. Process killed. The work up to that point was fine. It is simply gone, because it lived in a conversation history that no longer exists, and there was no external record of what had been done.

Three failures, three different fixes, and teams routinely apply the wrong one. Adding turn limits to an agent that stops too early makes it worse.

The agent loop with every way out drawn in On the left, a cycle: model call, tool call, observe result, then a decision diamond reading Done, with a return arrow back to the model call labelled next turn. From the diamond, five exits: verified complete, which is a check passing rather than the model’s opinion; budget exhausted, meaning turns, tokens, clock or writes; thrash detected, meaning the same call three times; escalated to a human for missing authority or information; and stopped for an unrelated reason such as context, a crash or a rate limit. Only the first exit is success. Model call Tool call Observe result Done? next turn Verified complete a check passed, not the model’s opinion Budget exhausted turns, tokens, clock, writes Thrash detected the same call, three times over Escalated to a human missing authority or information Stopped, unrelated context, crash, rate limit
The happy path is one exit out of five, and it is the only one most diagrams draw.

Ask the tests, not the model

The single highest-value change most agent loops can make is to stop asking the model whether the work is done and start asking something that cannot lie.

An exit code. A test suite. A schema validator. A linter. A diff that either applies or does not. A second model reading the output against the original task with no knowledge of how it was produced, which is weaker than a test but much better than self-report.

This turns the loop from open-ended into goal-seeking, and the change in behaviour is not subtle. An agent that can run its own tests will fix the bug it introduced in step 3 during step 7, without being asked, because it sees the red. An agent that cannot will hand you a summary saying the change is complete and the tests should pass.

The corollary is that the highest-leverage work in agent engineering is frequently not agent work at all. It is making the verification fast and legible: a test suite that runs in twelve seconds rather than nine minutes, error messages that name the file and the line, a build that fails loudly rather than warning quietly. You are not improving the agent. You are improving the feedback it gets, and the feedback is the only thing steering.

Detecting a thrash

Loops that never stop announce themselves in the trace long before a budget catches them. The signals are cheap to compute and almost nobody computes them.

  • The same tool called with the same arguments three times
  • A file edited back to a state it already had
  • The plan rewritten without any tool call in between
  • Turns getting shorter, which usually means the model has run out of things to try and is restating
  • An error string that has appeared four times

The right response is almost never to kill the run. It is to interrupt it with new information: tell the agent it is repeating, tell it what it repeated, and make it either change approach or escalate. A thrashing agent given evidence of its own thrashing often recovers in one turn. It was not stubborn, it just had no way to see the pattern, having experienced each attempt as a reasonable next step.

Budgets, plural

One turn limit is not a budget, it is a guess. Real loops need several, and they should be separate because they fail for different reasons. Turns, the crude stop, and the one to set generously. Tokens, which matter for cost and catch a loop generating enormous outputs rather than many of them. Wall clock, which matters when a human is waiting. Money, if the tools cost anything, which they do the moment one of them calls a paid API.

And blast radius, the count of side-effecting operations. Twenty file writes in a run that was meant to change one config file is a runaway, even at turn 6 and even under every other budget. That last one is underused and the one I would add first to a system that has none of them.

Externalise the state or lose it

The fix for the third failure mode is structural. If the only record of progress is the conversation history, then the run cannot survive compaction, a crash, a restart, or a handover, and it cannot be inspected while it runs.

So progress goes somewhere outside the window. A task file with checkboxes, a scratch directory, a database row, a branch with commits on it. The loop reads it at the start of each iteration and updates it at the end. Now compaction is a routine event rather than an amnesia episode, the run is resumable, and a human can look at the file mid-flight and see exactly where things stand without reading a transcript.

This single pattern does more for long-horizon reliability than any amount of prompt tuning, and it is the least glamorous thing in the field. It is a to-do list on disk.

Retry, replan, escalate

When a step fails, the loop has three moves, and the whole art is in picking the right one.

Retry the same thing, which is correct only for genuinely transient failures. Network blips, rate limits, a lock. If you retry a deterministic failure you get the same failure, slower.

Replan, which means going back and choosing a different approach. Correct when the step was reasonable but the approach was wrong. Needs the failure to be legible enough to learn from, which brings you back to error message quality.

Escalate, which means stopping and asking. Correct when the failure is about missing authority or missing information that the agent genuinely cannot obtain. Most loops escalate far too late, having burned twenty turns discovering something a human would have answered in ten seconds.

The classification is a small decision made constantly, and it is worth making explicitly in code rather than leaving it to be re-derived by the model in prose every time. Termination is the hard part. Everything else in the loop is plumbing.

Further reading

Part 2 of a ten-part series on the engineering layers around a language model. Previously: Harness engineering. Next up is context engineering, on why prompt order has a price attached.