Skip to content
Open Console

Stream and replay events

5 min readUpdated

Run events let your application render progress while an agent works. Treat the stream as a live delivery channel—not as the authoritative run record or a permanent audit log. A reliable consumer processes each event idempotently, stores a run-scoped cursor, and reads the run after every terminal signal or end-of-stream.

This guide assumes you already have a session ID, run ID, and a supported access-token path. See Sessions and runs first if those resource boundaries are unfamiliar.

One disconnect, no double-applyProcess first. Store the cursor second.
  1. Liveevent 41Processed; cursor stored
  2. Liveevent 42Processed; cursor pending
  3. NetworkdisconnectedStored cursor is still 41
  4. ReconnectLast-Event-ID: 41Resume strictly after 41
  5. Replayevent 42Already applied; skip
  6. Replayrun_completedTerminal stream signal
  7. ConfirmGET runAuthoritative: succeeded

Event 41 is processed and stored → event 42 is processed → the connection breaks before cursor 42 is stored → reconnect sends Last-Event-ID: 41 → event 42 arrives again and is ignored by deduplication → event 43 signals run_completed → GET run confirms authoritative status succeeded.

If cursor 42 had been stored successfully, reconnecting with Last-Event-ID: 42 would begin strictly after it. The duplicate above exists because application work completed before its cursor checkpoint did—not because the server replays the requested cursor.

Read events for one session and run:

GET /v1/sessions/{session_id}/runs/{run_id}/events

Terminal
curl --no-buffer --fail-with-body \
"$TANOMIS_API/v1/sessions/$SESSION_ID/runs/$RUN_ID/events" \
--header "Authorization: Bearer $TANOMIS_TOKEN" \
--header "X-Organization-ID: $TANOMIS_ORG" \
--header "X-Workspace-Tenant-ID: $TANOMIS_WORKSPACE" \
--header "Accept: text/event-stream"

The stream is scoped to this session and run. It does not provide ordering across runs or combine events from other sessions.

Wait for the blank line that ends a frame before processing it. Every public event frame contains:

SSE field Contract
id An opaque event ID. It orders events only inside this run.
event The event name. Only the three terminal names below have stable terminal meaning.
data One complete JSON event object, including its own id, type, session_id, run_id, and harness-specific data.

A synthetic stream can look like this:

id: 41
event: message_delta
data: {"id":"41","type":"message_delta","session_id":"ses_example","run_id":"run_example","created_at":"2026-09-03T09:00:00.041Z","data":{"text":"Draft"}}
id: 42
event: message_delta
data: {"id":"42","type":"message_delta","session_id":"ses_example","run_id":"run_example","created_at":"2026-09-03T09:00:00.042Z","data":{"text":" ready"}}
id: 43
event: run_completed
data: {"id":"43","type":"run_completed","session_id":"ses_example","run_id":"run_example","created_at":"2026-09-03T09:00:00.043Z","data":{}}

message_delta is illustrative, not a universal schema promise. Event names and fields inside the harness-specific data object can evolve.

Use this sequence for every connection:

  1. Open the events endpoint with Accept: text/event-stream. If a cursor exists for this run, send it in Last-Event-ID.
  2. Parse one complete frame. Verify its session and run IDs match the resource you requested.
  3. Build a deduplication key from the run ID and event ID. If that key is already processed, skip the side effect and advance the stored cursor if needed.
  4. Apply the event idempotently. Only after processing succeeds, record the deduplication key and persist the event ID as this run’s cursor.
  5. On run_completed, run_cancelled, or error, stop interpreting the stream and read the run resource for its authoritative lifecycle status and error details.
  6. On a clean EOF without a terminal event, read the run. Reconnect with bounded backoff only while the run remains queued or running.

Store the cursor with both the session ID and run ID. Treat the event ID as an opaque string; never compare IDs from separate runs. Where an event changes durable application state, make the state change and cursor checkpoint atomic when possible. Otherwise, make the handler idempotent so a replayed frame is safe.

Reconnect by sending the last successfully processed event ID:

Accept: text/event-stream
Last-Event-ID: 41

When live or persisted history is available, replay starts strictly after that ID.

Keep bearer credentials and cursor storage in your trusted backend. This pseudocode names the required behavior without prescribing an SDK or storage engine:

cursor = load_cursor(session_id, run_id)
loop:
stream = open_events(session_id, run_id, last_event_id=cursor)
for frame in stream:
event = parse_complete_json_frame(frame)
assert event.session_id == session_id and event.run_id == run_id
key = (run_id, event.id)
if already_processed(key):
cursor = checkpoint(event.id)
continue
apply_idempotently(event)
record_processed_and_checkpoint(key, event.id)
cursor = event.id
if event.type in {run_completed, run_cancelled, error}:
break
run = get_run(session_id, run_id)
if run.status in {succeeded, failed, cancelled}:
return run
wait_with_bounded_backoff()

Do not start a new run when this stream disconnects. Reconnect to the same run with its cursor.

The browser’s native EventSource constructor cannot attach an arbitrary Authorization header. Do not put bearer credentials in a query string to work around that boundary.

Choose an architecture that matches your application’s security model:

  • Stream through your authenticated same-origin backend, which keeps the Tanomis bearer token server-side and forwards only the events the signed-in user may see.
  • Use a fetch-based SSE parser only when the API is same-origin or your environment administrator explicitly configures CORS for your frontend origin and the required request headers. Your application must also be designed to hold and send the token in that browser context. Do not assume the hosted API accepts cross-origin browser requests. The same frame, cursor, deduplication, and final-read rules still apply.

Browser tab closure, page navigation, and sleep are ordinary disconnects. Persist the run-scoped cursor somewhere that survives the recovery behavior your product promises.

What you observe What it means What to do
A previously processed event arrives again The stored cursor lagged successful processing, or the consumer is recovering around a failure boundary. Deduplicate by run ID plus event ID; do not repeat the side effect.
200 OK closes with zero frames History may be absent, exhausted, or temporarily unreadable. This is a normal, fail-soft EOF—not proof of success. Read the run resource. Reconnect boundedly only if its status is non-terminal.
The stream closes after non-terminal events Delivery ended before a terminal signal reached this connection. Preserve the last processed cursor, read the run, and reconnect to the same run if needed.
run_completed, run_cancelled, or error arrives The stream emitted a terminal signal. Stop consuming and read the run for authoritative lifecycle status and error details.
An unknown event name or JSON field appears A compatible harness or platform addition is present. Unknown events are not terminal. Ignore it or render it generically; preserve fields you proxy and keep consuming.
A frame cannot be parsed or fails scope checks The frame was not processed successfully. Do not advance the cursor; close the connection, inspect the run, and reconnect boundedly.

Persisted replay is best-effort and fail-soft. It does not guarantee that every prior event remains available, so do not use the event stream as your audit record. The final run resource tells you the durable lifecycle status even when the detailed transcript is unavailable. It does not reconstruct agent output or event payloads that your consumer missed.

Treat data as potentially sensitive customer and agent content. Depending on the harness and run, it can carry user input, model output, or tool-related details. Apply your product’s authorization, redaction, retention, and display rules before storing or logging it. Avoid raw-frame logging by default; identifiers and event names are usually enough for transport diagnostics.