Agent-First Data v0.5: Logs Became Protocol Events
The v0.5 update made logging part of the same agent-readable contract as output: structured events, span context, secret redaction, and stdout-only channel discipline.
A tool can return perfect JSON and still confuse an agent through its logs.
That happens when results go to stdout, warnings go to stderr, debug messages use a different format, and request context is spread across several interleaved lines. The agent sees a transcript, not a state stream.
Agent-First Data v0.5 moved logging into the same contract as output. A log line is not terminal decoration. It is an event the agent may need to read, filter, redact, and connect to the work that produced it.
The problem: logs were structured after the fact
Most logging libraries are designed for humans and log backends. They can be
made JSON-ish, but the shape usually differs from the tool’s result protocol.
One line may use level, another uses severity, another hides request fields
inside a formatted message.
Agents need something simpler:
{"code":"info","message":"Processing","request_id":"abc-123","timestamp_epoch_ms":1739000000000}
The event has the same suffix rules as every other AFDATA value. If a field is
duration_ms, the unit is known. If a field is api_key_secret, it is redacted.
If a request span has request_id, every event inside the span carries it.
The change: native logging integrations speak AFDATA
The v0.5 line added AFDATA logging integrations across the supported languages:
- Rust uses a
tracinglayer. - Go uses a
log/sloghandler. - Python uses a
logginghandler withcontextvarsspan context. - TypeScript uses a zero-dependency logger with
AsyncLocalStoragespan context.
Each integration outputs through the same formatter family as normal AFDATA values: JSON for machines, plain for compact scanning, YAML for detailed human inspection.
use agent_first_data::afdata_tracing;
use tracing::{info, info_span};
use tracing_subscriber::EnvFilter;
afdata_tracing::init_json(EnvFilter::new("info"));
let span = info_span!("request", request_id = %uuid);
let _guard = span.enter();
info!(code = "log", "Processing");
The important part is not the exact language API. The important part is that log context stops being prose. It becomes fields.
The rule: stdout is the protocol channel
The same update also hardened the channel rule: machine-readable runtime events belong on stdout.
stderr is not a second protocol stream. It is reserved for unrecoverable
pre-protocol startup failures where the program cannot emit structured data at
all. If a normal runtime warning or error can be represented as AFDATA, it should
be an event on stdout.
That makes agent consumption boring in the best way. The agent reads one stream,
one object per line, and every line has a code.
The result: concurrent work keeps its context
Span fields matter most when work overlaps. Without spans, logs from two requests interleave and the agent has to guess which error belongs to which operation.
With AFDATA logging, span fields are flattened into each event line:
{"code":"error","message":"DNS lookup failed","request_id":"abc-123","domain":"api.example.com","duration_ms":23}
The agent does not reconstruct context from nearby lines. The context is inside the line it is reading.
Where this fits: tools that agents supervise
This update made AFDATA useful beyond final results. It applies to long-running CLIs, local daemons, HTTP clients, database tools, and any command where an agent needs to watch progress while preserving a reliable protocol boundary.
The design principle is simple: if an agent might need it, make it data. Logs are not exempt.