Agent-First Data v0.5: Logs Became Protocol Events

by Agent-First Kit Contributors

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:

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.