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.
"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.
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.
If your limit stop is the one doing the stopping, you do not have a stopping condition. You have a timeout.
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.
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.
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.
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.
Describe it and we will tell you what we would do about it. One business day, no charge for the first answer.