fix(execution): detect and finalize on successful repeated tool loops - #2689
Open
1688mengdie wants to merge 3 commits into
Open
fix(execution): detect and finalize on successful repeated tool loops#26891688mengdie wants to merge 3 commits into
1688mengdie wants to merge 3 commits into
Conversation
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)
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.
Problem
The tool-call loop in the execution engine is gated only by
max_rounds, andmax_rounds=0is the upstream default for unlimited rounds. A long-running roundcould 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'shas_repeated_tool_loop) and never surfaced as a signalthat 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
consecutive count of identical successful tool signatures (reusing
tool_call_signature+repeated_tool_signature_count).(
is_legitimate_poll_tool): Read, Grep, Glob, LS, WebSearch, WebFetch, ListModels.LoopRecoveryinternal reminder so the model changes strategy; after
MAX_SUCCESSFUL_LOOP_RECOVERY_ATTEMPTS, finalize the round without further toolcalls via the existing
finalization_reasonpath.= 0initializer onsuccessful_tool_signature_count(unused_assignments warning) and reflow the rustfmt deviations so the crate is
warning-clean and formatted.
Testing
legitimate_poll_tools_are_exempt_from_successful_loop_detectionandmutating_tools_are_not_exempt_from_successful_loop_detectionto confirmlegitimate read/poll tooling is not misfiring the detector.
cargo test -p bitfun-core --features agent-runtime --jobs 4— execution_engineloop-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 basefork_session_for_plugindead-code warning remains).git diff --checkclean.Closes #2688
Commit list:
68314eee6fix(execution): detect successful repeated tool loops— successfulconvergence signal + poll-tool whitelist + LoopRecovery reminder.
646e3ea74fix(execution): finalize on successful tool loop cap—deterministic backstop via
finalization_reason.400bc0355fix(execution): drop dead init in successful loop counter—removes the unused_assignments warning; rustfmt reflow.