Skip to content

Stream phases

StreamingContentType is a string enum with eight 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
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_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

* = "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).

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: that chunk comes from the agent loop above the client, and Agent.run takes no include=. Drop it on chunk.phase in your own loop if you do not want it.

See also