Agent-First Data v0.26.2: Which Mode Is This Command?

by Agent-First Kit Contributors

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:

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.