EngineerPrep / Engineering Research · Failure Pattern Study
47 successful iterations, zero state change, and nobody owning the word 'done'
2026-09-15 · 7 min read
A task went in at 09:14. At 11:52 the agent was on iteration 47 of the same task, and every one of those 47 iterations had succeeded.
No exception. No error code. No 500 in any log. Every tool call returned 200. The loop simply had no idea what "done" meant, and nothing in the system was going to tell it.
When an agent loop runs away, the instinct is to look for the broken call — the 500, the timeout, the exception. Runaway loops usually don't have one. The more painful shape is the loop where every step is locally defensible:
iter 41 search_catalog 200 3 results
iter 42 read_spec 200 spec v3
iter 43 summarize 200 0.9 confidence
iter 44 validate 200 "incomplete coverage"
iter 45 search_catalog 200 3 results
iter 46 read_spec 200 spec v3
iter 47 summarize 200 0.9 confidence
The validator keeps saying incomplete coverage. The model keeps responding the only way it knows how: gather more, try again. The catalog keeps returning the same three results, because that is all there is. Each iteration is reasonable. The sum is a task that can never terminate, executed at full price per step.
That is the failure under investigation in AT-02 of the production failure lab roadmap: a loop has no reliable completion or iteration boundary. Not a tool bug. Not a model bug. A missing contract at the loop level — which is exactly the layer nobody owns.
Every bounded loop in ordinary software answers three questions before it starts. Agent loops shipped without any of them:
| Boundary | The question it answers | What exists in a typical agent loop |
|---|---|---|
| Completion contract | What observable state means done? | "the model says it is done" |
| Iteration cap | What is the maximum price of being wrong? | nothing, or a number picked to not break demos |
| Progress check | Is the state actually changing? | none — confidence is not progress |
The third one is the subtlest and the one that does the damage. A model's self-reported confidence can sit at 0.9 forever while the underlying state does not move an inch. Confidence is the model's opinion about its own answer. Progress is a delta in the world — a file changed, a row written, a state transition observed. In 47 iterations above, one of those was present and it was the wrong one.
The runaway is bad enough at flat cost per iteration. It is worse than flat, because agent loops carry their history:
iter 5 prompt 1.9k tok $0.011
iter 15 prompt 6.4k tok $0.038
iter 25 prompt 12.8k tok $0.077
iter 35 prompt 19.1k tok $0.115
iter 45 prompt 25.4k tok $0.102
Every iteration appends the last one's tool results to the context, so iteration n costs more than iteration n−1 even with identical work. A loop with no boundary is not a linear burn — it is a snowball with a completion criterion that may never arrive. The same mechanism, one level over, is why a bounded loop can still 3x your bill: retries nested inside iterations multiply, which is its own lab (the fifteen-call request). The unbounded case is just that failure with the ceiling removed.
The reflexive fix is max_iterations = 10. Ship it and you have traded a runaway for a coin flip:
A cap answers how long, not when to stop. The completion contract is the part teams skip, because writing it forces the uncomfortable question: what observable state change does this agent actually produce? For a summarizer it is a document written. For an order agent it is an order in a stated state. For a research agent it is a set of citations that resolve. If you cannot write that predicate, the loop has nothing to converge on, and no iteration count will save it — it will just fail more cheaply.
The loop that replaced ours, reduced to its contract:
DONE := order.status == CONFIRMED (checked against the store,
never against the model's claim)
BUDGET := 8 iterations OR $0.50 OR 90 seconds (whichever fires first)
PROGRESS:= at least one state delta per 2 iters (else: stop, escalate)
UNRESOLVED := hand off with the trace attached (never silently absorb)
Three properties matter more than the specific numbers. Done is checked outside the model — against state it cannot edit, because a model grading its own homework will eventually pass itself. The budget is first-class — spending past it is an incident, not a rounding error, and it triggers the same reconcile-don't-retry logic as any other unresolved outcome. Giving up is a modeled outcome — the agent can end in "escalated", not only in "done" or "crashed". Most runaway loops in the wild are agents that were never given a legitimate way to quit.
Once stated as a boundary problem, it stops being an agent bug and becomes a systems invariant:
| The loop is | The missing boundary | Where you have seen it before |
|---|---|---|
| An agent iterating on a task | completion contract + cap | batch jobs without a deadline |
| A retry policy without a budget | iteration cap on retries | the retry storm |
| A workflow engine re-driving a step | progress check on state delta | duplicate side effects |
| Multi-agent delegation | who is allowed to hand work to whom | the recursive delegation loop |
The family resemblance is not accidental. Every one of these is a loop whose termination was delegated to a component with no authority to decide it — a scheduler that trusts the worker, a client that trusts the server, an agent that trusts itself.
The boundary contract above is what we teach in the AI agent course, and the pattern pairs with two earlier reports: why parent executions look healthy while children burn, and the agent that charged twice — the cousin failure, where the loop ended but the side effect did not.
The runnable AT-02 lab — The Agent That Would Not Stop — is live: the reproduced loop above, the evidence room, and the bounded rewrite with the test that fails before and passes after. The rest of the hundred are on the public roadmap with their real status, because "coming next" is a promise rather than a product.