Skip to main content
Every turn is a stream of typed events, decoded once so nothing above has to parse prose.

Quick Start

1

Decode a frame

decodeEvent never throws. Every rejection is a value with a reason, so a client can count them.
2

Stop at the terminal event

After a terminal event nothing else follows.

The 11 Events

Every event carries msgId. Fields listed are the ones beyond that.

How A Turn Streams


Fields That Trap Readers

callId vs approvalId are never interchangeable. callId says which row to attach the prompt to; approvalId is what gets sent back. In the two-approval case the prompts arrive in the opposite order to their tool rows, so zipping by index crosses rm with curl.
end.userIndex === null means “not on disk”. The write failed, so Fork and Delete must be withheld. Index 0 is a valid, persisted message — a falsy check (if (!userIndex)) is a trap.
Terminal events are mutually exclusive and final. After end, cancelled, or error, nothing else may follow. A cancelled turn is never persisted, so no end and no usage follow it.
Unknown ErrorKind degrades to "internal". decode.ts maps any kind it does not recognise to "internal" rather than dropping the event, so a newer engine’s category still reaches the user.

Error Kinds

ErrorKind selects the recovery the UI offers.

Best Practices

ok is the only signal of tool success. Inferring success from a non-empty output is the exact defect the field prevents.
Bind a decision to a row by approvalId, not by position — position holds only while exactly one approval is outstanding.
For end.userIndex and tool_result.seconds, null means “unknown / not persisted”, which is different from 0 and from absent.

Approvals & Cancellation

The human-in-the-loop flow on mobile.

Capabilities & Gaps

Which events each engine emits today.