Keep executed tool steps when a generation fails - #164
Open
DavertMik wants to merge 1 commit into
Open
Conversation
A tool turn can run several tool calls before the step that follows them fails — an invented tool name, plain text where toolChoice demanded a call, or a context-length overflow. withRetry then re-ran generateText from the original messages, so the calls that had already gone through left no trace in the retry: whatever they created on the far side got created again, once per attempt. Accumulate each completed step's response messages via onStepEnd, feed them into the next attempt's messages, and merge them into the returned responseMessages so invokeConversation records them on the conversation. The context-length recovery now recurses from those messages too, and the 'Tool choice is required' swallow returns them instead of an empty message list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uAMi5ctXDCk7vbhPnMmEi
DenysKuchma
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A tool turn can execute several tool calls before the step that follows them fails. When that happens,
withRetryre-rangenerateTextfrom the originalmessages— so the calls that had already gone through left no trace in the retry, and whatever they created on the far side got created again, once per attempt.Seen in Langfuse trace
7d5ddba053a054f6a3725503c9569340: Fisherman created the same suite + test pair four times in 16 seconds. Each attempt POSTed both (200, 200) at steps 3–4 and then died at step 5, alternating between two Groq 400s —attempted to call tool 'json'(the model emitting a harmony pseudo-tool) andTool choice is required, but model did not call a tool(the model writing its summary as prose undertoolChoice: 'required'). The writes escaped the retry boundary; the record of them did not.What changed
generateWithToolsaccumulates each completed step's response messages throughonStepEnd, and on failure:messages, so the model resumes from what it already did instead of starting over (the existing invalid-request feedback line is appended after them);responseMessages, soinvokeConversationrecords them on the conversation and the next loop iteration sees them too;Tool choice is requiredswallow, which previously returned an empty message list;messages— the same duplication hazard by a third route.This is provider-level, so it covers every agent that runs write-effect tools, not just Fisherman.
Not in scope
When retries exhaust and
generateWithToolsthrowsAiError, executed steps still do not reach the conversation. That is moot for Fisherman (its loop stops on error) but a live hazard for Tester, which keeps iterating on the same conversation after an error. Fixing it means changing what a failed invocation returns to its caller, which is a bigger decision than this PR.Independently of this,
toolChoice: 'required'insrc/ai/fisherman.tsis what turns "model finished and said so in prose" into a hard 400. Worth revisiting separately.Testing
Three unit tests in
tests/unit/provider.test.ts, each verified to fail without the change:responseMessages, and the tool runs once;Tool choice is requiredswallow returns them;bun test tests/unit/(1123 pass) andbun test tests/integration/(82 pass, 1 skip) are green;bunx tsc --noEmitreports the same six pre-existing errors inprovider.tsasmain.bun run buildfails identically onmainin this checkout — a missingchromium-bidiin localnode_modules, unrelated to the change.🤖 Generated with Claude Code
https://claude.ai/code/session_017uAMi5ctXDCk7vbhPnMmEi