id— the event’s own unique id (evt_…). The client idempotency key for steering is separate (deduped per session — see steering).turn_id— the turn that produced the event, when applicable; filter a turn’s output with?turn_id=.type— the one discriminator: a namespaced string (agent.message,turn.completed, …). See below.actor— who produced it:{ id, display?, type: "human" | "agent" | "system" }.level— visibility; see below.body/refs— payload and related ids.
Event shape
The stable contract is the top-leveltype — switch on it, don’t parse prose. Unknown types render fine from their text/summary, so new ones are additive. (There’s no separate kind; coarse buckets are just type prefixes like turn.* or agent.*.)
type | body shape | Use it for |
|---|---|---|
agent.message | { text } | agent output (the result is whichever one turn.completed points to) |
user.message | { text } | a steer you sent |
turn.started | { turn_id, input_from_seq, input_to_seq } | a turn began, consuming input events in [from,to] |
turn.completed | { turn_id, yield_reason, result_event_id? } | the “done” signal — await this |
tool.call | { tool, args_summary } | the agent invoked a tool |
exec.completed | { command, exit_code, summary, content_ref?, bytes? } | a finished command |
error.runtime · error.model · error.task | { code, message, retriable? } | a failure |
test.results, diff/patch, pr.result, preview.url, action.required (agent needs an approval / tool result).
Correlate a steer to its turn. Steers coalesce — one turn can pick up several — so POST /messages returns the event seq, not a turn. The turn that consumes your steer emits turn.started with input_from_seq ≤ your_seq ≤ input_to_seq; await its turn.completed, then GET …/events?turn_id= for everything it produced.
Large output. A large body is offloaded to a content_ref; fetch the bytes with GET /sessions/:id/events/:eventId/content (e.g. full exec.completed output). Richer artifacts (named files, diffs, screenshots, reports) ride a forward refs.artifacts[], with a first-class artifacts & previews API coming soon. refs also carries optional correlation/evidence keys (trace_id, sources[]).
Levels
Every event carries alevel saying who it’s for. You filter the stream (and webhook deliveries) by it, so each surface shows the right amount of detail.
| Level | What lands here | Reach for it when you want… |
|---|---|---|
user | What a person is meant to read — the answer, results, a question back to them. The default for webhook delivery. | a chat bubble, the final result, a notification |
progress | Plain-language step updates: “cloning the repo”, “running tests”, “found 3 failures”. | a live “what’s it doing right now” feed or status line |
internal | Tool calls and runtime detail — verbose, not meant for end users (the model’s private reasoning is never surfaced). | debugging, an audit trail, your own internal console |
user is the narrowest, internal is everything:
?level=user→ justuser?level=progress→progress+user?level=internal→ everything
user; an agent-activity view asks for progress; debugging your agent asks for internal.
seq, head, input_cursor
seq— a monotonic id assigned to each event.head— the highestseqin the session.input_cursor— how far the runtime has consumed (a session field). As a reader you don’t track this — you just passafter=<seq>.
after=<seq> and you get everything since that point. This is how resume works: reconnect with the last seq you saw — you miss nothing and get no duplicates. In the browser, native EventSource does this automatically — each event’s seq is the SSE id, so it resumes via Last-Event-ID on reconnect (see the quickstart).