I watched my agent try to write the same file six times in a row last week.
Each attempt looked reasonable in isolation. The agent saw an error, course-corrected, and ran again — but the "correction" put things right back where they started. It was stuck in a local minimum of correct-looking wrongness, and it had no idea.
This is one of the most common failure modes in AI agent pipelines, and almost nobody talks about it honestly.
The Loop That Looks Like Progress
Here's the pattern in its simplest form:
def run_agent_task(task: str, max_attempts: int = 5) -> str:
for attempt in range(max_attempts):
result = agent.execute(task)
if is_satisfactory(result):
return result
# Feedback injection — the agent "learns" from the error
task = f"{task}\n\nPrevious attempt failed: {result.error}. Fix it."
raise AgentLoopError(f"Failed after {max_attempts} attempts")
This looks fine. It even has a feedback loop! But under certain conditions, the agent will converge on a fixed point — a response that keeps triggering the same "error" (or a slightly different error that maps to the same corrective action).
The problem isn't the loop. The problem is that the agent's correction signal contains no information about whether it's been applied before.
Why Fixed-Point Loops Happen
There are three root causes I've encountered most often:
1. The error message doesn't describe the actual problem.
The error returned to the agent is "Permission denied" but the real issue is that the path was wrong three steps earlier. The agent fixes permissions, the path is still wrong, permissions fail again.
2. The state the agent is trying to fix was already fixed.
This is the smolagents bug pattern. The agent produces valid output, but the validator rejects it for a reason that no longer applies after the next step. The agent retries the same valid code, hits a different validation gate, and cycles.
3. The agent's context window contains its own previous failed attempts.
This is the subtlest one. If your agent's system prompt includes a history of "what went wrong," and the corrective action was already taken, the history becomes noise that drowns out the signal. The agent reads its own failure log and concludes the problem is still active.
Loop Detection: A Practical Pattern
The fix isn't complicated. You need to track whether the observable state has actually changed, not just whether the agent ran successfully.
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
import hashlib
@dataclass
class LoopDetector:
max_history: int = 5
_state_history: deque = field(default_factory=deque)
def state_hash(self, result: AgentResult) -> str:
"""Fingerprint of the *observable outcome*, not the agent's reasoning."""
key_fields = (
result.output[:200], # first 200 chars of output
result.error_type, # error category, not message
result.files_written, # files that were modified
)
return hashlib.md5(str(key_fields).encode()).hexdigest()
def is_looping(self, result: AgentResult) -> bool:
state = self.state_hash(result)
if state in self._state_history:
return True
self._state_history.append(state)
if len(self._state_history) > self.max_history:
self._state_history.popleft()
return False
def should_abort(self, result: AgentResult) -> bool:
"""Returns True when we've seen this outcome before."""
return self.is_looping(result)
The key insight: we're hashing observable state, not agent reasoning. If the files written are the same and the output is similar and the error type is the same, we're in a loop — regardless of what the agent thinks is happening.
Applying It
def run_with_loop_detection(task: str) -> str:
detector = LoopDetector(max_history=4)
for attempt in range(10):
result = agent.execute(task)
if result.is_successful:
return result.output
if detector.should_abort(result):
raise AgentLoopError(
f"Detected loop at attempt {attempt + 1}. "
f"State repeated: {detector.state_hash(result)[:8]}"
)
# Inject *only* the actionable error signal, not the full history
task = f"{task}\n\nError: {result.error_type} — {result.actionable_hint}"
# Clear the agent's internal history; we track state externally
agent.reset_conversation_history()
raise AgentLoopError("Max attempts reached")
Two things changed here from the naive version:
State tracking is external to the agent. The agent doesn't carry its own failure history. We track the observable outcomes and abort when they repeat.
Error messages are pre-processed into actionable hints. Instead of
"Permission denied on /app/data/output.json", the error layer returns"Path does not exist — create parent directory first". The agent gets a corrective action, not a symptom.
The Question to Ask Before Every Retry
Before injecting feedback and looping, ask: has the observable state actually changed?
If the agent is going to retry, it needs to be working with new information — not the same file it already read, not the same path that was already wrong, not the same context that led it here.
When state hasn't changed but the agent is about to retry, you have two options:
- Abort — surface the loop to the human
- Change the approach — switch models, add tools, reduce context. Give the retry a fundamentally different angle of attack.
Retrying the same reasoning with the same context is not a recovery strategy. It's a while loop with no exit condition.
What I Learned
The agent wasn't broken. The loop-detection logic was missing.
Once I added external state fingerprinting, these cycles became visible and actionable. The agent now surfaces a clear error: "I'm stuck — I've tried the same approach 4 times and the output keeps converging on the same result." That's a much better failure mode than silently burning tokens for six rounds.
If you're building agent pipelines in 2026 and you don't have loop detection, you're probably running more iterations than you think — and most of them aren't helping.
Have a loop detection pattern that works differently? I'd genuinely like to hear it — this is an unsolved corner of agentic engineering.
Top comments (1)
I found the example of the agent trying to write the same file six times in a row particularly relatable, as I've encountered similar issues in my own projects. The concept of a "local minimum of correct-looking wrongness" is a great way to describe this problem, and I appreciate how you've identified the root causes, such as the error message not describing the actual problem or the agent's context window containing its own previous failed attempts. The
LoopDetectorclass you provided is a clever solution, and I'm interested in exploring how this approach could be applied to other areas of AI development - do you think this pattern could be generalized to detect loops in more complex agent pipelines, or are there specific limitations to its applicability?