Skip to main content

Module verify

Module verify 

Source
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

RuleRejected
RunEndedanything after RUN_FINISHED / RUN_ERROR
DuplicateRunStarteda second RUN_STARTED
DuplicateStartopening a message, reasoning block, tool call, step or subagent whose id is already open — or a subagent id that already finished in this run
NotOpencontent or a terminator for something that was never opened, including SUBAGENT_FINISHED / SUBAGENT_ERROR for a subagent that is not active
UnknownIdTOOL_CALL_RESULT for a call id that was never introduced, or a parentSubagentRunId that was never started
OutOfOrderTOOL_CALL_RESULT before the call’s TOOL_CALL_END
OpenAtFinishRUN_FINISHED while a message, reasoning block, tool call, step or subagent is open
OwnerMismatcha 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.