Skip to main content
Every step a session takes is an event, appended to the log in order. The log is the source of truth — you read it, stream it, and resume from any point. Everything you build on top — a chat UI, a live “what’s it doing” feed, notifications, an audit trail — comes from reading or streaming these events and filtering them by level.
{ "id": "evt_…", "seq": 12, "ts": "…", "session": "sess_…",
  "actor": { "id": "agent", "type": "agent" },
  "type": "agent.message", "level": "user",
  "body": { "text": "3 tests fail in auth.spec.ts" },
  "refs": {} }
  • 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.
Inbound and outbound messages share one envelope shape.

Event shape

The stable contract is the top-level typeswitch 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.*.)
typebody shapeUse 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
Coming soon (already shaped, additive): 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 a level saying who it’s for. You filter the stream (and webhook deliveries) by it, so each surface shows the right amount of detail.
LevelWhat lands hereReach for it when you want…
userWhat 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
progressPlain-language step updates: “cloning the repo”, “running tests”, “found 3 failures”.a live “what’s it doing right now” feed or status line
internalTool 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
Filtering is cumulativeuser is the narrowest, internal is everything:
  • ?level=user → just user
  • ?level=progressprogress + user
  • ?level=internal → everything
So a customer-facing chat asks for 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 highest seq in the session.
  • input_cursor — how far the runtime has consumed (a session field). As a reader you don’t track this — you just pass after=<seq>.
Read or stream with 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).