How ML Intern's Doom Loop Detection Stops AI Agents From Spinning — And How You Can Use It

TL;DR: HuggingFace’s ml-intern project includes a 200-line doom_loop.py module that detects when AI agents get stuck repeating the same tool calls. It uses two algorithms — identical consecutive detection and repeating sequence detection — then injects a corrective [SYSTEM: REPETITION GUARD] prompt to break the cycle. We adapted it into a cron watchdog scanning 86+ cron job outputs across our blog empire. The core implementation is hash-based, dependency-free, and fits in ~50 lines of production code.
What Is a Doom Loop?
A doom loop is when an AI agent makes the same tool call — with the exact same arguments — over and over, each time getting the same result. The agent thinks it’s making progress, but it’s burning API tokens in a tight circle.
Real example from our cron pipeline: an auto-fix loop tries to fix a lint error, calls write_file with identical content 5 times, each time the linter returns the same error, and the agent escalates from “fix lint error” to “rewrite entire module” to “delete and recreate” — all without changing the root cause.
The key distinction: legitimate polling (same tool, same args, changing result) is not a doom loop. Only identical args + identical result repeated 3+ times triggers detection.
How HuggingFace ml-intern Solved It
The doom_loop.py module at agent/core/doom_loop.py (6.7 KB) in ml-intern implements two detection strategies on a hash-based call signature [1].
The Signature System
Every tool call gets a frozen, hashable fingerprint:
from dataclasses import dataclass
import json
import hashlib
@dataclass(frozen=True)
class ToolCallSignature:
"""Hashable signature for a single tool call plus its observed result."""
name: str
args_hash: str
result_hash: str | None = None
def _normalize_args(args_str: str) -> str:
"""Canonicalize JSON args before hashing so key order doesn't matter."""
if not args_str:
return ""
try:
return json.dumps(json.loads(args_str), sort_keys=True, separators=(",", ":"))
except (json.JSONDecodeError, TypeError, ValueError):
return args_str
def _hash_args(args_str: str) -> str:
"""Short MD5 of the normalized arguments."""
return hashlib.md5(_normalize_args(args_str).encode()).hexdigest()[:12]
The normalization is critical — {"model":"gpt-4","temperature":0.7} and {"temperature":0.7,"model":"gpt-4"} hash identically, which is the correct behavior for loop detection.
Algorithm 1: Identical Consecutive Detection
Scans the last N messages (default 30) for two or more of the same signature in a row:
def detect_identical_consecutive(
signatures: list[ToolCallSignature], threshold: int = 3
) -> str | None:
"""Return the tool name if threshold+ identical consecutive calls exist."""
count = 0
prev: ToolCallSignature | None = None
for sig in signatures:
if prev is not None and sig == prev:
count += 1
else:
count = 0
prev = sig
if count >= threshold:
return sig.name
return None
When triggered, it injects: “[SYSTEM: REPETITION GUARD] You have called ‘{tool_name}’ with the same arguments multiple times and gotten the same result. STOP repeating this approach — it is not working.”
Algorithm 2: Repeating Sequence Detection
Some agents don’t repeat the same call — they oscillate between two or three calls in a pattern (e.g., [write_file, read_file, write_file, read_file]). This catches that:
def detect_repeating_sequence(
signatures: list[ToolCallSignature],
) -> list[ToolCallSignature] | None:
"""Detect patterns of length 2–5 that repeat 2+ times at the tail."""
n = len(signatures)
for pattern_len in range(2, min(6, n // 2 + 1)):
tail = signatures[-pattern_len:]
prev = signatures[-2 * pattern_len:-pattern_len]
if tail == prev:
return tail
return None
This catches the worst pattern: an agent ping-ponging between two dead-end approaches, each producing the same result.
The Auto-Fix Loop: When Loops Become Doom Loops
Not all repetition is bad. An agent polling a job status endpoint should call the same tool with the same args — but the result changes. That’s why the signature includes result_hash. If the result changes between calls, the signatures don’t match, and no loop is detected.
The dangerous pattern is an auto-fix loop that escalates:
Step 1: write_file(file.py, content_v1) → lint error E201
Step 2: write_file(file.py, content_v2) → lint error E201 (same!)
Step 3: write_file(file.py, content_v3) → lint error E201 (still same!)
Step 4: os.remove(file.py) → error: file in use
Step 5: os.system("rm -rf project/") → 🔴
Each step feels like progress to the agent — it tried something different — but the lint error hasn’t changed. The doom loop detector catches steps 1-3 (identical args? no, different content. But the result is identical). This is where the full ToolCallSignature with result_hash shines.
Our Cron Watchdog Version
We adapted this pattern as ~/.hermes/scripts/doom-loop-guard.py — a no_agent cron that scans all 86 cron job outputs for identical consecutive responses or [SILENT] bailout patterns across our 6-blog empire [2].
The key additions beyond ml-intern’s design:
- Content fingerprinting — Strips timestamps and run metadata before hashing, so a job that reports the same thing but with a different timestamp is still caught
- SILENT detection — Identical [SINGLE] responses 3x in a row signals topic exhaustion, not a tool loop
- Differential reporting — Only reports new findings (previously flagged jobs are skipped until a new pattern emerges)
- no_agent job exclusion — Deterministic watchdog scripts are excluded automatically
def content_fingerprint(text: str) -> str:
"""Hash content ignoring timestamps and run metadata."""
cleaned = re.sub(r'(Run Time:|Job ID:|Next Run:|^\*\*.*?\*\*)', '', text)
cleaned = re.sub(r'\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}', '', cleaned)
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
return hashlib.md5(cleaned.encode()).hexdigest()[:16]
Practical Implementation: ~50 Lines, No External Dependencies
You don’t need ml-intern’s full 6.7 KB module. The core algorithm fits in ~50 lines of pure Python:
import hashlib
import json
from collections import defaultdict
from dataclasses import dataclass
@dataclass(frozen=True)
class ToolCall:
name: str
args_hash: str
def hash_args(args: dict) -> str:
return hashlib.md5(
json.dumps(args, sort_keys=True).encode()
).hexdigest()[:12]
def detect_doom_loop(calls: list[ToolCall], threshold: int = 3) -> str | None:
"""Returns tool name if stuck, None otherwise."""
count, prev = 0, None
for call in calls:
if prev and call == prev:
count += 1
else:
count = 0
prev = call
if count >= threshold:
return call.name
return None
def detect_oscillation(calls: list[ToolCall]) -> bool:
"""Detect [A, B, A, B] repeating patterns."""
n = len(calls)
for plen in range(2, min(6, n // 2 + 1)):
if calls[-plen:] == calls[-2*plen:-plen]:
return True
return False
Drop this into any agent loop. When detect_doom_loop or detect_oscillation fires, inject a corrective prompt into the conversation — no external dependencies, no API calls, no infrastructure.
Why This Matters in Production
In a 2025 survey by LangChain, 57% of organizations reported AI agents in production, but 48% skip offline evaluation and 63% skip production monitoring [3] source. Doom loops are invisible unless you instrument for them — they don’t crash, they just silently waste money.
The ml-intern approach is elegant because it’s in-loop: it runs inside the agent’s own message history, not as an external monitor. The moment a loop pattern emerges, the corrective prompt redirects the agent before it burns more tokens. Our cron watchdog complements this by catching jobs that escape the in-loop detector or run in no_agent mode.
Both are hash-based, pure-Python, and cost nothing to run. If you have AI agents in production — or cron-based agent pipelines — this is the cheapest monitoring you can add.
[1] HuggingFace ml-intern doom_loop.py source: https://github.com/huggingface/ml-intern/blob/main/agent/core/doom_loop.py
[2] Our doom-loop-guard.py: see ~/.hermes/scripts/doom-loop-guard.py in the blog-empire repository
[3] LangChain State of AI Agents 2025: https://www.langchain.com/blog/state-of-ai-agents-2025
← Back to all posts

