Escalation Doom-Loop Detection

Session-level doom-loop detection that records actions and progress markers and recommends a recovery action

The escalation DoomLoopDetector watches a long-running agent session for stuck states — repeated actions, repeated failures, no progress, resource exhaustion, or repeated output — and recommends a RecoveryAction so the agent can break out instead of burning its whole iteration budget.

Python

python
from praisonaiagents.escalation import DoomLoopDetector

detector = DoomLoopDetector()
detector.start_session()

detector.record_action("read_file", {"path": "foo.py"}, "content", success=True)
detector.record_action("read_file", {"path": "foo.py"}, "content", success=True)
detector.record_action("read_file", {"path": "foo.py"}, "content", success=True)

if detector.is_doom_loop():
    print(detector.get_recovery_action())  # e.g. RecoveryAction.RETRY_DIFFERENT

TypeScript

typescript
import { DoomLoopDetector, RecoveryAction } from 'praisonai';

const detector = new DoomLoopDetector();
detector.startSession();

detector.recordAction('read_file', { path: 'foo.py' }, 'content', true);
detector.recordAction('read_file', { path: 'foo.py' }, 'content', true);
detector.recordAction('read_file', { path: 'foo.py' }, 'content', true);

if (detector.isDoomLoop()) {
  console.log(detector.getRecoveryAction()); // e.g. RecoveryAction.RETRY_DIFFERENT
}

The detector consumes the actions and progress markers you feed it, classifies the stuck state into a DoomLoopType, and maps that to a RecoveryAction on the recovery ladder.

Diagram

📥 Actions &
progress markers

🛡️ DoomLoopDetector

🔎 DoomLoopType

🎯 RecoveryAction

Quick Start

  1. Construct with defaults
    python
    from praisonaiagents.escalation import DoomLoopDetector
    
    detector = DoomLoopDetector()
    detector.start_session()
  2. Record actions and progress markers
    python
    detector.record_action("search", {"q": "config"}, "no results", success=False)
    detector.mark_progress("wrote initial draft")  # call on real progress
  3. Check for a loop and get the recovery action
    python
    if detector.is_doom_loop():
        event = detector.get_loop_event()
        print(event.loop_type, event.description)
        print(detector.get_recovery_action())

How It Works

The detector keeps an ordered history of record_action(...) calls plus a separate list of mark_progress(...) markers. After each recorded action it re-runs every check; is_doom_loop() returns True as soon as any check fires, and get_loop_type() reports which one.

Diagram
DoomLoopDetectorAgentDoomLoopDetectorAgentrecord_action(…)run all checksis_doom_loop()? loop_type?get_recovery_action()RecoveryAction

Loop types

DoomLoopTypeWhat it detects
REPEATED_ACTIONSame action (name + args) repeated max_identical_actions times, or same action type repeated max_similar_actions times
REPEATED_FAILUREmax_consecutive_failures failing actions in a row
NO_PROGRESSmax_no_progress_steps steps with identical results and no recent progress marker
CIRCULAR_PLANPlan loops back to a previous state
RESOURCE_EXHAUSTIONElapsed time exceeds max_total_time
REPEATED_OUTPUTModel output repeats — max_repeated_chunks+ identical text chunks (content chanting)

Recovery ladder

get_recovery_action() walks a ladder based on how many recovery attempts have already been made:

RecoveryActionWhen it is chosen
CONTINUENo loop detected
RETRY_DIFFERENTFirst recovery attempt — try a different approach
ESCALATE_MODELSecond attempt, when escalate_on_loop=True — try a stronger model
REQUEST_HELPAfter escalation — ask the user for clarification
ABORTRESOURCE_EXHAUSTION always aborts; also when max_recovery_attempts is reached

Configuration Options

Pass a DoomLoopConfig to tune thresholds:

python
from praisonaiagents.escalation import DoomLoopDetector, DoomLoopConfig

detector = DoomLoopDetector(
    DoomLoopConfig(
        max_no_progress_steps=3,
        max_consecutive_failures=2,
        escalate_on_loop=True,
    )
)
OptionTypeDefaultDescription
max_identical_actionsint3Max identical consecutive actions
max_similar_actionsint5Max same-type actions (fuzzy match)
max_consecutive_failuresint3Max failures before intervention
max_no_progress_stepsint5Max steps without progress
max_time_per_actionfloat60.0Max seconds per action
max_total_timefloat300.0Max total execution time (seconds)
enable_auto_recoveryboolTrueAuto-attempt recovery
max_recovery_attemptsint2Max recovery attempts before abort
escalate_on_loopboolTrueEscalate model on loop detection
initial_backofffloat1.0Initial backoff in seconds
backoff_multiplierfloat2.0Backoff multiplier
max_backofffloat30.0Maximum backoff in seconds
max_repeated_chunksint8Max identical output chunks before flagging
content_chunk_sizeint50Sliding-window chunk size (chars)

How progress markers work

mark_progress() records a (marker, timestamp) tuple. When checking NO_PROGRESS, the detector only counts markers within the current no-progress window — markers older than the window boundary (the timestamp of the action immediately preceding the window) are filtered out.

python
from praisonaiagents.escalation import DoomLoopDetector, DoomLoopConfig

detector = DoomLoopDetector(DoomLoopConfig(max_no_progress_steps=5))
detector.start_session()

detector.mark_progress("read config.yaml")  # early success

# ... many later steps that repeat the same unproductive result ...
for _ in range(5):
    detector.record_action("retry", {"n": 1}, "same error", success=False)

detector.is_doom_loop()  # True — the early marker no longer suppresses NO_PROGRESS

Before this recency filter (PR #3960), one early mark_progress(...) disabled NO_PROGRESS detection for the rest of the run, no matter how long the agent then spun on unproductive work. Now NO_PROGRESS fires correctly on long runs where an early success previously suppressed it forever.


Common Patterns

Drive recovery from the detected action

python
from praisonaiagents.escalation import DoomLoopDetector
from praisonaiagents.escalation.doom_loop import RecoveryAction

detector = DoomLoopDetector()
detector.start_session()

# ... record actions during the run ...

if detector.is_doom_loop():
    action = detector.get_recovery_action()
    if action == RecoveryAction.RETRY_DIFFERENT:
        detector.increment_recovery()
        # switch strategy and continue
    elif action == RecoveryAction.ESCALATE_MODEL:
        detector.increment_recovery()
        # swap in a stronger model
    elif action in (RecoveryAction.REQUEST_HELP, RecoveryAction.ABORT):
        # stop and surface to the user
        pass

Inspect session statistics

python
stats = detector.get_stats()
print(stats["total_actions"], stats["failed_actions"], stats["progress_markers"])

Best Practices

Call mark_progress() on real progress only

Mark progress when the agent genuinely advances (file modified, test passed, sub-goal reached) — not on every log line. Markers gate NO_PROGRESS detection, so noisy markers hide real stalls.

Keep max_no_progress_steps small

A smaller max_no_progress_steps catches stalls sooner. Start at 3–5 for autonomous runs and raise it only if you see false positives.

Increment recovery between attempts

Call increment_recovery() after acting on a recommendation so the ladder advances (RETRY_DIFFERENT → ESCALATE_MODEL → REQUEST_HELP) and eventually aborts at max_recovery_attempts.

Reset per session

Call start_session() at the top of each run to clear action history, markers, and recovery counters.


Related