Expand description
The ordering state machine.
TEXT_MESSAGE_CONTENT without a preceding TEXT_MESSAGE_START is a bug
that currently surfaces as a confused frontend, three network hops from
where it was caused. Neither the TypeScript SDK (which verifies on the
client) nor the .NET one (which does not verify) catches it on the server.
This crate does, on by default.
§What it rejects
| Rule | Rejected |
|---|---|
RunEnded | anything after RUN_FINISHED / RUN_ERROR |
DuplicateRunStarted | a second RUN_STARTED |
DuplicateStart | opening a message, reasoning block, tool call, step or subagent whose id is already open — or a subagent id that already finished in this run |
NotOpen | content or a terminator for something that was never opened, including SUBAGENT_FINISHED / SUBAGENT_ERROR for a subagent that is not active |
UnknownId | TOOL_CALL_RESULT for a call id that was never introduced, or a parentSubagentRunId that was never started |
OutOfOrder | TOOL_CALL_RESULT before the call’s TOOL_CALL_END |
OpenAtFinish | RUN_FINISHED while a message, reasoning block, tool call, step or subagent is open |
OwnerMismatch | a tagged continuation, terminator or re-open whose subagentRunId is not the one that opened the entity — a message, reasoning block, tool call, activity, or the entity a REASONING_ENCRYPTED_VALUE names; a tool call tagged with one subagent whose parent message belongs to another |
RUN_ERROR is exempt from OpenAtFinish: a run that blew up mid-message
could not have closed it.
§Subagents
Every entity is opened by someone — a subagent, or the parent agent when
the opener carries no subagentRunId — and the verifier remembers who.
A later event that names a different owner is rejected; one that names
none is accepted, because attribution is optional on every event and a
bare continuation is what a pre-subagent producer sends — and it does not
hand the entity to the parent either: the first writer stays the owner, as
upstream records it. Steps are keyed by owner as well as name, so a
subagent cannot close the parent’s step, or a sibling’s, and two agents may
run a step of the same name at once. A MESSAGES_SNAPSHOT seeds ownership
from the messages it carries and is authoritative; the RUN_STARTED input
echo seeds it too, for ids not yet recorded; a TOOL_CALL_RESULT mints the
tool message it names under its own attribution.
What is deliberately not checked, because the protocol does not require
it: that an attributing subagentRunId was announced by SUBAGENT_STARTED
(attribution without lifecycle events is a supported mode), that a
subagent’s own messages are closed before its SUBAGENT_FINISHED, or that
events stop after it — a parentSubagentRunId may even name a subagent
that already finished, since a parent legitimately finishes before its
child. What is required is that every started subagent is closed before
RUN_FINISHED.
§What it lets through
The *_CHUNK events are self-contained by design, so a chunk carrying a new
id registers that id rather than being rejected for having no start. The
deprecated THINKING_* family is not tracked at all. State, activity, raw
and custom events are unordered — though an activity delta, like any
continuation, may not name an owner other than its activity’s.
§Cost
A handful of maps and one lookup per event. Turning the verify feature
off replaces the whole state machine with a zero-sized type whose
observe is an inlined Ok(()). In debug builds a rejection additionally
carries a dump of everything still open, which is the expensive part and is
why it is debug-only.