Streaming¶
Streaming lets users see results as the LLM generates them — essential for responsive UIs and real-time API endpoints. Instead of waiting for the full response, you receive partial output that progressively fills in.
Three Streaming Primitives¶
BaseAgent provides three streaming methods that mirror call_model() but yield results incrementally:
| Method | Level | Returns | Tool visibility |
|---|---|---|---|
stream_output(prompt, state) |
High | Async generator of OutputT partials |
Hidden |
stream_events(prompt, state) |
High | Async generator of AgentStreamEvent |
Visible |
call_model_stream(prompt, state) |
Low | StreamedRunResult context manager |
Hidden |
All three manage state.message_history identically to call_model() — history is updated after the stream is consumed.
stream_output() — Structured Output Streaming¶
An async generator that yields partial OutputT Pydantic model instances. Each yield is a progressively more complete version of the structured output:
async for partial in agent.stream_output(prompt, state, debounce_by=0):
# partial is an OutputT instance with fields filling in
print(f"Title so far: {partial.title}")
print(f"Findings: {len(partial.key_findings)} items")
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt |
str |
required | The user prompt to send |
state |
StateT |
required | Conversation state — history is read and updated |
debounce_by |
float \| None |
None |
Minimum interval (seconds) between yields. Use 0 for maximum granularity |
History is updated after the generator is fully consumed (the async for loop completes).
stream_events() — Full Event Streaming with Tool Visibility¶
An async generator that yields AgentStreamEvent instances — including model response tokens and tool call/result events. This uses pydantic-ai's Agent.iter() under the hood for full node-by-node control.
async for event in agent.stream_events(prompt, state):
if event.event_kind == "function_tool_call":
print(f"Calling tool: {event.part.tool_name}")
elif event.event_kind == "function_tool_result":
print(f"Tool returned: {event.result.content}")
elif event.event_kind == "final_result":
print("Output schema matched — generating final output")
Event Types¶
| Event kind | Type | Description |
|---|---|---|
part_start |
PartStartEvent |
A new response part begins (text or tool call) |
part_delta |
PartDeltaEvent |
Incremental token(s) for a part |
part_end |
PartEndEvent |
A response part is fully received |
final_result |
FinalResultEvent |
Output schema matched |
function_tool_call |
FunctionToolCallEvent |
A tool is about to be called |
function_tool_result |
FunctionToolResultEvent |
A tool returned a result |
Use this when you need to show the agent's reasoning process in a UI: "Calling API..." → "Got result" → "Generating response..."
call_model_stream() — Low-Level Context Manager¶
An async context manager that yields the raw StreamedRunResult from pydantic-ai. Use this when you need direct access to the stream — for example, to read metadata or use stream_responses():
async with agent.call_model_stream(prompt, state) as streamed:
print(f"Run ID: {streamed.run_id}")
output = await streamed.get_output()
The StreamedRunResult provides:
stream_output()— validated partial outputs (same as the high-level method)stream_responses()— rawModelResponseobjects withdebounce_bycontrolget_output()— consume the stream and return the final validated outputall_messages()— full message history after consumption
History is updated when the context manager exits.
Multi-Turn Streaming¶
Streaming methods wire history the same way call_model() does. You can freely mix streaming and non-streaming calls:
state = GlobalState()
# Turn 1 — non-streaming
state.add_message("user", "Analyze this report...")
result = await agent.call_model(state.get_latest_message("user"), state)
# Turn 2 — streaming (agent sees full history from turn 1)
state.add_message("user", "Now focus on the financials.")
async for partial in agent.stream_output(state.get_latest_message("user"), state):
print(partial)
Transport Integration¶
Because stream_output() and stream_events() are plain async generators, they plug into any async transport:
FastAPI SSE¶
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/analyze/stream")
async def analyze_stream(text: str):
state = GlobalState()
state.add_message("user", text)
async def event_generator():
async for partial in agent.stream_output(
state.get_latest_message("user"), state, debounce_by=0.05
):
yield f"data: {partial.model_dump_json()}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
)
WebSockets¶
@app.websocket("/ws/analyze")
async def ws_analyze(websocket: WebSocket):
await websocket.accept()
text = await websocket.receive_text()
state = GlobalState()
state.add_message("user", text)
async for partial in agent.stream_output(
state.get_latest_message("user"), state
):
await websocket.send_json(partial.model_dump())
await websocket.close()
orqest stays transport-agnostic — the streaming methods produce data, you choose how to deliver it.