Events
Live umbilical event stream (Server-Sent Events) and per-agent replay windows.
On this page
Stream live events (Server-Sent Events)
/eventsStarts 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
| Name | Type | Description |
|---|---|---|
agentId | string | Only this agent's events |
since | integer | Replay buffered frames whose cursor is greater than this. Process-local; resets on daemon restart. Wins over |
epoch | string | The |
Header parameters
| Name | Type | Description |
|---|---|---|
Last-Event-ID | string | Standard SSE resume header: the last |
Responses
| Status | Description | Body | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 200 | SSE stream (text/event-stream) | text/event-stream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Option 2object | Data of the 3 fields of | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
epochrequired | string | Identifies this daemon run; cursors are only comparable within one epoch |
oldestCursorrequired | integer | null | Oldest buffered cursor (null: nothing buffered) |
latestCursorrequired | integer | Newest cursor published (0: none yet) |
Option 3Data of the stream.gap control frame: the resume could not be exact; reload state from the REST API.
5 fields of Option 3 · StreamGap
reasonrequired | string |
|
epochrequired | string | |
requestedCursorrequired | integer | null | |
oldestCursorrequired | integer | null | |
latestCursorrequired | integer |
Errors 400 · 401 · 403 · 503
| 400 | Invalid 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 | |
| 401 | Missing or wrong bearer token (unauthorized) | |
| 403 | The 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 | |
| 503 | The subsystem is not configured on this daemon |
Error bodies use the error format.
Example
curl -N "http://127.0.0.1:7385/events" \
-H "Authorization: Bearer $ADF_DAEMON_TOKEN" \
-H "Accept: text/event-stream"[
{
"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
/agents/{id}/umbilical/eventsOpt-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
| Name | Type | Description |
|---|---|---|
idrequired | string | Loaded agent: its id, handle or name. |
Query parameters
| Name | Type | Description |
|---|---|---|
since_seq | integer | Events with seq strictly greater than this |
limit | integer | Max events (default 500, max 2000) |
Responses
| Status | Description | Body | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 200 | A page of the replay window | UmbilicalEventsResponse | ||||||||||||||||||||||||||||||||||||
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
| 400 | Invalid 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 | |
| 401 | Missing or wrong bearer token (unauthorized) | |
| 403 | The 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 | |
| 404 | Unknown agent (or the named resource: loop, task, file, …) | |
| 500 | Unexpected runtime failure |
Error bodies use the error format.
Example
curl "http://127.0.0.1:7385/agents/agent-1/umbilical/events" \
-H "Authorization: Bearer $ADF_DAEMON_TOKEN"