Skip to content

Stream phases

StreamingContentType is a string enum with nine values, attached to every StreamChunk via chunk.phase.

Phase Value Emitted when chunk.content type
THINKING "thinking" Reasoning model emits internal-monologue tokens str (token)
TOOL_CALLING "tool_calling" A tool call has just been dispatched and its result is available dict {"name": str, "arguments": dict, "response": str}
CONTINUING "continuing" A streamed agent-loop driver is about to inject a round of its own (a nudge after an empty turn, or the forced tools-disabled wrap-up at the round cap); emitted once per injected round, immediately before that round's own chunks dict {"kind": "continuation" \| "final_answer", "prompt": str}, where kind matches the provenance tag written onto the injected message
INBOX "inbox" A streamed agent-loop driver is about to open a round that delivers an inbox message: an Inbox source had a message pending at a round boundary, and the round that follows is the model reading it. Distinct from CONTINUING, which names a prompt the loop composed for itself: this one is the user's dict {"text": str}
GENERATING "generating" The final response stream str (token)
IMAGE_GENERATING "image_generating" Per-step image-generation progress (HF callback / Gemini start-done) dict {"step": int, "total_steps": int, "image": PIL.Image \| None, "final": bool, "result": str \| bytes \| None}
AUDIO_GENERATING "audio_generating" Per-step audio-generation progress (one final chunk for MusicGen; N progress + 1 final for diffusers) dict {"step": int, "total_steps": int, "final": bool, "result": str \| bytes \| tuple \| None, "duration_s": float}
SPEECH_GENERATING "speech_generating" Per-chunk speech-generation progress (one final chunk for HuggingFace single-pass; N chunks for OpenAI HTTP stream) dict {"chunk_index": int, "total_chunks": int \| None, "final": bool, "result": str \| bytes \| ... \| None}
DONE "done" Terminal marker (rarely yielded; reserved) str (usually empty)

The enum inherits from str, so members compare equal to their string values, so chunk.phase == "thinking" is fine.

Helpers on StreamChunk

chunk.is_text()             # True for THINKING and GENERATING
chunk.is_tool_call()        # True for TOOL_CALLING
chunk.is_continuing()       # True for CONTINUING (both kinds: read content["kind"] for which)
chunk.is_inbox()            # True for INBOX (a round delivering a message the host handed the run)
chunk.is_image_progress()   # True for IMAGE_GENERATING
chunk.is_audio_progress()   # True for AUDIO_GENERATING
chunk.is_speech_progress()  # True for SPEECH_GENERATING
chunk.is_done()             # True for DONE

These avoid sprinkling phase comparisons through your code. Each is named for its phase and not for one of the kinds that phase can carry, which is why is_continuing() is also true of the forced wrap-up, a round that tells the model to stop calling tools.

Typical sequences

Model behaviour Phase sequence
Plain text generation GENERATING*
Thinking model, no tools THINKING*, then GENERATING*
Tool-using model THINKING* (if thinking), TOOL_CALLING per call, GENERATING*
Tool-using model, multi-round TOOL_CALLING* → THINKING*? → GENERATING* repeated; chunk.iteration increments per round (agent context)
A round the loop injected CONTINUING, then that round's own chunks (THINKING*?, TOOL_CALLING* for a nudge, GENERATING*); chunk.iteration on the CONTINUING chunk is the injected round's own index
A round delivering an inbox message INBOX, then that round's own chunks (THINKING*?, TOOL_CALLING*, GENERATING*); chunk.iteration on the INBOX chunk is that round's own index

* = "one or more chunks".

chunk.iteration tells you the round number moved, not who moved it: a tool round, a nudge after an empty turn, and the forced wrap-up all advance it identically. CONTINUING is what separates the last two from the first (see why it needs its own phase). INBOX is a third case iteration alone cannot tell apart from a tool round: the words are a host's, delivered between rounds a run was already going to make, not the loop's own and not the model's.

Async surface

aimu.aio yields the same StreamChunk type; phase, content, agent, iteration are unchanged. The consumption protocol differs: Iterator[StreamChunk] on the sync surface vs AsyncIterator[StreamChunk] on the async surface. Phase semantics and filtering are identical on both. See how-to: use async.

Filtering

Pass include=[...] to chat() or generate() to drop phases:

client.chat("hi", stream=True, include=["generating"])
client.chat("hi", stream=True, include=["thinking", "generating"])
client.chat("hi", stream=True, include=[StreamingContentType.GENERATING])  # enum form

Accepts strings or enum members. Omitting include= yields all phases.

include= is a chat() / generate() argument, so it does not reach CONTINUING or INBOX: both chunks come from the agent loop above the client, and Agent.run takes no include=. Drop them on chunk.phase in your own loop if you do not want them.

See also