Streaming¶
agent.stream(prompt) starts a run and returns it. Iterate over it with async for to get its events as they
happen:
Events¶
Events are small, read-only objects backed by Rust. They work with isinstance and with match.
| Event | When | Fields |
|---|---|---|
TextDelta |
the model streams text | text |
ToolStarted |
a tool call starts | call_id, name, arguments (dict) |
ToolFinished |
a tool call ends | call_id, name, result (text), content (with images), is_error, metadata |
InputRequested |
a tool call waits for a person (human in the loop) | call_id, kind, data, ... |
StateChanged |
a tool changed the state | state, delta |
SubagentStarted, SubagentFinished |
a subagent starts or stops | call_id, name, ... |
RunFinished |
the run ends (always last) | text, stop_reason, output, turns, usage, response_id, messages, new_messages, pending, state |
When the model asks for several tool calls at once, they run at the same time, and each ToolFinished comes as
soon as its call ends: a fast call's result does not wait for a slow one. The model still reads the results in the
order it asked for the calls.
A subagent's events come in the same stream, with subagent_id set (it is None for the main agent's events).
await agent.run(prompt) does the same run without streaming and returns the RunFinished event. It has no
web streams: ai_sdk() and ag_ui() are on the run stream() returns. Without an
event loop (scripts, Django, Celery), use agent.run_sync(prompt) and for event in agent.stream_sync(prompt):
see Sync code.
One Agent can serve many runs at the same time: each run has its own history. Create it once and reuse it.
New fields may be added to events in minor versions; existing ones keep their meaning.
How a run ends¶
Every run that does not fail ends with RunFinished, whose stop_reason says why:
stop_reason |
Meaning |
|---|---|
"end" |
the model finished its answer |
"waiting" |
tool calls wait for a person: see pending (human in the loop) |
"length" |
the answer was cut by the output token limit (max_output_tokens, max_completion_tokens) |
"refusal" |
the model refused, or a content filter stopped it; text has the refusal |
"stopped" |
run.stop() was called |
An answer cut by "length" may end in a half-written tool call: it is never run.
Stopping a run. run.stop() ends it at the next safe point and keeps what it did: an answer being streamed
ends at once (its text so far is kept, as the last assistant message), tools already running finish, and no new
request is sent. Keep reading the events: the run still ends with RunFinished, whose messages are a valid
history to continue from. (Leaving the async for loop with break instead aborts the run and keeps nothing.)
run = agent.stream("Write a long story")
async for event in run:
if isinstance(event, TextDelta) and user_pressed_stop():
run.stop()
Errors keep the conversation. An AgentError raised by a run has the history up to the failure in
error.messages, so a retry can continue instead of starting again. MaxTurnsExceededError (after max_turns
model requests) is one too:
try:
result = await agent.run("Plan my trip")
except MaxTurnsExceededError as error:
result = await agent.run("Wrap up with what you have", history=error.messages)
Token usage¶
RunFinished.usage holds the tokens used by all the model requests of the run, with both APIs:
| Field | Meaning |
|---|---|
input_tokens |
tokens sent to the model |
cached_input_tokens |
the part of input_tokens read from the prompt cache (cheaper) |
output_tokens |
tokens the model generated |
reasoning_tokens |
the part of output_tokens spent reasoning (reasoning models) |
requests |
model requests that reported usage |
Counts the API did not report are 0. If requests is lower than turns, some requests sent no usage (some
OpenAI-compatible servers don't).
result = await agent.run("Hi")
print(result.usage.input_tokens, result.usage.output_tokens)
total = Usage() # all zeros
for result in results:
total += result.usage
Tools and hooks see the tokens used so far as ctx.usage. A failed run still used (and paid for) tokens: an
AgentError raised by a run has them in error.usage.
try:
result = await agent.run("Hi")
except AgentError as error:
print("used before failing:", error.usage)
Grouping streamed text¶
Models stream text in tiny pieces, and each TextDelta costs a little Python work. To get fewer, bigger events,
choose how text is grouped with text_chunks:
text_chunks |
One TextDelta per... |
|---|---|
"delta" (default) |
piece sent by the API (lowest latency) |
10 (any number N) |
N pieces |
"paragraph" |
markdown paragraph; fenced code blocks are never split |
"message" |
whole assistant message |
Set it on the agent, or override it for one run:
agent = Agent(model="gpt-5-mini", text_chunks="paragraph")
async for event in agent.stream("Explain asyncio", text_chunks=20):
...
Which one?
For a live UI, a number like 10 keeps almost all the savings with no visible delay.
"paragraph" and "message" are cheapest but make the user wait for the whole paragraph or answer.
See the benchmarks.
Batches of events¶
Each step of async for event in run hands one event from Rust to Python, and the hand-over costs more than most
events. run.batches() hands over every event that is already waiting, as a list:
It never waits to fill a batch: when events come one at a time (a real model streaming at its own pace), each
list has one event and nothing changes. When they pile up (cached or very fast answers, a busy server falling
behind), one step carries them all. run.ai_sdk() and run.ag_ui(...) (see Web frontends)
read the run this way too. Numbers in the benchmarks.