Debugging an Agent That Loops Forever
When an agent gets stuck in a loop, it burns budget and never returns an answer. This case study walks through diagnosing and fixing a real infinite-loop agent step by step.
from hashlib import sha256
def action_fingerprint(action):
return sha256(f"{action.name}:{sorted(action.args.items())}".encode()).hexdigest()
def agent_loop(state, max_steps=20):
seen = {}
for step in range(max_steps):
action = model_decide(state, tools)
fp = action_fingerprint(action)
seen[fp] = seen.get(fp, 0) + 1
if seen[fp] >= 3:
# Same action three times: inject a nudge, don't just spin.
state = add_note(state, "You have repeated this exact action. "
"It is not producing new information. Try a "
"different approach or report what you found.")
result = run_tool(action)
state = update_state(state, action, result)
if action.name == "finish":
return action.args["answer"]
return force_answer(state)Picture this: you're a backend engineer on call, and a dashboard alert says your document-processing agent has been running for eleven minutes on a single request. It should take twelve seconds. You open the trace and watch it call
search_documentssearch_documentsThis is one of the most common failure modes in production agents, and it's also one of the most fixable once you understand what's actually happening. This is a real case (details anonymized) I helped debug, walked through end to end.
The Problem the On-Call Engineer Faced
The agent was a research assistant for an insurance claims team. Given a claim ID, it was supposed to pull the claim, find related policy documents, and summarize coverage. Most runs finished in under fifteen seconds. But roughly 3% of runs never finished at all. They looped until the 30-step ceiling forced a garbage answer, costing 6x the tokens of a normal run and returning something useless.
The engineer's first theory was a model problem — maybe the model was just "confused" on hard claims. But the stuck runs weren't on unusually hard claims. Some were on trivially simple ones. That mismatch is the first clue that you're looking at a loop bug, not a reasoning limit.
⚡ Pro tip: When a subset of runs fails and the failures don't correlate with input difficulty, suspect a structural loop issue before you blame the model. Difficulty-independent failures almost always come from the harness, not the reasoning.
The Wrong Approach
The team's initial fix was to lower the step ceiling from 30 to 8. Their reasoning: if the agent can't loop as long, it can't waste as much. Within a day they rolled it back. The lower ceiling didn't stop the looping — it just made the loops fail faster and more often, and now legitimately complex claims that needed nine or ten real steps were also getting cut off. They'd treated the symptom (long runs) and made the disease (bad answers) worse.
The second wrong turn was adding a blanket instruction to the system prompt: "Do not repeat tool calls." This helped a little and felt like progress, but it didn't address why the agent was repeating. The model wasn't repeating out of stubbornness. It was repeating because each
search_documentsHere's the mental model that unlocked the fix. An agent loop is a function that maps state to action. If the state barely changes between steps — because the tool keeps returning the same empty result — the model keeps computing nearly the same action. The loop isn't broken because the model is dumb; it's broken because the state stopped moving. Fix the loop by guaranteeing that either the state changes meaningfully each step, or the loop notices it isn't changing and intervenes. Everything below follows from that one idea.
The engineer spent an afternoon on a third dead end worth mentioning, because it's a common one: increasing the model's temperature, on the theory that more randomness would shake the agent out of its rut. It did — sometimes. Higher temperature occasionally jittered the query enough to escape, but it also made the healthy 97% of runs less reliable, and the escapes were random rather than reasoned. Randomness is not a recovery strategy. If your only tool against loops is turning up the temperature, you're gambling, not engineering.
The Correct Fix
The real problem surfaced once the engineer logged the full arguments and results of every tool call, not just the tool names. The search was returning empty because the claim's documents were filed under a slightly different tenant ID, and the tool silently returned
[]Two changes fixed it. First, make the loop detect a repeated action and break the cycle instead of relying on the model to notice.
[object Object], hashlib ,[object Object], sha256
,[object Object], ,[object Object],(,[object Object],):
,[object Object], sha256(,[object Object],.encode()).hexdigest()
,[object Object], ,[object Object],(,[object Object],):
seen = {}
,[object Object], step ,[object Object], ,[object Object],(max_steps):
action = model_decide(state, tools)
fp = action_fingerprint(action)
seen[fp] = seen.get(fp, ,[object Object],) + ,[object Object],
,[object Object], seen[fp] >= ,[object Object],:
,[object Object],
state = add_note(state, ,[object Object],
,[object Object],
,[object Object],)
result = run_tool(action)
state = update_state(state, action, result)
,[object Object], action.name == ,[object Object],:
,[object Object], action.args[,[object Object],]
,[object Object], force_answer(state)What this does: It fingerprints each action and, after the same call repeats three times, injects an explicit note telling the model the action is unproductive — turning a silent infinite loop into a self-correcting one. This alone dropped the stuck-run rate from 3% to under 0.3%.
Second, fix the lying tool. A tool that returns empty when it should signal "no access" or "wrong tenant" starves the model of the one fact it needs.
[object Object], ,[object Object],(,[object Object],):
results = backend.search(query, tenant_id)
,[object Object], results ,[object Object], ,[object Object],: ,[object Object],
,[object Object], ToolError(,[object Object],)
,[object Object], {,[object Object],: results, ,[object Object],: ,[object Object],(results)}What this does: It distinguishes "searched successfully, found nothing" from "couldn't search at all," so the model gets an actionable error instead of an ambiguous empty list it keeps retrying against.
Results and What Changed
After both fixes, the stuck-run rate fell from about 3% of traffic to effectively zero over two weeks. Median latency was unchanged for healthy runs, but p99 latency dropped from 11 minutes to 18 seconds because the pathological tail disappeared. Token spend on the claims agent fell 22%, almost entirely from eliminating the runaway loops.
The subtler win was trust. The claims team had quietly stopped relying on the agent for anything urgent because "sometimes it just hangs." Once the hangs were gone, usage climbed back up. Reliability is a feature, and an agent stuck in a loop is the fastest way to lose it.
⚡ Pro tip: When you ship a loop guard, emit a distinct log event every time it fires — not just a metric bump, but a full trace of the run that triggered it. Those traces are gold: they show you the exact tool-and-result patterns that cause loops in your system, which is how you fix root causes instead of just catching symptoms forever.
⚠️ Common mistake: Treating a repeated-action loop as something to suppress in the prompt rather than detect in the code. Prompts nudge behavior probabilistically; a loop guard catches it deterministically. You want both, but the code-level guard is what actually guarantees the loop ends.
How to Apply This to Your Situation
Start with three checks whenever you suspect an agent stuck in a loop. First, log full tool arguments and results, not just names — the bug is almost always in what a tool returned, not which tool ran. Second, add action fingerprinting so identical repeated calls are visible and breakable. Third, audit your tools for silent empties: any tool that can return "nothing" should clearly distinguish "found nothing" from "failed to run."
Here's how three different teams applied the same pattern. A healthcare scheduling agent was looping because its calendar tool returned an empty slot list for closed clinics without saying the clinic was closed; a clearer error ended it. An e-commerce returns bot looped on orders older than its lookup window, which returned
[]⚡ Pro tip: Add a cheap "novelty" check to your loop — if the last three tool results are byte-identical, the loop is not making progress regardless of what the model intends. Break or escalate. Progress, not intent, is what you want to measure.
How to Spot an Agent Stuck in a Loop Before It Costs You
Detection is the part teams skip, and it's the cheapest insurance you can buy. You don't want to learn about a runaway loop from a billing alert or an angry user; you want the loop to flag itself in real time. Three lightweight signals cover almost every case.
The first is action repetition, which you already saw fingerprinted above. The second is result stagnation: hash each tool result and watch for the same hash appearing repeatedly, which catches loops where the model varies its arguments slightly but keeps getting identical output. The third is a simple progress heuristic — track whether the agent's estimate of "what's left to do" is shrinking. If it's been flat or growing for several steps, the agent isn't converging.
[object Object], ,[object Object],(,[object Object],):
recent = history[-window:]
,[object Object], ,[object Object],(recent) < window:
,[object Object], ,[object Object],
results = [h.result_hash ,[object Object], h ,[object Object], recent]
,[object Object], ,[object Object],(,[object Object],(results)) == ,[object Object],:
,[object Object], ,[object Object], ,[object Object],
actions = [h.action_fp ,[object Object], h ,[object Object], recent]
,[object Object], ,[object Object],(,[object Object],(actions)) == ,[object Object],:
,[object Object], ,[object Object], ,[object Object],
,[object Object], ,[object Object],What this does: It inspects a sliding window of recent steps and returns a health status — "stagnant" when results stop changing, "repeating" when actions do — giving you a signal to intervene, escalate, or alert before the loop runs away.
Wire this into your monitoring so that a "stagnant" or "repeating" status increments a metric you can alarm on. The goal is that the on-call engineer from the opening never has to notice the eleven-minute run by accident — the system tells them the moment a loop stops making progress.
Next Steps
Instrument first, then guard, then fix the root cause. Add tracing that captures arguments and results, drop in an action-fingerprint guard so no loop can run away, and then hunt the specific tool that's feeding the model ambiguous output. In most stuck-agent cases, one lying tool is at the bottom of it.
Once you've built a loop guard that works, don't rewrite it for every project. Keep the guard code and the "you are repeating yourself" nudge prompt in a reusable library. Tools like PromptABCD make it easy to store and version these guardrail snippets so your next agent ships with loop protection built in from day one.
Continue 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.
