Draft — not yet ratified. This document is published for review. It
describes the intended 1.0 behaviour, its wording is not final, and nothing
here is covered by a compatibility promise until a version is frozen. Do not
cite it as a stable reference.
The binary binding, for consumers that want a compact wire. It shares the
SSE binding’s request half entirely —
the run input is the same JSON POST — and differs only in the response.
Negotiation
- A client that wants this binding includes
application/vnd.ag-ui.event+proto in its Accept header, alongside
text/event-stream. Admission is what opts in — a wildcard range
(*/*, application/*) admits the media type too — so a client that
cannot consume protobuf MUST send an explicit Accept naming what it can,
rather than a wildcard or down-ranked entry: relative quality values
between the two media types are not consulted.
- A producer that supports the binding SHOULD answer with it whenever the
client’s
Accept admits the media type with a positive quality, and MUST
answer SSE otherwise. A producer that does not support it ignores the media
type — which is why a client MUST always be prepared to receive SSE.
- The response’s
Content-Type is exactly application/vnd.ag-ui.event+proto;
the consumer selects its parser by this header.
Framing
The response body is a sequence of frames, each one protocol event:
- A frame is a 4-byte length header — an unsigned 32-bit big-endian integer —
followed by exactly that many bytes of one encoded event.
- Frames abut with no separator. A consumer MUST tolerate frames split across
transport chunks and multiple frames within one chunk.
- A body that ends mid-frame is a
truncated run.
The wire schema
The protobuf message definitions are generated from the same
JSON Schema the SDKs are generated from; the wire
schema is not a second source of truth. Cross-implementation parity is part of
the conformance surface: a corpus of canonical events pins the encoded bytes,
and every first-party encoder MUST reproduce the corpus byte for byte. (The
corpus is the guarantee — two encoders handed the same semantic value may
still order the entries of an open JSON object differently, which protobuf
map encoding makes visible.)
Semantics stay as close to the SSE binding as a binary wire allows:
- An event decoded from a frame enters the same
processing pipeline as an event parsed from
SSE — middleware first, enforcement after, identically.
- Material the wire schema carries as open payloads —
RUN_FINISHED.outcome,
metadata, state, rawEvent — survives decoding and reaches that pipeline:
an unrecognised outcome arriving on this binding is stripped by enforcement
with a warning, exactly as on SSE, and the binding MUST NOT reject it at
decode time.
- The binary wire is inherently narrower for the rest. A field this build’s
wire schema predates is skipped by protobuf decoding itself, silently — the
stripped-with-warning behaviour is fully observable only on the JSON wire.
The same narrowing covers protocol-legal open members the wire schema does
not model: a JSON Patch operation’s extension members, which the JSON wire
MUST preserve, do not survive this one.
An event whose envelope arm this build predates carries no type string to
hand onward, so the binding drops the frame with a warning — the same
answer enforcement gives, spelled at the transport.
- A frame whose bytes do not decode as a message is malformed transport
material and fatal to the stream. A body that simply ends mid-frame is not:
that is a truncated run.
Errors
Identical to the SSE binding:
rejection before the stream is an HTTP error status; failure inside the stream
is RUN_ERROR, as a frame like any other.