Agent Loop · Deep Dive
Every agent developer eventually faces the same sinking feeling: you send an agent off to solve a problem, and it never comes back. Not because it failed, but because it entered an infinite loop — calling tools, parsing observations, calling tools again, forever. Without explicit stop conditions, even a well-prompted agent will happily spin until it hits your token limit or your wallet gives out.
Stop conditions are the guardrails that force an agent to either produce a final answer or admit defeat. This guide covers the four essential stop conditions every agent needs: a hard step budget, repeated-action detection, a final-answer signal, and a fallback response.
LLM-based agents loop because they have no intrinsic sense of elapsed time or progress. Each turn looks the same as the last: read an observation, decide on an action, emit a tool call. If the observation is ambiguous, or the tool returns an error the agent ignores, it will pick the same action again.
The root cause is almost always a missing termination signal. The agent was told to keep working until it had an answer, but it was never told what counts as 'having an answer.'
A step budget is the maximum number of tool-calling turns an agent is allowed before it must stop. This is your last line of defense. Always set one — there is no scenario where an unbounded agent is a good idea. A reasonable default is 10-15 steps.
Track it with a counter incremented after every tool response, and when the budget is exhausted, skip the normal agent loop and go straight to fallback.
One of the most reliable signals that an agent is stuck is when it calls the same tool with the same arguments more than once. Implement this by maintaining a counter of (tool_name, normalized_args) tuples. Set a threshold of 2-3 repetitions, and if the agent hits it, stop and fall back.
Use a reserved format like 'Final Answer: <result>' that the agent emits when done. Parse for this signal after every turn. If it appears, extract and return the answer immediately. The format must be strict enough to be unambiguous.
When every stop condition is exhausted and the agent still has no final answer, return a clear message: 'I could not find an answer after N steps' rather than a hallucinated guess or cryptic error.
Concrete Example
import re
from dataclasses import dataclass, field
@dataclass
class AgentState:
step: int = 0
max_steps: int = 15
action_history: list[tuple[str, str]] = field(default_factory=list)
status: str = "running"
final_answer: str | None = None
def normalize_args(args: str) -> str:
return " ".join(sorted(args.strip().lower().split()))
def is_final_answer(action_text: str) -> bool:
return bool(re.match(r"^Final Answer:\s*", action_text.strip()))
def detect_repetition(state: AgentState, threshold: int = 2) -> bool:
counts: dict[tuple[str, str], int] = {}
for tool, args in state.action_history:
key = (tool, normalize_args(args))
counts[key] = counts.get(key, 0) + 1
if counts[key] > threshold:
return True
return False
def run_agent_with_stop_checks(problem: str) -> str:
state = AgentState()
observation = problem
while state.status == "running":
if state.step >= state.max_steps:
state.status = "budget_exhausted"
break
action = agent_step(observation)
if is_final_answer(action):
state.status = "answered"
state.final_answer = action.split("Final Answer:", 1)[1].strip()
break
tool, args = parse_action(action)
state.action_history.append((tool, args))
if detect_repetition(state):
state.status = "stuck"
break
observation = execute_tool(tool, args)
state.step += 1
if state.status == "answered":
return state.final_answer
elif state.status == "stuck":
return "I could not find an answer: the agent repeated the same action multiple times."
else:
return f"I could not find an answer after {state.max_steps} steps."This run loop checks every stop condition at each turn: hard step budget, repeated action detection, and structured 'Final Answer:' parsing. When any condition triggers, it returns a clear fallback message.
A hard limit prevents infinite loops. Default to 15 steps.
Track (tool, normalized_args) pairs. If same action repeats > 2 times, fall back.
Require 'Final Answer:' with exact formatting. Parse with a regex.
Explain why the agent stopped rather than returning a generic error.