stream Claude replies token-by-token from stream_event deltas

A live capture against claude 2.1.150 (both --print and the interactive
stream-json transport clide uses) confirms --include-partial-messages
emits the in-progress reply as stream_event envelopes wrapping Anthropic
streaming deltas — NOT assistant events with partial:true, which is what
T-168 assumed, so that handler never fired and streaming was inert.

Replace it: accumulate content_block_delta text per message id (tracked
from message_start, since deltas carry no id) and emit a placeholder under
a stable partial-<id> uuid the controller upserts in place; the matching
single-text-block assistant event reuses that uuid to finalize, while
tool_use / thinking blocks keep their own uuids and append in order. Tests
rewritten against the captured shape; spike doc records it.

T-184.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:16:52 +02:00
co-authored by Claude
parent 494a6f13a5
commit d0ad8ed4aa
6 changed files with 185 additions and 79 deletions
@@ -68,6 +68,19 @@ One JSON object per line. Types seen:
`1000000`) **and `maxOutputTokens`** — i.e. the context-window *size* IS exposed
here (relevant to the T-158 budget gap; the remaining-budget % still is not).
- `rate_limit_event``{rate_limit_info:{status,resetsAt,rateLimitType,…}}`.
- `stream_event`**only with `--include-partial-messages`** (T-184). Wraps the
raw Anthropic streaming deltas as they arrive: `{type:"stream_event", event:{…}}`
where `event.type` is `message_start` (carries `message.id`) → `content_block_start`
`content_block_delta` (`delta:{type:"text_delta",text}` / `thinking_delta` /
`input_json_delta`, with `index`) → `content_block_stop``message_delta`
`message_stop`. The full per-block `assistant` event still arrives interleaved.
**Verified against 2.1.150 in BOTH `--print` and the interactive `--input-format
stream-json` transport** — the flag's `--help` claim that it "only works with
--print" is wrong; it streams in interactive mode too. `content_block_delta`
events do NOT carry a message id, so track the id from the preceding
`message_start`. (This is the real shape; the earlier guess of
`assistant`+`partial:true` was wrong — `StreamJsonSession` streams text from
`stream_event` and finalises via the matching `assistant` event.)
The existing `parseTranscriptChunk` (transcript_reader.dart) parses the
`assistant`/`user` events as-is (same `message.content` shapes; missing