Live SSE stream with Last-Event-ID replay
Streaming events
Streaming events, at a glance
Live SSE stream with Last-Event-ID replay
GET /v1/runs/{id}/events- 01ConnectAccept: text/event-stream
- 02Consumestrictly ordered IDs
- 03Persistlast durable cursor
- 04ResumeLast-Event-ID replay
GET /v1/runs/{id}/events returns a Server-Sent Events stream so you can follow a run as it happens, instead of polling. Each event has a type and a numeric id (the sequence number). If your connection drops, reconnect and replay everything you missed by sending the last sequence you saw as a Last-Event-ID header, or as the ?after= query parameter. The stream is ordered and at least once: deduplicate by run ID plus sequence, and atomically persist the last processed sequence with your side effects because a disconnect before that commit can replay an event. Successfully persisted frames remain replayable, but intermediate text, reasoning, tool, step, billing, pause/resume, status, and error appends are best-effort and can be absent after a transient storage failure. Terminal status and done are transactionally admitted with Agent Run terminal state. Always reconcile current state with GET /v1/runs/{id}.
import os, httpx
BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}
run_id = "7a1b2c3d-4e5f-4678-9abc-def012345678"
last_seq = 0 # persist this so a reconnect can replay
# httpx streams the SSE body line by line. Reconnect with Last-Event-ID.
with httpx.stream(
"GET",
f"{BASE}/runs/{run_id}/events",
headers={**HEADERS, "Last-Event-ID": str(last_seq)},
timeout=None,
) as resp:
event_type = "message"
for line in resp.iter_lines():
if line.startswith("id:"):
last_seq = int(line[3:].strip())
elif line.startswith("event:"):
event_type = line[6:].strip()
elif line.startswith("data:"):
data = line[5:].strip()
print(event_type, data)
if event_type == "done":
breakevent: done. The other is event: timeout carrying {"reason":"stream_max_duration"}, sent when the connection reaches its own maximum duration. It says nothing about the run, which is very likely still going. A client that waits only for done will hang on a long run. On timeout, reconnect with Last-Event-ID to continue from your cursor, or fall back to GET /v1/runs/{id}. The same two endings apply to GET /v1/workflows/runs/{id}/events.Reading a finished run
GET /v1/runs/{id}/log is the supported way to read a run's full trajectory. SSE is built for watching a run live; replaying a stream to inspect one that already ended is awkward and lossy in a way that is easy to get wrong. The log endpoint reads the same source and returns it as ordinary paginated JSON, one record per agent step, already assembled. It needs the runs:read scope.
Each step arrives with the model's reasoning already unwrapped from its internal <cua-section> markup, split into analysis, next_action, and grounded_action. Alongside them, actions lists every tool call in order with its arguments and outcome, plus what the step cost and which screenshot the agent was looking at. No parsing on your side.
log_complete first. The event log is best effort and is deliberately not written inside the run's transaction, so a step can execute and bill while its log rows are lost. When log_complete is false, steps_completed is the authoritative count and a step missing from data[] did not necessarily fail to happen. Never bill, reconcile, or alert off steps_logged.Pair the log with a run's result.summary, which is only the last 2,000 characters of the trajectory, and with GET /v1/runs/{id}/screenshots when you need the exact frames the agent saw. Errors on this endpoint are 404 RUN_NOT_FOUND for an unknown, cross-tenant, or wrong-key-mode id (never an empty log, which would confirm the id exists) and 503 DB_UNAVAILABLE with Retry-After when the event store is briefly unreachable.