[Feature request] Guard against re-issuing identical tool calls across turns (cross-turn tool-call loop guard) #1404
msoukhomlinov
started this conversation in
Feature requests
Replies: 1 comment
|
@kevinjosethomas Following up on #1326, which you closed when Prime Agent moved to the discussion-first process. I've brought it here per the new CONTRIBUTING.md guidance (searched first, no duplicate) with a proposed design and an offer to draft a PR. Would be glad to hear if this fits the roadmap. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
An agent can fall into a silent, unbounded loop: issuing the exact same tool call (same tool, same arguments) every turn, reading the result, concluding "let me try a different approach," and then issuing the same call again, repeating dozens of times until a human aborts. This was reported and reproduced in #1326 (closed): 3 loops, 52 identical iterations (e.g. 23× a
grep, 20× azodToJsonSchemascript, 9× asedread), with no harness layer detecting or interrupting it.I'm bringing this to Discussions per the new contribution process in
CONTRIBUTING.md(Issue queue → maintainer work queue; public feedback starts here). I searched existing Discussions and found none on this topic, so this is not a duplicate. There is already maintainer-level interest and a concrete design sketch on the issue thread (see BILLKISHORE's comment on #1326), which I've built on and grounded against currentmain.The idea is a cross-turn tool-call loop guard: a bounded, in-memory detector that notices when the same
(toolName, inputHash)pair recurs on N consecutive turns and injects a single, non-blocking advisory message so the model is nudged to change approach, while still letting a legitimately repetitive call (e.g. a poll loop waiting on a build) continue.Why this needs a harness-level guard (not just a model fix)
grepcalled 30-50× (claude-code#59318), 10+ identicalReads on one file (#53578), an identical failingEditretried repeatedly (#29944). A deterministic harness counter is the reliable layer.maintoday there is no guard of any kind: I verifiedpackages/agentandpackages/aicontain no max-turn/max-tool-call limit and no repetition detector. Related [stack 8/8] fix(runtime): bound transcript and autonomous recovery #1165 (degenerate text repetition + dropping duplicate tool results during transcript repair) is closed and not merged, and targets a different phenomenon anyway; it would be complementary, not redundant.Proposed design (grounded in current
main, commit 97b994c)runLoopinpackages/agent/src/agent-loop.ts(lines 307-461). Per turn, tool calls are extracted at line 358 (message.content.filter(c => c.type === "toolCall")) and executed at line 363. The guard records the turn's(toolName, inputHash)pairs there, and delivers the advisory via the loop's existingpendingMessagesinjection block (lines 337-345), the same mechanism already used for steering messages, before the nextstreamAssistantResponseat line 348.packages/agent/src/tool-loop-guard.ts:detectToolCallLoop(ring, threshold)over a bounded ring buffer of(toolName, inputHash)entries. It fires only when the same pair appears on N consecutive turns, not N-1, not when the input differs by a byte, not on alternating calls, and is disabled atthreshold <= 0. This matches the design constraints already agreed on the issue thread and keeps it trivially unit-testable.toolLoopGuardfield onAgentLoopConfig(following the existing camelCase optional-field convention), default threshold 5 (industry consensus), default enabled, with a clean disable (threshold0/disabled: true) and an optionalPI_AGENT_TOOL_LOOP_THRESHOLDenv override following the repo'sPI_*env-var convention.Prior art that validates this approach
ToolCallLoopGuard(can1357/oh-my-pi), default threshold 5, canonicalized args hash, hidden system redirect that lets the turn continue, the closest match to the design above. Its origin issue #3971 notes "threshold 3 is too aggressive for bash/read"; its extreme case #6315 (11k identical sends from a budget-aborted subagent) shows an advisory alone has a ceiling when a model ignores steering.LoopDetectionService(fires on 5 consecutive identical calls / repeating cycles), but it stops and asks the user, a documented pipeline breaker (#19953), and still has open false-positive issues (#18551).These independently converged on the same signal (identical tool call repeated N times, N≈5) and on "nudge, then continue" as the least-disruptive intervention, which is exactly what this proposes.
Scope (v1) and limitations
(toolName, inputHash)only, with no output comparison and no ratio heuristic. Comparing outputs is expensive and brings its own failure modes; a short, truncated result summary in the advisory gives the model steering context without making outputs part of the detection signal.Files I'd touch if invited
packages/agent/src/tool-loop-guard.ts(new): containsToolLoopEntry, puredetectToolCallLoop, advisory builderpackages/agent/src/agent-loop.ts: wire the ring intorunLoop+ advisory injection/resetpackages/agent/src/types.ts: add optionaltoolLoopGuardtoAgentLoopConfigpackages/agent/src/agent.ts:createLoopConfigdefault (5) / env / disable resolutionpackages/agent/test/tool-loop-guard.test.ts: pure-function unit testspackages/agent/test/agent-loop.test.ts: one integration test for advisory injection (matching the existing multi-turn test style)Offer
Happy to draft a PR implementing this, following the development guide, adding the tests, and describing validation. I will wait for a maintainer invitation before opening anything, per the current contribution policy, and am happy to adjust threshold defaults, naming, or scope to fit the roadmap.
For the maintainers:
toolLoopGuard, env var)?No credentials, keys, prompts, or other sensitive info included.
All reactions