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¶
- Explanation: StreamChunk model: why one chunk type instead of three
- How-to: stream output: practical patterns
- How-to: generate images:
IMAGE_GENERATINGchunks - How-to: generate audio:
AUDIO_GENERATINGchunks - How-to: generate speech:
SPEECH_GENERATINGchunks aimu.models.StreamingContentType: API reference