Loops that know when to stop

"Keep going until the task is complete" is not a stopping condition. It is a wish. What to write instead, and how to make a loop fail in a way you can act on.

· 7 min read· Closing Brackets

The most common bug we find in a production agent is not that it does the wrong thing. It is that it does not know it has finished. It circles — re-reading the same file, re-running the same query, restating its conclusion in slightly different words — until a turn cap it did not know about cuts it off mid-sentence, and the user gets a truncated answer with no indication that anything went wrong.

The cause is nearly always the same. The stopping condition was written as an instruction to the model instead of as a check the loop can run.

Three kinds of stop, and only one is optional

A loop needs a success stop, a failure stop, and a limit stop. Most implementations have the third by accident and neither of the first two on purpose.

  • Success. A predicate over state, evaluated by code. "The output validates against this schema and every required field is populated" is a success stop. "The task is complete" is not.
  • Failure. The conditions under which continuing is pointless: a dependency is down, the input was malformed, the model has produced the same tool call three times running. Detecting a stuck loop is a stopping condition, not an error handler.
  • Limit. Turns, wall-clock time, and cost. These exist to catch the cases the other two missed, which means every limit stop that fires is a bug report about your other two conditions.
If your limit stop is the one doing the stopping, you do not have a stopping condition. You have a timeout.

Make the exit checkable

The reliable move is to give the loop a shape whose completion is mechanical. Instead of asking a model to research a company and stop when it knows enough, give it a form to fill in: eight named fields, each with a type and a source requirement. The loop stops when the form validates. Under-specified fields are visible rather than assumed, and "enough" stops being a judgement call the model makes about its own work.

This is not a constraint on the model's reasoning — it can approach those eight fields however it likes. It is a constraint on what counts as done, which is the one judgement you should never delegate.

Detecting the circle

A loop that repeats itself is the clearest signal available that the current strategy is exhausted, and it is trivially detectable. Hash each tool call with its arguments. If the same hash appears three times in a run, the loop is not making progress and no number of further turns will change that.

What to do about it is a design choice worth making explicitly. The weakest response is to stop. Better is to tell the model what it is doing — "you have now called search with these arguments three times; it will not return anything different" — and let it change approach. Best, when the task supports it, is to escalate: hand the state to a fresh loop with a different tool set, or to a person, with the trace attached.

Failing usefully

When a loop does stop short, what it returns matters more than most teams treat it as. A bare error tells the caller nothing about whether to retry, escalate, or accept partial work. A useful bail carries four things: which stop fired, what the loop had established before it fired, what it was attempting when it stopped, and whether the state is resumable.

return {
  stopped: "budget",        // which condition fired
  partial: state.findings,  // what survived — often most of the value
  attempting: state.pending,// what it was mid-way through
  resumable: true,          // can a later run pick this up?
  trace: state.traceId,
};

That structure is what lets the layer above make a decision. Partial findings with a resumable flag can be queued for a retry with a larger budget. Partial findings without one can still be shown to a person, who is usually perfectly happy with eighty percent of an answer as long as nobody pretends it is a hundred.

The underlying principle is dull and worth repeating: a loop should never be surprised by its own ending. Every way a run can terminate should be a state you named in advance, and every one of them should produce something the caller can act on.

More in Loop engineering

Loop engineering: the loop is the product

Everyone tunes the prompt. Almost nobody designs the loop around it — which is where agents actually succeed, stall, or quietly burn a thousand dollars overnight.

Got this problem right now?

Describe it and we will tell you what we would do about it. One business day, no charge for the first answer.