Agent-First Data v0.23: The Shell Speaks Protocol Too

by Agent-First Kit Contributors

AFDATA's structured log/result/error protocol was available to anything that linked an SDK or called the CLI — except the layer where agent tooling actually runs: the Bash script that invokes cargo, npm, and wrangler. v0.23 fixes that with `afdata emit` (one protocol event from shell-safe scalar arguments) and `afdata shell bash`, a sourceable Bash 3.2+ authoring kit embedded in the binary. It gives a plain script AFDATA-style argument parsing, config reads, and structured events — under one firm rule: structure your own words, and pass everything else through untouched. `afdata_run` wraps a child without touching its stdout, stderr, TTY, colors, or exit status; `afdata_call` keeps one script the sole owner of the terminal result even when scripts orchestrate scripts.

AFDATA has always been about one thing: output an agent understands the first time, with no schema to ship on the side. Results on stdout, errors on stderr, a stable error.code, _secret fields redacted. Any program that links the Rust, Go, Python, or TypeScript SDK gets it; so does anything that calls the afdata CLI.

Except the layer where agent tooling actually runs. The release script, the deploy wrapper, the “build then publish then tag” glue — that’s a Bash script calling cargo, npm, wrangler. And Bash was the one place that couldn’t speak the protocol without hand-rolling JSON with printf or staying entirely unstructured. The most agent-facing layer was the least agent-legible.

v0.23 closes that gap.

One event from the shell

The smallest piece is afdata emit. It writes exactly one protocol-v1 event from plain scalar arguments — nothing to quote-escape, nothing to assemble:

afdata emit log info "Starting build"
afdata emit result "Build complete"
afdata emit error build_failed "Build failed" --hint "Inspect the compiler output"

The same routing every AFDATA CLI uses applies: the log lands on stderr, the result on stdout, the error on stderr with exit code 1. --output yaml and --output-to stdout work here too, so a consumer that wants one unified stream gets it. This is the whole protocol, reachable from a line of shell.

A kit, embedded in the binary

Emitting events by hand still leaves a script rebuilding the boring parts — option parsing, --help, reading a value out of a config file. So the CLI now carries a sourceable Bash authoring kit and prints it on demand:

#!/usr/bin/env bash
set -euo pipefail
_AFDATA_BASH_SOURCE="$("${AFDATA_BIN:-afdata}" shell bash)"
source /dev/stdin <<<"$_AFDATA_BASH_SOURCE"
unset _AFDATA_BASH_SOURCE

afdata_args_begin "deploy.sh [OPTIONS] PROJECT [-- WRANGLER_ARG ...]"
afdata_args_option config_path --config PATH "Configuration file" config.toml
afdata_args_flag  dry_run     --dry-run      "Prepare without deploying"
afdata_args_positional project PROJECT       "Project to deploy"
afdata_args_rest  WRANGLER_ARG               "Arguments forwarded to wrangler"
afdata_args_parse "$@"

account_id="$(afdata_config_get "$config_path" cloudflare.account_id)"
afdata_log info "Preparing ${project} for account ${account_id}"

[ "$dry_run" = true ] || afdata_run wrangler deploy "${AFDATA_ARGS_REST[@]}"
afdata_result "Deployment complete"

The declarations assign straight into the named Bash variables — flags become true/false, options and positionals are strings, trailing arguments survive byte-for-byte in AFDATA_ARGS_REST. Every parser gets -h/--help, --output, and --output-to for free, and malformed arguments come back as a structured cli_error, not a stack of echos. afdata_config_get delegates to afdata value, so the same format detection, dot-path grammar, and secret gate apply.

It’s embedded, not installed. cargo install, Homebrew, Scoop, and the release archives all expose the exact same afdata shell bash with no separate shared-data directory to keep in sync — and the kit is Bash 3.2 compatible, so it runs on a stock macOS shell. Sourcing it changes no shell options and prints nothing.

The firm rule: structure your own words, pass everything else through

The interesting design decision isn’t the parser. It’s what happens when your script calls cargo.

The tempting thing would be to capture the child’s output and relabel each line as an AFDATA log event. It’s also wrong. cargo’s colored diagnostics, npm’s progress bar, wrangler login’s interactive prompt — those are the child’s interface, and rewrapping them breaks the TTY, mangles colors, and turns a readable error into JSON noise.

So afdata_run refuses to. It emits a start log and a completion log — the script’s own words — and otherwise gets out of the way:

afdata_run cargo test --all-features   # colors, progress, exit status: untouched
afdata_run wrangler login              # stays fully interactive

The child’s stdin, stdout, stderr, signals, and exit status pass through exactly. Only the script’s lifecycle is structured; a child failure becomes a terminal child_process_failed error carrying the child’s real exit code. Command arguments are deliberately left out of the wrapper’s logs, because they may carry secrets.

For noninteractive steps whose output is only noise until something breaks, --quiet is an explicit policy on top:

afdata_run --quiet cargo test --all-features

It buffers the child’s combined output in memory, discards it on success to save an agent’s tokens, and replays it on stderr only if the child fails — before the terminal error. Buffering in memory rather than a temp file means an interrupted script leaves nothing behind to clean up.

One owner of the result

The last piece shows up once scripts orchestrate scripts. A protocol stream has exactly one terminal event — one result, or one error. But if release.sh calls build.sh and deploy.sh, and each ends with its own afdata_result, you’d get three “done“s in one stream. Which one is the result?

afdata_call answers it. It runs a cooperating AFDATA-Bash child with its logs and errors fully live, but demotes the child’s successful afdata_result to an info log:

afdata_call "$SCRIPT_DIR/build-assets.sh"
afdata_call "$SCRIPT_DIR/deploy.sh"
afdata_result "Release complete"       # the only terminal result in the stream

The child still owns its failures — an error stays terminal and its exit status propagates — but only the outermost script owns success. The stream validates as one finite protocol run, every time.

Where it lands

AFDATA now reaches from the SDK down to the shell script that ties a release together, with the same envelopes, the same routing, the same secret rules. And it does it without pretending a Bash script is something it isn’t: the kit structures the words the script owns and passes the rest through untouched. Run afdata shell bash to read the whole thing — it’s the library that gets printed, not hidden.