fix(guardrails): soft-block exact repeated tool-call failures by default - #57303
Open
lEWFkRAD wants to merge 2 commits into
Open
fix(guardrails): soft-block exact repeated tool-call failures by default#57303lEWFkRAD wants to merge 2 commits into
lEWFkRAD wants to merge 2 commits into
Conversation
teknium1
reviewed
Jul 15, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for isolating the default guardrail gap and covering the sequential recovery path.
Problems
- The new default is documented in
cli-config.yaml.example, but the public configuration docs still describe warning-only defaults and sayhard_stop_enabledis what blocks calls:website/docs/user-guide/configuration.md:1392-1410andwebsite/docs/user-guide/docker.md:73-82on current main. Withblock_enabled: trueathermes_cli/config.py:1320in this PR, those statements become inaccurate.
Suggested changes
- Update both user-facing documents to distinguish default soft blocks, the
block_enabled: falsewarn-only opt-out, and hard stops that end the turn. - Add a concurrent-path soft-block test; the current new runtime coverage is sequential, while current execution has separate sequential and concurrent paths in
agent/tool_executor.py:482-484and:1132-1141.
Automated hermes-sweeper review.
The block tier of the tool-loop guardrail only ever fired with hard_stop_enabled=true, so default sessions got an identical warning appended to every repeated failure and nothing else. On smaller local models this is a proven non-fix: a production transcript shows the same rejected terminal call re-sent 20+ times with warnings firing at count=11..17, burning the max_iterations budget on a call that failed deterministically in milliseconds. Add a middle tier: with block_enabled (default true), an exact call that has failed hard_stop_after.exact_failure times (or a read-only call that returned an identical result hard_stop_after.idempotent_no_progress times) is no longer executed; the model receives a synthetic error result and the turn continues. Soft-block wording rotates between attempts on purpose: replay experiments show these loops are context self-conditioning, and identical guidance gets absorbed into the pattern while varied guidance breaks it. Hard-stop behavior is unchanged; block_enabled=false restores warn-only. Fixes NousResearch#41490
lEWFkRAD
force-pushed
the
fix/tool-call-loop-breaker
branch
from
July 15, 2026 14:37
8703d86 to
1406187
Compare
lEWFkRAD
force-pushed
the
fix/tool-call-loop-breaker
branch
from
July 15, 2026 14:45
1406187 to
faddd11
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Makes the tool-loop guardrail's block tier actually fire in default configuration — as a soft block that skips execution but keeps the turn alive — instead of being dead code unless
hard_stop_enabledis set.Today, with default config, a model that repeats a byte-identical failing tool call gets a
repeated_exact_failure_warningappended to every failure… and nothing else, forever. In a production session on a local model (qwen3.6-27b), the same rejectedterminalcall was re-sent 20+ consecutive times; the stored transcript shows the guardrail warning firing atcount=11, 13, 15, 17with zero effect, burning the iteration budget (default 300) on a call that failed deterministically in ~3ms. Context compaction did not break the loop — the compaction summary described the loop and the model resumed it as its first post-compaction action.This PR adds a middle tier between "warn" and "halt the turn":
hard_stop_after.exact_failure(default 5) identical failed calls, the exact call is no longer executed. The model receives a synthetic error result and the turn continues, so it can recover on its own.hard_stop_after.idempotent_no_progressidentical results from a read-only call) — this is the exact scenario in [Bug]: Agent loops on identical tool calls despite being blocked — need better re-prompting after repeat detection #41490.dry_multiplier 0.8/repeat_penalty 1.1penalize re-sending the command string, so the model mangles the command instead of changing the failing argument.)hard_stop_enabled: true) are completely unchanged: blocks still end the turn with the controlled halt response, thresholds and messages identical.tool_loop_guardrails.block_enabled: falserestores today's warn-only behavior.Blocked attempts are recorded inside
before_call(blocked calls never reachafter_call), so consecutive blocks keep escalating the count and, when hard stops are enabled, streaks stay accurate.Known limitation (shared with the existing opt-in hard block): a soft-blocked read-only signature stays blocked for the rest of the turn even if a later mutation would have changed its result; the model can always issue a modified call.
Relationship to nearby work: #37490 adds turn-continuation redirect guidance at the conversation-loop layer for tool-reported loop blocks; #54340 addresses the same-tool (varying-args) failure axis; #49189 flips hard stops on for non-interactive platforms. This PR is orthogonal to all three — it makes the exact-identical block tiers exist at all in default mode — and implements the failure/no-progress subset of RFC #35573 within the existing guardrail framework.
Related Issue
Fixes #41490
Type of Change
Changes Made
agent/tool_guardrails.py:block_enabledconfig flag (default true);softfield onToolGuardrailDecision(should_haltexcludes soft decisions,to_metadata()reports it);before_callreturns soft blocks for exact-failure and idempotent-no-progress streaks when hard stops are off; rotating message templates for both axes.hermes_cli/config.py:block_enabled: TrueinDEFAULT_CONFIG["tool_loop_guardrails"]+ comment.cli-config.yaml.example: document the new tier and flag.tests/agent/test_tool_guardrails.py: soft-block coverage (escalation, message variation, reset-on-success, metadata, opt-out, config parsing); updated the two default-behavior tests that pinned warn-only forever.tests/run_agent/test_tool_call_guardrail_runtime.py: sequential-path soft block (no execution, no halt) and a fullrun_conversationrecovery test (5 executed failures → 2 soft blocks with distinct wording → model answers, turn ends normally, every tool_call keeps a paired tool result).How to Test
python -m pytest tests/agent/test_tool_guardrails.py tests/run_agent/test_tool_call_guardrail_runtime.py -q→ 35 passed.python -m pytest tests/agent tests/run_agent -q→ passes (see PR checks).repeated_exact_failure_blockguardrail JSON without executing, wording varies on subsequent attempts, and the turn continues (noguardrail_halt).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — module + config docstringscli-config.yaml.exampleif I added/changed config keysCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Production transcript excerpt (message ids from the local session DB) showing warn-only failing at scale: