Knowing When to Stop: The Hardest Part of an Agent Loop
Everyone who designs an agent loop spends their time on two steps. They tune the generate step, because that's where the intelligence appears to live. They harden the verify step, because that's where the discipline lives. Almost nobody budgets serious design effort for the third step, which is the one that actually determines whether the loop is trustworthy: the stop.
Here is the uncomfortable mechanical fact underneath every loop you have ever built. A turn that produces text with no tool call is the only place a run can stop. That's it. That's the entire exit condition — the absence of a function call. And that single signal conflates three completely different situations, which is why agents routinely "finish" tasks they never did.
This post assumes you already have a loop — generate, verify, correct — and that it has burned you at least once. We're going one level down, into the exit.
One signal, three meanings
When the model emits a turn with no tool call, exactly one of these is true:
done— the agent delivered a real, complete answer.question— the agent is deliberately asking the user for something. An agent is always allowed to ask. A genuine ask is a first-class stopping point, never a stall.stalled— the agent stopped with the task demonstrably unfinished without asking: it narrated an action it never took, returned partial data, or skipped a step.
Read that third one again, because it's the whole post. Narrated an action it never took. Every agent builder recognises this instantly. "I've updated the configuration and re-run the checks." No it hasn't. There is no tool call in that turn. The model produced a paragraph describing work in the past tense, the loop saw no function call, the loop exited, and the harness handed a fluent lie to whoever was waiting.
The naive loop treats all three as one event, because at the transport layer
they are one event: a message with an empty tool-call list. Left alone,
done, question, and stalled all become "the run ended successfully," and
your completion signal is decoupled from reality.
So we put a gate exactly there — at the no-tool-call boundary — and make it branch.
This pattern has a name in coding agents — a stop hook — and it generalises cleanly to any agent loop. The interesting part isn't the shape. It's the four design decisions inside it.
Decision one: no heuristics, on purpose
The gate asks the LLM to classify the stop. There are deliberately no string or phrase heuristics, because only the model that produced the turn can reason about whether it actually finished.
This is the decision people push back on hardest, and it's the one we're most confident about. The tempting move is obvious: regex the turn for "I've completed", "Done!", "Let me know if". Ship it in an afternoon, zero extra model calls, deterministic.
It's a trap, and the reason is structural. The sentence "I've completed the
migration" is exactly as likely to appear in a done turn as in a stalled
turn. That's what makes stalls dangerous — a stalled agent is not confused or
hedging. It's confident. It writes the completion sentence because it believes
the completion sentence. The surface text is identical in both cases; the only
difference is whether the preceding transcript contains the tool calls that
would make the sentence true. A regex cannot see that. It matches the claim, not
the evidence for the claim.
The thing that can see it is a model looking at the turn and the work that led to it. Judging "did this actually happen" is a reading-comprehension problem over a transcript, and the only tool we have that does reading comprehension is the model. So we use it, and we accept the cost of an extra call rather than pretend a pattern match is a judgement.
The gate reads a bounded window — the last 8 messages — not the whole run. The classification is about the stop, not the history. You're asking "is this turn honest given what just happened", not "summarise the last forty minutes."
Decision two: put the expensive check at the boundary
The gate runs only at the no-tool-call boundary. Turns that emit tool calls route straight to the tool node and never reach it.
That one sentence is what makes the design affordable. A long run might be forty turns, thirty-eight of which call tools. Those thirty-eight never touch the gate. The judging happens at candidate stopping points — roughly once per run, a handful of times if the agent stalls and recovers.
Generalise it: put your expensive check at the decision boundary, not in the hot loop. People reach for per-turn monitors and per-turn critics, discover the overhead is untenable, and rip the whole idea out. Usually the check didn't need to run per turn. It needed to run at the moment a decision becomes irreversible. Ending a run is irreversible — the answer goes to the user. Calling a tool isn't; another turn is coming that will re-examine everything. Spend your judgement where the door closes.
Decision three: the plan inverts authority
This is the strongest idea we have on the subject.
When a run carries a plan, the plan — not the model and not the judge — decides completion. Every step marked completed ⇒ stop. Any step unmarked ⇒ the harness rejects the stop and re-injects the specific next step, so the model drives its own plan to completion and cannot hallucinate the whole task done with work remaining.
Sit with the inversion. The agent writes the plan. The plan then binds the agent. A model that decided at step two what needed to happen does not get to decide at step five that actually it's finished — the artifact it produced when it had a clear view of the task now overrules the model that has drifted, gotten tired of the task, or convinced itself the remaining steps were optional.
That's the trick, and it's more general than agents: externalise the completion criterion out of the model's judgement and into an artifact it can't retroactively edit. Completion stops being a vibe the model reports and starts being a property you can check. Six of seven boxes ticked is not a matter of opinion.
And note what the rejection does. It doesn't scold. It doesn't say "you're not done, try harder." It re-injects the specific next step, because a loop is only as good as its feedback string — and "step 4: verify the new records resolve" is a task, while "incomplete" is a mood.
Decision four: a budget that can't be gamed
There's a cap of 20 continuations. The anti-gaming rule matters more than the number:
Each stall consumes one unit, and a tool call in between does NOT refund it — so an agent that keeps making hollow tool calls but never completes (e.g. a fetch that always returns partial data) still terminates.
Refunding on tool calls is the obvious-looking design and it's wrong. If real work resets the budget, then anything shaped like real work resets the budget, and an agent stuck in a genuinely unresolvable situation — a source that always returns half the rows — will loop forever while looking productive the entire time. Non-refundable units mean the budget measures stalls, not activity.
Same reasoning excludes bookkeeping tools. Updating the plan and marking a task complete don't count as real work. Otherwise an agent keeps its budget alive by ticking its own boxes, which is a very sophisticated way of doing nothing.
On the last unit the gate injects one "wrap up with what you have" nudge, so the run ends with a best-effort answer rather than a dangling "let me re-fetch." A budget that expires silently mid-thought produces the worst possible artifact: a truncated intention. Land the plane.
Decision five: precedence is policy
One small thing at the router with a real lesson in it. The router checks the hard tool-budget stop first, before checking whether a user requested a stop, then falls through to continuing.
def route(state):
# This is checked FIRST — a runaway that also has a
# pending stop must still end.
if state.tool_calls >= TOOL_BUDGET:
return "end"
if state.user_stop_requested:
return "end"
return "continue"
Both branches end the run, so the order looks cosmetic. It isn't. The ordering
encodes which authority wins when two conditions fire at once, and every one of
these little if chains is a policy document that nobody reviews as one. Write
them in the order you'd defend out loud: hard safety limits, then user intent,
then the default. In a router, precedence is policy — you are ranking your
principles whether or not you meant to.
The honest tradeoff. The gate is a model call, which means it has a false-positive rate: sometimes it calls a real completion a stall and the agent does a redundant lap. We take that trade deliberately. A redundant lap costs latency; a missed stall costs trust in every answer the system produces. When your classifier is imperfect, choose the error you can survive.
What the stop is really for
Upstream of the exit, everything is optional in a way you can talk yourself out of. You can ship a loop with a weak verifier and tell yourself the model is usually right. You can skip the plan. The system will look like it works.
The stop is where that stops being true, because the stop is the only step whose output the user actually sees. A loop that can't tell "I'm done" from "I gave up" from "I need you" will confidently hand you all three as the same thing — and the one it hands you most often is the middle one wearing the first one's clothes.
Getting the stop right is what makes everything upstream of it worth building.
Written by CatalEx Engineering. We build the AI operating layer for AI-native companies — one platform to build, deploy, and run AI agents in production. More at catalex.co.