Events

Updated Nov 4, 2026View as Markdown

A session is an event log. The engine appends an event before it acts, and streams every event to any client that is watching.

Use events to render a conversation, to follow a turn, and to monitor a run.

The stream

GET /api/client/sessions/{session_id}/events/stream

The response is text/event-stream. Each frame carries three fields.

Frame fieldHolds
idThe event's seq.
eventThe event type.
dataAn Event object.
id: 7
event: message.new
data: {"session_id":"0193a1","seq":7,"occurred_at":"2026-07-14T10:00:03Z","payload":{"type":"message.new","message":{"id":"m2","role":"assistant","content":"Hi"}}}

To resume after a drop, pass ?after_seq=<n>. EventSource does this for you. On reconnect the browser sends the last frame ID in a Last-Event-ID header, which wins over the query parameter.

Each stream covers one session. A subagent is its own session with its own stream. Open a second stream with the child's ID to watch it.

Event shape

type Event = {
    session_id: string
    seq: number
    occurred_at: string         // RFC 3339
    turn_id?: string            // the turn running when this happened
    payload: EventPayload       // tagged by `type`
}

seq orders the log. It starts at 1 and never repeats.

Conversation

Render a chat from these.

EventPayload
session.createdThe session exists.
message.newA message joined the tree: { message, parent_id?, copied_from? }. copied_from names the original of a copy.
head.movedThe active leaf changed. A client branched or edited.
turn.startedA turn began: { turn_id, metadata? }. metadata is the metadata the turn was submitted with. For a merged turn, a later submit's key wins.
turn.joinedMessages sent with queue: "join" run in the running turn: { turn_id, next_turn_id?, metadata? }. next_turn_id is the next_turn_id their submit got. metadata is the keys they add to the turn.
turn.completedA turn ended: { turn_id, data?, turn_cost, turn_token_usage, turn_calls, turn_started_at?, error? }. See Turn calls.
session.doneThe session finished its work.
session.cancelledThe current run was cancelled. A turn.completed with the error code cancelled follows. The session stays usable.

Turn calls

turn_calls on turn.completed totals the calls that the turn made, for each kind of call:

{
  "llm":      { "count": 6,  "errors": 0, "cost": "0.0101", "tokens": { "input": 41000, "output": 900, "…": "…" } },
  "tool":     { "count": 14, "errors": 2 },
  "subagent": { "count": 1,  "errors": 0, "cost": "0.0022", "tokens": { "…": "…" } },
  "system1":  { "count": 2,  "errors": 0, "cost": "0.00006", "tokens": { "input": 400, "output": 6, "…": "…" } }
}
  • count is the number of calls. A retry is not a new call.
  • errors is the number of attempts that failed, including the ones that were retried.
  • cost and tokens are present only when they are not zero.
  • A subagent call costs what its child spent to answer it. The child's own calls are in the child's turn.completed. subagent.started gives the child's session.

turn_cost and turn_token_usage are the totals of all kinds. turn_started_at is when the turn started.

Model calls

EventMeaning
llm.call.requestedA call was recorded.
llm.call.dispatchedThe call went out.
llm.call.completedThe call returned.
llm.call.erroredThe call failed.

Partial output arrives between events as llm.token.delta frames.

event: llm.token.delta
data: {"type":"llm.token.delta","session_id":"0193a1","call_id":"llm-1","seq":3,"text":"Hi"}

These carry a StreamDelta. The engine does not record them in the log.

Tool calls

EventMeaning
tool.call.requestedThe model asked for a tool: { id, name }.
tool.call.dispatchedThe call went to its handler.
tool.call.completedThe call returned: { id, result }.
tool.call.erroredThe call failed: { id, error }.

Subagents

EventMeaning
subagent.requestedThe model called a child agent.
subagent.dispatchedThe spawn went out.
subagent.startedThe child session exists.
subagent.turn_completedThe child's turn ended.
subagent.erroredThe child failed.

The id on each of these is the model tool call the child answers. subagent.requested also carries session_id, the child session. Open a stream with that to watch the child.

Connectors

EventMeaning
connector.sync.requestedThe engine is fetching a connection's tools.
connector.sync.completedThe tools arrived.
connector.sync.erroredThe fetch failed. The turn runs without those tools.

Interrupts

EventPayload
session.interrupted{ interrupt_id, origin, reason, payload, anchor? }. anchor is the head it pauses. Without it, every branch pauses.
session.interrupt_resumed{ interrupt_id, payload }.

State and config

EventMeaning
worker.state.updatedA decision wrote new state.
agent.updatedA decision wrote a new config.

Context

See Make room.

EventPayload
context.limitedA model call waits for room: { reason, strategy?, tokens, window?, call }. A context.limit decision follows.
context.replacedThe history was set on a new branch: { reason?, summary?, kept, dropped }. summary holds { id, text }.
context.keptThe held call runs on the history as it is.

A model call between context.limited and the next context.replaced or context.kept writes the summary. Its text is not an answer.

Decisions

Engine bookkeeping. Useful for monitoring.

EventMeaning
decision.queuedA decision is waiting for the slot.
decision.dispatchedThe request went to the worker.
decision.completedThe worker answered.
decision.erroredThe decision failed.
decision.droppedThe decision was abandoned: { id, messages? }. messages holds the ids of the client messages that it held.
call.voidedA call was canceled before it settled.
session.message_requestedA message.send targeted another session.

Delivery

The engine hands events to its own readers: the session list, messages to other sessions, Slack, and the agent's events URL. A reader that fails retries with backoff, and each failure is logged. These events go to no reader.

Work goes to queues. Work that cannot be queued waits and goes out again as the same attempt.

EventPayload
work.unsent{ kind, id, attempt?, error, retry_at }. The work goes out again at retry_at.
delivery.failed{ subscription, seq, attempt, error, retry_at }. A retry follows at retry_at.
delivery.recovered{ subscription, through, attempts }. Delivery works again.
delivery.skipped{ subscription, seq, attempt, error }. The reader gave up on that event.
delivery.parked{ subscription, seq, attempt, error }. The reader stopped until it is resumed.
delivery.resumed{ subscription }. A parked reader runs again.

Branches

Engine bookkeeping. The engine keeps the active branch in memory and loads other branches from the log when a view needs them. These events go to no reader.

EventMeaning
branch.loadedA branch came back into memory from the log.
branch.unloadedBranches off the active path left memory. They stay in the log.

AG-UI

The AG-UI endpoints send AG-UI protocol events instead of these. See AG-UI.

Next steps