Repository navigation
fix(agent): stop tool-call loop from scalar-vs-array tool inputs - #101
sm0keyafterdark wants to merge 2 commits into
Conversation
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>
c51e3af to
771c52b
Compare
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>
|
Hi @sm0keyafterdark, thanks for the PR! A few things I noticed during review:
Once these are fixed I will take another look :D |
|
Is this still relevant? @sm0keyafterdark @bebiksior |
|
@Sytten I think this will be still relevant after AI updates, affects only smaller local models. I could finish this and merge when I'll be working on things around Shift |
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:
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.ensureToolCallsResolvedinjects a synthetic error result for any assistant tool_call left unanswered before the next model call.hasRepeatedToolCalls) as a secondstopWhencondition to bound identical-call loops well before the cap.<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.