Agent-First Data v0.21: Even `--version` Speaks the Protocol

by Agent-First Kit Contributors

v0.21 turns the last bespoke line of CLI output into the protocol. `--version` no longer prints `tool 1.2.3` as human text — it always answers with a structured protocol-v1 version event, JSON by default, carrying name, version, and optional display_name/build. The pre-parser that intercepts it before your argument parser now recognizes your own value-taking global flags, so `tool --stdout-file x --version` still works. Shipped identically across Rust, Go, Python, and TypeScript.

An agent-first CLI collapses its output into one protocol: a kind:"result" (or kind:"error") envelope with a trace, JSON by default, the same shape whether it left over stdout, an HTTP body, an MCP tool result, or an SSE frame. Every output followed that rule except one. --version still printed tool 1.2.3 — a bespoke line of human text, the single place an agent had to stop parsing the protocol and start scraping a string.

v0.21 closes that crack. It’s a breaking release; AFDATA is still pre-1.0 and carries no compatibility layer.

One less line to special-case

--version and -V now always answer with a structured version event — JSON by default, --output yaml|plain or --json for another renderer. There is no conventional-text mode and no knob to bring one back:

$ tool --version          # v0.20
tool 1.2.3

$ tool --version          # v0.21
{"kind":"result","result":{"code":"version","name":"tool","version":"1.2.3"},"trace":{}}

The old API carried a conventional_default() seam precisely so bare --version could stay human text while an explicit --output went structured. That seam is gone. Version output is a result like any other result, so the same reader that handles a tool’s real output handles its version too.

A richer payload

The event now names itself. Alongside version it carries name (the short bin identity), and optionally a human-facing display_name and an opaque build identifier the caller supplies — a git commit SHA, most usefully:

{"kind":"result","result":{"code":"version","name":"afdata",
  "version":"0.21.0","display_name":"Agent-First Data","build":"a1b2c3d"},"trace":{}}

display_name and build are omitted when absent, so the minimal event stays {code, name, version}. What build means is entirely the caller’s business; AFDATA just carries it through.

The pre-parser learned your flags

Here is the part that was actually hard. --version has to be handled before your real argument parser — clap, argparse, commander — or the parser’s own built-in version handling swallows it and --version --output json never gets a chance to be structured. So AFDATA scans argv for a top-level version request first, stopping at the first positional (your subcommand) so that tool sub --version <value> stays the subcommand’s business.

The trap is a value-taking global flag standing in front of --version:

tool --stdout-file /var/log/out --version

A naive scanner sees /var/log/out, decides “that’s a positional, must be the subcommand,” and stops — one token short of the --version it was looking for. The version request vanishes.

v0.21 fixes it by asking the caller which of their flags take a value. In Rust you hand over your clap Command and AFDATA introspects it; in Go, Python, and TypeScript you pass an explicit list of your value-taking global flags. Either way the pre-parser consumes the value and keeps scanning:

let build = option_env!("GIT_SHA");
cli_handle_version_or_continue(&argv, &Cli::command(), "tool", Some("Tool"), version, build)?;

There is no hardcoded flag list inside AFDATA. It recognizes global flags it has never heard of — including ones you add long after this release — because the knowledge lives in your command definition, not in ours. The subcommand-boundary check itself is unchanged: tool sub --version 1.3.0 still belongs to sub.

Four languages, one behavior

This lands identically in Rust, Go, Python, and TypeScript. The normative spec’s version-output section is rewritten to match, and the four-language end-to-end suite now drives --version through every SDK and asserts the same structured event out of each — so the behavior can’t quietly drift apart again.

The shape now

Version is no longer the exception. Everything an agent-first CLI emits — its results, its errors, and now its own version — is one protocol an agent can read without a single special case. The one line of bespoke text is gone.