Skip to content

fix(execution): detect and finalize on successful repeated tool loops - #2689

Open
1688mengdie wants to merge 3 commits into
GCWing:mainfrom
BitFun-SIG:upfix-5
Open

fix(execution): detect and finalize on successful repeated tool loops#2689
1688mengdie wants to merge 3 commits into
GCWing:mainfrom
BitFun-SIG:upfix-5

Conversation

@1688mengdie

Copy link
Copy Markdown

Problem

The tool-call loop in the execution engine is gated only by max_rounds, and
max_rounds=0 is the upstream default for unlimited rounds. A long-running round
could therefore keep calling the same tool with identical arguments many times:
each call succeeds, so no failure recovery fires, and the round never makes
progress toward a final response.

Root cause

Successful repeats were only logged at debug level
(log_policy_thresholds's has_repeated_tool_loop) and never surfaced as a signal
that the model is stuck. The failure path tracks a consecutive count of failed tool
calls, but the successful path had no equivalent convergence detection, so a
repeating successful call stream continued indefinitely.

Fix

  • Mirror the failed-tool recovery detection onto the successful path: track a
    consecutive count of identical successful tool signatures (reusing
    tool_call_signature + repeated_tool_signature_count).
  • Exempt rounds whose calls are all legitimate read/poll tools
    (is_legitimate_poll_tool): Read, Grep, Glob, LS, WebSearch, WebFetch, ListModels.
  • Once the effective loop threshold is crossed, inject a LoopRecovery
    internal reminder so the model changes strategy; after
    MAX_SUCCESSFUL_LOOP_RECOVERY_ATTEMPTS, finalize the round without further tool
    calls via the existing finalization_reason path.
  • Drop the dead = 0 initializer on successful_tool_signature_count
    (unused_assignments warning) and reflow the rustfmt deviations so the crate is
    warning-clean and formatted.

Testing

  • Test degree: tested (focused behavioral tests).
  • Added legitimate_poll_tools_are_exempt_from_successful_loop_detection and
    mutating_tools_are_not_exempt_from_successful_loop_detection to confirm
    legitimate read/poll tooling is not misfiring the detector.
  • cargo test -p bitfun-core --features agent-runtime --jobs 4 — execution_engine
    loop-detection tests pass (successful_loop x2, zero_max_rounds,
    failed_tool_round_signature x2).
  • cargo check -p bitfun-core --features agent-runtime — no new warnings (the base
    fork_session_for_plugin dead-code warning remains).
  • git diff --check clean.
  • AI-assisted: yes (generated with review; commands above recorded).

Closes #2688

Commit list:

  • 68314eee6 fix(execution): detect successful repeated tool loops — successful
    convergence signal + poll-tool whitelist + LoopRecovery reminder.
  • 646e3ea74 fix(execution): finalize on successful tool loop cap
    deterministic backstop via finalization_reason.
  • 400bc0355 fix(execution): drop dead init in successful loop counter
    removes the unused_assignments warning; rustfmt reflow.

user added 3 commits August 30, 2026 17:47
The tool-call loop only had a max_rounds gate, which is disabled when
max_rounds=0 (the upstream default for unlimited), so a long-running
round could keep calling the same tool with identical arguments many
times. Successful repeats were only logged at debug level
(log_policy_thresholds's has_repeated_tool_loop), never surfaced as a
signal that the model is stuck.

Mirror the failed-tool recovery detection onto the successful path: track
a consecutive count of identical successful tool signatures (reusing
tool_call_signature + repeated_tool_signature_count), exempt rounds that
are legitimate read/poll tools (is_legitimate_poll_tool), and inject a
LoopRecovery internal_reminder once the effective loop threshold is crossed
so the model changes strategy. The deterministic finalize backstop after
the recovery cap is added in the follow-up commit.

max_rounds=0 stays untouched; this adds a convergence signal, not a new
round cap, so long tasks with varied signatures keep running.

Test: cargo test -p bitfun-core --features agent-runtime --jobs 4
AI: lightly tested
The convergence reminder is a soft signal; the model may ignore it and keep
repeating the same successful tool signature. Add a deterministic backstop
for the loop: once successful_recovery_attempts reaches
MAX_SUCCESSFUL_LOOP_RECOVERY_ATTEMPTS and the round is still repeating the
same non-poll tool signature, finalize without further tool calls. This
reuses the existing finalize path via finalization_reason, so the round
ends with a response instead of looping indefinitely.

Test: cargo test -p bitfun-core --features agent-runtime --jobs 4
AI: lightly tested
`successful_tool_signature_count` was initialized to 0 at the top of the
execution loop, but that initial value is never read: every read is reached
through the tool_call_signature if-let chain that re-assigns it on all three
branches. rustc flags the initializer as an unused_assignments warning.

Drop the dead `= 0` initializer (deferred initialization) and reflow the three
rustfmt deviations in the same convergence-detection block so the crate is
warning-clean and formatted. The convergence/finalization behavior is
unchanged: the counter is still re-assigned before each read on every path.

Test: cargo check -p bitfun-core --features agent-runtime (no new warnings;
the base `fork_session_for_plugin` dead-code warning remains); 5 execution_engine
loop-detection tests pass (successful_loop x2, zero_max_rounds,
failed_tool_round_signature x2).
AI: light-tested (cargo check + targeted unit tests)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: a round can keep calling the same successful tool with identical arguments indefinitely

1 participant