[BUG] Text after a tool result isn't streamed (arrives in one burst) when thinking is enabled
What's Wrong?
With --include-partial-messages, when thinking is enabled (adaptive thinking), the assistant text that follows a tool result isn't streamed progressively. Claude Code emits no text for about 10 seconds after the tool result. Then almost the whole reply arrives as text_delta events within 0.3–1 s.
The first response in a turn, before any tool call, streams normally with the same model and effort. With thinking disabled, text after a tool result also streams normally. The silent gap is roughly as long as generating the text takes when it streams normally, which suggests the text is generated during the gap and held back, not streamed.
Downstream UIs built on stream-json (for example Paseo desktop and mobile) show every post-tool reply appearing all at once.
What Should Happen?
text_delta events after a tool result should arrive progressively as the text is generated, the same way they do before a tool call or when thinking is disabled.
Steps to Reproduce
claude -p "Run the shell command 'echo hi' once using your Bash tool. After it finishes, write a plain-text story of about 400 words about a lighthouse keeper. No tools after the first one." \
--model claude-opus-5-5 --effort high \
--output-format stream-json --include-partial-messages --verbose \
--permission-mode bypassPermissions < /dev/null
Timestamp each stdout line, then look at the stream_event / content_block_delta / text_delta events after the tool_result user message.
Measurements
Timings are seconds from process start. Share in 0.3 s is the largest fraction of post-tool text characters that arrived within any 0.3 s window. A value near 1.0 means the text arrived in one burst.
Claude Code CLI directly (no wrapper):
| Model | Thinking | Tool call | Tool result → first text | Text span | Share in 0.3 s |
|---|---|---|---|---|---|
| opus-5-5 | effort high | yes | 10.9 s | 0.9 s | 0.94 |
| opus-5-5 | effort high | yes (repeat) | 9.6 s | 1.0 s | 0.90 |
| opus-5-5 | effort low | yes | 9.6 s | 1.2 s | 0.92 |
| opus-5-5 | effort high + --thinking-display summarized | yes | 10.4 s | 1.2 s | 0.88 |
| sonnet-5 | effort high | yes | 10.4 s | 2.5 s | 0.81 |
| sonnet-5 | effort high + --thinking-display summarized | yes | 11.7 s | 1.3 s | 0.91 |
| sonnet-5 | --thinking disabled | yes | 1.4 s | 11.4 s | 0.05 |
| haiku-4-5 | default | yes | 1.3 s | 6.7 s | 0.07 |
| opus-5-5 | effort high | no | n/a | 4.8 s | 0.09 |
The same pattern shows up through Paseo, which uses the Agent SDK with includePartialMessages: true. Opus 5.5 high with a tool call: 0.75. Sonnet 5 high with a tool call: 0.80. Sonnet 5 with thinking off: 0.04. Opus 5.5 high without a tool call: 0.06. A Codex agent on the same host streams normally after a tool call: 0.07.
Summary:
- It happens only when thinking is enabled and the text follows a tool result.
- It happens on Opus 5.5 and Sonnet 5, at effort low and high.
--thinking-display summarizeddoesn't change it.- It reproduces with the plain CLI, so it isn't caused by a wrapper.
I haven't tested the raw Messages API directly (subscription login only), so I can't tell whether the buffering happens in Claude Code or upstream.
Error Messages/Logs
None. Every run exits 0 with complete output.
Is this a regression?
I don't know.
Claude Code Version
2.1.280
Platform
Anthropic API (first-party, claude.ai subscription OAuth login)
Operating System
macOS 27.2 (arm64, Mac mini)
Terminal/Shell
zsh (headless -p / stream-json)
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
I can reproduce this against the raw Messages API, so it isn't Claude Code. I used plain HTTP (Python
http.clientandcurl -N) straight toapi.anthropic.com, with no SDK or proxy, and timestamped each SSE line as it arrived.After a
tool_result, the API holds roughly the first 2,100 characters of text, sends them in one burst, then streams the rest normally. The cut lands mid-sentence (2,099 / 2,115 / 2,089 chars across 3 runs), so it looks like a fixed-size buffer, not a semantic boundary. Most replies after a tool call are shorter than that, so they arrive in one piece. When no thinking block comes first, even the HTTP headers wait for the buffer. On Opus 5.5 the headers and an empty thinking block arrive early, and the text is held.It lines up exactly with the models that write progress updates between tool calls:
| Model | Reply after a tool_result |
|---|---|
| Sonnet 5.5, Opus 5.5, Fable 5.1, Fable 5 | held, then burst |
| Opus 5, Sonnet 5, Opus 4.8, Sonnet 4.6, Haiku 4.5 | streams |
Things that don't change it:
thinkingomitted,display: "summarized",display: "updates",between_tools, effortmediumandhigh, andeager_input_streamingall still hold.web_search(one request, no client tool round trip) holds too.thinking_tokens: 0, so the wait isn't thinking.One difference from your table: Sonnet 5 streams fine against the raw API for me at effort
mediumandhigh, so its burst in Claude Code may be something separate.Matched pairs, same prompt and settings:
req_011CfWPc37GgV44S9iwFoEui. Sonnet 5 streamed:req_011CfWPcbEoHKgWFG2Mdik7Yreq_011CfWSK9TtGPnGmitLkFV6j. Opus 5 streamed:req_011CfWSYK8fhtkVR2KWbJi56Repro (swap
MODELto compare). Onclaude-sonnet-5-5nothing arrives for about 6s, then the whole reply lands within 20ms. Onclaude-sonnet-5it streams over about 7s.The thinking docs mention "a pause of several seconds before a progress-update block opens", but nothing about holding the reply itself. In a chat UI, any reply under ~2,100 characters that follows a tool call shows up all at once.
I have the same issue. Hard to believe that nobody at Anthropic is realizing this bug. That makes the models basically unusable for user facing chat use cases. Is there a better place to raise this issue ?
I can reproduce this against the raw Messages API (no Claude Code or Agent SDK involved), and it also affects
claude-sonnet-5-5. On Sonnet 5.5 it happens even with thinking at its lowest setting, so disabling thinking isn't an available workaround there.Setup: Node
fetchstraight toapi.anthropic.com/v1/messageswithstream: true. It's the same through Cloudflare AI Gateway.Minimal repro: a request whose history ends in a
tool_result. Default settings, nothinkingfield:Output:
Test matrix: same prompt, direct API. "Burst" means about 5 seconds of silence, then every text delta within about 1–30ms.
| | no tools | tools declared, no tool result yet |
tool_resultin history ||---|---|---|---|
| Sonnet 5.5,
between_tools| streams | streams | burst || Sonnet 5.5, adaptive (
omitted/summarized) | streams | streams | burst || Sonnet 5.5, adaptive +
display: "updates"(beta) | – | – | burst || Sonnet 5.5, server-side
web_search(in-turn) | – | – | burst (4.9s gap, then all text within 616ms) || Sonnet 5,
disabledor adaptivemedium| streams | streams | streams |Replaying the previous assistant turn exactly as returned, or with thinking blocks removed, makes no difference.
Event trace of a real three-step turn (Sonnet 5.5,
display: "updates", ms from request start):Two things stand out:
message_startor aping, so a client can't tell the request is alive.tool_usestart within about a second, and a short text block right before atool_usearrives on time.One difference from the original report: in my tests Sonnet 5 streams normally after a tool result, even with adaptive thinking at
medium. Only Sonnet 5.5 bursts. That may depend on how much thinking actually happens in the turn.For any chat UI that streams answers after tool calls, this looks like the app has hung, and then the whole message appears at once. Is this expected behaviour on the newer models? If so, is there a request option to stream the final text as it's generated?
Three things we measured on the raw Messages API (Python SDK,
claude-sonnet-5-5, default thinking, one client tool) that may help narrow this down or work around it.1. It depends on where the
tool_resultsits, not on whether history contains one.| History | First text | Pattern |
|---|---|---|
| A. ends in a
tool_result| 5.2-5.5s | whole reply in one burst || B. the same tool pair in an earlier, finished turn (assistant text, then a new user message) | 0.9-1.0s | streams |
| C.
tool_result, then the question as a text block in the same user message | 5.1-5.4s | burst || D. the same information as plain text, no tool pair | 1.0s | streams |
|
claude-sonnet-5, case A | 0.8-1.3s | streams |Request ids: A
req_011CfXyedu4TPjttTfhiGnSB,req_011CfXyf1pgL4mAiqQqhTwgK; Breq_011CfXyfRUUdmSXqEEmstabx; Creq_011CfXyg8ACnQww3LtPboN5f; Dreq_011CfXygtqYME8tyeRaUtWML; Sonnet 5 Areq_011CfXyhsXgtp6HtZqbrumLd.2. The memory tool puts every first reply behind the hold. With
memory_20250818, the API adds "ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE" to the system prompt. So Claude callsview /memorieseven for a request that needs nothing from memory, and the answer that follows is held: 4.8s and 5.1s of silence, then all the text at once (req_011CfXyopdUXpQokJTB7wqic,req_011CfXypFvjxkQpGsQ9J7kJZ). In a chat product with memory on, that's the first reply of every conversation.3. A workaround for context your app fetches itself. If you pre-run a tool and inject the call and its result into the history, to save the model a round trip, send the result as plain text in the user message instead (case D). On our production request this took time to first text from about 9s to about 2.7s, and the model still used the context. It doesn't help replies after tool calls the model makes on its own.
We also saw
message_startheld in some runs (5.2s) and sent at once in others (0.9s) with identical requests, so it can't be relied on as a sign the request is alive.