Skip to main content

Events and streaming

Events are flat tagged-union objects. The type field selects the remaining shape; persisted events receive an id and processed_at.

Send events

POST /v1/sessions/{id}/events

{
"events": [{
"type": "user.message",
"content": [{"type": "text", "text": "Inspect the failure"}]
}]
}

The list must be non-empty. Clients cannot provide id or processed_at, and server-emitted event types are rejected.

The PostgreSQL/Temporal control plane currently accepts:

EventCurrent behavior
user.messageStarts a model turn
user.interruptCancels the active turn, or is acknowledged as an idle no-op; session_thread_id is not supported
user.custom_tool_resultSupplies a result for a pending custom tool call
user.tool_confirmationAllows or denies a pending always_ask built-in
user.define_outcomeStored and validated

Generic user.tool_result, system.message, and targeted multi-agent interrupt shapes return 422 unsupported_error.

An interrupt is first committed to PostgreSQL and then delivered to the Session Workflow as a metadata-only wakeup. An interrupt that commits before turn completion wins that ordering point: the turn ends with exactly one session.status_idle whose stop reason is end_turn. If completion commits first, a later interrupt is an idle control event. A batch may place a new user.message after user.interrupt to redirect the Session into another turn.

The response echoes only the submitted events:

{"data": []}

Status and agent output are asynchronous and appear in list/stream results.

List events

GET /v1/sessions/{id}/events

Supported query parameters:

ParameterMeaning
limitPage size, 11000; default 100. Values above 1000 return a validation error.
orderasc or desc; default asc
pageOpaque forward cursor
types[]Repeatable event type filter
created_at[gt|gte|lt|lte]RFC 3339 bounds currently applied to processed_at
{
"data": [{
"id": "sevt_...",
"type": "agent.message",
"content": [{"type": "text", "text": "Done"}],
"processed_at": "2026-07-27T00:00:01Z"
}],
"next_page": null
}

Ordering and cursors currently use the internal session sequence, even for queries whose public name is created_at. Exact processed_at null/tie semantics remain incomplete.

Stream events

GET /v1/sessions/{id}/events/stream

The endpoint returns text/event-stream. Persisted frames use their event type as the SSE discriminator:

event: agent.message
data: {"id":"sevt_...","type":"agent.message","content":[...],"processed_at":"..."}

The stream starts after the latest committed event at subscription time. It does not replay earlier history and does not implement Last-Event-ID.

For reconnect without gaps:

  1. open a new stream;
  2. list persisted history while the stream is open;
  3. merge both sources and deduplicate by event id.

An active stream receives session.deleted and then EOF when its session is deleted.

Live message previews

Opt in to ephemeral assistant text:

GET /v1/sessions/{id}/events/stream?event_deltas[]=agent.message

The stream may first emit:

event: event_start
data: {"type":"event_start","event":{"type":"agent.message","id":"sevt_..."}}

event: event_delta
data: {"type":"event_delta","event_id":"sevt_...","delta":{"type":"content_delta","index":0,"content":{"type":"text","text":"partial"}}}

The preview and eventual persisted agent.message share the same event ID. Preview frames:

  • are delivered only to opted-in subscribers;
  • are never written to the event log;
  • never appear in list results;
  • may end without an authoritative event if generation or the process fails.

agent.thinking is accepted as an opt-in value but no thinking previews are currently emitted.

Backpressure

NATS Core carries best-effort wakeups and previews across API/worker processes; PostgreSQL remains authoritative. Each subscriber periodically reconciles its durable PostgreSQL cursor, so a lost wakeup delays a persisted event but does not lose it. The output buffer is bounded: a slow subscriber is disconnected and should reconnect using the open-stream-then-list procedure above. Preview frames are ephemeral and can be lost.