Go

Agent-First Data (AFDATA) — Suffix-driven output formatting and protocol templates for AI agents.

The field name is the schema. Agents read latency_ms and know milliseconds, api_key_secret and know to redact, no external schema needed.

Installation

go get github.com/agentfirstkit/agent-first-data/go

Quick Example

A backup tool invoked from the CLI — flags, env vars, and config all use the same suffixes:

API_KEY_SECRET=sk-1234 cloudback --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz

For CLI diagnostics, enable log categories explicitly:

--log startup,request,progress,retry,redirect
--verbose   # shorthand for all categories

Without these flags, startup diagnostics should stay off by default.

The tool reads env vars, flags, and config — all with AFDATA suffixes — and can emit a startup diagnostic event:

import afdata "github.com/agentfirstkit/agent-first-data/go"

startup := afdata.BuildJson(
    "log",
    map[string]any{
        "event":  "startup",
        "config": map[string]any{"timeout_s": 30, "max_file_size_bytes": 10737418240},
        "args":   map[string]any{"input_path": "/data/backup.tar.gz"},
        "env":    map[string]any{"API_KEY_SECRET": os.Getenv("API_KEY_SECRET")},
    },
    nil,
)

Three output formats, same data:

JSON:  {"code":"log","event":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
YAML:  code: "log"
       event: "startup"
       args:
         input_path: "/data/backup.tar.gz"
       config:
         max_file_size: "10.0GB"
         timeout: "30s"
       env:
         API_KEY: "***"
Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***

--timeout-stimeout_stimeout: 30s. API_KEY_SECRETAPI_KEY: "***". The suffix is the schema.

API Reference

Total: 15 public APIs and 2 types + AFDATA logging (3 protocol builders + 2 redacted value helpers + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + OutputFormat + RedactionPolicy)

Protocol Builders (returns map[string]any)

Build AFDATA protocol structures. Return JSON-serializable objects for transport payloads.

// Success (result)
BuildJsonOk(result any, trace any) map[string]any

// Error (simple message, optional hint — empty string means no hint)
BuildJsonError(message string, hint string, trace any) map[string]any

// Generic (any code + fields)
BuildJson(code string, fields any, trace any) map[string]any

Redacted Values (returns any)

Use these before raw HTTP/MCP/SSE serializers that do not call OutputJson.

RedactedValue(value any) any
RedactedValueWith(value any, redactionPolicy RedactionPolicy) any

Use case: structured protocol payloads (frameworks serialize to JSON)

Example:

import afdata "github.com/agentfirstkit/agent-first-data/go"

// Startup
startup := afdata.BuildJson(
    "log",
    map[string]any{
        "event":  "startup",
        "config": map[string]any{"api_key_secret": "sk-123", "timeout_s": 30},
        "args":   map[string]any{"config_path": "config.yml"},
        "env":    map[string]any{"RUST_LOG": "info"},
    },
    nil,
)

// Success (always include trace)
response := afdata.BuildJsonOk(
    map[string]any{"user_id": 123},
    map[string]any{"duration_ms": 150, "source": "db"},
)

// Error
err := afdata.BuildJsonError("user not found", "", map[string]any{"duration_ms": 5})

// Error with hint
errHint := afdata.BuildJsonError("wallet not found", "list wallets with: afpay wallet list", map[string]any{"duration_ms": 5})

// Specific error code
notFound := afdata.BuildJson(
    "not_found",
    map[string]any{"resource": "user", "id": 123},
    map[string]any{"duration_ms": 8},
)

CLI/Log Output (returns string)

Format values for CLI output and logs. OutputJson uses full _secret redaction by default. OutputJsonWith supports explicit scoped policies. YAML and Plain always redact _secret and apply human-readable formatting.

OutputJson(value any) string   // Single-line JSON, original keys, for programs/logs
OutputJsonWith(value any, redactionPolicy RedactionPolicy) string
OutputYaml(value any) string   // Multi-line YAML, keys stripped, values formatted
OutputPlain(value any) string  // Single-line logfmt, keys stripped, values formatted
type RedactionPolicy string
const (
    RedactionTraceOnly RedactionPolicy = "RedactionTraceOnly"
    RedactionNone      RedactionPolicy = "RedactionNone"
    RedactionStrict    RedactionPolicy = "RedactionStrict"
)

Example:

import afdata "github.com/agentfirstkit/agent-first-data/go"

data := map[string]any{
    "user_id":              123,
    "api_key_secret":       "sk-1234567890abcdef",
    "created_at_epoch_ms":  int64(1738886400000),
    "file_size_bytes":      5242880,
}

// JSON (secrets redacted, original keys, raw values)
fmt.Println(afdata.OutputJson(data))
// {"api_key_secret":"***","created_at_epoch_ms":1738886400000,"file_size_bytes":5242880,"user_id":123}

// YAML (keys stripped, values formatted, secrets redacted)
fmt.Println(afdata.OutputYaml(data))
// ---
// api_key: "***"
// created_at: "2025-02-07T00:00:00.000Z"
// file_size: "5.0MB"
// user_id: 123

// Plain logfmt (keys stripped, values formatted, secrets redacted)
fmt.Println(afdata.OutputPlain(data))
// api_key=*** created_at=2025-02-07T00:00:00.000Z file_size=5.0MB user_id=123

Internal Tools

InternalRedactSecrets(value any)  // Manually redact secrets in-place

Most users don’t need this. Output functions automatically protect secrets.

Utility Functions

ParseSize(s string) (uint64, bool)  // Parse "10M" → bytes

Returns (0, false) for invalid, negative, or overflow input.

Example:

import afdata "github.com/agentfirstkit/agent-first-data/go"

size, _ := afdata.ParseSize("10M")   // 10485760
size, _ = afdata.ParseSize("1.5K")   // 1536
size, _ = afdata.ParseSize("512")    // 512

CLI Helpers (for tools built on AFDATA)

Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing --output and --log handling in each tool.

type OutputFormat string  // "json" | "yaml" | "plain"

CliParseOutput(s string) (OutputFormat, error)    // Parse --output flag; error on unknown
CliParseLogFilters(entries []string) []string     // Normalize --log: trim, lowercase, dedup, remove empty
CliOutput(value any, format OutputFormat) string  // Dispatch to OutputJson/Yaml/Plain
BuildCliError(message string, hint string) map[string]any  // {code:"error", error_code:"invalid_request", hint?, retryable:false, trace:{duration_ms:0}}

Canonical pattern — parse all flags before doing work, emit JSONL errors to stdout:

import (
    "log/slog"
    afdata "github.com/agentfirstkit/agent-first-data/go"
)

format, err := afdata.CliParseOutput(outputFlag)
if err != nil {
    fmt.Println(afdata.OutputJson(afdata.BuildCliError(err.Error(), "")))
    os.Exit(2)
}

log := afdata.CliParseLogFilters(strings.Split(logFlag, ","))
// ... do work ...
fmt.Println(afdata.CliOutput(result, format))

See examples/agent_cli/ for the complete working example (go test ./...).

Usage Examples

Example 1: REST API

import (
    "log/slog"
    afdata "github.com/agentfirstkit/agent-first-data/go"
)

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    response := afdata.BuildJsonOk(
        map[string]any{"user_id": 123, "name": "alice"},
        map[string]any{"duration_ms": 150, "source": "db"},
    )
    // API returns raw JSON — no output processing, no key stripping
    json.NewEncoder(w).Encode(response)
}

Example 2: CLI Tool (Complete Lifecycle)

import afdata "github.com/agentfirstkit/agent-first-data/go"

func main() {
    // 1. Startup
    startup := afdata.BuildJson(
        "log",
        map[string]any{
            "event":  "startup",
            "config": map[string]any{"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
            "args":   map[string]any{"input_path": "data.json"},
            "env":    map[string]any{"RUST_LOG": "info"},
        },
        nil,
    )
    fmt.Println(afdata.OutputYaml(startup))
    // ---
    // code: "log"
    // event: "startup"
    // args:
    //   input_path: "data.json"
    // config:
    //   api_key: "***"
    //   timeout: "30s"
    // env:
    //   RUST_LOG: "info"

    // 2. Progress
    progress := afdata.BuildJson(
        "progress",
        map[string]any{"current": 3, "total": 10, "message": "processing"},
        map[string]any{"duration_ms": 1500},
    )
    fmt.Println(afdata.OutputPlain(progress))
    // code=progress current=3 message=processing total=10 trace.duration=1.5s

    // 3. Result
    result := afdata.BuildJsonOk(
        map[string]any{
            "records_processed":    10,
            "file_size_bytes":      5242880,
            "created_at_epoch_ms":  int64(1738886400000),
        },
        map[string]any{"duration_ms": 3500, "source": "file"},
    )
    fmt.Println(afdata.OutputYaml(result))
    // ---
    // code: "ok"
    // result:
    //   created_at: "2025-02-07T00:00:00.000Z"
    //   file_size: "5.0MB"
    //   records_processed: 10
    // trace:
    //   duration: "3.5s"
    //   source: "file"
}

Example 3: JSONL Output

import afdata "github.com/agentfirstkit/agent-first-data/go"

func processRequest() {
    result := afdata.BuildJsonOk(
        map[string]any{"status": "success"},
        map[string]any{"duration_ms": 250, "api_key_secret": "sk-123"},
    )

    // Print JSONL to stdout (secrets redacted, one JSON object per line)
    // Channel policy: machine-readable protocol/log events must not use stderr.
    fmt.Println(afdata.OutputJson(result))
    // {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
}

Complete Suffix Example

import afdata "github.com/agentfirstkit/agent-first-data/go"

data := map[string]any{
    "created_at_epoch_ms":   int64(1738886400000),
    "request_timeout_ms":    5000,
    "cache_ttl_s":           3600,
    "file_size_bytes":       5242880,
    "payment_msats":         50000000,
    "price_usd_cents":       9999,
    "success_rate_percent":  95.5,
    "api_key_secret":        "sk-1234567890abcdef",
    "user_name":             "alice",
    "count":                 42,
}

// YAML output (keys stripped, values formatted, secrets redacted)
fmt.Println(afdata.OutputYaml(data))
// ---
// api_key: "***"
// cache_ttl: "3600s"
// count: 42
// created_at: "2025-02-07T00:00:00.000Z"
// file_size: "5.0MB"
// payment: "50000000msats"
// price: "$99.99"
// request_timeout: "5.0s"
// success_rate: "95.5%"
// user_name: "alice"

// Plain logfmt output (same transformations, single line)
fmt.Println(afdata.OutputPlain(data))
// api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice

AFDATA Logging

AFDATA-compliant structured logging via Go’s log/slog. Every log line is formatted using the library’s own OutputJson/OutputPlain/OutputYaml functions. Span fields are carried via WithAttrs / context, automatically flattened into each log line.

API

import (
    "log/slog"
    afdata "github.com/agentfirstkit/agent-first-data/go"
)

// Convenience initializers — set up the default slog logger with AFDATA output to stdout
// Default minimum level is INFO.
afdata.InitJson()    // Single-line JSONL (secrets redacted, original keys)
afdata.InitPlain()   // Single-line logfmt (keys stripped, values formatted)
afdata.InitYaml()    // Multi-line YAML (keys stripped, values formatted)

// Optional level-aware initializers
afdata.InitJsonLevel(slog.LevelDebug)
afdata.InitPlainLevel(slog.LevelInfo)
afdata.InitYamlLevel(slog.LevelWarn)

// Low-level — create a handler for custom logger stacks
afdata.NewAfdataHandler(w io.Writer, format LogFormat) *AfdataHandler  // implements slog.Handler
afdata.NewAfdataHandlerWithLevel(w io.Writer, format LogFormat, level slog.Level) *AfdataHandler
afdata.FormatJson | afdata.FormatPlain | afdata.FormatYaml

// Context-based spans for concurrent code
afdata.WithSpan(ctx context.Context, fields map[string]any) context.Context
afdata.LoggerFromContext(ctx context.Context) *slog.Logger

// Deprecated global span helper (mutates slog.Default)
afdata.Span(fields map[string]any, fn func())

Setup

import (
    "log/slog"
    afdata "github.com/agentfirstkit/agent-first-data/go"
)

// JSON output for production (one JSONL line per event, secrets redacted)
afdata.InitJson()

// Plain logfmt for development (keys stripped, values formatted)
afdata.InitPlain()

// YAML for detailed inspection (multi-line, keys stripped, values formatted)
afdata.InitYaml()

// Enable debug logs when needed:
afdata.InitJsonLevel(slog.LevelDebug)

Log Output

Standard slog functions work unchanged. Output format depends on the init function used.

slog.Info("Server started")
// JSON:  {"timestamp_epoch_ms":1739000000000,"message":"Server started","code":"info"}
// Plain: code=info message="Server started" timestamp_epoch_ms=1739000000000
// YAML:  ---
//        code: "info"
//        message: "Server started"
//        timestamp_epoch_ms: 1739000000000

slog.Warn("DNS lookup failed", "error", err, "domain", domain)
// JSON:  {"timestamp_epoch_ms":...,"message":"DNS lookup failed","domain":"example.com","error":"timeout","code":"warn"}
// Plain: code=warn domain=example.com error=timeout message="DNS lookup failed" ...

Span Support (WithAttrs)

Create child loggers with span-level fields. All log events from the child include the span fields.

reqLogger := slog.Default().With("request_id", uuid)
reqLogger.Info("Processing")
// {"timestamp_epoch_ms":...,"message":"Processing","request_id":"abc-123","code":"info"}

reqLogger.Warn("Not found", "path", "/users/42")
// {"timestamp_epoch_ms":...,"message":"Not found","request_id":"abc-123","path":"/users/42","code":"warn"}

For concurrent code (goroutines), use context-based spans:

ctx := afdata.WithSpan(ctx, map[string]any{"request_id": uuid})

// In handler or goroutine
logger := afdata.LoggerFromContext(ctx)
logger.Info("Handling request", "method", "GET")
// {"timestamp_epoch_ms":...,"message":"Handling request","request_id":"abc-123","method":"GET","code":"info"}

afdata.Span(fields, fn) is kept for compatibility but mutates slog.Default; prefer WithSpan + LoggerFromContext in concurrent code.

Custom Code Override

The code field defaults to the log level. Override with an explicit field:

slog.Info("Server ready", "code", "log", "event", "startup")
// {"timestamp_epoch_ms":...,"message":"Server ready","code":"log","event":"startup"}

Output Fields

Every log line contains:

FieldTypeDescription
timestamp_epoch_msnumberUnix milliseconds
messagestringLog message
codestringLevel (trace/debug/info/warn/error) or explicit override
span fieldsanyFrom WithAttrs / WithSpan
event fieldsanyFrom slog call arguments

Log Output Formats

All three formats use the library’s own output functions, so AFDATA suffix processing applies to log fields too:

FormatFunctionKeysValuesUse case
JSONInitJsonoriginal (with suffix)rawproduction, log aggregation
PlainInitPlainstrippedformatteddevelopment, compact scanning
YAMLInitYamlstrippedformatteddebugging, detailed inspection

All formats automatically redact _secret fields in log output.

Output Formats

Three output formats for different use cases:

FormatStructureKeysValuesUse case
JSONsingle-lineoriginal (with suffix)rawprograms, logs
YAMLmulti-linestrippedformattedhuman inspection
Plainsingle-line logfmtstrippedformattedcompact scanning

All formats automatically redact _secret fields.

Supported Suffixes

Repository

This package is part of the agent-first-data repository, which also contains:

To run tests, clone the full repository (tests use shared cross-language fixtures from spec/fixtures/):

git clone https://github.com/agentfirstkit/agent-first-data
cd agent-first-data/go
go test ./...

License

MIT