Skip to content

feat(agent): detect cyclic tool-call loops (A→B→A→B) in tool guardrails - #69695

Closed
teknium1 wants to merge 1 commit into
mainfrom
gemini-cli-port/cyclic-tool-loop-detection
Closed

feat(agent): detect cyclic tool-call loops (A→B→A→B) in tool guardrails#69695
teknium1 wants to merge 1 commit into
mainfrom
gemini-cli-port/cyclic-tool-loop-detection

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

The tool-loop guardrails now detect cyclic tool-call loops — an agent bouncing between two or more identical calls (A→B→A→B… or A→B→C→A→B→C…) — which the existing per-call repetition counters structurally cannot see, because each call differs from its predecessor.

Ported/adapted from google-gemini/gemini-cli#28429 (weekly Gemini CLI PR scout), where the same gap let indirect prompt injection drive infinite ReAct loops and quota-drain. Hermes' existing guards (exact_failure, same_tool_failure, idempotent_no_progress) key on a single signature or tool, so a success-status alternating loop never accumulates a count anywhere.

Changes

  • agent/tool_guardrails.py: track the per-turn sequence of exact call signatures (success and failure alike); detect the trailing k-call window (k = 2..5) repeating consecutively. New decision codes tool_call_cycle_warning / tool_call_cycle_halt flow through the existing warn/halt plumbing — zero changes to run_agent.py or the executor.
  • hermes_cli/config.py: tool_loop_guardrails.warn_after.cycle: 3 and hard_stop_after.cycle: 5 (full cycle repetitions). Nested-key addition, no _config_version bump needed.
  • website/docs/user-guide/configuration.md: documented the new thresholds.
  • tests/agent/test_tool_guardrails.py: 7 new tests (alternating, length-3 cycle, hard-stop halt, broken pattern, uniform self-repeat exclusion, precedence, turn reset) + config parsing coverage.

Adaptation notes (vs. the gemini-cli original)

  • Length-1 cycles excluded (gemini-cli includes them): pure self-repeats are already covered by exact_failure / idempotent_no_progress, and successful self-repeats of mutating tools — e.g. polling a background process — are legitimate in Hermes.
  • Soft-warn by default, halt opt-in via the existing hard_stop_enabled switch, matching the established guardrail posture (interactive sessions get a nudge; unattended deployments circuit-break). gemini-cli hard-halts unconditionally.
  • More specific failure/no-progress warnings take precedence over the cycle warning; a cycle halt takes precedence over everything.
  • gemini-cli's companion change (default session turn cap 15) was not ported — Hermes already has iteration budgets.

Validation

Check Result
tests/agent/test_tool_guardrails.py + tests/run_agent/test_tool_call_guardrail_runtime.py 30/30 pass
E2E: real DEFAULT_CONFIGfrom_mapping → controller → append_toolguard_guidance warn at 6th call (3 reps), halt at 10th (5 reps) with hard_stop_enabled
30 varied terminal calls (legit work) zero trips
ruff check on changed files clean

Related open work this complements (none of which detects signature cycles): #52139 (cross-tool failure counter), #57816 (idempotent streaks), #59638 (content/stream loops), issues #67889 / #60084 (varying-args loops — different gap, not addressed here).

Port from google-gemini/gemini-cli#28429: the LoopDetectionService there
gained detection of alternating/cyclic tool-call execution patterns
(A->B->A->B and longer cycles) that per-call repetition counters
structurally cannot see, because every call differs from its predecessor.

Adapted to Hermes' ToolCallGuardrailController:
- Track the per-turn sequence of exact tool-call signatures (success or
  failure alike) and detect the trailing k-call window (k=2..5) repeating
  consecutively.
- warn_after.cycle (default 3 full repetitions) injects the standard soft
  warning; hard_stop_after.cycle (default 5) halts the turn when
  hard_stop_enabled is on, via the existing warn/halt plumbing (zero
  runtime changes).
- Length-1 cycles are deliberately excluded: pure self-repeats are already
  covered by exact_failure / idempotent_no_progress, and successful
  self-repeats of mutating tools (e.g. polling a background process) are
  legitimate.
- More specific failure/no-progress warnings take precedence over the
  cycle warning; cycle halt takes precedence over everything.

Validation: 30/30 tests in tests/agent/test_tool_guardrails.py +
tests/run_agent/test_tool_call_guardrail_runtime.py; E2E via real
DEFAULT_CONFIG -> from_mapping -> controller -> append_toolguard_guidance.
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on f21f388

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence is publishing...


debug info

CI timings

CI timings · View job

Wall time 7m20s vs 8m7s (-9.7%). 10 job(s) slower, 10 faster, 1 unchanged.

  • Build&Test Docker image / build (arm64, ubuntu-24.04-arm, linux/arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope...: -96.0s
  • Python tests / Run tests slice 3/8: -40.0s
  • Python tests / Run tests slice 6/8: +19.0s
  • Build&Test Docker image / build (amd64, ubuntu-latest, linux/amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=do...: -14.0s
  • Python tests / Run tests slice 5/8: +13.0s

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades P3 Low — cosmetic, nice to have labels Jul 23, 2026
@teknium1 teknium1 closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants