Agentic AI / Agent architectures, tool use and MCP / How an agent loop ends, and the three ways it fails to

How an agent loop ends, and the three ways it fails to

The ReAct note showed the loop with one exit: the model answers without calling a tool. That is the exit you want, and it is not the only one you need. A model is probabilistic, tools fail, and context fills up, so a loop that relies on the model to stop will sometimes never stop, sometimes stop too soon, and sometimes do the same thing over and over. This note sets out the full termination condition a production loop needs, and then takes the three failure modes apart: what you see, why it happens, and what fixes it.

The short version

A loop ends in one of two ways: the model decides, by responding without a tool call, or the runtime decides, by hitting a step cap, a time budget or a cost budget. You need both, because the model's decision is a judgement and judgements are sometimes wrong in each direction. Infinite loops come from stop criteria the model cannot satisfy and errors it is told to retry; the fix is hard caps and error results that say whether retrying can help. Premature stops come from the model believing it is done when it is not; the fix is a gate that checks the required work happened before the answer is accepted. Repeated identical calls come from the model losing track of what it already tried; the fix is a deduplication check in the runtime and observations that say what they mean.

01What a termination condition is

A termination condition is the set of checks the runtime makes at the end of every iteration to decide whether to go round again. It has two layers, and production loops need both.

        ┌──────────────────────────┐
        │  model generates a turn  │
        └────────────┬─────────────┘
                     ▼
        ┌──────────────────────────┐
        │  run any tool calls      │
        └────────────┬─────────────┘
                     ▼
        ┌──────────────────────────┐
        │  evaluate termination    │
        └────┬──────────┬────────┬─┘
             ▼          ▼        ▼
     natural exit  breaker   continue
     the model      a limit   next iteration
     answered       was hit
     with no        (steps,
     tool call      time, cost)
             │          │
             └────┬─────┘
                  ▼
        return the answer, or the
        partial state and the reason
  • The task-driven stop comes from the model. It responds with text and no tool call, which means it believes the task is complete, or that it needs to ask the user something. Some systems add a programmatic check on top: the row count matches, the update was verified, the user clicked approve.
  • The guardrails come from the runtime and ignore what the model thinks. A maximum number of iterations, a wall-clock limit, a token or cost budget. They exist because the first layer is a judgement call by a probabilistic system, and the runtime has to be able to stop regardless of that judgement.

The pairing is sometimes called a double stop. The model's exit handles the normal case; the breakers handle the cases where the model's exit does not come, or comes at the wrong time.

02Reading the stop reason

The API tells you why the model stopped generating, and the loop should branch on it rather than on the content.

Stop reasonMeaningWhat the loop does
tool_use / tool_callsThe model wants a tool run and is waitingExecute, append results, continue
end_turn / stopThe model finished a response with no callExit; return the text
max_tokens / lengthOutput was cut off at the token limit, possibly mid-callA failure, not an exit: do not execute a truncated call; retry with a larger limit or ask the model to continue
refusal or a content filterThe model declinedExit with that information; do not loop

The third row is the one that gets missed. A tool call truncated by the output limit arrives as incomplete JSON, and a loop that treats any non-tool stop as "done" will either crash on the parse or, worse, present the half-generated text as an answer. Treat it as its own case.

Note also that end_turn means the turn ended, not that the task did. The model may have answered, or it may have asked the user a question. Both return control to the user, and both are correct behaviour. The loop's job is to end the turn; whether the task is complete is a separate question that only a goal check can answer.

03Circuit breakers: the runtime's own limits

  • Step cap. A maximum number of iterations, typically ten to thirty depending on the task. The single most important guardrail, because it bounds every other cost.
  • Time budget. A wall-clock limit on the whole run, with a shorter per-tool timeout inside it so one hanging network call cannot consume the budget alone.
  • Cost budget. A cap on cumulative tokens or money per run. Agent costs grow faster than linearly in steps, because the history is resent each time, so a step cap alone can still allow an expensive run.
  • A told budget. Hard caps cut the model off without warning. A softer complement is to tell the model its budget, in steps or tokens, so it paces itself and wraps up before the cap rather than being truncated at it. Some APIs expose this directly; otherwise put the remaining budget in the context each turn.

Breakers are set per task type, not globally. A step cap that is right for "look up an order" is far too low for "refactor this module", and setting one value for both produces either runaway loops or premature cut-offs.

04Failure 1: the loop that never ends

What you see. Token usage and elapsed time climb, the trace shows step after step, and nothing in the task's state moves. The loop ends only when a breaker trips, or never, if there is no breaker.

Why it happens.

  • A stop criterion the model cannot satisfy. "Search until you have enough information" with no definition of enough. Each search returns something, the model is never sure it has enough, and the instruction says to keep going. The same happens with "make sure the answer is correct" and no way to check.
  • Retrying what cannot succeed. A tool returns an error that no retry will fix, such as an authentication failure or a resource that does not exist, and the prompt says to retry on failure. The model obeys. Each attempt returns the same error, which looks like a transient failure to a model that has no way to tell the difference.
  • Oscillation. Two actions each undo the other's effect, or two tools each suggest the other. The model alternates between them, making local progress every step and global progress never.

The fix. A hard step cap, always. Then make the errors informative: a result that says retryable: false and why, so the model has the information to stop. Define "done" in the prompt in terms the model can verify from observations, and give it an explicit way to say it cannot finish, such as a give_up tool or an instruction that an unanswerable task should be reported rather than pursued.

05Failure 2: the loop that ends too soon

What you see. A confident final answer, often a well-formed one, with tools the task required never called. The agent reports the record updated and the update tool was never invoked; it summarises search results it never fetched.

Why it happens.

  • Hallucinated progress. The model predicts what the answer would look like and writes it, skipping the step of finding out. This is more likely when the context contains enough to make a plausible guess, and when the system prompt rewards sounding finished.
  • Empty results read as completion. A search returns []. To the model that can look like "nothing more to do" rather than "nothing found; try differently". The ambiguity is in the observation, and the model resolves it in the direction of stopping.
  • Breakers set too tight. A step cap of five on a task that needs twelve. The loop is cut off and the runtime returns whatever was last generated, which looks like an answer.
  • Missing information treated as optional. A required parameter is unknown, and rather than ask, the model answers with an assumption.

The fix. A validation gate between the model's exit and the accepted answer. For a task that must write, the runtime checks that the write tool was called with a success result before it accepts a final response; if not, it rejects the exit and sends the model back with a message saying so. For retrieval tasks, require the answer to cite an observation. A structured final-answer tool, which the model must call to finish and whose schema requires the evidence fields, makes the gate mechanical. And format empty results so they cannot be misread: {"results": [], "note": "No matches. Consider different terms or a different tool."}.

06Failure 3: the same call, again and again

What you see. The trace shows search_user(id="101") on consecutive steps, five times, identical arguments, identical results. Sometimes a longer cycle of two or three calls repeating.

Why it happens.

  • State blindness. As the history grows the model attends less reliably to what it already did. It forgets it ran the query, or remembers running it but not what came back, and runs it again as the obvious next step.
  • An uninformative observation. The result was ambiguous or unhelpful, and nothing in it said that repeating the call will not help. The model's reasonable inference is that something went wrong and trying again is cheap.
  • No memory across steps of what has been tried. Each turn is generated afresh from the history; there is no separate record of attempts unless the runtime keeps one.

The fix. A deduplication check in the runtime: before executing a call, compare its name and arguments to the last several calls. If identical, do not execute; return a system notice instead, "You already ran this with these arguments and received: [result]. Choose different arguments or a different tool." The model almost always changes course once told. Secondly, make observations say what they mean: a result that is the same as last time should say so, and a result that is final should say not to re-query unless the arguments change. Thirdly, manage the context, by truncating or summarising old tool results, so that recent history is where the model's attention actually lands.

07What to do when a breaker trips

A breaker is not a crash. When the step cap, time budget or cost budget is hit, the runtime should return something useful rather than an error with nothing attached.

  • Return the partial state: what was done, what was found, what remains, and which limit was hit.
  • Give the model one final turn without tools, asking it to summarise progress and say what it would have done next. This costs one call and turns a dead run into a resumable one.
  • Record the trip with the full trace. A breaker that trips often on one task type is telling you the cap is wrong or the task has a loop in it, and the traces are how you find out which.
  • Escalate where the task matters. A human sees the partial state and decides whether to resume with a larger budget, finish by hand, or abandon.

08The three failures side by side

FailureSymptom in the traceRoot causeFix
Infinite loopSteps and cost climb; a limit trips or nothing endsUnsatisfiable stop criterion; retrying unrecoverable errors; oscillationHard step cap; retryable: false in error results; a defined, observable "done"; a way to give up
Premature stopConfident answer; required tools never calledHallucinated progress; empty results read as completion; caps too tightValidation gate on exit; structured final-answer tool requiring evidence; unambiguous empty results
Repeated callsIdentical name and arguments on consecutive stepsState blindness in a long context; uninformative observationsRuntime deduplication with a notice; observations that say what they mean; context management

09Takeaways

  1. Two layers of termination. The model's exit for the normal case; the runtime's breakers for when the model's judgement is wrong.
  2. Branch on the stop reason, and handle truncation as its own case. A cut-off tool call is neither an answer nor a call to execute.
  3. End of turn is not end of task. A goal check is a separate thing from the loop exit.
  4. Set breakers per task type. One global cap is wrong for everything.
  5. Errors must say whether retrying can help. That one flag removes most infinite loops.
  6. Gate the exit. Check the required work happened before accepting the answer.
  7. Deduplicate in the runtime. The model will repeat itself; tell it so, with the result it already got.
  8. A tripped breaker returns partial state, not silence.