On this page

Stream live events (Server-Sent Events)

GET/events

Operation streamEventsAuthBearer token

Starts with a : connected comment and a stream.hello frame, replays buffered frames after the resume cursor, then streams new ones; a : heartbeat comment every 30 s. Each event frame: id: <cursor>, event: <event_type>, data: <EventFrame JSON>. Resume: pass the last cursor as ?since= or in the standard Last-Event-ID header (what EventSource sends on reconnect); ?since wins when both are present. Add ?epoch= (from stream.hello) so the daemon can tell you when the cursor is from an earlier run. Control frames (named SSE events, no id: line, data is not an EventFrame): stream.hello {epoch, oldestCursor, latestCursor} on every connect; stream.gap {reason, epoch, requestedCursor, oldestCursor, latestCursor} when the resume cannot be exact: evicted (the cursor is older than the buffer: frames were dropped) or epoch_changed (the daemon restarted: cursors restarted at 1, and the whole buffer is replayed). On a gap, or when stream.hello shows an epoch other than the one you had, reload state from the REST API. The buffer is bounded (1000 frames) and process-local: order and deduplicate by event.agent_id + event.seq (durable), and read durable history from GET /agents/{id}/loop. Events of a turn carry event.turn_id: the turnId POST …/chat and …/trigger answered with. The token goes in the Authorization header (use fetch, or an EventSource polyfill that sends headers). Event catalog: docs/guides/umbilical-events.md.

Query parameters

NameTypeDescription
agentIdstring

Only this agent's events

sinceinteger

Replay buffered frames whose cursor is greater than this. Process-local; resets on daemon restart. Wins over Last-Event-ID.

range: ≥ 0

epochstring

The stream.hello epoch the resume cursor came from. A different epoch (the daemon restarted) yields stream.gap epoch_changed and a full replay.

Header parameters

NameTypeDescription
Last-Event-IDstring

Standard SSE resume header: the last id: (cursor) received. Same as ?since=, which wins when both are sent. Not an integer: 400.

Responses

StatusDescriptionBody
200SSE stream (text/event-stream)text/event-stream
Option 1object

The data of one SSE frame. The SSE id is the cursor, the SSE event is event.event_type.

2 fields of Option 1 · EventFrame
cursorrequiredinteger

Resume token for ?since= / Last-Event-ID (process-local; see the stream.hello epoch)

eventrequiredobject
9 fields of event · UmbilicalEvent
seqrequiredinteger

Per-agent sequence number

event_typerequiredstring

e.g. agent.state.changed, turn.completed, task.created: see docs/guides/umbilical-events.md

timestamprequiredinteger

Epoch ms

sourcerequiredstring
agent_idstring | null
loopstring

Inner loop that produced it; absent = main

payloadrequiredobject
sigstring
turn_idstring

The turn that produced it: the turnId POST /agents/{id}/chat or /trigger answered with (kept when an interrupting chat is replayed), else the runtime's own turn id. Absent outside a turn. A chat consumed into a running turn (it arrived mid-tool-use) is listed in that turn's turn.completed / agent.error payload as absorbed_turn_ids.

Option 2object

Data of the stream.hello control frame, sent on every connect.

3 fields of Option 2 · StreamHello
epochrequiredstring

Identifies this daemon run; cursors are only comparable within one epoch

oldestCursorrequiredinteger | null

Oldest buffered cursor (null: nothing buffered)

latestCursorrequiredinteger

Newest cursor published (0: none yet)

Option 3object

Data of the stream.gap control frame: the resume could not be exact; reload state from the REST API.

5 fields of Option 3 · StreamGap
reasonrequiredstring

evicted: frames after the cursor were dropped from the buffer. epoch_changed: the cursor is from an earlier daemon run.

One of evicted, epoch_changed

epochrequiredstring
requestedCursorrequiredinteger | null
oldestCursorrequiredinteger | null
latestCursorrequiredinteger
Errors 400 · 401 · 403 · 503
400Invalid request: missing or malformed field, query parameter or body. A body Fastify cannot parse gets Fastify's own shape (statusCode, code, error, message).bad_requestFST_ERR_CTP_EMPTY_JSON_BODY
401Missing or wrong bearer token (unauthorized)
403The request guard refused it: Host header not allowed (host_not_allowed, DNS-rebinding protection) or a browser cross-site request (cross_origin)host_not_allowedcross_origin
503The subsystem is not configured on this daemon

Error bodies use the error format.

Example

Request
curl -N "http://127.0.0.1:7385/events" \
  -H "Authorization: Bearer $ADF_DAEMON_TOKEN" \
  -H "Accept: text/event-stream"
Response 200
[
  {
    "cursor": 42,
    "event": {
      "seq": 7,
      "event_type": "agent.state.changed",
      "timestamp": 1790000000000,
      "source": "system:runtime",
      "agent_id": "Xk3v9QpLm2",
      "payload": {
        "state": "thinking"
      }
    }
  }
]

Catch up on the agent's in-memory replay window

GET/agents/{id}/umbilical/events

Operation getAgentUmbilicalEventsAuthBearer token

Opt-in (umbilical.log.enabled), in-memory and bounded; nothing is persisted. Answers log_enabled: false with no events when the window is off. since_seq < oldest_seq - 1 means the cursor predates the window: re-snapshot.

Path parameters

NameTypeDescription
idrequiredstring

Loaded agent: its id, handle or name.

example: agent-1

Query parameters

NameTypeDescription
since_seqinteger

Events with seq strictly greater than this

range: ≥ 0

limitinteger

Max events (default 500, max 2000)

range: 1–2000

Responses

StatusDescriptionBody
200A page of the replay windowUmbilicalEventsResponse
agentIdrequiredstring

Agent id

eventsrequiredarray<object>
6 fields of events
seqrequiredinteger
event_typerequiredstring
timestamprequiredinteger
sourcerequiredstring
payloadrequiredany
truncatedrequiredboolean

Payload JSON exceeded 4 KB and was replaced by { _truncated, preview }

last_seqrequiredinteger | null

Highest seq returned, or the since_seq passed when nothing is new

log_enabledrequiredboolean
oldest_seqinteger

Lowest seq still retained; since_seq < oldest_seq - 1 means the client fell off the back and must re-snapshot

Errors 400 · 401 · 403 · 404 · 500
400Invalid request: missing or malformed field, query parameter or body. A body Fastify cannot parse gets Fastify's own shape (statusCode, code, error, message).bad_requestFST_ERR_CTP_EMPTY_JSON_BODY
401Missing or wrong bearer token (unauthorized)
403The request guard refused it: Host header not allowed (host_not_allowed, DNS-rebinding protection) or a browser cross-site request (cross_origin)host_not_allowedcross_origin
404Unknown agent (or the named resource: loop, task, file, …)
500Unexpected runtime failure

Error bodies use the error format.

Example

Request
curl "http://127.0.0.1:7385/agents/agent-1/umbilical/events" \
  -H "Authorization: Bearer $ADF_DAEMON_TOKEN"