Skip to main content
Loop Guard stops broken or misconfigured tools from burning tokens by counting per-turn calls and reacting differently to safe-to-repeat vs state-changing tools.
Looking for bot-to-bot reply-loop protection? See Bot-to-Bot Loop Protection — a separate gateway-layer primitive that caps how many exchanges a pair of bots can trade.
Sync and async parity (as of 2026-08-17). Loop Guard runs on both the sync tool-execution path (agent.chat(), agent.start()) and the async one (agent.achat(), agent.astart(), async workflows). The async path gained the same pre/post check() and record() wiring in PR #4005 — before that, async runs had no loop-guard protection. No config change is required to pick this up; every Agent already gets it.
The user sends a task; Loop Guard tracks tool calls per turn and blocks runaway loops automatically.
No-progress detection became result-aware in PR #3080 (release after 2026-07-16, fixes #3073). Async / long-running tool workflows that return changing results (polling a job IN_PROGRESS → COMPLETE, pacing with a wait tool) are no longer falsely halted at 8 tool calls. Only a genuinely stuck loop of identical results still trips no_progress_halt. No config changes required to pick this up.
Loop Guard now covers the async tool-execution path as of PR #4005 (merged 2026-08-17, fixes #4000). Tool calls made via agent.achat() and async workflows are checked pre-execution, recorded post-execution, and can escalate through WARN → BLOCK → HALT the same way sync calls always did. Async tool timeouts and raised exceptions also count as failures now, so identical repeats accumulate toward the thresholds. No config changes required to pick this up.

Quick Start

1

It's already on

2

Tune the thresholds

For power users, you can customize thresholds after agent creation:
3

Turn it off

Disabling Loop Guard removes the safety net for misbehaving tools.

How It Works


User Interaction Flow

Here’s what happens when an agent repeatedly calls the same tool: Two signals now drive the outcome: how many times the tool has been called this turn, and whether the results are still changing. A busy-but-progressing tool sees at most a WARN; a stuck-with-identical-results tool escalates through WARN to HALT.

Tool Classification

Loop Guard categorizes tools into two buckets with different thresholds: Idempotent tools (safe to repeat): read_file, list_files, search_files, web_search, get_memory, git_status, git_log, db_query, etc. Mutating tools (state-changing): write_file, edit_file, delete_file, execute_code, shell, git_commit, git_push, sql_insert, install_package, etc.
If you can’t move your tool into the explicit set, name it well — Loop Guard’s heuristic looks for substrings like read, get, list, search, find, show, view, check (case-insensitive) to classify tools as idempotent.

Configuration Options

GuardAction values: ALLOW, WARN, BLOCK, HALT LoopGuardDecision fields: action, code, message, metadata

No-Progress Detection

No-progress detection watches for identical repeated results, not raw tool-call counts. What counts as progress (resets the streak):
  • The result changes for the same tool + same args (e.g. check_status(job=42) returning "IN_PROGRESS" then "COMPLETE").
  • A different tool is called (streaks require the same tool, not just the same result value).
  • agent._loop_guard.mark_progress("...") is called — marks the current position so only later tool calls can contribute to a future streak.
What does NOT reset the streak:
  • The same tool returning the same result over and over — that is the “stuck” pattern the guard exists to catch.
  • Falsy repeated results ("", [], {}, 0, False) — these are wrapped internally so they still fingerprint, closing a loophole that used to disable no-progress detection.
Count thresholds are also stuck-aware. The idempotent (5 / 8 / 12) and mutating (3 / 5 / 7) thresholds still count tool calls, but a BLOCK or HALT from those thresholds is downgraded to a WARN when the trailing identical-result run is below no_progress_warn. So a legitimately busy but progressing tool (many calls, changing output) will not be blocked purely on frequency.

Example: polling that used to be halted, now allowed

Example: genuine stall, still halted

Manually marking progress

If your agent completes a logical sub-step (a checkpoint, a milestone), call mark_progress() so any future no-progress streak starts fresh from that point:

What happens at each threshold


Sync and Async coverage

Loop Guard applies identically to both execution paths — sync (agent.start() / agent.chat()) and async (agent.achat() / async workflows). What triggers Loop Guard on the async path:
  • Repeated identical async tool calls whose results do not change (no_progress_* streak grows).
  • Repeated async tool timeouts — each asyncio.wait_for timeout is now recorded as a failure and counts toward the streak.
  • Repeated raised exceptions inside an async tool — each raised call is recorded as a failure the next pre-execution check can act on.
The sync and async paths share the same LoopGuard instance per Agent, so a mixed workload (sync then async on the same agent) accumulates counts against one turn’s thresholds.

Common Patterns

Async polling with changing results

Polling tools whose output actually transitions state (e.g. pending → running → done) do not need any config change — the no-progress guard sees each transition as progress and never trips. Only the count-based idempotent thresholds (5 / 8 / 12) could otherwise fire on high call volume, and those are now downgraded to WARN while results keep changing.
If your polling tool paces itself with a wait / sleep tool that returns the same value each call, wrap the pair (check_job_status + wait) with an explicit progress marker between transitions so wait’s identical returns do not build a stuck streak:

Polling a slow status endpoint

Strict mode for production database agents

Observability with stats


Relationship to Loop Detection

Loop Detection is also always-on as of PR #3005 — every Agent runs result-aware name + args + result-hash detection in the same tool-execution path. The two are complementary: Loop Guard counts per-turn tool calls with idempotent-vs-mutating thresholds, while Loop Detection catches repeated identical fingerprints at any frequency and back-fills result hashes so polling with progress is not flagged. 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

The default thresholds (5/8/12 for idempotent, 3/5/7 for mutating) work well for most agents. Only customize when you have specific use cases like status polling or strict production environments.
Resist the urge to disable Loop Guard entirely. Instead, adjust thresholds to match your agent’s workflow. A monitoring agent might need higher idempotent thresholds, but even monitoring agents can benefit from mutating tool limits.
If your custom tool isn’t in the explicit IDEMPOTENT_TOOLS or MUTATING_TOOLS sets, name it descriptively. Tools named check_status, read_config, or search_logs will be classified as idempotent automatically.
Monitor your agents’ tool usage patterns with agent._loop_guard.get_stats(). High tool counts might indicate the agent is struggling with a task and needs different instructions or tools.
No-progress detection is result-aware, so a tool that keeps producing new output is trusted even past the count thresholds. Reach for mark_progress() only when your workflow has a natural checkpoint (a sub-step completed, a milestone reached) that the result-hash on its own would not reflect.
Since PR #4005, agent.achat() and async workflows route tool calls through the same Loop Guard as agent.start(). If you were adding manual retry caps around async polling, you can drop them — the default idempotent thresholds (5 / 8 / 12) and the result-aware no-progress streak apply on both paths.

Loop Detection
Agent Autonomy