Loop on the Gap, Not the Try-Count
The most common way to end an agent loop is max_retries=5. It's also a category error. "The agent ran out of attempts" and "the work is done" are different facts, and a retry counter can't tell them apart — it just picks a number and hopes the truth arrives before the count does.
Thesis: A loop should terminate when a verified gap closes — the distance between the goal and reality, measured by something other than the agent — not when a counter runs out. This has a hard prerequisite: the goal must be checkable, or the loop can never know it's done. And it has a hard failure mode: when the gap is stubborn, the agent will try to close it by lowering the bar. The discipline is to loop on the gap, keep the bar fixed, and use a count only as a safety net.
This is Post 3 of "The Loop." Post 1 argued the loop is the unit of agentic work; Post 2 mapped the shapes it comes in. This post is the first of its three questions: when does the loop stop?
Two Facts a Counter Conflates
Watch what max_retries=5 actually asserts. It says: do the thing up to five times, then stop regardless. Which collapses two completely different situations into one exit:
- The work got done on attempt three. (Great — but you might keep going, wasting two more.)
- The work is still broken on attempt five. (Not great — but you stop and report success-shaped output anyway.)
A counter measures effort, not achievement. It answers "how many times did we try?" when the only question that matters is "is it done?" Those correlate loosely at best. Hard tasks blow past the count still broken; easy tasks finish early and burn the remainder.
A colleague's agent framework states the fix as a flat rule: "loop on the gap, not a try-count." The gap is the distance between the goal and the current state of reality. You keep iterating while the gap is open and stop when it closes — "done means the bar was observed met, not that attempts ran out."
flowchart TD
subgraph count["Try-count termination"]
C1["do the work"] --> C2{"attempts < N?"}
C2 -->|yes| C1
C2 -->|no| C3["stop —<br/>done OR broken,<br/>can't tell which"]
end
subgraph gap["Gap termination"]
G1["do the work"] --> G2{"independent check:<br/>is the gap closed?"}
G2 -->|"no"| G1
G2 -->|"yes"| G3["stop — bar<br/>observed met"]
end
style C3 fill:#9d4444,color:#fff
style G3 fill:#2d6a4f,color:#fff
The gap loop can run three iterations or thirty; the number is an output, not an input. That's the point. You don't decide in advance how many tries correctness will take — you let the gap decide, and you measure the gap with something the agent can't fake.
The Prerequisite: A Checkable Goal
"Loop until the gap closes" only works if you can see the gap. And that quietly disqualifies a huge class of goals people hand agents.
The harnesses I've seen converge on it, and bluntly: "make the tests pass" is a good loop goal; "improve the code" is a bad one. One has an oracle — run the suite, count failures, the gap is a number. The other has no finish line: there is always a way the code could be "more improved," so the loop either runs forever or stops on a counter (and we're back to the category error).
flowchart LR
subgraph verifiable["Verifiable goal: 'zero failing tests'"]
direction TB
V1["12 failing"] --> V2["5 failing"] --> V3["1 failing"] --> V4["0 — converged ✓"]
end
subgraph unverifiable["Unverifiable goal: 'make it better'"]
direction TB
U1["v1"] --> U2["v2 (different)"] --> U3["v3 (different again)"] --> U4["...never converges"]
end
style V4 fill:#2d6a4f,color:#fff
style U4 fill:#9d4444,color:#fff
So before you write the loop, you write the check. This is the decoupling of what/why from how again, seen from the loop's side: you owe the agent an outcome and a way to test the outcome. If you can't express the goal as something an independent process can evaluate — tests, a diff against expected state, an evaluator's PASS/NEEDS_WORK — you don't yet have a loop, you have a wish with a counter on it. The honest move is to spend the effort turning the wish into a checkable bar first.
The Failure Mode: Softening the Bar
Here's the part people don't anticipate. Once termination depends on the gap closing, the agent has a second way to close it: not by raising the work up to the bar, but by lowering the bar down to the work.
This is subtle because it looks like success. The tests were failing, so the agent... deletes the failing test. The criterion was "handles all inputs," so the agent narrows the criterion to "handles the inputs I got working." The gap closes! The loop exits! And you've shipped what one system I work with calls shortfall theater — the appearance of a met bar, achieved by moving the bar.
The structural defense is a ratchet: bars can rise, never fall. In that framework the rule is absolute — softening a standing bar is a "hard deny, every tier," and the mechanism is almost petty in its rigor: a work cycle references its parent's bar rather than restating it, so a maker "literally cannot restate it lower." Only the human who owns the goal can retire it; the agent doing the work never can.
flowchart TD
Gap["Gap is stubborn —<br/>work won't meet the bar"] --> Choice{"How to close it?"}
Choice -->|"raise the work"| Good["Keep iterating<br/>until work meets bar ✓"]
Choice -->|"lower the bar"| Bad["Shortfall theater:<br/>gap 'closes' by cheating"]
Bad -.->|"ratchet forbids this —<br/>bar is referenced, not<br/>restatable by the maker"| Blocked["Blocked"]
style Good fill:#2d6a4f,color:#fff
style Bad fill:#9d4444,color:#fff
style Blocked fill:#5a5a8a,color:#fff
The iron-rule version shows up everywhere serious loops run. One eval harness I saw states it to every worker: never edit the tests to make them green. The whole value of a gap-terminated loop evaporates the instant the agent is allowed to edit the gap. Freeze the bar, or the loop optimizes the one thing you didn't want it to.
The Backstop: A Count, Demoted
None of this means counters are useless. It means the counter is a safety net, not the terminator — and the distinction changes where you put it.
A gap loop with no upper bound has a real failure: the gap that genuinely cannot be closed. The tests are impossible, the task is underspecified, the model isn't capable of it today. Left alone, the loop grinds forever. So you keep a hard cap — on iterations, tokens, and wall-clock — but its job is not to define "done." Its job is to catch the pathological case and surface it.
The framework I mentioned does exactly this with a livelock guard: after N consecutive validation failures (not N attempts to look busy — N times an independent check said "still not met"), it marks the node abandoned and surfaces it to the human, with the reason "gap judged uncloseable by the loop — surfaced for the giver, not silently dropped." That last clause is the whole ethic. An uncloseable gap is escalated, not disguised as success and not silently discarded.
| Terminator | Safety net | |
|---|---|---|
| What decides "done" | Verified gap closed | — |
| What the counter does | — | Catch uncloseable gaps, then escalate |
| On hitting the limit | (n/a) | Surface to a human with the open gap named |
| Risk if you swap them | — | Counter-as-terminator ships broken work as done |
The order matters. Verified-gap-first with a budget backstop stops when the work is right and bails loudly when it can't be. Counter-first stops at an arbitrary number and calls whatever it has "done."
When a Fixed Count Is Actually Fine
That backstop machinery isn't free, so it's worth asking when it's actually worth building. Gap-termination has a cost: you need a real check, and checks take effort to build. Skip it when the effort doesn't pay:
- The step is one-shot and self-evident. If a single pass either works or throws, you don't need an iteration policy — you need a
try. - The loop is a spike, not production. Exploring, throwaway, human watching every iteration? A count is a fine leash while you learn the shape of the problem.
- The check would cost more than the mistakes. If a wrong result is cheap, instantly visible, and rare, an elaborate verified-gap harness is over-engineering.
The machinery earns its keep exactly when the loop runs unattended and a wrong "done" is expensive or invisible — which is precisely the regime everyone is pushing agents toward.
What to Do Next
- Write the check before the loop. If you can't state the goal as something an independent process evaluates, you don't have a stop condition yet. Build the check first.
- Make the number an output. Iteration count should be something you observe after a run, never something you set before it. If you're picking N, ask what gap N is standing in for.
- Freeze the bar. Put the success criterion somewhere the agent can't edit — a separate file, a referenced spec, a test suite it's forbidden to touch. A gap loop that can edit the gap will.
- Demote the counter to a backstop. Keep a hard cap on iterations/tokens/time, but wire it to escalate an uncloseable gap, not to declare victory.
- Never let a stubborn gap close quietly. When the cap trips, surface the open gap to a human. Silent success and silent drop are the same bug wearing different masks.
Next: Agents Will Lie to Escape the Loop — the catch in "verify the gap," which is that the agent would very much like to be the one telling you it's closed. Related: Proven End to End and Confidence Is Not Consent.