Skip to content

Stream events with the SDK

The Python SDK’s client.events.stream(session_id) returns an async iterator over typed events. It handles reconnection, heartbeat-based stale detection, and resumption with since_id automatically.

async for event in client.events.stream(session.id):
if event.type == "output.message.delta":
print(event.data.get("delta", ""), end="", flush=True)
elif event.type == "turn.completed":
print()
break
elif event.type == "turn.failed":
print(f"\n[failed: {event.data.get('error')}]")
break

To show what the agent is doing while it works, listen for tool.started and tool.completed:

async for event in client.events.stream(session.id):
if event.type == "tool.started":
tool_call = event.data.get("tool_call", {})
print(f" [tool] {tool_call.get('name')}")
elif event.type == "tool.completed":
status = "ok" if event.data.get("success") else "error"
print(f" [tool] {event.data.get('tool_name')}: {status}")
elif event.type == "turn.completed":
break

output.message.completed carries the complete final message after streaming finishes:

async for event in client.events.stream(session.id):
if event.type == "output.message.completed":
message = event.data.get("message", {})
for part in message.get("content", []):
if part.get("type") == "text":
print(part["text"])
elif event.type == "turn.completed":
break
  • Reconnection. The control plane cycles SSE connections every 5 minutes; the SDK reconnects transparently using since_id.
  • Stale detection. The server sends a heartbeat every 30s; the SDK treats >45s of silence as a dead connection and reconnects.
  • Backoff. Network errors trigger exponential backoff with jitter.
  • Typing. Each event has .type and .data attributes parsed from SSE.

While a stream is open the SDK manages reconnection internally. You only need since_id when restarting your application and resuming from a previously recorded event ID:

# Persisted somewhere — file, DB, etc.
last_seen_id = load_cursor()
async for event in client.events.stream(session.id, since_id=last_seen_id):
handle(event)
save_cursor(event.id) # so the next restart can resume from here

Inside the loop the SDK already remembers the last ID it yielded and reconnects with it on transient failures — save_cursor here is for application restart recovery, not per-iteration SDK state.