Agent-First Data v0.17: Units in the Key, Structure in the Tag

by Agent-First Kit Contributors

v0.17 removes the _size config suffix and the parse_size helper — a byte count is a number, and its unit belongs in the field name — and finishes the lint sweep: _bcp47 and _rfc3339 structure are validated, _url must be a single URL, and duration and currency suffixes are checked to be numbers.

AFDATA had two ways to talk about bytes. _bytes held an integer — the honest, computable form. _size held a string with a unit baked in, buffer_size: "10MiB", meant for humans hand-writing config files.

Two ways to say one thing is one way too many, and this was the wrong one to keep. v0.17 removes the _size suffix and the parse_size / ParseSize / parseSize helper from all four SDKs, the spec, the skill, and the machine- readable registry. It is a breaking release — AFDATA is still pre-1.0 and carries no compatibility layer.

Why _size had to go

The whole point of AFDATA is that a field name tells an agent everything it needs to know about the value. _size broke that promise three ways at once.

It collided with names that mean something else. page_size, batch_size, pool_size are counts, not byte sizes. A suffix that fires on the most common “how many” field names is a suffix agents apply in the wrong places. Compare _bytes, _secret, _epoch_ms — nobody writes those by accident, and that is exactly what makes them safe.

It put a decision on every single write. "10MB" is 10,000,000 bytes; "10MiB" is 10,485,760 — a 4.8% gap hiding in one letter. "10M" is rejected outright, and so is a bare 10. Every value written is a fresh chance to pick the wrong unit or the wrong casing. A numeric _bytes field decides the unit once, at schema-design time, and never again.

It smuggled a string into structured output. _size passed through unformatted, so "10MiB" could land in a result payload where the next agent has to parse units back out before it can compare or sum. _bytes is a number the whole way down.

You never needed the string anyway

The reason _size existed was human ergonomics — "128MiB" reads better than 134217728 in a config file. But AFDATA already solves that on the output side, and has for a long time.

_bytes auto-formats in YAML and plain output, while JSON stays exact:

JSON     {"max_buffer_bytes": 134217728}
YAML     max_buffer: "128.0MiB"
plain    max_buffer=128.0MiB

The integer is what the agent stores and computes on; the human reading a YAML dump still sees 128.0MiB. _size was paying a parse-and-collision tax to buy a readable form that _bytes was already handing out for free.

The rule underneath

This is the same rule durations have always followed. A timeout is timeout_s: 30, not timeout: "30 minutes". The unit is welded into the key, the value is a plain number, and there is nothing to parse. Bytes now work the same way, in config files and outputs alike:

The unit belongs in the field name. The value is just the number.

_size was the one place AFDATA broke that rule. Now it doesn’t.

The other theme: tags stop trusting their strings

A _bcp47 field holds a language tag — content_language_bcp47: "en-US". Until v0.17 it accepted any string, which let the single most common mistake sail through: zh_CN, with an underscore. That form is correct in POSIX locales and Java’s Locale.toString(), so an agent carrying that habit writes it, it looks right, and nothing complains — even though a BCP 47 tag uses a hyphen, zh-CN.

v0.17 adds is_valid_bcp47 and wires it into afdata lint. It is a structural check, not a registry lookup: hyphen-separated subtags, a 2–3 letter primary language.

zh-CN        →  ok
zh-Hant-TW   →  ok
zh_CN        →  rejected  (underscore — the POSIX habit)
chinese      →  rejected  (primary subtag isn't 2–3 letters)
zz-ZZ        →  ok        (well-formed but unregistered — see below)

It deliberately stops at structure. Checking that zh and CN are actually registered IANA subtags is a heavier guarantee, so zz-ZZ passes the structural gate; a tool that needs the registry validates further on its own. But the change that matters catches the mistake agents actually make.

The theme is the same as the size change: take meaning an agent would otherwise have to remember and put it somewhere the convention can check.

Finishing the sweep

_bcp47 was the first tag to stop trusting its own value. Once one suffix earns a structural check, the ones sitting next to it that look validated but aren’t become the real hazard — they read as safe. So v0.17 finishes the pass: every suffix that can be checked now is.

_rfc3339 requires its offset. A _rfc3339 field names an instant — expires_rfc3339: "2026-02-14T10:30:00Z". RFC 3339 makes the trailing offset mandatory, because a wall-clock reading with no offset isn’t a point in time. But the field used to check only that the value was a string, so the mistake agents actually make walked right through: write the date and time, forget the Z.

2026-02-14T10:30:00Z              →  ok
2026-02-14T10:30:00.5+08:00       →  ok        (fractional seconds, explicit offset)
2026-02-14T10:30:00               →  rejected  (no offset — not a real instant)
2026-02-14 10:30:00Z              →  rejected  (space instead of T)
2026-02-14T10:30:00Asia/Shanghai  →  rejected  (an offset is not a timezone name)

is_valid_rfc3339 is composed from the _rfc3339_date and _rfc3339_time validators that already shipped — a full date, a T, a partial time with optional fractional seconds, and a real offset (Z or ±HH:MM). The three RFC 3339 suffixes now share one spine instead of one of them trusting the string.

_url must be a single URL. A _url value is already special — redaction scrubs credentials inside it. afdata lint now also checks it is a single URL: no internal whitespace, and no bare user:pass@host connection string wearing a URL’s field name. A scheme-prefixed URL keeps its userinfo (https://u:p@host is a real URL); a relative reference (/cb?page=2) passes; a DSN or a value with a space does not.

Durations and currency are numbers, not strings. This is the _size mistake in a different costume: timeout_s: "30 minutes" and price_usd_cents: 12.5 are exactly the “unit smuggled into the value” and “wrong shape” errors from earlier in this post. Lint used to say nothing about them; now _s/_ms/_minutes/… must be numeric and _cents/_micro/_jpy must be whole integers.

None of these change the wire format or the registry — the constraints were always written down. What changed is that afdata lint now enforces every one it can, so a malformed value is loud at author time instead of silent until some downstream agent trips over it.

Upgrading

The convention got one entry smaller, one collision safer, and every silent mistake louder. Bytes are numbers again, a language tag is a tag again, and a timestamp is an actual instant.