When to Break the Agent Loop: Stopping Conditions
An agent once ran 240 iterations on a task that needed four, and billed accordingly. Good agent loop stopping conditions would have caught it at six. Here's how to build them.
def check_stop(state):
if state.reply.finished:
return ("success", state.reply.answer)
if state.step >= state.max_steps:
return ("step_limit", state.best_partial())
if state.tokens_spent >= state.token_budget:
return ("budget", state.best_partial())
if state.no_progress_streak >= 3:
return ("stalled", state.best_partial())
if state.same_action_repeated >= 2:
return ("looping", state.best_partial())
return (None, None)A team I advised once shipped an agent that ran 240 iterations on a task that needed four. It wasn't broken in any dramatic way — it just kept "almost" finishing, re-checking its own work, calling one more tool, and never quite deciding it was done. Nobody noticed until the bill arrived. Good agent loop stopping conditions would have caught it around iteration six. Instead, a single stuck run cost more than the whole feature's monthly budget. Stopping conditions are the cheapest reliability insurance you can buy, and almost nobody designs them deliberately. They're the seatbelt of agent engineering: invisible when things go right, and the only thing that matters when they go wrong. A team that spends an hour on them saves itself a very bad afternoon later.
A stopping condition is any rule that ends the loop. Most agents ship with exactly one — a max-step cap — and treat it as a formality. That's the trap. One coarse cap can't tell the difference between "succeeded," "stuck," "looping," and "out of money," and those four endings need four different responses. This guide builds the full set.
Quick-Start (Copy This Right Now)
[object Object], ,[object Object],(,[object Object],):
,[object Object], state.reply.finished:
,[object Object], (,[object Object],, state.reply.answer)
,[object Object], state.step >= state.max_steps:
,[object Object], (,[object Object],, state.best_partial())
,[object Object], state.tokens_spent >= state.token_budget:
,[object Object], (,[object Object],, state.best_partial())
,[object Object], state.no_progress_streak >= ,[object Object],:
,[object Object], (,[object Object],, state.best_partial())
,[object Object], state.same_action_repeated >= ,[object Object],:
,[object Object], (,[object Object],, state.best_partial())
,[object Object], (,[object Object],, ,[object Object],)What this does: checks five independent exit conditions every turn and returns a labeled reason plus the best available result, so the loop always ends on purpose and always tells you why.
Wire this into the top of your loop and you've already eliminated the 240-iteration disaster. The rest of this guide is about tuning each condition so it fires at the right moment — not too early, not too late.
Understanding the Variables
Five signals drive good agent loop stopping conditions, and each answers a different question.
finishedstepmax_stepstokens_spenttoken_budgetno_progress_streaksame_action_repeatedThe last two are where the money hides. Step and budget caps are backstops that fire late — after you've paid. Progress and loop detection fire early, the moment the agent stops moving forward, which is exactly when you want to intervene.
One nuance about token budgets versus step caps: they diverge hard once your agent uses compaction. A compacted step might cost a tenth of an uncompacted one, so ten steps late in a run can be cheaper than two steps early. Budget only in steps and you'll cut off a cheap, productive late phase while happily allowing an expensive early one. Budget in tokens and the limit tracks actual cost — which is the thing you were trying to control in the first place.
⚡ Pro tip: Define "progress" concretely for your task before you write the check. New file created? New fact added to the scratchpad? A tool returning data it hadn't seen? Without a definition,
no_progress_streakStep-by-Step: Building Agent Loop Stopping Conditions
Layer the conditions from cheapest to most nuanced.
First, add the success exit — an explicit finish tool. Second, add the two hard backstops, step and budget, so a runaway can never truly run away. Third, add loop detection: hash each
(tool, args)[object Object], ,[object Object],(,[object Object],):
recent = [ (h.tool, canonical(h.args)) ,[object Object], h ,[object Object], history[-window:] ]
,[object Object], ,[object Object],(recent) == window ,[object Object], ,[object Object],(,[object Object],(recent)) == ,[object Object],What this does: canonicalizes the last few tool calls and flags when the agent repeats the identical action, catching the tight cycle where an agent calls the same tool with the same arguments over and over.
Fourth, add progress tracking. After each turn, ask whether the agent advanced by your task-specific definition; if not, increment the streak; if it did, reset it.
[object Object], made_progress(state):
state.no_progress_streak = ,[object Object],
,[object Object],:
state.no_progress_streak += ,[object Object],What this does: keeps a running count of turns without forward movement, so a stalled agent — one that's busy but not advancing — exits after a few idle turns instead of burning the whole budget.
A word on ordering these checks: put the cheap, common exits first. Success and the hard caps are near-free to evaluate, so they lead. Loop and progress detection do a little more work — hashing actions, computing progress — so they follow. Any expensive check, like scoring answer quality, goes last, gated behind the cheaper ones, so you only pay for it on turns that survived everything else. The order of your stop checks is itself a small cost optimization almost nobody thinks about.
⚡ Pro tip: Reset the progress streak on real advancement only. An agent re-reading the same file counts as no progress even though it "did something." If busywork resets your counter, the stall detector never fires and you're back to relying on the expensive backstops.
Pro-Level Variations
Different roles weight the conditions differently.
A fintech engineer running a reconciliation agent sets a tight token budget and a loose step cap — steps are cheap when each is small, but total spend is the number the finance team watches. A research scientist running a literature agent inverts it: generous budget, strict progress detection, because the failure mode is subtle spinning, not raw cost. A support-ops lead adds a wall-clock timeout on top, because a triage agent that takes ninety seconds has already failed the user regardless of tokens or steps.
A game studio running an NPC-dialogue agent adds a quality stop on top: end early if a scoring pass rates the current line above a threshold, rather than spending more turns polishing something already good enough. Stopping conditions aren't only about failure — sometimes the right moment to stop is when you've already succeeded and further turns can only make it worse.
Same conditions, weighted to the cost that actually hurts in each context.
⚡ Pro tip: A "good enough" stop is underrated. If you can cheaply score the current answer, exit the moment it clears the bar. Plenty of agents keep grinding past the point of diminishing returns purely because nothing ever told them they were already done.
⚡ Pro tip: Add a wall-clock deadline as a sixth condition for any user-facing agent. Token and step budgets protect your bill; a time budget protects the person waiting. They're different limits and latency-sensitive agents need both.
Troubleshooting Common Issues
If your agent stops too early, your progress or loop detection is too aggressive — a task with a legitimately repetitive step (paginating through results) looks like looping. Whitelist that pattern or widen the window.
If your agent still runs away despite a step cap, check that every path through the loop actually calls
check_stopcontinueIf your agent exits with nothing, your stop conditions return but you never captured a partial result. Always keep a
best_partial()If your agent exits on a false loop, your action canonicalization is off — two calls that differ only by a timestamp or request ID look identical after normalization, or two genuinely different calls collapse to the same key. Log the canonical form you're actually comparing; the fix is nearly always in how you normalize args, not in the detector itself.
⚠️ Common mistake: Using a single max-step cap as your only stopping condition. It fires last, after full cost, and it can't distinguish success from stall from loop — so every non-success looks identical in your logs and you learn nothing about why runs fail. Layer the cheap early-exit conditions on top and reserve the step cap for what it actually is: a last-resort backstop, not your primary control.
Your Turn
Take your current agent and add just two conditions today: loop detection and a no-progress streak with a concrete definition of progress. Run it on the task that worries you most and watch how early the new conditions fire compared to your old step cap. The 240-iteration disaster becomes a six-iteration graceful exit — same code, five extra lines, and a bill that no longer surprises anyone.
Once you've tuned a set of agent loop stopping conditions that work, don't rewrite them per project. I keep my full
check_stopContinue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
