Skip to content

fix(streaming): emit input_json events for server_tool_use blocks#1740

Open
sean-kim05 wants to merge 1 commit into
anthropics:mainfrom
sean-kim05:fix/stream-server-tool-use-input-json
Open

fix(streaming): emit input_json events for server_tool_use blocks#1740
sean-kim05 wants to merge 1 commit into
anthropics:mainfrom
sean-kim05:fix/stream-server-tool-use-input-json

Conversation

@sean-kim05

Copy link
Copy Markdown

What

When streaming a message that contains a server-side tool call (server_tool_use, e.g. web_search), the SDK accumulates the tool's streamed input into the message snapshot but never emits an input_json stream event for it. on_input_json callbacks and code iterating the event stream therefore never see the input of server tools — only client tool_use (and, in the beta path, mcp_tool_use) fire input_json.

Root cause

accumulate_event decides what to accumulate with TRACKS_TOOL_INPUT:

# src/anthropic/lib/streaming/_messages.py
TRACKS_TOOL_INPUT = (ToolUseBlock, ServerToolUseBlock)
...
if isinstance(content, TRACKS_TOOL_INPUT):
    ...  # build up content.input from input_json_delta

But build_events decides what to emit with a narrower, hard-coded check:

elif event.delta.type == "input_json_delta":
    if content_block.type == "tool_use":          # non-beta: server_tool_use missing
        events_to_fire.append(build(InputJsonEvent, ...))

The beta path (_beta_messages.py) tracks (BetaToolUseBlock, BetaServerToolUseBlock, BetaMCPToolUseBlock) but only emits for tool_use / mcp_tool_use — again dropping server_tool_use. So the emitter and the accumulator disagree about which blocks carry input.

Fix

Emit input_json for exactly the block types whose input is accumulated, by reusing TRACKS_TOOL_INPUT via isinstance in build_events (both non-beta and beta). Emission and accumulation now share one source of truth and can't drift apart.

Tests

  • New fixture tests/lib/streaming/fixtures/server_tool_use_response.txt — a stream with a web_search server_tool_use block whose input streams in as {"query": "weather in Paris"}.
  • test_server_tool_use_input_json (sync + async) in both test_messages.py and test_beta_messages.py, asserting input_json events fire and the final snapshot equals the accumulated input.
  • Verified RED→GREEN: without the fix, input_json_events == [] for the server_tool_use block in all four tests; with it, they fire.
  • Full tests/lib/streaming/ suite (44 tests) passes; ./scripts/lint (ruff + pyright + mypy) clean.

`build_events` only fired an `InputJsonEvent` when the streamed block was a
client `tool_use` (and, in the beta path, `mcp_tool_use`). But
`accumulate_event` tracks input for every type in `TRACKS_TOOL_INPUT`, which
also includes `server_tool_use`. So when a server-side tool (e.g. `web_search`)
streams its input, the deltas are accumulated into the message snapshot but no
`input_json` event is ever emitted — `on_input_json` handlers and the iterated
event stream silently miss server-tool input entirely.

Emit `input_json` for exactly the block types whose input we accumulate, by
reusing `TRACKS_TOOL_INPUT` (`isinstance`) instead of hard-coding a subset of
`.type` values. This keeps emission and accumulation in lockstep so the two
can't drift apart again.
@sean-kim05 sean-kim05 requested a review from a team as a code owner July 6, 2026 19:33
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.

1 participant