The envelope & identity

The 16 context attributes every Event carries, how they nest into an identity, and why (epoch, seq) is the mechanism behind ordering and resume.

Why 16 attributes and not a free-form payload

Every Event is one JSON object: a fixed set of context attributes plus an optional data payload. The attribute set is fixed and small for routing: a relay, a filter, or a dashboard needs to decide which session an event belongs to, whether it's urgent, and whether it can show the content, all without parsing data. The full normative list, with types and requirement levels, is AEP-0001 §5.1; this page is a guided tour of what each one is for, not a restatement of the table.

Grouped by purpose:

  • Wire identity: aep (protocol version), id (the idempotency key every consumer dedupes on), type (the bare, dot-separated event name), time (display and joins; never ordering, see below).
  • Who and what: source (a URI naming the emitting agent/session), agent (the stable logical identity, e.g. claude-code), subject (the thing acted upon: a tool name, a file, an attention-request id).
  • Where in the work: session, run, step, the nesting described below.
  • Order and causality: seq, epoch (this page, next section), cause (the id of the event that immediately caused this one; the edge in the causal DAG the taxonomy tour traces through the attention lifecycle), traceparent (W3C Trace Context, for OpenTelemetry interop).
  • Handling: severity (six-level, for filtering and backpressure) and capture (how much content is present: see capture-and-redaction.md).
  • Content: data, the type-specific payload. Its schemas live in AEP-0002.

Keeping this list closed and small means a consumer that has never seen a given agent before can still route, order, and filter its events correctly: the routing surface never depends on understanding that agent's payloads.

Identity nesting: source, agent, session, run, step

An agent is not a flat stream of events. It has conversations, activations within a conversation, and sometimes phases within an activation. AEP names three nesting levels so a fleet observer can group correctly without inventing its own convention:

  • Session: the unit of conversation or work an emitter groups events under.
  • Run: one activation within a session: a turn, a task, a job.
  • Step: an optional finer phase within a run (an agentic-loop phase).

source and agent sit above all of this: they name which emitter produced the session. A subagent's own session links back to its parent via session.started.data.parent (AEP-0002 §5.3), so delegation trees are reconstructable without a special protocol concept for them.

erDiagram
    AGENT ||--o{ SESSION : emits
    SESSION ||--o{ RUN : contains
    RUN ||--o{ STEP : contains
    SESSION }o--|| EVENT : "carries epoch+seq"
    RUN }o--|| EVENT : scopes
    STEP }o--|| EVENT : scopes

Not every event carries every level: step is optional, and run is optional for session-level facts. What's fixed is the direction of nesting: an event's run and step (when present) always resolve inside the session it names. The conditions under which session/seq are required versus omitted (agent-scoped events, and the foreign-emitter exception for inbound control events) are normative: see AEP-0001 §5.2.

Ordering & replay: why (epoch, seq) exists

time looks like the obvious ordering key, but it deliberately isn't one: clocks skew, retries reorder delivery, and a wall-clock timestamp can't express "this is a fresh counter because the process restarted." seq is a counter the emitter itself owns and increments, so ordering authority stays with the party that actually knows what happened first.

epoch handles the one case a bare counter can't: a restart. Bumping epoch says "this session's counter started over," so a consumer never confuses a low seq after a crash with going back in time. The full ordering contract, including that gaps are legal but regressions are not, is AEP-0001 §7.

What this buys a consumer is resume: reconnect with the last (session, epoch, seq) you durably processed, and the endpoint replays exactly the tail after that position, with no gap (delivery is at-least-once; id is the dedupe key). That's what makes "kill a consumer, reattach" a safe, ordinary operation instead of a special recovery path. The resume contract and its per-binding carriage (SSE Last-Event-ID, WebSocket subscribe.from) are defined in AEP-0003 §7, building on the (epoch, seq) semantics in AEP-0001 §7.

sequenceDiagram
    participant C as Consumer
    participant R as Relay
    Note over C: last durable position:<br/>(s_8f2c, epoch 0, seq 187)
    C->>R: subscribe<br/>(from session s_8f2c @ epoch 0, seq 187)
    R-->>C: replay tail (seq 188..203)
    R-->>C: live events (seq 204...)

A consumer that never disconnects never needs this: replay is what turns a crash or a deliberate restart into a non-event.

See also

On this page