ax

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

ax-go

Agentic Experience (AX) foundation for Go CLI tools — the "Common DNA" for the rshade portfolio.

Go License Docs

📖 Documentation: https://rshade.github.io/ax-go/

Status: Released (pre-v1.0, 0.x). The current pinnable release is v0.1.0: go get github.com/rshade/ax-go@v0.1.0. The v0.1.0 output contracts are frozen in code — core primitives such as ax.Error, ax.Execute, ax.NewLogger, ax.ParseConfig, and ax.NewEntityID are covered by contract tests, and all public output shapes are pinned by golden fixtures. v0.1.0 was tagged by release-please from the Conventional Commit history; see CHANGELOG.md for release history.

Stability guarantee while in 0.x: a patch upgrade (0.x.PATCH) is always safe — patch releases are bug-fixes-only and stay backward-compatible. A minor bump (0.MINOR.0) MAY contain breaking changes (to the Go API or to machine-payload shapes like ax.Error / __schema), and breaking changes never auto-promote to 1.0.0. The full policy — including what counts as a breaking change and the deprecation lifecycle — lives in the constitution's Stability & SemVer and Deprecation Lifecycle principles (.specify/memory/constitution.md).

Mission

ax-go is the shared foundation that standardizes Agentic Experience (AX) across Go-based CLI tools. Its goal is simple:

Ensure all Go-based CLI tools are as powerful and predictable for LLM agents as they are for human engineers.

Rather than every CLI reinventing how it talks to an autonomous agent — how it emits data, reports errors, exposes its command tree, and stays safe under retries — ax-go encodes those conventions once so the whole portfolio shares the same predictable behavior.

Public Import Surfaces

Use the root package for full CLI runtime behavior:

import ax "github.com/rshade/ax-go"

The root ax facade remains the ergonomic surface for complete Cobra CLIs: ax.Execute, telemetry lifecycle, structured logging, __schema command wiring, HTTP/gRPC helpers, and trace-aware envelopes.

Use isolated contract packages for thin consumers that only need stable machine contracts without root runtime adapters:

import (
    "github.com/rshade/ax-go/config"
    "github.com/rshade/ax-go/contract"
    "github.com/rshade/ax-go/id"
    "github.com/rshade/ax-go/schema"
)
  • contract: exit codes, mode resolution, context metadata, success/error envelopes, and strict JSON/NDJSON writers.
  • config: bounded Hujson reads and comment-preserving RFC 6902 patches.
  • schema: ax-native and MCP-compatible command schema shapes/builders.
  • id: UUID v4 idempotency keys and UUID v7 entity/resource IDs.

Import-isolation tests keep those public contract packages free of the root facade, telemetry exporters/SDK setup, logger/Loki, HTTP instrumentation, and gRPC runtime adapters.

Core Standards

These are the non-negotiable mandates every tool built on ax-go must follow.

The Golden Rule — stream separation
  • stdout is strictly reserved for the final data payload (JSON).
  • stderr carries everything else: logs, progress indicators, and structured error envelopes.

This lets an agent pipe stdout straight into a JSON parser while humans and log collectors read stderr.

Deterministic exit codes
Code Meaning
0 success
1 unknown / internal error
2 validation / bad input
3 network / timeout
4 authentication / permission
Determinism is machine-readable trust

Given the same inputs, two runs of the same command produce byte-identical stdout (modulo documented non-deterministic fields — timestamps, trace_id, auto-generated idempotency_key). This is the machine equivalent of trust: agents diff outputs across runs to detect drift, so determinism is what lets an agent safely delegate to an ax-go CLI. It is a stronger guarantee than the AX literature demands, and it is the project's clearest differentiator.

Machine discoverability (__schema)

Every tool implements a __schema command that emits a structured JSON map of its command tree, flags, types, and examples — so agents can ground themselves without guessing. The primary format is ax-native JSON, with __schema --as=mcp available as an MCP-compatible adapter. The discoverability contract is now owned by the schema package and the absorbed decisions in specs/010-import-isolated-contracts/research.md.

Asymmetric JSON flow
  • Input: accept Hujson (comments and trailing commas) for human convenience. Config reads are capped at 1 MiB by default at the read boundary; use ax.WithMaxConfigBytes when a CLI intentionally supports a larger bounded config. The public helpers are ax.ParseConfig(ctx, reader, &cfg, ...) and ax.ParseConfigFile(ctx, path, &cfg, ...), so slow cooperative sources can be canceled through context.Context.
  • Output: emit strict, minified JSON for bounded payloads; emit NDJSON for streaming / unbounded result sets.

Config read rejections are standard ax.Error envelopes. Oversized input uses the frozen error_code config_too_large; an out-of-range cap (negative or above ax.MaxConfigBytesCeiling, 1 GiB) uses config_max_bytes_invalid. Invalid Hujson or schema mismatches use config_invalid, and a nil ParseConfigOption uses config_option_invalid. All map to exit code 2 and are discoverable with errors.As(err, &axErr). Reads accept Hujson extensions, but payload writes remain strict JSON.

To mutate an existing Hujson config without stripping user comments, ax.PatchConfig(ctx, reader, patch, ...) and ax.PatchConfigFile(ctx, path, patch, ...) apply RFC 6902 JSON Patch operations to the Hujson AST. Comments survive; whitespace is normalized to canonical Hujson formatting. PatchConfigFile writes back atomically (temp file + rename) and preserves file permissions. An invalid patch document or a failed patch operation uses the frozen error_code config_patch_invalid (exit code 2); an invalid existing config uses config_invalid.

Agent-safety primitives
  • --idempotency-key — auto-generated UUID v4 if absent and surfaced in the output envelope, killing duplicate-create from agent retries.
  • --dry-run — universal middleware that emits the same envelope with dry_run: true and performs no side effects.
  • --format flag / AGENT_MODE env var / TTY auto-detect — selects machine vs. human output mode. The precedence is --format flag, then AGENT_MODE, then TTY detection.
Standard ax.Error envelope

A structured, machine-readable error format emitted to stderr. Schema defined by the root ax.Error facade and the isolated contract.Error type.

Engineering Standards

  • Allocation discipline: track allocations via standard testing.B benchmarks; target zero or near-zero allocations on hot paths. Benchmark serializer choices rather than asserting numeric bars.
  • Trace propagation: contexts carry and propagate W3C Trace Context IDs by default, via the OpenTelemetry SDK (ADR-0004; real export lifecycle delivered by spec 004).
  • Observability backends: Grafana Loki for log aggregation via opt-in direct push (AX_LOKI_URL; see specs/007-loki-direct-push/research.md); Tempo / Jaeger / Honeycomb-compatible for traces via OTel.
  • ID strategy: OTel trace/span IDs for observability; UUID v4 for idempotency keys; UUID v7 for resource and entity IDs. Never mix observability IDs with resource/entity IDs.
  • CLI framework: built on Cobra (ADR-0008). ax.Execute() wraps Cobra execution for mode resolution, schema wiring, error-envelope output, and OTel flush-on-exit.
  • Structured logging: ax.NewLogger(ctx) returns an ax.Logger backed by zerolog with trace correlation wired in (ADR-0009).
  • Telemetry lifecycle: ax.Execute() opens a recording root span around the command, so logs written with cmd.Context() carry non-zero trace_id and span_id even when no collector is configured. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP HTTP trace export; a plaintext http:// local collector is allowed, while https:// uses verified TLS and TLS verification is never disabled. Set AX_OTEL_DEBUG=1 to print human-readable span data to stderr for local debugging. Both destinations can be enabled together, all telemetry stays off stdout, and exporter failures degrade to a stderr diagnostic without changing the command's stdout payload or exit code. Export attempts and shutdown are bounded by the telemetry shutdown budget (default 2s, configurable with ax.WithTelemetryShutdownTimeout).
  • Idiomatic Go: package name is ax. Keep abstractions narrow and tied to accepted ADRs.

Architecture Decisions (ADRs)

The ADRs are a frozen legacy decision log. New public API or runtime behavior changes go through the Spec Kit workflow and record decisions in the feature's research.md; retired ADR decisions are absorbed there before the ADR file is deleted.

ADR Title Status
0004 Trace ID Format Accepted (2026-05-28)
0008 CLI Framework — Cobra Accepted (2026-05-28)
0009 Structured Logger — ZeroLog Accepted (2026-05-28)

Absorbed decisions for mode resolution, error envelopes, schema output, ID strategy, and import layout live in specs/010-import-isolated-contracts/research.md. Remaining frozen ADR text and rationale live in docs/adr/.

Repository Layout

The primary public import path remains github.com/rshade/ax-go as ax. Narrow public contract packages exist only for thin consumers: contract, config, schema, and id. Private implementation mechanics live under internal/ so they do not become accidental public API. Public JSON contract fixtures live under testdata/. Runnable support binaries belong under cmd/ when real command behavior exists. pkg/, src/, and broad public subpackages remain intentionally avoided.

Examples

The runnable integration command in examples/integration/ exercises the public ax-go API from a real Cobra CLI. It covers bounded JSON envelopes, NDJSON streaming, Hujson config parsing, in-place Hujson patching, __schema, structured ax.Error output, idempotency keys, and stderr logging.

go run ./examples/integration --format=json --idempotency-key=demo-key --name=Ada
go run ./examples/integration stream --format=json --count=3
go run ./examples/integration patch-config --format=json --config=config.json \
  --patch='[{"op":"replace","path":"/name","value":"Grace"}]'
go run ./examples/integration __schema
go run ./examples/integration fail --format=json

Build-time version injection

Production CLIs built on ax-go should resolve their version once at process startup and pass the same value to every version surface. Keep the linker target as a writable var, then use ax.ResolveVersion:

var version string // set by -ldflags "-X main.version=..."

func run(ctx context.Context, root *cobra.Command) int {
    resolved := ax.ResolveVersion(version)

    logger := ax.NewLogger(ctx, ax.WithLoggerLabels(ax.Labels{
        Application: "mytool",
        Version:     resolved,
    }))
    _ = logger

    return ax.Execute(ctx, root, ax.WithVersion(resolved))
}

ResolveVersion returns a non-placeholder injected value when present, otherwise it falls back to the running binary's Go build metadata (Main.Version, then vcs.revision with a dirty marker) and finally to 0.0.0-unknown. It never returns an empty string or the bare placeholders dev or unknown.

Build the integration example with the documented injection target:

make build-example
./bin/ax-integration __schema

The target injects git describe --tags --always --dirty into main.version:

go build -ldflags "-X main.version=$(git describe --tags --always --dirty)" \
  -o bin/ax-integration ./examples/integration

Override VERSION for release and reproducible builds:

make build-example VERSION=v1.2.3

The same resolved value feeds __schema.version, the ax.Error envelope version, and the logger version label.

Roadmap

Sequenced from the accepted ADRs and the current scaffold:

  1. Harden __schema — enforce example coverage, expand output-mode declarations, and mature the MCP adapter owned by the schema package.
  2. Implement Loki direct push — keep stderr shipping as the default and add opt-in AX_LOKI_URL direct push from ADR-0006.
  3. Expand examples and benchmarks — keep examples/integration/ current with public API changes and benchmark hot paths with testing.B / -benchmem.

Contributing

Before changing public behavior, use the Spec Kit feature workflow. Read the constitution, absorb any governing frozen ADR decisions into the feature's research.md, and keep README plus examples/integration/ current with the public contract. Do not create or edit ADRs for new work.

License

Licensed under the Apache License 2.0.

Documentation

Overview

Package ax provides the Agentic Experience foundation for Go CLI tools.

The package keeps machine payloads on stdout, operational output on stderr, and exposes shared primitives for mode resolution, error envelopes, discoverability schemas, idempotency keys, logging, and trace propagation.

Index

Examples

Constants

View Source
const (
	// DefaultMaxConfigBytes is the default maximum config size: 1 MiB.
	DefaultMaxConfigBytes int64 = isolatedconfig.DefaultMaxBytes
	// MaxConfigBytesCeiling is the largest valid config read limit: 1 GiB.
	MaxConfigBytesCeiling int64 = isolatedconfig.MaxBytesCeiling
)
View Source
const (
	// ExitSuccess indicates successful completion.
	ExitSuccess = contract.ExitSuccess
	// ExitInternal indicates an unknown or internal error.
	ExitInternal = contract.ExitInternal
	// ExitValidation indicates invalid input or failed validation.
	ExitValidation = contract.ExitValidation
	// ExitNetwork indicates a network failure or timeout.
	ExitNetwork = contract.ExitNetwork
	// ExitAuth indicates an authentication or permission failure.
	ExitAuth = contract.ExitAuth
)
View Source
const (
	// ModeJSON is the machine-readable mode used by agents and pipelines.
	ModeJSON = contract.ModeJSON
	// ModeHuman is the human-readable mode used for interactive terminals.
	ModeHuman = contract.ModeHuman
)
View Source
const (
	// ZeroTraceID is a valid zero-value W3C trace ID for no-active-span cases.
	ZeroTraceID = contract.ZeroTraceID
	// ZeroSpanID is a valid zero-value W3C span ID for no-active-span cases.
	ZeroSpanID = contract.ZeroSpanID
)
View Source
const (
	// ErrorSchemaVersion is the current SemVer version of the error envelope.
	ErrorSchemaVersion = contract.ErrorSchemaVersion
)
View Source
const ModeDetectionRule = contract.ModeDetectionRule

ModeDetectionRule documents the output-mode resolution precedence applied by ResolveMode. It is surfaced verbatim in __schema output.

View Source
const SchemaVersion = isolatedschema.SchemaVersion

SchemaVersion is the current SemVer version for ax-native schemas.

Variables

This section is empty.

Functions

func DryRunFromContext

func DryRunFromContext(ctx context.Context) bool

DryRunFromContext reports whether dry-run behavior is active.

func ErrorExitCode

func ErrorExitCode(err error) int

ErrorExitCode maps an error to the deterministic ax-go process exit code.

func Execute

func Execute(ctx context.Context, root *cobra.Command, opts ...ExecuteOption) int

Execute wraps Cobra execution with AX mode resolution, idempotency, schema, error-envelope, and telemetry lifecycle behavior. It returns a deterministic exit code and leaves process termination to the caller.

func Flush added in v0.1.0

func Flush(ctx context.Context, l Logger) error

Flush performs a best-effort, non-destructive drain of any buffered Loki log entries for the given Logger. It blocks until the buffer is empty, the context is cancelled, or an internal 2-second deadline elapses — whichever comes first. Remaining entries are dropped after the deadline.

Flush is a no-op (returns nil) when:

  • l has no Loki sink (AX_LOKI_URL was not set)
  • l is nil
  • the sink's background goroutine already stopped because its logger context was cancelled

Callers may invoke Flush multiple times; later writes remain deliverable by a later Flush call. Callers should invoke Flush in their shutdown path, before os.Exit or cobra.Command cleanup, to ensure in-flight log lines reach Loki.

Example

ExampleFlush shows the shutdown pattern: Flush drains any buffered Loki log entries before the process exits. When no Loki sink is active (AX_LOKI_URL unset), Flush is a no-op that returns nil immediately.

package main

import (
	"bytes"
	"context"
	"os"
	"time"

	ax "github.com/rshade/ax-go"
)

func main() {
	os.Unsetenv("AX_LOKI_URL") // ensure no-op for this example
	var buf bytes.Buffer
	logger := ax.NewLogger(
		context.Background(),
		ax.WithLoggerWriter(&buf),
		ax.WithLokiFromEnv(),
	)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	_ = ax.Flush(ctx, logger)
}

func GRPCDial

func GRPCDial(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)

GRPCDial dials target with OTel client instrumentation.

func HTTPClient

func HTTPClient() *http.Client

HTTPClient returns an HTTP client with OTel propagation instrumentation.

func IdempotencyKeyFromContext

func IdempotencyKeyFromContext(ctx context.Context) (string, bool)

IdempotencyKeyFromContext returns the idempotency key stored in ctx.

func NewEntityID

func NewEntityID() (string, error)

NewEntityID returns a UUID v7 resource/entity identifier.

Example

ExampleNewEntityID returns a UUID v7 resource identifier in canonical 36-character form.

package main

import (
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	id, err := ax.NewEntityID()
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(len(id))
}
Output:
36

func NewIdempotencyKey

func NewIdempotencyKey() string

NewIdempotencyKey returns a UUID v4 idempotency key.

Example

ExampleNewIdempotencyKey returns a UUID v4 string in canonical 36-character form, surfaced in the output envelope so retries are safe.

package main

import (
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	key := ax.NewIdempotencyKey()
	fmt.Println(len(key))
}
Output:
36

func NewSchemaCommand

func NewSchemaCommand(root *cobra.Command, opts ...SchemaOption) *cobra.Command

NewSchemaCommand builds the reserved __schema command.

func ParseConfig

func ParseConfig(ctx context.Context, r io.Reader, dst any, opts ...ParseConfigOption) error

ParseConfig parses Hujson from r under a bounded read cap and unmarshals into dst.

Example

ExampleParseConfig shows the read-side Hujson asymmetry: comments and trailing commas are accepted on input, and the result decodes into a normal struct.

package main

import (
	"context"
	"fmt"
	"strings"

	ax "github.com/rshade/ax-go"
)

func main() {
	const hujson = `{
		// comments and trailing commas are allowed on reads
		"name": "ax",
		"replicas": 3,
	}`

	var cfg struct {
		Name     string `json:"name"`
		Replicas int    `json:"replicas"`
	}
	if err := ax.ParseConfig(
		context.Background(),
		strings.NewReader(hujson),
		&cfg,
		ax.WithMaxConfigBytes(1<<10),
	); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("%s x%d\n", cfg.Name, cfg.Replicas)
}
Output:
ax x3

func ParseConfigFile

func ParseConfigFile(ctx context.Context, path string, dst any, opts ...ParseConfigOption) error

ParseConfigFile opens path and applies ParseConfig's contract to its contents.

Example

ExampleParseConfigFile reads and decodes a Hujson configuration file from disk, applying the same 1 MiB read cap as ParseConfig.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	ax "github.com/rshade/ax-go"
)

func main() {
	dir, err := os.MkdirTemp("", "ax-config")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer os.RemoveAll(dir)

	path := filepath.Join(dir, "config.hujson")
	if err := os.WriteFile(path, []byte(`{"name": "ax"}`), 0o600); err != nil {
		fmt.Println("error:", err)
		return
	}

	var cfg struct {
		Name string `json:"name"`
	}
	if err := ax.ParseConfigFile(context.Background(), path, &cfg); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.Name)
}
Output:
ax

func PatchConfig added in v0.0.2

func PatchConfig(ctx context.Context, r io.Reader, patch []byte, opts ...ParseConfigOption) ([]byte, error)

PatchConfig reads Hujson from r, applies RFC 6902 JSON patch operations, and returns the patched Hujson content with comments preserved.

Example

ExamplePatchConfig shows the comment-preserving write path: RFC 6902 patch operations mutate the Hujson AST in place so user comments survive.

package main

import (
	"context"
	"fmt"
	"strings"

	ax "github.com/rshade/ax-go"
)

func main() {
	const existing = `{
	// service endpoint
	"host": "localhost",
	"port": 8080,
}`
	patch := []byte(`[{"op":"replace","path":"/port","value":9090}]`)

	patched, err := ax.PatchConfig(
		context.Background(),
		strings.NewReader(existing),
		patch,
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(strings.Contains(string(patched), "// service endpoint"))
	fmt.Println(strings.Contains(string(patched), "9090"))
}
Output:
true
true

func PatchConfigFile added in v0.0.2

func PatchConfigFile(ctx context.Context, path string, patch []byte, opts ...ParseConfigOption) error

PatchConfigFile reads path as Hujson, applies RFC 6902 patch operations, and writes the patched result back to path atomically, preserving comments.

Example

ExamplePatchConfigFile reads a Hujson config file, applies RFC 6902 patch operations, and writes the result back atomically, preserving user comments.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	ax "github.com/rshade/ax-go"
)

func main() {
	dir, err := os.MkdirTemp("", "ax-patch")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer os.RemoveAll(dir)

	path := filepath.Join(dir, "config.hujson")
	initial := []byte(`{
	// production endpoint
	"host": "prod.example.com",
	"port": 443,
}`)
	if err := os.WriteFile(path, initial, 0o600); err != nil {
		fmt.Println("error:", err)
		return
	}

	patch := []byte(`[{"op":"replace","path":"/port","value":8443}]`)
	if err := ax.PatchConfigFile(context.Background(), path, patch); err != nil {
		fmt.Println("error:", err)
		return
	}

	result, err := os.ReadFile(path)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(strings.Contains(string(result), "// production endpoint"))
	fmt.Println(strings.Contains(string(result), "8443"))
}
Output:
true
true

func ResolveVersion

func ResolveVersion(injected string) string

ResolveVersion returns a non-empty tool version for agent-visible surfaces.

Resolution is deterministic for a given binary and uses this precedence: first the injected link-time value, then Go build metadata from the running binary, then the sentinel "0.0.0-unknown". The result is never empty and is never the bare strings "dev" or "unknown"; pass the returned value to WithVersion and WithLoggerLabels so __schema.version, ax.Error.version, and the logger "version" label agree.

Example

ExampleResolveVersion shows the deterministic injected-version path used by release builds. When the injected value is empty, ResolveVersion falls back to the running binary's build metadata and finally to "0.0.0-unknown".

package main

import (
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	fmt.Println(ax.ResolveVersion("v1.2.3"))
}
Output:
v1.2.3

func SpanIDFromContext

func SpanIDFromContext(ctx context.Context) string

SpanIDFromContext returns the active W3C span ID or ZeroSpanID.

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) string

TraceIDFromContext returns the active W3C trace ID or ZeroTraceID.

func WithDryRun

func WithDryRun(ctx context.Context, dryRun bool) context.Context

WithDryRun returns a context carrying the dry-run state.

func WithIdempotencyKey

func WithIdempotencyKey(ctx context.Context, key string) context.Context

WithIdempotencyKey returns a context carrying the idempotency key for the run.

func WithMode

func WithMode(ctx context.Context, mode Mode) context.Context

WithMode returns a context carrying the resolved output mode.

func WriteError

func WriteError(w io.Writer, err error) error

WriteError writes err as a strict minified JSON error envelope followed by a newline.

func WriteJSON

func WriteJSON(w io.Writer, v any) error

WriteJSON writes v as strict minified JSON followed by a newline.

func WriteJSONLine

func WriteJSONLine(w io.Writer, v any) error

WriteJSONLine writes a single NDJSON line.

Types

type CommandSchema

type CommandSchema = isolatedschema.CommandSchema

CommandSchema describes a Cobra command and its direct children.

type Envelope

type Envelope[T any] = contract.Envelope[T]

Envelope is the standard bounded JSON success payload shape.

Example

ExampleEnvelope shows the envelope shape directly: a typed Data field and a Metadata block. span_id is omitted when empty.

package main

import (
	"fmt"
	"os"

	ax "github.com/rshade/ax-go"
)

func main() {
	env := ax.Envelope[string]{
		Data: "hello",
		Meta: ax.Metadata{TraceID: ax.ZeroTraceID},
	}
	if err := ax.WriteJSON(os.Stdout, env); err != nil {
		fmt.Println("error:", err)
	}
}
Output:
{"data":"hello","meta":{"trace_id":"00000000000000000000000000000000"}}

func NewEnvelope

func NewEnvelope[T any](ctx context.Context, data T) Envelope[T]

NewEnvelope wraps data with standard AX metadata from ctx.

Example

ExampleNewEnvelope wraps a payload in the standard success envelope, carrying trace metadata from the context. With no active span the IDs are the zero W3C values, so the output is deterministic.

package main

import (
	"context"
	"fmt"
	"os"

	ax "github.com/rshade/ax-go"
)

func main() {
	type result struct {
		ID string `json:"id"`
	}

	env := ax.NewEnvelope(context.Background(), result{ID: "abc"})
	if err := ax.WriteJSON(os.Stdout, env); err != nil {
		fmt.Println("error:", err)
	}
}
Output:
{"data":{"id":"abc"},"meta":{"trace_id":"00000000000000000000000000000000","span_id":"0000000000000000"}}

type Error

type Error = contract.Error

Error is the structured error envelope emitted to stderr.

Example

ExampleError shows how a consumer classifies a failure: recover the *ax.Error envelope with errors.As, then branch on the stable error_code and exit code without parsing human-facing text.

package main

import (
	"context"
	"errors"
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	err := ax.NewError(
		context.Background(),
		"config_max_bytes_invalid",
		"config max bytes must be between 0 and 1073741824",
		ax.WithErrorExitCode(ax.ExitValidation),
	)

	var axErr *ax.Error
	if errors.As(err, &axErr) {
		fmt.Println(axErr.ErrorCode)
		fmt.Println(axErr.ExitCode())
	}
}
Output:
config_max_bytes_invalid
2

func NewError

func NewError(ctx context.Context, code, message string, opts ...ErrorOption) *Error

NewError builds a structured error envelope using trace information from ctx.

Example

ExampleNewError builds a structured error envelope with an actionable fix and a deterministic exit code, then reads the exit code back out.

package main

import (
	"context"
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	err := ax.NewError(
		context.Background(),
		"config_too_large",
		"config exceeds maximum size of 1048576 bytes",
		ax.WithActionableFix("reduce the config or raise the limit with WithMaxConfigBytes"),
		ax.WithErrorExitCode(ax.ExitValidation),
	)

	fmt.Println(err)
	fmt.Println(ax.ErrorExitCode(err))
}
Output:
config exceeds maximum size of 1048576 bytes
2

type ErrorOption

type ErrorOption = contract.ErrorOption

ErrorOption configures a structured Error.

func WithActionableFix

func WithActionableFix(fix string) ErrorOption

WithActionableFix sets a best-effort remediation hint.

func WithErrorCause

func WithErrorCause(err error) ErrorOption

WithErrorCause attaches the underlying source error to the envelope.

func WithErrorContext

func WithErrorContext(fields map[string]any) ErrorOption

WithErrorContext merges domain-specific context fields into the envelope.

func WithErrorExitCode

func WithErrorExitCode(code int) ErrorOption

WithErrorExitCode sets the deterministic process exit code.

func WithErrorTool

func WithErrorTool(tool string) ErrorOption

WithErrorTool sets the emitting tool name.

func WithErrorVersion

func WithErrorVersion(version string) ErrorOption

WithErrorVersion sets the emitting tool version.

func WithSuggestions

func WithSuggestions(suggestions ...string) ErrorOption

WithSuggestions sets optional candidate recovery actions.

type ErrorSchemaInfo

type ErrorSchemaInfo = isolatedschema.ErrorSchemaInfo

ErrorSchemaInfo describes the shared stderr error envelope.

type ExecuteOption

type ExecuteOption func(*executeConfig)

ExecuteOption configures Execute.

func WithEnv

func WithEnv(env func(string) string) ExecuteOption

WithEnv sets the environment lookup used by Execute.

func WithStderr

func WithStderr(w io.Writer) ExecuteOption

WithStderr sets the operational output stream.

func WithStdin

func WithStdin(r io.Reader) ExecuteOption

WithStdin sets the input stream for Cobra.

func WithStdout

func WithStdout(w io.Writer) ExecuteOption

WithStdout sets the machine payload output stream.

func WithStdoutIsTTY

func WithStdoutIsTTY(isTTY bool) ExecuteOption

WithStdoutIsTTY overrides TTY detection, primarily for tests.

func WithTelemetryShutdownTimeout

func WithTelemetryShutdownTimeout(timeout time.Duration) ExecuteOption

WithTelemetryShutdownTimeout sets the OTel shutdown timeout.

func WithVersion

func WithVersion(version string) ExecuteOption

WithVersion sets the tool version reported in schema and error envelopes.

type FlagSchema

type FlagSchema = isolatedschema.FlagSchema

FlagSchema describes a command flag.

type Labels

type Labels struct {
	Environment string
	Application string
	Host        string
	Version     string
}

Labels are low-cardinality Loki-indexed fields.

type Logger

type Logger interface {
	Debug(ctx context.Context) *zerolog.Event
	Info(ctx context.Context) *zerolog.Event
	Warn(ctx context.Context) *zerolog.Event
	Error(ctx context.Context) *zerolog.Event
	WithLabels(labels Labels) Logger
	Zerolog() *zerolog.Logger
}

Logger is the ADR-0009 logging surface, initially backed by zerolog.

func NewLogger

func NewLogger(ctx context.Context, opts ...LoggerOption) Logger

NewLogger returns an ax Logger backed by zerolog and wired for trace correlation. When LoggerOptions include additional sinks (e.g. WithLokiFromEnv), every log line is fanned out to all sinks via io.MultiWriter alongside the primary writer.

type LoggerOption

type LoggerOption func(*loggerConfig)

LoggerOption configures NewLogger.

func WithLoggerLabels

func WithLoggerLabels(labels Labels) LoggerOption

WithLoggerLabels attaches low-cardinality labels to every log line.

func WithLoggerLevel

func WithLoggerLevel(level zerolog.Level) LoggerOption

WithLoggerLevel sets the minimum zerolog level.

func WithLoggerWriter

func WithLoggerWriter(w io.Writer) LoggerOption

WithLoggerWriter sets the logger output writer. Defaults to stderr.

func WithLokiFromEnv added in v0.1.0

func WithLokiFromEnv() LoggerOption

WithLokiFromEnv returns a LoggerOption that enables direct Loki push when the AX_LOKI_URL environment variable is set. It reads AX_LOKI_URL and AX_LOKI_AUTH_TOKEN at construction time. If AX_LOKI_URL is empty or malformed, the option is a no-op and a warning is written to the logger's configured writer. Push is non-blocking; network failures are silently dropped and do not affect the CLI exit code. The caller must invoke ax.Flush to drain buffered entries at shutdown.

Example

ExampleWithLokiFromEnv shows the no-op path: when AX_LOKI_URL is unset, WithLokiFromEnv adds no Loki sink and the logger behaves identically to a logger without it. No network connection is attempted. CLI authors add this option once; it activates only when operators set AX_LOKI_URL at runtime.

package main

import (
	"bytes"
	"context"
	"os"

	ax "github.com/rshade/ax-go"
)

func main() {
	os.Unsetenv("AX_LOKI_URL") // ensure deterministic no-op output
	var buf bytes.Buffer
	logger := ax.NewLogger(
		context.Background(),
		ax.WithLoggerWriter(&buf),
		ax.WithLokiFromEnv(),
	)
	logger.Info(context.Background()).Msg("no loki sink active")
}

type MCPSchema

type MCPSchema = isolatedschema.MCPSchema

MCPSchema is the lightweight MCP-compatible adapter shape.

func BuildMCPSchema

func BuildMCPSchema(root *cobra.Command) MCPSchema

BuildMCPSchema adapts the command tree to a simple MCP tools list.

type MCPTool

type MCPTool = isolatedschema.MCPTool

MCPTool describes one command as an MCP-compatible tool.

type Metadata

type Metadata = contract.Metadata

Metadata carries common machine-readable envelope fields.

type Mode

type Mode = contract.Mode

Mode describes whether output should be optimized for agents or humans.

Example

ExampleMode shows agent-mode resolution: with no --format flag, no AGENT_MODE, and a non-TTY stdout, ax resolves to machine-readable JSON.

package main

import (
	"fmt"

	ax "github.com/rshade/ax-go"
)

func main() {
	mode, err := ax.ResolveMode("", "", false)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(mode)
}
Output:
json

func ModeFromContext

func ModeFromContext(ctx context.Context) (Mode, bool)

ModeFromContext returns the resolved output mode stored in ctx.

func ParseMode

func ParseMode(value string) (Mode, error)

ParseMode parses an explicit output mode.

func ResolveMode

func ResolveMode(explicitFormat, agentMode string, stdoutIsTTY bool) (Mode, error)

ResolveMode applies output-mode precedence: explicit --format flag, then AGENT_MODE, then TTY detection.

type ParseConfigOption

type ParseConfigOption = isolatedconfig.Option

ParseConfigOption configures ParseConfig and ParseConfigFile.

func WithMaxConfigBytes

func WithMaxConfigBytes(maxBytes int64) ParseConfigOption

WithMaxConfigBytes sets the maximum config bytes for one parse invocation.

type Schema

type Schema = isolatedschema.Schema

Schema is the ax-native reflective JSON tree emitted by __schema.

func BuildSchema

func BuildSchema(root *cobra.Command, opts ...SchemaOption) Schema

BuildSchema reflects a Cobra command tree into the ax-native schema.

type SchemaOption

type SchemaOption = isolatedschema.Option

SchemaOption configures BuildSchema and NewSchemaCommand.

func WithSchemaVersion

func WithSchemaVersion(version string) SchemaOption

WithSchemaVersion sets the tool version reported by __schema.

type Telemetry

type Telemetry struct {
	TracerProvider *sdktrace.TracerProvider
}

Telemetry owns the OTel provider lifecycle for a short-lived CLI process.

func StartTelemetry

func StartTelemetry(ctx context.Context, opts ...TelemetryOption) (context.Context, *Telemetry, error)

StartTelemetry installs W3C trace propagation and extracts TRACEPARENT.

It configures telemetry from the supplied environment lookup. An OTEL_EXPORTER_OTLP_ENDPOINT value enables OTLP HTTP export with synchronous, bounded attempts; AX_OTEL_DEBUG enables human-readable span output on the configured stderr writer. With neither set, telemetry is no-op for export while still allowing Execute to create a recording root span for log correlation.

StartTelemetry is fail-open: telemetry setup failures are reported to stderr and the returned error is reserved for signature compatibility and currently always nil.

Example

ExampleStartTelemetry installs W3C propagation and configures the telemetry lifecycle with explicit stderr, service identity, and shutdown budget options.

package main

import (
	"bytes"
	"context"
	"fmt"
	"time"

	ax "github.com/rshade/ax-go"
)

func main() {
	var stderr bytes.Buffer
	ctx, telemetry, err := ax.StartTelemetry(
		context.Background(),
		ax.WithTelemetryEnv(func(string) string { return "" }),
		ax.WithTelemetryStderr(&stderr),
		ax.WithTelemetryServiceName("example"),
		ax.WithTelemetryServiceVersion("v1.2.3"),
		ax.WithTelemetryShutdownBudget(50*time.Millisecond),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	if err := telemetry.Shutdown(context.Background()); err != nil {
		fmt.Println("shutdown:", err)
		return
	}

	fmt.Println(telemetry != nil)
	fmt.Println(ax.TraceIDFromContext(ctx))
	fmt.Println(stderr.Len())
}
Output:
true
00000000000000000000000000000000
0

func (*Telemetry) Shutdown

func (t *Telemetry) Shutdown(ctx context.Context) error

Shutdown flushes and shuts down the configured tracer provider.

type TelemetryOption

type TelemetryOption func(*telemetryConfig)

TelemetryOption configures StartTelemetry.

func WithTelemetryEnv

func WithTelemetryEnv(env func(string) string) TelemetryOption

WithTelemetryEnv sets the environment lookup used for telemetry configuration and trace extraction. StartTelemetry resolves OTEL_EXPORTER_OTLP_ENDPOINT and AX_OTEL_DEBUG (exporter and debug selection) as well as TRACEPARENT and TRACESTATE (inbound trace continuation) through this lookup, so a custom function controls all of them in tests and embedding scenarios.

func WithTelemetryServiceName added in v0.0.2

func WithTelemetryServiceName(name string) TelemetryOption

WithTelemetryServiceName sets the OTel resource service.name value.

The value should be a low-cardinality CLI name and must not include PII, credentials, resource IDs, or user-controlled command input.

func WithTelemetryServiceVersion added in v0.0.2

func WithTelemetryServiceVersion(version string) TelemetryOption

WithTelemetryServiceVersion sets the OTel resource service.version value.

The value should be the deterministic build-injected version reported by the adopting CLI and must not include PII or high-cardinality runtime data.

func WithTelemetryShutdownBudget added in v0.0.2

func WithTelemetryShutdownBudget(d time.Duration) TelemetryOption

WithTelemetryShutdownBudget sets the timeout budget for telemetry shutdown and synchronous exporter attempts.

Non-positive durations are ignored by StartTelemetry, which falls back to the default Execute telemetry shutdown timeout. A stuck collector maps to a fail-open stderr diagnostic rather than a command failure.

func WithTelemetryStderr added in v0.0.2

func WithTelemetryStderr(w io.Writer) TelemetryOption

WithTelemetryStderr sets the operational telemetry writer.

StartTelemetry writes fail-open diagnostics and AX_OTEL_DEBUG span output to this writer. It defaults to os.Stderr and must not be the command stdout.

Directories

Path Synopsis
Package config provides bounded Hujson config reads and patch operations.
Package config provides bounded Hujson config reads and patch operations.
Package contract provides import-isolated machine contracts for ax-go consumers.
Package contract provides import-isolated machine contracts for ax-go consumers.
examples
integration command
Package id provides non-observability identifier helpers.
Package id provides non-observability identifier helpers.
internal
cli
cmd/doccover command
Command doccover enforces ExampleXxx coverage on ax-go's primary API surface.
Command doccover enforces ExampleXxx coverage on ax-go's primary API surface.
mcp
testutil
Package testutil provides reusable in-process test helpers for the ax-go module.
Package testutil provides reusable in-process test helpers for the ax-go module.
Package schema provides import-isolated command discoverability contracts.
Package schema provides import-isolated command discoverability contracts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL