Agent Skill

This skill content is tool-agnostic. It can be used by any AI coding agent workflow. The frontmatter keys at the top are metadata for skill runners and do not change the AFDATA conventions.

Three parts:

  1. Naming — encode units and semantics in field names so agents parse structured data without external schemas
  2. Output — suffix-driven formatting with key stripping, value formatting, and automatic secret redaction
  3. Protocol — optional JSONL protocol with code (required) and trace (recommended)

Part 1: Naming Convention

The field name is the schema. Always encode units and semantics in the field name.

Duration

SuffixUnitExample
_nsnanosecondsgc_pause_ns: 450000
_usmicrosecondsquery_us: 830
_msmillisecondslatency_ms: 142
_ssecondsdns_ttl_s: 3600
_minutesminutessession_timeout_minutes: 30
_hourshourstoken_validity_hours: 24
_daysdayscert_validity_days: 365

Timestamps

SuffixFormatExample
_epoch_msmilliseconds since Unix epochcreated_at_epoch_ms: 1707868800000
_epoch_sseconds since Unix epochcached_epoch_s: 1707868800
_epoch_nsnanoseconds since Unix epochcreated_epoch_ns: 1707868800000000000
_rfc3339RFC 3339 date-time stringexpires_rfc3339: "2026-02-14T10:30:00Z"

Strict string formats

SuffixFormatExample
_bcp47BCP-47 language tag stringlanguage_bcp47: "zh-CN"
_utc_offsetfixed UTC offset stringtimezone_utc_offset: "+08:00"
_rfc3339_dateRFC 3339 full-date stringinvoice_due_rfc3339_date: "2026-06-13"
_rfc3339_timeRFC 3339 partial-time stringmarket_open_rfc3339_time: "09:30:00"

*_bcp47 identifies a BCP-47 language tag string. AFDATA does not implement the full BCP-47 registry; tools may validate tags when needed.

*_utc_offset identifies a fixed UTC offset. Canonical persisted and structured output values are "UTC" or ±HH:MM, with HH in 00..23 and MM in 00..59; zero offsets normalize to "UTC". This is not an IANA timezone name, DST rule, or timezone database field.

*_rfc3339_date identifies an RFC 3339 full-date string (YYYY-MM-DD). It is a calendar date, not an instant, and has no time, offset, or timezone.

*_rfc3339_time identifies an RFC 3339 partial-time string (HH:MM:SS[.fraction]). It is a time-of-day, not an instant, and MUST NOT include Z, ±HH:MM, IANA timezone names, or other timezone annotations. Use _rfc3339 or _epoch_* for instants.

Do not create companion timezone-name fields as an AFDATA core pattern. If a future tool needs IANA timezone semantics with a timestamp, prefer a self-contained standard value such as RFC 9557.

Avoid magic string sentinels such as "auto" inside strict-format fields. If a tool needs auto/default behavior, define it in that tool’s own config semantics, not as an AFDATA-wide rule.

Size

SuffixExample
_bytespayload_bytes: 456789 (always numeric)
_sizebuffer_size: "10M" (config files only, human-readable)

_size parsing rules (binary): B=1, K=1024, M=1024², G=1024³, T=1024⁴. Case-insensitive.

parse_size("10M")10485760. Returns null for invalid or negative input.

Percentage

SuffixExample
_percentcpu_percent: 85

Currency

Bitcoin:

SuffixExample
_msatsbalance_msats: 97900
_satswithdrawn_sats: 1234
_btcreserve_btc: 0.5

Fiat — _{iso4217}_cents for currencies with 1/100 subdivision, _{iso4217} for currencies without. Generic _{code}_cents matches only 3-4 ASCII letters:

SuffixExample
_usd_centsprice_usd_cents: 999
_eur_centsprice_eur_cents: 850
_jpyprice_jpy: 1500
_usdt_centsdeposit_usdt_cents: 1000

Sensitive

SuffixHandlingExample
_secretredact the entire value/subtree to ***api_key_secret: "sk-or-v1-abc..."
_urlscrub secrets inside the URL value, keep the restcallback_url: "https://h/cb?code_secret=..."

All CLI output formats (JSON, YAML, Plain) automatically redact _secret fields. Matching recognizes _secret and _SECRET only — no mixed case. The entire _secret value/subtree becomes ***, including objects and arrays. For legacy fields that cannot be renamed, configure OutputOptions.redaction with secret_names/secretNames such as ["api_key", "authorization"]; names match exact field names at any nesting level; no trim, case folding, hyphen/underscore normalization, globs, regex, or substring matching. Callers that need schema-preserving YAML/plain rendering can pass OutputOptions with the Raw output style.

Name URL-valued fields _url so the userinfo password and any _secret/secret_names query parameter inside them are scrubbed automatically (the rest of the URL is preserved; the suffix is not stripped). For a URL inside a free-form message, redact it with redact_url_secrets before interpolating — _url only fires on whole-URL field values, never on prose.

_url scrubs the userinfo password and suffix-named params only — not arbitrary credential params. Common parameters like ?access_token=, ?api_key=, ?code=, ?sig= are NOT redacted unless their name ends in _secret or is passed in secret_names. Rename params you own to the suffix (?access_token_secret=); list the rest in secret_names. A _url value that is not a clean scheme-prefixed URL but carries internal whitespace or an @ credential sigil (e.g. a schemeless user:pass@host/db) is redacted wholesale to *** (fail-closed).

Environment variables

Same suffixes, UPPER_SNAKE_CASE:

DATABASE_URL_SECRET=postgres://user:pass@host/db
CACHE_TTL_S=3600
TOKEN_VALIDITY_HOURS=24

No suffix needed

Fields whose meaning is obvious: redb_path, proof_count, search_enabled, method, domain, model. (URL fields are the exception — use _url so embedded secrets are scrubbed.)

Database columns

Use suffixes on generic types (INTEGER, BIGINT, TEXT). Native types that carry semantics (TIMESTAMPTZ, INTERVAL) don’t need suffixes.

ColumnTypeSuffix?Why
created_atTIMESTAMPTZnotype says timestamp
duration_msINTEGERyesinteger is ambiguous
api_key_secretTEXTyesenables auto-redaction
retry_countINTEGERnomeaning obvious

ORM struct fields preserve the suffix: duration_ms: i64, not duration: i64.

Common mistakes

BadGoodWhy
timeout: 30timeout_s: 3030 what? seconds? ms?
timestamp: 1707868800cached_epoch_s: 1707868800what unit? what event?
size: 456789payload_bytes: 456789bytes? KB?
price: 999price_usd_cents: 999what currency? what unit?
latency: 142latency_ms: 142seconds? milliseconds?
api_key: "sk-..."api_key_secret: "sk-..."won’t be auto-redacted
cpu: 85cpu_percent: 8585 what?
buffer: "10M"buffer_size: "10M"only _size gets parsed

Part 2: Output Processing

Three output formats. Default YAML and Plain apply key stripping + value formatting.

Formats

Key stripping (YAML and Plain)

Remove recognized formatting suffix from key. Longest match first, exact lowercase or uppercase only:

  1. _epoch_ms, _epoch_s, _epoch_ns
  2. _usd_cents, _eur_cents, _{code}_cents (code is 3-4 ASCII letters)
  3. _rfc3339, _minutes, _hours, _days
  4. _msats, _sats, _bytes, _percent, _secret
  5. _btc, _jpy, _ns, _us, _ms, _s

_size, _bcp47, _utc_offset, _rfc3339_date, and _rfc3339_time are NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting). Redaction runs before collision handling, so fallback never restores a secret.

Value formatting (YAML and Plain)

Type constraints: _bytes/_epoch_* require integer. _usd_cents/_eur_cents/_jpy/_{code}_cents require non-negative integer. Duration/Bitcoin/_percent accept any number. Wrong type → raw value + original key.

Plain logfmt details

Key ordering

YAML and Plain sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785). For ASCII keys this equals byte-order sorting.


Part 3: Protocol Template (Optional)

Every output line carries a code field:

codeWhen
"log"Diagnostic event (event field identifies startup/request/progress/retry/redirect)
tool-definedStatus/progress ("request", "progress", "sync", etc.)
"ok"Success result
"error"Error result

Channel policy:

Optional stream redirection:

Recommended enforcement:

Templates

{"code": "log", "event": "startup", "version": "0.1.0", "argv": ["tool", "--log", "startup"], "config": {...}, "args": {...}, "env": {...}}
{"code": "ok", "result": {...}, "trace": {"duration_ms": 12, "source": "redb"}}
{"code": "error", "error": "message", "trace": {"duration_ms": 3}}
{"code": "not_found", "resource": "user", "id": 123, "trace": {"duration_ms": 8}}

Always include trace for execution context: duration, token counts, cost, data source. Startup payload fields are tool-defined; config is recommended, while version/argv/args/env are optional.

Same structure, any transport

TransportFormat
CLI stdoutJSONL
REST APIJSON body
MCP toolJSON
SSE streamJSONL

All use code / result / error / trace. Do not split protocol events across stdout and stderr.


Library Usage

Use the local language README for installation and the full overview/spec for API reference. Keep this skill focused on naming, output, protocol, logging, and review rules rather than duplicating import snippets that drift across languages.

Required cross-language behavior to rely on:

AFDATA Logging

Structured logging that outputs via the library’s own output_json/output_plain/output_yaml. Each language integrates with its native logging ecosystem. All three formats apply the same suffix processing, key stripping, and secret redaction as the core output API.

Init (pick one format per process)

FormatRustGoPythonTypeScript
JSONafdata_tracing::init_json(filter)afdata.InitJson()init_logging_json("INFO")initJson()
Plainafdata_tracing::init_plain(filter)afdata.InitPlain()init_logging_plain("INFO")initPlain()
YAMLafdata_tracing::init_yaml(filter)afdata.InitYaml()init_logging_yaml("INFO")initYaml()

Rust requires cargo add agent-first-data --features tracing.

Spans (add fields to all log events in scope)

// Rust — tracing spans
let span = info_span!("request", request_id = %uuid);
let _guard = span.enter();
// Go — context-based
ctx := afdata.WithSpan(ctx, map[string]any{"request_id": uuid})
logger := afdata.LoggerFromContext(ctx)
# Python — contextvars
with span(request_id=uuid):
    logger.info("Processing")
// TypeScript — AsyncLocalStorage
await span({ request_id: uuid }, async () => {
  log.info("Processing");
});

Output fields

Every log line contains timestamp_epoch_ms, message, code: "log", level (debug/info/warn/error), plus span fields and event fields. Do not use the log level as code; code:"error" is reserved for terminal protocol errors.

Log redaction is by field name (the same _secret/_url rule as all output), applied when the line is emitted. So name the secret field — info!(api_key_secret = %key) — rather than logging a whole object by its Debug/string rendering, which hides the inner field names from redaction. For structured/nested secret-bearing data, build a value, redact it (redact_secrets_in_place), then emit via output_* — do not pass the struct to a ?/%-rendered log field.

CLI Flags

CLI tools that use AFDATA should support output and logging flags:

--output json|yaml|plain    # default is tool-defined (interactive → yaml, scripting/logging → json)
--log startup,request,progress,retry,redirect
--verbose                   # shorthand for all log categories

Review Checklist

When reviewing code that produces structured output:

  1. Every numeric field with a unit has the correct suffix (_ms, _bytes, _sats, _percent, etc.)
  2. Timestamps use _epoch_ms / _epoch_s / _rfc3339; date-only/time-only strings use _rfc3339_date / _rfc3339_time
  3. Sensitive values end in _secret and are redacted in all output paths
  4. Transport payloads / CLI output use code / result / error / trace structure
  5. Config files use the same suffixes as output
  6. No unit-less ambiguous fields (timeout: 30 — 30 what?)
  7. Config size values use _size suffix (buffer_size: "10M", not buffer: "10M")
  8. Environment variables follow UPPER_SNAKE_CASE with the same suffixes
  9. Logging uses AFDATA init functions (init_json/init_plain/init_yaml) — not raw println!/fmt.Println/console.log for structured output
  10. Database columns use AFDATA suffixes on generic types (duration_ms INTEGER, not duration INTEGER); native types like TIMESTAMPTZ don’t need suffixes
  11. CLI flag parsing uses cli_parse_output/cli_parse_log_filters/build_cli_error/version helpers — not custom reimplementations; uses try_parse() not parse() in Rust so clap errors go to stdout as JSONL