Skip to main content

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 until io.EOF, then calls Result for 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

EventWhat it says
StartEventresponse started. Its content list is empty
TextStartEventa text block opened at ContentIndex
TextDeltaEventdisplay fragment for that text block
TextEndEventtext block closed. Text is its complete value
ThinkingStartEventa provider-returned reasoning block opened
ThinkingDeltaEventdisplay fragment for that reasoning block
ThinkingEndEventreasoning block closed. Thinking is its complete value
ToolCallStartEventa tool call block opened
ToolCallDeltaEventdisplay fragment of its raw arguments
ToolCallEndEventtool call block closed. ToolCall is the complete call
DoneEventresponse completed. Message is the final message
ErrorEventresponse 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)
  • DoneEvent returns its message and a nil error.
  • ErrorEvent returns its message and ErrorEvent.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.

API reference