- The event model — this page: the envelope every event shares, its general fields, and the identifiers that tie events together.
- Run input — the one message that travels the other way.
- Metadata — the open channel on everything, and how it merges.
- Event patterns — the ways events compose into streams.
- Transports — how streams are framed and delivered.
- Processing model — what a consumer does with material it does not recognise.
- Versioning and compatibility — talking to an older or newer peer.
Events
Every event is a JSON object sharing one envelope, defined by the schema asBaseEvent:
type— REQUIRED. The discriminator, one of the 31 values ofEventType. Every rule in this specification attaches to events through this field.timestamp— OPTIONAL. When the event was created. Informational: a consumer MUST NOT use it to order events — arrival order is the protocol’s order. By convention the unit is milliseconds since the Unix epoch.rawEvent— OPTIONAL. The provider-native event this one was translated from, carried verbatim. A consumer MUST NOT derive protocol behaviour from it.metadata— OPTIONAL. See General fields.
subagentRunId (the schema’s Attributable); the
subagent rules govern it. Run-scoped events —
RUN_STARTED, RUN_FINISHED, RUN_ERROR, MESSAGES_SNAPSHOT — describe the
run or the conversation as a whole and carry no attribution.
General fields
metadata
The open channel on everything: open by key, any JSON value under a key,
accumulated across the events that build an item — key by key, last write
winning, no recursion — with per-family merge targets and a reserved ag-ui
key. It has its own page: Metadata.
Absent means absent
An optional field that has no value MUST be omitted rather than sent asnull.
The rule binds a field whose schema does not itself admit null — which is
most of them. Some fields carry arbitrary JSON, rawEvent and
RUN_FINISHED.result among them, and for those null is a value the schema
permits: a producer MAY send it, a consumer MUST preserve it, and nothing can
distinguish it from an “absent” null because there is no such thing there.
Whether a field admits null is a structural question, so the
schema answers it.
This is not a style preference. A consumer that must accept both spellings
has to treat them as equal everywhere, and every such tolerance is permanent
once shipped. The protocol carries the one spelling every implementation can
represent.
null value under an open-by-key object — a metadata value, something
inside state — is data and MUST be preserved. Only a null standing in for a
whole absent field is prohibited.
Where the protocol has already shipped tolerance for a null in an optional
position, it is handled by a compatibility shim rather than by this rule. See
Versioning and compatibility.
Identifiers
threadIdidentifies a conversation. It is minted by the application and is stable across runs.runIdidentifies one run. It MUST NOT be reused for another run on the same thread.messageIdidentifies a message. It crosses run boundaries — a snapshot in a later run MAY restate a message by its id — so it MUST be unique within its thread.toolCallIdidentifies one tool call, and ties its result and any interrupt concerning it back to it.subagentRunIdis an opaque handle for one subagent invocation, not a reusable name for a subagent definition: two invocations of the same subagent carry two different values.
The events
Thirty-one event types, in eight families:Schema
The full structure of the protocol is defined by the JSON Schema at/spec/draft/schema.json. It is the source of
truth: the TypeScript, Python and .NET models are generated from it, and where
this document mentions a field it is naming something the schema defines, never
restating its shape.
The schema deliberately does not say what this document says: ordering,
lifecycle, attribution, error handling and compatibility are behavioural, and
the schema cannot express them. If the two ever disagree about structure, the
schema wins and this document has a bug.