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¶
- 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- How-to: give a running agent an inbox: what produces an
INBOXchunk aimu.agents.inbox.Inbox: the source anINBOXchunk's round reads from