Reading a stream back
The other half. Reassemble drains any Source and hands back the finished turn, so you do not walk events yourself unless you want to. The code is in reassemble.go.
src := sse.NewSource(resp.Body) // or a nats/ws Source, or a SliceSource in a test
parsed := essessey.Reassemble(ctx, src)
fmt.Println(parsed.Text) // every text delta, concatenated
fmt.Println(parsed.Thinking) // every reasoning delta, concatenated
fmt.Println(parsed.ToolNames) // tools the model called, in order
fmt.Println(parsed.Timeline) // reasoning, text and tool activity, in order
ParsedStream
| Field | Holds |
|---|---|
StreamID | stream_id from message_start |
Thinking | the content of every thinking block, concatenated |
Text | the content of every text block, concatenated |
ToolNames | the name of every tool use block, in the order they started |
Tools | every tool use block as a ToolCall: Name, Params (the joined input_json_delta fragments as json.RawMessage, {} when there were none) and ToolUseID |
Executions | every tool call matched to its result as a ToolExecution: Name, Params, Result, ToolUseID |
Timeline | TimelineItem entries in stream order: Kind is thinking, text or tool; Text holds the segment for the first two, Execution the matched call for tool |
Error | the error text when the source failed, empty otherwise |
Separate thinking blocks stay separate timeline entries, while Thinking holds their concatenated content. Same for text.
A tool result is matched to its call by tool_use_id. A result whose call never appeared (a truncated upstream, or two streams rejoined) is logged and dropped rather than turned into an execution with no name or parameters.
When it stops
message_stopends reassembly right away, even if the source has more.ErrNoMoreEventsfrom the source ends it cleanly. Blocks that never got acontent_block_stopare closed as if they had, so a truncated stream still returns what arrived.- Any other error from
Nextstops reassembly and is recorded inParsedStream.Error.Reassembleitself never returns an error; checkError.
A malformed event (JSON that does not parse, a thinking, tool input or tool result delta for a block that is not open, a stop with no open block, an unknown block or delta type) is warn-logged with a reason field and dropped. One corrupted event degrades the result instead of failing the caller.
Reassemble blocks for as long as the source does. The NATS and WebSocket sources wait in Next until an event arrives, the source is closed, or the context is done, so pass a context with a deadline or close the source when the producer is gone.
Walking events yourself
A Source is just an iterator:
for {
ev, err := src.Next(ctx)
if errors.Is(err, essessey.ErrNoMoreEvents) {
break
}
if err != nil {
return err
}
fmt.Println(ev.Event, string(ev.Data))
}
Unmarshal ev.Data into the payload structs from types.go (MessageStartData, ContentBlockDeltaData and the rest) to read a payload. Event protocol lists which struct fits which event.