Agent-First Data v0.26.2: Which Mode Is This Command?
The CLI Event Framing spec defined two consumption modes but never said how to tell which one a command is in — so two spores independently shipped caller-needed data onto the diagnostic stream. v0.26.2 adds the missing test: a command producing more than one caller-needed output over time is an event stream, defaults to stdout, and refuses split.
The spec’s Channel policy has always described two consumption modes. Finite
one-shot commands split by kind — result to stdout, diagnostics to stderr —
so x=$(tool …) can never capture a failure as data. Event streams keep every
event on one stream, because splitting them would destroy the ordering that
makes them a stream.
What it never said was how to tell which one you have. That turned out to matter, because two spores answered it differently and both got it wrong in the same direction.
The two commands that fell through
afpsql --stream-rows streams a large result set as an opening event, N batches
carrying the rows, and a terminator. Under the finite-mode split, the batches
are kind:"progress" and go to stderr, while the terminator — which carries a
row count and no rows — goes to stdout. So rows=$(afpsql --stream-rows …)
captured an empty terminator and the actual data went to the diagnostic stream.
afmail triage review prints a URL a human must open, waits an unbounded time
for that human to decide, then reports the decision. The URL was progress and
went to stderr; only the decision reached stdout. An agent that captured stdout
had no way to tell the user where to go.
Both look like single operations. Neither is finite, and the spec gave no way to say so.
The missing test
A command is an event stream when it produces more than one caller-needed output over time. Two shapes qualify:
- a payload delivered in chunks, where the batches carry the data and therefore cannot fit in the single terminal event finite mode allows
- an operation that reports something the caller must act on before it can finish, and then reports its outcome
Finite mode’s defining limit is at most one terminal event. Neither shape can live inside that, and splitting either strands the caller’s data on stderr.
So an event-stream command now defaults to --output-to stdout rather than
split, and rejects an explicit --output-to split as a usage error — the same
way a raw-scalar reader already rejects a non-default destination. Its
intermediate data-carrying events stay kind:"progress": once the whole stream
shares one destination, kind no longer decides routing, and the command still
emits exactly one terminal event. Nothing about the event shapes changes.
The invariant worth remembering
Data the caller captures is never routed to the diagnostic stream.
Which gives a diagnostic of its own. If you find yourself emitting a
kind:"progress" event carrying a payload the caller has to read, that is not a
finite command with an unusual payload — it is an event stream that has not
declared itself. Both bugs above look exactly like that from the outside, and
now there is a rule that names them.
Upgrading
v0.26.2 is documentation: the spec’s Channel policy section and the OutputTo
rustdoc. No API, wire format, or behavior changes, and no code in this crate
moved. What changes is what a spore is supposed to do — agent-first-psql
v0.8.0 and agent-first-mail already implement the rule for their streaming and
review/serve commands respectively.