Skip to content

fix(agent): stop tool-call loop from scalar-vs-array tool inputs - #101

Open
sm0keyafterdark wants to merge 2 commits into
caido-community:mainfrom
sm0keyafterdark:fix/agent-tool-call-loop
Open

fix(agent): stop tool-call loop from scalar-vs-array tool inputs#101
sm0keyafterdark wants to merge 2 commits into
caido-community:mainfrom
sm0keyafterdark:fix/agent-tool-call-loop

Conversation

@sm0keyafterdark

Copy link
Copy Markdown

Weak/local models (e.g. qwen via Ollama/LiteLLM) emit a scalar where an array-typed tool input is expected (e.g. TodoAdd {"content":"x"}). This failed Zod validation, the tool never executed, no role:"tool" result was produced, the todo store was never mutated, and the model looped re-issuing the same call until the iteration cap.

Fixes:

  • Accept scalar-or-array on todo tool inputs via scalarOrArray/toArray (TodoAdd/TodoComplete/TodoRemove), and coerce scalars to arrays in the tool-input repair pass as a general backstop for all array-typed params.
  • Guarantee the tool-use contract: ensureToolCallsResolved injects a synthetic error result for any assistant tool_call left unanswered before the next model call.
  • Add a no-progress circuit breaker (hasRepeatedToolCalls) as a second stopWhen condition to bound identical-call loops well before the cap.
  • Strip inline <think>...</think> reasoning both from replayed history (context savings) and at the display layer, so empty think blocks no longer clutter or bloat the transcript.

Adds unit tests for repair coercion, the loop guard, tool-call resolution, and think-tag stripping.

Weak/local models (e.g. qwen via Ollama/LiteLLM) emit a scalar where an
array-typed tool input is expected (e.g. TodoAdd {"content":"x"}). This
failed Zod validation, the tool never executed, no role:"tool" result was
produced, the todo store was never mutated, and the model looped re-issuing
the same call until the iteration cap.

Fixes:
- Accept scalar-or-array on todo tool inputs via `scalarOrArray`/`toArray`
  (TodoAdd/TodoComplete/TodoRemove), and coerce scalars to arrays in the
  tool-input repair pass as a general backstop for all array-typed params.
- Guarantee the tool-use contract: `ensureToolCallsResolved` injects a
  synthetic error result for any assistant tool_call left unanswered before
  the next model call.
- Add a no-progress circuit breaker (`hasRepeatedToolCalls`) as a second
  `stopWhen` condition to bound identical-call loops well before the cap.
- Strip inline `<think>...</think>` reasoning both from replayed history
  (context savings) and at the display layer, so empty think blocks no
  longer clutter or bloat the transcript.

Adds unit tests for repair coercion, the loop guard, tool-call resolution,
and think-tag stripping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sm0keyafterdark
sm0keyafterdark requested a review from a team as a code owner July 15, 2026 20:44
@bebiksior bebiksior self-assigned this Jul 15, 2026
@sm0keyafterdark
sm0keyafterdark force-pushed the fix/agent-tool-call-loop branch from c51e3af to 771c52b Compare July 15, 2026 21:25
Two defects surfaced by weak/local models (qwen via Ollama) that broke the
todo workflow mid-task:

1. A recoverable tool error wiped the todo store. `onError` in the transport
   called `context.clearTodos()`, so a single malformed tool call destroyed
   the user's todos mid-run — the model then reported "the todos are cleared
   each turn" and restarted endlessly. Todos are now cleared only on normal
   completion (onFinish).

2. TodoComplete/TodoRemove only accepted `ids` as an array, but the model
   commonly sends a singular `id` and/or a numeric string (e.g.
   `TodoComplete {"id":"1"}`). That failed validation, never executed, and
   produced no tool result. These tools now accept `id` or `ids`, scalar or
   array, and numeric strings (coerced to numbers); TodoStart likewise accepts
   numeric-string ids. Shared helpers `idNumber`/`idListInput`/`collectIds`.

Adds schema tests covering the accepted shapes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bebiksior

Copy link
Copy Markdown
Contributor

Hi @sm0keyafterdark, thanks for the PR!

A few things I noticed during review:

  • collectIds mutates the original ids array and can append the same ID multiple times. This can show incorrect todo counts or execute the same operation twice. I think you should copy and deduplicate the array before returning it.

  • The five call limit feels too low for RequestSend, since legitimate testing may repeat the same request several times. I’d increase the limit.

  • The scalar-to-array repair doesn’t handle nested values inside arrays. For example, a scalar list inside payloads[] would still fail validation. I think this should also recurse through array items and their matching union branch.

Once these are fixed I will take another look :D

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.

2 participants