Problem
In long-running turns the agent can get stuck emitting placeholder tool calls that make no progress. The existing ToolCallDeduplicator only blocks exact duplicates within the same step, so the model can still loop by using slightly different no-op calls such as:
Bash(':')
Bash('true')
Read('/dev/null')
Read('')
echo / printf placeholders
When this happens, the turn keeps running until it hits max_steps_per_turn, wasting tokens and time instead of returning a response to the user.
Anonymized transcript excerpt
step 2: Read('/dev/null')
step 3: Read('/dev/null')
step 4: Read('/dev/null')
step 5: Read('')
step 9: Bash('true')
step 10: Bash("printf ''")
step 14: Bash('true')
step 22: Bash(':')
step 23: Bash('true')
step 24: Bash(': ')
step 25: Bash(':')
...
step 36: Bash(':')
step 37: Bash(':')
step 38: Bash(':')
These calls produce no file changes, no new background work, and no useful new information.
Proposed solution
Add a ProgressDetector that watches external, observable state rather than trying to interpret model intent:
- External state changes
git status --porcelain output in the working directory.
- Background task lifecycle/status changes.
- Information gain
- Successful tool outputs that are non-trivial and have not been seen before in the current turn.
After a configurable number of consecutive idle steps, the harness should:
- inject a system reminder asking the model to stop calling tools and respond in text;
- run the next model step with no tools available (
{ tools: [] }), forcing a text-only response.
This provides a graceful fallback instead of letting the turn crash into max_steps_exceeded.
Configuration
Two new loop_control options would be useful:
progress_stall_threshold — number of consecutive idle steps before forcing text-only mode (default 8).
progress_min_info_gain_length — minimum successful output length to count as information gain (default 60).
Example config.toml:
[loop_control]
progress_stall_threshold = 12
progress_min_info_gain_length = 120
I have a PR ready that implements this approach.
Problem
In long-running turns the agent can get stuck emitting placeholder tool calls that make no progress. The existing
ToolCallDeduplicatoronly blocks exact duplicates within the same step, so the model can still loop by using slightly different no-op calls such as:Bash(':')Bash('true')Read('/dev/null')Read('')echo/printfplaceholdersWhen this happens, the turn keeps running until it hits
max_steps_per_turn, wasting tokens and time instead of returning a response to the user.Anonymized transcript excerpt
These calls produce no file changes, no new background work, and no useful new information.
Proposed solution
Add a
ProgressDetectorthat watches external, observable state rather than trying to interpret model intent:git status --porcelainoutput in the working directory.After a configurable number of consecutive idle steps, the harness should:
{ tools: [] }), forcing a text-only response.This provides a graceful fallback instead of letting the turn crash into
max_steps_exceeded.Configuration
Two new
loop_controloptions would be useful:progress_stall_threshold— number of consecutive idle steps before forcing text-only mode (default8).progress_min_info_gain_length— minimum successful output length to count as information gain (default60).Example
config.toml:I have a PR ready that implements this approach.