Loop Detection

Detect and break stuck tool-call loops automatically

Loop detection prevents agents from calling the same tool repeatedly with no progress.

python
from praisonaiagents import Agent

agent = Agent(
    name="assistant",
    instructions="Stop repeating the same failing tool pattern",
)

agent.start("Fix the deployment issue")

Every Agent you create has this protection on by default — no import, no flag. Polling a status endpoint that actually changes state is not flagged; a genuine stall or A -> B -> A -> B oscillation is.

Diagram

3 Detectors

Warning

Critical

🔄 Generic Repeat
Same tool + same args

Threshold?

📊 Poll No Progress
Same result each time

🏓 Ping Pong
A → B → A → B

⚠️ Warn Agent

🛑 Block Execution

Quick Start

  1. It's already on

    Result-aware tool-loop detection runs on every Agent. No import, no configuration.

    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="assistant",
        instructions="You are a helpful assistant.",
    )
    
    # Result-aware tool-loop detection is enabled on every Agent.
    # No import, no configuration - just run the agent.
    agent.start("Read config.yaml and summarise it.")
  2. Tune the thresholds

    Override the per-agent config before the first tool call.

    python
    from praisonaiagents import Agent
    from praisonaiagents.agent.loop_detection import LoopDetectionConfig
    
    agent = Agent(instructions="Poll a job status endpoint.")
    
    agent._loop_detector_config = LoopDetectionConfig(
        enabled=True,
        warn_threshold=5,
        critical_threshold=10,
    )
    
    agent.start("Check whether job-42 finished.")
  3. Turn it off

    Disable detection for deterministic replay agents that repeat identical calls on purpose.

    python
    from praisonaiagents import Agent
    from praisonaiagents.agent.loop_detection import LoopDetectionConfig
    
    agent = Agent(instructions="Deterministic replay agent")
    agent._loop_detector_config = LoopDetectionConfig(enabled=False)

How It Works

The detector records each tool call before execution, checks three detectors, then back-fills the result hash after execution so progress breaks the streak.

Diagram
ToolDetectorAgentUserToolDetectorAgentUseralt[Not stuck][Warning][Critical]loop[Each tool call]agent.start(“…”)record_tool_call(name, args)verdictExecuteResultrecord_tool_outcome(result-hash)Execute anyway (one chance)ResultQueue self-correction nudgefor next autonomous turnSkip toolResult = { error, loop_blocked: true }

A warning fires once per autonomous turn — the next iteration’s prompt is prefixed with [System: repeated <detector> detected. Try a different approach. …], giving the model one chance to change course before a critical verdict blocks execution entirely.


Detectors

DetectorWhat It DetectsExample
generic_repeatSame tool + identical args N timesread_file("config.py") called 10 times
poll_no_progressSame args AND same result (no progress)check_status("job-1") returns identical “pending” 10 times
ping_pongAlternating A → B → A → B patternTwo tools oscillating back and forth

Configuration Options

OptionTypeDefaultDescription
enabledboolTrue (per-Agent) / False (bare LoopDetectionConfig())Agent._ensure_loop_detector() constructs a config with enabled=True. Constructing LoopDetectionConfig() directly still defaults to False, matching the module docstring.
history_sizeint30Sliding window of recent tool calls
warn_thresholdint10Identical calls before warning
critical_thresholdint20Identical calls before blocking (auto-corrected to critical > warn)
detectorsdict[str, bool]{generic_repeat: True, poll_no_progress: True, ping_pong: True}Enable / disable individual detectors

Common Patterns

Polling a status endpoint

Raise thresholds and keep detection on.

python
from praisonaiagents import Agent
from praisonaiagents.agent.loop_detection import LoopDetectionConfig

agent = Agent(instructions="Poll deployment status every second.")
agent._loop_detector_config = LoopDetectionConfig(
    enabled=True,
    warn_threshold=30,
    critical_threshold=60,
)
agent.start("Wait for deployment-42 to finish.")

Aggressive detection for short-lived agents

python
from praisonaiagents import Agent
from praisonaiagents.agent.loop_detection import LoopDetectionConfig

agent = Agent(instructions="Quick web scraper")
agent._loop_detector_config = LoopDetectionConfig(
    enabled=True,
    warn_threshold=3,
    critical_threshold=5,
)
agent.start("Fetch the homepage title.")

Disable a specific detector

Turn off ping_pong when two tools legitimately alternate.

python
from praisonaiagents import Agent
from praisonaiagents.agent.loop_detection import LoopDetectionConfig

agent = Agent(instructions="Two-tool workflow")
agent._loop_detector_config = LoopDetectionConfig(
    enabled=True,
    detectors={"generic_repeat": True, "poll_no_progress": True, "ping_pong": False},
)

User Interaction Flow

Here’s what a user sees when the detector fires during an autonomous run.

Diagram

Yes

No, hits critical_threshold

Turn 1: agent calls read_file’missing.txt’ 10x

Warning verdict
Streak reaches warn_threshold

Queue self-correction:
System: repeated generic_repeat detected

Turn 2 starts with the nudge
injected as a user message

Agent changes
approach?

Continue as normal

Tool call blocked
result = error, loop_blocked=true

  • After the warning fires, the next autonomous turn’s prompt is prefixed with [System: repeated <detector> detected. Try a different approach. <message>].
  • If the agent still cannot break out of the pattern and hits critical_threshold on the same (tool, args) fingerprint, the offending call is skipped and the tool result becomes {"error": "...", "loop_blocked": True} — the model sees this on its next observation and typically self-corrects.
  • The whole path is a soft-block, not an exception — trace spans, stream events, and AFTER_TOOL hooks all still fire, so telemetry stays consistent.

Relationship to Loop Guard

Loop Detection is now always-on alongside Loop Guard (docs). Loop Detection catches identical-argument / no-progress / ping-pong fingerprints at any frequency and back-fills result hashes so polling with progress is not flagged. Loop Guard counts per-turn tool calls with idempotent-vs-mutating thresholds. Both fire through the same blocked_result path, so trace spans, stream events, and AFTER_TOOL hooks stay consistent regardless of which one triggers.


Best Practices

Trust the defaults

Every Agent already has result-aware detection with reasonable thresholds. Only tune when a legitimate use case (long polling, deterministic replay) requires it.

Tune thresholds, not the on/off switch

Set agent._loop_detector_config = LoopDetectionConfig(enabled=True, warn_threshold=...) before the first tool call. Disabling entirely removes the safety net.

Polling != loop

The detector uses name + args + result-hash, so a poll that returns changing output does not increment the streak. If your polling tool returns identical output while it waits, disable the poll_no_progress detector for that agent.

Zero overhead

The detector is stdlib-only (hashlib, json) and lazy-constructed on the first tool call. Agent(...) init is unaffected.

Legacy plugin (advanced)

The praisonaiagents.plugins.loop_detection_plugin module still exists as an advanced hook-based override — most users no longer need it. Use it only to override thresholds globally via the BEFORE_TOOL hook without per-agent code.


Related