Repository navigation
fix(claude): don't execute a tool call cut off by max_tokens - #720
Open
Mike-7777777 wants to merge 1 commit into
Open
Mike-7777777 wants to merge 1 commit into
Mike-7777777 wants to merge 1 commit into
Conversation
The SDK stream accumulator parses a truncated tool_use input in partial mode and returns a plausible dict, so a call cut off by max_tokens was dispatched with incomplete arguments. Pass the trailing tool_use's input as a str, the form _dispatch_call already refuses to execute (hud-evals#480), and document that use on MCPToolCall.arguments.
Mike-7777777
force-pushed
the
fix/claude-truncated-tool-call
branch
from
October 14, 2026 12:41
ba4d766 to
2efabd8
Compare
Mike-7777777
marked this pull request as ready for review
October 14, 2026 12:41
This branch has not been deployed
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.
Problem
When Claude hits
max_tokensin the middle of atool_useblock,ClaudeAgentstill runs that call with whatever part of the arguments arrived.The anthropic SDK's stream accumulator parses
input_json_deltawithjiter.from_json(..., partial_mode=True), which drops the unfinished trailing value and returns a well-formed dict. Feeding the real SDK stream (overhttpx.MockTransport, no network) the input{"command": "rm -rf build/tmp", "args": ["--keep", "src", "tefollowed by
stop_reason: "max_tokens"gives, onmain:message_to_agent_stepturns that into a dict-argumentMCPToolCalland_dispatch_callexecutes it. The defaultstop_onis empty, so nothing else stops it.Fix
Reuse the contract from #480: a
strinMCPToolCall.argumentsmeans the arguments arrived incomplete, and_dispatch_callanswers it with an error result instead of running the tool. Inmessage_to_agent_step, whenstop_reason == "max_tokens"and thetool_useis the last content block, its arguments becomejson.dumps(block.input). Earliertool_useblocks in the same message stay dicts and run. The comment onMCPToolCall.argumentsnow mentions this case.What follows from the existing contract:
stop_on={"malformed_tool_call"}now also ends a Claude rollout at that turn, as it already does for the OpenAI paths.response.content, so thetool_useinput stays an object.Limitations
tool_usethat was actually complete whenmax_tokenshit is also refused; the model is asked to re-issue it. Telling the two apart would need the raw JSON buffer, which the SDK only keeps on an undocumented attribute of the stream snapshot.message_to_agent_step, so the rule applies there too. Only the streaming path is tested; I have not checked what a real non-streamedmax_tokensresponse puts in a cut-offtool_use.Test
test_tool_use_cut_off_by_max_tokens_is_not_executedrunsClaudeAgentthrough__call__against a FastMCP tool, with the realAsyncAnthropicstream accumulator overhttpx.MockTransport. The first turn streams a completetool_useand then a cut-off one, ending inmax_tokens. The test asserts that only the complete call runs and that the next request carries anis_errortool result saying "arrived incomplete" for the cut-off one. It fails onmain(both calls run) and passes with the fix.ruff format --check,ruff check,ty check --error-on-warning(dev, train, modal, daytona extras) andpytest -qwith all extras pass locally.Overlap with #700
#700 deletes
hud/agents/tests/test_claude_agent.pyand moves agent tests totests/agents/, so this test will conflict with it. Whichever lands second, the test belongs next to the existing cut-off-arguments case intests/agents/test_faults.py. The test adds no findings under #700'sscripts/check_tests.py. #700's changes tohud/agents/claude/agent.pydon't touchmessage_to_agent_step.Note
Medium Risk
Changes which Claude tool calls execute at the token limit; misclassifying a complete trailing call would skip execution, but the change reduces risk from running partially parsed shell arguments.
Overview
Stops Claude from running tool calls whose arguments were truncated when the model hits
max_tokens.In
message_to_agent_step, whenstop_reason == "max_tokens"and thetool_useblock is the last content block, arguments are passed asjson.dumps(block.input)instead of the SDK’s leniently parsed dict. That reuses the existing rule that stringMCPToolCall.argumentsare never executed—_dispatch_callreturns an error tool result (“arrived incomplete”) so the model can retry. Earliertool_useblocks in the same message still use dict arguments and run normally.MCPToolCall.argumentsis documented for this token-cap case. A new end-to-end test drives the realAsyncAnthropicstream accumulator over mocked SSE with one complete and one cut-offbashcall, asserting only the complete command runs and the truncated call getsis_error.Reviewed by Cursor Bugbot for commit 2efabd8. Bugbot is set up for automated code reviews on this repo. Configure here.