Observing generation events
A stream carries the response twice. Once as live events, and once as the settled answer. Events fit when a screen, log,
or trace needs progress before the final message. The result stays the source of truth, the terminal event decides what
that result is, and Stream.Result(ctx) is the source for the answer and error.
Picking a consumer
The stream has two consumption modes, and the first successful call decides which one applies.
- Result mode. Call
stream.Result(ctx)first when all you want is the finished answer. The queue is released and later events are dropped. - Event mode. Call
stream.Next(ctx)first when you want live progress. One consumer reads events in order untilio.EOF, then callsResultfor the same terminal fact.
stream.Snapshot() never claims the stream. It returns the latest message immediately and leaves the mode open.
The chosen mode stays fixed. After Result claims result mode, Next returns io.EOF. After Next claims event mode,
Result returns the terminal message while later Next calls keep reading. A Next call made with a context that is
already canceled or expired claims nothing, so the mode is still open.
Following the event sequence
A valid producer sends one StartEvent, zero or more content block lifecycles, and one terminal event. ContentIndex
identifies a block inside Partial.Content. Each block kind is optional and lifecycles can interleave.
StartEvent
→ zero or more interleaved lifecycles:
TextStart → TextDelta* → TextEnd
ThinkingStart → ThinkingDelta* → ThinkingEnd
ToolCallStart → ToolCallDelta* → ToolCallEnd
→ DoneEvent
The * means zero or more. A block can close without any fragment, so TextDelta* also allows TextStart → TextEnd
directly.
A response may contain no content blocks. A failed response can stop with an open block and ends with ErrorEvent
instead of DoneEvent.
Every content event carries Partial, the complete cumulative assistant message at that moment. Delta is
display-only. It does not have to reproduce Partial when concatenated.
Displaying the useful events
For a terminal or a trace, display deltas, completed calls, and the terminal error.
switch event := event.(type) {
case generation.TextDeltaEvent:
// visible answer fragment
case generation.ThinkingDeltaEvent:
// provider-returned reasoning fragment
case generation.ToolCallEndEvent:
// complete block. Execute only after a successful Result
case generation.ErrorEvent:
// Result reports the same terminal error
}
Use TextEndEvent.Partial or the final message for complete text. Use ToolCallEndEvent.ToolCall or
answer.ToolCalls() for complete arguments, since a delta is commonly only a fragment of JSON.
Knowing the twelve events
| Event | What it says |
|---|---|
StartEvent | response started. Its content list is empty |
TextStartEvent | a text block opened at ContentIndex |
TextDeltaEvent | display fragment for that text block |
TextEndEvent | text block closed. Text is its complete value |
ThinkingStartEvent | a provider-returned reasoning block opened |
ThinkingDeltaEvent | display fragment for that reasoning block |
ThinkingEndEvent | reasoning block closed. Thinking is its complete value |
ToolCallStartEvent | a tool call block opened |
ToolCallDeltaEvent | display fragment of its raw arguments |
ToolCallEndEvent | tool call block closed. ToolCall is the complete call |
DoneEvent | response completed. Message is the final message |
ErrorEvent | response failed. Message is partial and Err is live error |
Content start events can already carry provider-known fields, and end events can add fields that earlier partials did not carry. Do not assume a start block is empty.
Treating thinking as provider-optional
Thinking blocks appear only when the provider returns them. When an OpenAI-compatible request replays an assistant
message, it replays thinking only when the message's Provider and RequestedModel both match the request. The adapter
joins non-empty thinking blocks using the first block's signature, which must be reasoning_content, reasoning, or
reasoning_text. A missing or unsupported first signature omits replayed thinking. Keep a supported signature when
replaying the message to the same provider and model.
Avoiding unvalidated tool calls
A tool call end means the block closed, not that the call is valid. Validate its ID, name, arguments, and application policy before execution, and never execute a call from a failed, aborted, or length-truncated response.
Finishing with Result
answer, err := stream.Result(ctx)
DoneEventreturns its message and a nil error.ErrorEventreturns its message andErrorEvent.Err.- an invalid producer event returns a zero message and an error matching
generation.ErrInvalidStreamEvent. - closing before a terminal returns a zero message and an error matching
generation.ErrStreamClosedBeforeTerminal.
An ErrorEvent.Message may stop in the middle of a content block. That is the accepted output, and it remains available
for display or diagnosis.
Taking snapshots
Progress display depends on the consumption mode. In event mode, Next delivers every delta, so the events are the
progress. In result mode the queue is released and later events are dropped, leaving Snapshot as the only way to see
where the response is.
Snapshot returns a copy of the latest accepted assistant message immediately. It never waits and never chooses a
consumer mode, so it works alongside either mode. Before StartEvent it is the zero message. It can be newer than the
last event read, because a producer can publish metadata, usage, or a finish reason without any content event, and that
is how progress reaches a result-mode consumer.
Writing a Generator
Adapters and custom Generators use Push, PublishPartial, and Close. Ordinary callers should use Next, Result,
and Snapshot. A producer must push exactly one StartEvent, valid indexed block lifecycles, and exactly one
DoneEvent or ErrorEvent. PublishPartial can advance metadata without queueing an event, and its content must agree
with the latest accepted content.
In event mode, generation events remain queued until consumed, and the producer never waits for a consumer. In result mode, the unclaimed queue is released and later events are not retained. An event consumer that stops early misses the rest while the answer can still settle.