Streaming
Runs stream Server-Sent Events (SSE) by default. The SDK wraps them in a RunStream iterator.
Quick Patterns
Text only, live:
from m8tes import M8tes
client = M8tes()
for chunk in client.runs.stream_text(agent_id=1, message="Summarize open tickets", user_id="customer_42"):
print(chunk, end="", flush=True)Text + post-run data (run_id, full text). Use with so the connection closes cleanly:
with client.runs.create(agent_id=1, message="Draft weekly report", user_id="customer_42") as stream:
for chunk in stream.iter_text():
print(chunk, end="", flush=True)
print(stream.text) # all text deltas joined
print(stream.run_id) # available after iterationJust the final output (no streaming):
run = client.runs.create_and_wait(agent_id=1, message="Generate report", user_id="customer_42")
print(run.output)Follow-up replies (runs.reply()) stream the same way and inherit the run's settings. See Follow up on a run.
Event Types
These are the event.type values the Python SDK yields (it normalizes the raw
Claude-native SSE frames into these friendlier names; see SSE Frame Format
for the raw wire if you consume the HTTP stream directly).
Filter events by class
from m8tes import TextDeltaEvent, ToolCallStartEvent
for event in client.runs.create(agent_id=1, message="Close tickets", user_id="customer_42"):
if isinstance(event, TextDeltaEvent):
print(event.delta, end="", flush=True)
elif isinstance(event, ToolCallStartEvent):
print(f"\nUsing tool: {event.tool_name}")Detecting Failures
A run can fail mid-stream (expired credential, model rate limit, quota). The default iter_text() / stream.text path drops error events, so a failed run can otherwise look like an empty success.
The Python SDK also emits an error event for a terminal frame with subtype="success" and is_error=true; the subtype alone does not mean the run succeeded. This includes provider tool-count rejections. Read the persisted run's error_code and auto_retryable before retrying; tool_limit_exceeded is not automatically retryable. See Run error codes.
Opt into raising, or check after iterating:
# Raise RunFailedError if the run fails mid-stream
for event in client.runs.create(agent_id=1, message="...", raise_on_error=True, user_id="customer_42"):
...
# Or check without raising
with client.runs.create(agent_id=1, message="...", user_id="customer_42") as stream:
for chunk in stream.iter_text():
print(chunk, end="")
if stream.has_errors:
print("run failed:", stream.errors)Reconnecting to a dropped stream
A stream can stop before the run does (proxy idle-timeout, network blip). The run keeps working on the server, so the SDK raises StreamInterruptedError with the run_id instead of ending as if the run were done. Wait for the result, and never send the request again: that starts a second run. To keep streaming instead, runs.stream(run_id) replays the run's full history then live deltas, so reset any local accumulation on reconnect; it returns 409 once the run is no longer executing (use runs.get(run_id) for the result).
from m8tes import StreamInterruptedError
stream = client.runs.create(agent_id=1, message="long task", user_id="customer_42")
try:
for chunk in stream.iter_text():
print(chunk, end="", flush=True)
except StreamInterruptedError as exc:
if exc.run_id is None:
raise
# A long task needs a wait budget past the 300 second default.
run = client.runs.wait(exc.run_id, user_id="customer_42", timeout=1800)
print(run.output)The TypeScript SDK does the same. A stream that stops before the run does throws StreamInterruptedError with the runId, instead of ending as if the run were done. The run keeps working, so wait for its result and never send the request again:
import { M8tes, StreamInterruptedError } from "@m8tes/sdk";
const client = new M8tes();
const stream = client.runs.create({ agent_id: 1, message: "long task", user_id: "customer_42" });
try {
for await (const chunk of stream.iterText()) process.stdout.write(chunk);
} catch (err) {
if (!(err instanceof StreamInterruptedError) || err.runId === null) throw err;
// A long task needs a wait budget past the 300 second default.
const run = await client.runs.wait(err.runId, { user_id: "customer_42", timeout: 1800 });
console.log(run.output);
}To keep streaming instead, call client.runs.stream(err.runId): it replays the run from its start, and throws RunNotStreamingError once the run has finished (read the result with client.runs.get).
The server sends a keepalive every 15 seconds, so a long, quiet tool call does not trip the timeout. On a stream, timeout is the longest silence tolerated, never a limit on how long the run may take.
SSE Frame Format
Raw wire example
data: {"type": "metadata", "run_id": 123, "mode": "chat", "sandbox_enabled": true}
data: {"type": "content_block_start", "id": "block_1", "block_type": "text"}
data: {"type": "content_block_delta", "id": "block_1", "delta": {"type": "text_delta", "text": "Here are "}}
data: {"type": "content_block_delta", "id": "block_1", "delta": {"type": "text_delta", "text": "the open tickets..."}}
data: {"type": "content_block_stop", "id": "block_1"}
data: {"type": "tool_use", "id": "tc_1", "name": "gmail_search", "input": {"query": "is:open"}}
data: {"type": "tool_result", "tool_use_id": "tc_1", "content": "...", "result": "..."}
data: {"type": "run_metrics", "execution_time_ms": 4200, "input_tokens_used": 1200, "completion_state": "complete"}
data: {"type": "done", "completion_state": "complete", "stop_reason": "end_turn", "message_count": 4}
Delegation (subagents) in the stream
An agent may delegate part of a job to a subagent: a worker that runs in its own context and reports a result back. You will see this as a tool call named Agent:
data: {"type": "tool_use", "id": "tc_2", "name": "Agent", "input": {"description": "Pull last week's spend", "subagent_type": "general-purpose", "prompt": "..."}}
data: {"type": "tool_result", "tool_use_id": "tc_2", "content": "...the subagent's report..."}
- Treat it like any other tool call. The subagent's own steps are not streamed; you get the delegation and its result. Nothing about your event handling has to change.
- Its tokens are part of the run. Delegated work is included in the run's cost and counts against the run's spending limits.
run_metricscarriessubagent_countandsubagent_tokensso you can see how much of a run was delegated. - Permissions still apply. A subagent's tool calls go through the same approval rules as the agent's own. If a tool needs a human, it still asks, and the approval says which subagent asked for it.
The Python SDK normalizes these into the events (text-delta, tool-call-start, tool-result-end) shown in the table above.
Next: Runs · Human-in-the-Loop · Webhook Events