Debugging a Stuck Agent: A Triage Runbook
Stuck is four different failures with four different fixes. How to tell hung from looping from stalled from quietly truncated, and what to do about each one.
An agent has been running for eleven minutes on a task that normally takes ninety seconds. The obvious move is to kill it and retry. That works often enough to be a habit, and it is why the same failure keeps happening.
Stuck is not one condition. It is at least four, they look identical from outside, and the fix for one makes another worse. Triage means identifying which you have before you touch anything.
The four shapes
Hung. No progress because nothing is executing. A tool call is blocked on a socket with no timeout, a subprocess is waiting on stdin, a rate limit is being retried with unbounded backoff. Token usage is flat; wall clock is climbing.
Looping. The model repeats the same action, or oscillates between two, because the state keeps looking like it calls for that action. Token usage climbs steadily, step count climbs, nothing in the environment changes.
Stalled. The model is producing plausible, varied, forward-looking output that never converges. Each step is different, so repetition detectors miss it, but no step brings the task closer to done. This one is the hardest to spot and the most expensive.
Quietly finished. The run ended, correctly or not, and something downstream is waiting on a signal that never came. Nothing is stuck at all; your orchestration is.
The first thing to look at is not the transcript. It is the token counter and the step counter over the last two minutes. Flat tokens with rising wall clock means hung. Rising tokens with a flat environment means looping or stalled. Both flat means the run is already over.
Hung: look outside the model
Nothing about the model is relevant here, which is why it is worth ruling out first. Find the last span that opened and never closed. If your tracing is in place this takes seconds — an open tool span with no end timestamp names the culprit directly, which is one of the practical payoffs of building a proper trace tree.
The recurring causes are dull and fixable. A shell command that prompts for confirmation and waits forever. An HTTP client with no timeout configured, inheriting an infinite default. A retry loop with exponential backoff and no ceiling, now sleeping for four minutes between attempts. A database query with no statement timeout.
The fix is a timeout on every external call, always, with a value chosen from the observed distribution rather than guessed. And every retry policy needs a total deadline in addition to an attempt limit — rate limits and retries covers why the attempt limit alone is insufficient.
Looping: find the repeat and read the error
Normalise each tool call to a signature — name plus canonicalised arguments — and scan the run for repeats. If the same signature appears three times, you have your answer, and the interesting question becomes why the model thinks repeating is reasonable.
In practice it is almost always because the failure it is getting back is uninformative. A tool that returns Error: operation failed gives the model nothing to change, so the only move available is to try again. A tool that returns the specific reason and a suggested next action usually breaks the loop on the first retry.
The second common cause is a state mismatch. The model believes a file exists, or a service is reachable, or a previous edit applied, and the transcript contains nothing that contradicts it. It will keep acting on that belief until something states otherwise, plainly, in a recent message. Detecting agent loops covers making this automatic rather than manual.
Stalled: the expensive one
A stalled run looks productive. Every step is a different, sensible-sounding action, so nothing trips a repetition check. It is only when you read fifteen steps end to end that you notice the agent has been exploring rather than converging.
Two causes account for most of it. The first is that the task is underspecified, and there is no test, no schema and no acceptance condition that would let the model know it is done. Given no definition of done, exploration is the rational behaviour.
The second is context degradation. As the transcript grows, early instructions recede into the region models attend to least, and the agent drifts toward whatever was said recently — usually its own speculation. Plot context size against step number for the stuck run; if it is near the ceiling, you are debugging the transcript, not the model. The lost-in-the-middle problem and context compaction strategies both bear directly on this.
The practical detector is progress rather than repetition: define what a state change means for your agent — a file written, a test outcome changed, a record created — and count steps since the last one. Five or six steps with no state change is a stall regardless of how varied the output looks.
Check the finish reasons before blaming the model
Before concluding the model behaved badly, confirm you actually received what it sent. A response truncated at the output limit looks like a malformed tool call, and a malformed tool call frequently sends the harness into an error-and-retry cycle that reads as a loop.
Scan the run for any completion that ended on length rather than a clean stop or a tool call. Scan for stream disconnections. Both produce partial output that downstream parsing rejects, and neither is visible unless you recorded the finish reason. This is a five-second check that resolves a surprising fraction of apparently inexplicable behaviour.
Get to a minimal reproduction
Once you know the shape, stop reasoning about the full run. Take the transcript as it stood two or three steps before the failure, replay from there with the tool results served from the recorded values, and you have a loop you can iterate on in seconds.
Then bisect. Remove half the accumulated context and replay. If the failure disappears, the problem is context volume or a specific poisoned entry. If it persists on a short transcript, the problem is the prompt, the tool schema or the task itself. This single test separates the two most common root causes faster than any amount of reading.
Fixes in order of durability
Improve the tool result first: a specific error with a suggested action fixes more stuck runs than any other change, and it helps whichever model you run. Then add the missing bound — a timeout, a step cap, a cost ceiling, a no-progress detector — so the next occurrence terminates instead of burning budget.
Then define done. An explicit acceptance check the agent can run itself converts stalling into either success or a clean failure. Only after those three is it worth touching the prompt, and changing the model is last: it sometimes helps, it never fixes a missing timeout, and it resets everything you have tuned. Agent error recovery patterns covers building these in rather than bolting them on.
Finally, record the run. Every stuck run you diagnose is a ready-made regression case, and the shape you identified — hung, looping, stalled, finished — is the label that tells you which metric would have caught it sooner.
Common questions
How do I tell a hung agent from a looping one?
Watch token usage over the last minute or two. Flat tokens with rising wall clock means something is blocked on I/O and the model is not running at all. Rising tokens with an unchanged environment means the model is looping or stalling.
Why does the model keep retrying the same failing call?
Usually because the error it gets back contains nothing actionable. A generic failure message leaves retrying as the only available move. Returning the specific reason plus a suggested next action breaks most of these loops immediately.
What is a stalled run and how do I detect it?
One where every step is different and plausible but none moves the task forward, so repetition detectors miss it. Detect it by progress instead: define a real state change for your agent and halt when five or six consecutive steps produce none.