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 #238
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.
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 #238
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.