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.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
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 aWARN; 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.
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.
- 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.
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), callmark_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_fortimeout 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.
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.
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 — everyAgent 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
Trust the defaults
Trust the defaults
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.
Tune thresholds, not the on/off switch
Tune thresholds, not the on/off switch
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.
Name custom tools so the heuristic works
Name custom tools so the heuristic works
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.Use get_stats() for observability
Use get_stats() for observability
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.Reach for mark_progress() only at real checkpoints
Reach for mark_progress() only at real checkpoints
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.Drop manual retry caps around async polling
Drop manual retry caps around async polling
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.Related
Loop Detection
Agent Autonomy

