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 ¶
- Constants
- func DryRunFromContext(ctx context.Context) bool
- func ErrorExitCode(err error) int
- func Execute(ctx context.Context, root *cobra.Command, opts ...ExecuteOption) int
- func Flush(ctx context.Context, l Logger) error
- func GRPCDial(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)
- func Guard(ctx context.Context, effect func(context.Context) error) (bool, error)
- func HTTPClient() *http.Client
- func IdempotencyKeyFromContext(ctx context.Context) (string, bool)
- func NewEntityID() (string, error)
- func NewHTTPClient(opts ...HTTPClientOption) *http.Client
- func NewIdempotencyKey() string
- func NewSchemaCommand(root *cobra.Command, opts ...SchemaOption) *cobra.Command
- func ParseConfig(ctx context.Context, r io.Reader, dst any, opts ...ParseConfigOption) error
- func ParseConfigFile(ctx context.Context, path string, dst any, opts ...ParseConfigOption) error
- func PatchConfig(ctx context.Context, r io.Reader, patch []byte, opts ...ParseConfigOption) ([]byte, error)
- func PatchConfigFile(ctx context.Context, path string, patch []byte, opts ...ParseConfigOption) error
- func Perform(ctx context.Context, rehearse, commit func(context.Context) error) error
- func ResolveVersion(injected string) string
- func SpanIDFromContext(ctx context.Context) string
- func TraceIDFromContext(ctx context.Context) string
- func WithDryRun(ctx context.Context, dryRun bool) context.Context
- func WithIdempotencyKey(ctx context.Context, key string) context.Context
- func WithMode(ctx context.Context, mode Mode) context.Context
- func WithNonDeterministicFields[T any](cmd *cobra.Command)
- func WriteError(w io.Writer, err error) error
- func WriteJSON(w io.Writer, v any) error
- func WriteJSONLine(w io.Writer, v any) error
- type CommandSchema
- type Envelope
- type Error
- type ErrorOption
- func WithActionableFix(fix string) ErrorOption
- func WithErrorCause(err error) ErrorOption
- func WithErrorContext(fields map[string]any) ErrorOption
- func WithErrorExitCode(code int) ErrorOption
- func WithErrorTool(tool string) ErrorOption
- func WithErrorVersion(version string) ErrorOption
- func WithRetryAfterSeconds(seconds int64) ErrorOption
- func WithRetryable(retryable bool) ErrorOption
- func WithSuggestions(suggestions ...string) ErrorOption
- type ErrorSchemaInfo
- type ExecuteOption
- func WithEnv(env func(string) string) ExecuteOption
- func WithStderr(w io.Writer) ExecuteOption
- func WithStdin(r io.Reader) ExecuteOption
- func WithStdout(w io.Writer) ExecuteOption
- func WithStdoutIsTTY(isTTY bool) ExecuteOption
- func WithTelemetryShutdownTimeout(timeout time.Duration) ExecuteOption
- func WithVersion(version string) ExecuteOption
- type FlagSchema
- type HTTPClientOption
- type Labels
- type Logger
- type LoggerOption
- type MCPSchema
- type MCPTool
- type Metadata
- type Mode
- type ParseConfigOption
- type Schema
- type SchemaOption
- type Telemetry
- type TelemetryOption
- func WithTelemetryEnv(env func(string) string) TelemetryOption
- func WithTelemetryServiceName(name string) TelemetryOption
- func WithTelemetryServiceVersion(version string) TelemetryOption
- func WithTelemetryShutdownBudget(d time.Duration) TelemetryOption
- func WithTelemetryStderr(w io.Writer) TelemetryOption
Examples ¶
Constants ¶
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 )
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 )
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 )
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 )
const DefaultHTTPTimeout = 30 * time.Second
DefaultHTTPTimeout is the default total timeout applied to clients returned by HTTPClient and NewHTTPClient. It bounds the full request lifecycle (connect, redirects, reading the response body) so an outbound call can never block forever.
const ( // ErrorSchemaVersion is the current SemVer version of the error envelope. ErrorSchemaVersion = contract.ErrorSchemaVersion )
const ModeDetectionRule = contract.ModeDetectionRule
ModeDetectionRule documents the output-mode resolution precedence applied by ResolveMode. It is surfaced verbatim in __schema output.
const SchemaVersion = isolatedschema.SchemaVersion
SchemaVersion is the current SemVer version for ax-native schemas.
Variables ¶
This section is empty.
Functions ¶
func DryRunFromContext ¶
DryRunFromContext reports whether dry-run behavior is active.
func ErrorExitCode ¶
ErrorExitCode maps an error to the deterministic ax-go process exit code.
func Execute ¶
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.
The version reported in __schema output and error envelopes comes from WithVersion. When WithVersion is not supplied, Execute falls back to ResolveVersion("") — link-time injection, then Go build metadata, then the "0.0.0-unknown" sentinel — so the version surfaced to agents is never empty.
When the command returns an *Error, Execute normalizes a copy of it (filling in trace ID, tool, and version) before writing the envelope to stderr; the caller's *Error value is never mutated.
Example ¶
ExampleExecute wraps a Cobra root command with the full AX lifecycle — telemetry, mode resolution, and idempotency-key injection — and maps the result to a deterministic exit code instead of exiting the process. The command reads the resolved values back out of its context. With an explicit --idempotency-key and a buffered, non-TTY stdout the run is fully deterministic: the payload is the only stdout output and stderr stays empty.
package main
import (
"bytes"
"context"
"fmt"
"github.com/spf13/cobra"
ax "github.com/rshade/ax-go"
)
func main() {
var stdout, stderr bytes.Buffer
root := &cobra.Command{
Use: "app",
RunE: func(cmd *cobra.Command, _ []string) error {
mode, _ := ax.ModeFromContext(cmd.Context())
key, _ := ax.IdempotencyKeyFromContext(cmd.Context())
return ax.WriteJSON(cmd.OutOrStdout(), struct {
Mode ax.Mode `json:"mode"`
DryRun bool `json:"dry_run"`
Key string `json:"key"`
}{
Mode: mode,
DryRun: ax.DryRunFromContext(cmd.Context()),
Key: key,
})
},
}
root.SetArgs([]string{"--dry-run", "--idempotency-key=abc"})
code := ax.Execute(
context.Background(),
root,
ax.WithStdout(&stdout),
ax.WithStderr(&stderr),
ax.WithEnv(func(string) string { return "" }),
ax.WithStdoutIsTTY(false),
)
fmt.Println("exit:", code)
fmt.Print(stdout.String())
fmt.Println("stderr bytes:", stderr.Len())
}
Output: exit: 0 {"mode":"json","dry_run":true,"key":"abc"} stderr bytes: 0
func Flush ¶ added in v0.1.0
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.
The error return is reserved for future sink implementations: the Loki sink's Drain returns nil on every path (push failures are fail-open diagnostics on the configured writer, never returned errors), so Flush currently always returns nil. Callers should keep checking it — new sinks may surface drain failures — but a failed Loki push must never change the CLI exit code.
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)
}
Output:
func GRPCDial ¶
func GRPCDial(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)
GRPCDial dials target with OTel client instrumentation.
This helper is absent from builds using the ax_no_grpc build constraint, where calling it fails at compile time with "undefined: ax.GRPCDial". See grpc_disabled.go for the restoration step.
func Guard ¶ added in v0.3.0
Guard runs effect unless dry-run is active in ctx.
When dry-run is inactive it executes effect and returns (true, effect's error), preserving the error's wrap chain so errors.Is/errors.As keep working. When dry-run is active it skips effect entirely — guaranteeing no side effect — emits a single suppression line to stderr (only when effect is non-nil), and returns (false, nil). A nil effect is a no-op returning (false, nil); Guard never panics on a missing callback.
Guard maps no exit code itself: it returns effect's error verbatim for the caller to map via ErrorExitCode. The suppression line is written to stderr (never stdout) via the canonical logger, so stdout payload determinism is unaffected. A nil context is treated as dry-run inactive (the real path runs); Guard never panics on a missing context.
Example ¶
ExampleGuard shows the skip-only guard: the effect runs normally, but under --dry-run it is suppressed entirely and Guard reports executed=false. The suppression line Guard writes to stderr is not shown here (examples capture stdout only).
package main
import (
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
effect := func(context.Context) error {
fmt.Println("writing report")
return nil
}
// Real run: the effect executes.
executed, err := ax.Guard(context.Background(), effect)
fmt.Printf("executed=%v err=%v\n", executed, err)
// Dry-run: the effect is skipped.
dryRun := ax.WithDryRun(context.Background(), true)
executed, err = ax.Guard(dryRun, effect)
fmt.Printf("executed=%v err=%v\n", executed, err)
}
Output: writing report executed=true err=<nil> executed=false err=<nil>
func HTTPClient ¶
HTTPClient returns an HTTP client with OTel propagation instrumentation and the default bounded request timeout (DefaultHTTPTimeout). To override the timeout or set other options, use NewHTTPClient with WithHTTPTimeout. TLS verification uses the http.DefaultTransport secure defaults; never relax them.
func IdempotencyKeyFromContext ¶
IdempotencyKeyFromContext returns the idempotency key stored in ctx.
func NewEntityID ¶
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 NewHTTPClient ¶ added in v0.4.0
func NewHTTPClient(opts ...HTTPClientOption) *http.Client
NewHTTPClient returns an HTTP client with OTel propagation instrumentation and a bounded request timeout (DefaultHTTPTimeout unless overridden with WithHTTPTimeout). TLS verification uses the http.DefaultTransport secure defaults; never relax them.
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 ¶
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 ¶
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 Perform ¶ added in v0.3.0
Perform runs commit when dry-run is inactive, or the read-only rehearse preview when dry-run is active, so a dry-run surfaces the same validation errors as a real run without performing the mutation.
Real run: commit is executed (rehearse is ignored) and its error is returned with its wrap chain intact; a nil commit is a no-op returning nil. Dry-run: rehearse is executed (when non-nil) and its error is returned; commit is never executed. When the dry-run preview succeeds (or rehearse is nil) and a real commit would have run (commit is non-nil), a single suppression line is written to stderr. A failed rehearsal returns its error WITHOUT a suppression line, since the command already surfaces that error. A nil rehearse means a pure skip, equivalent to Guard.
Perform maps no exit code itself: it returns the running branch's error verbatim for the caller to map via ErrorExitCode. The suppression line is written to stderr (never stdout), so stdout payload determinism is unaffected. A nil context is treated as dry-run inactive (the real path runs); Perform never panics on a missing context.
Example ¶
ExamplePerform shows the rehearse/commit pair: a real run performs commit, while --dry-run runs the read-only rehearse preview instead (surfacing the same validation errors) without performing the mutation.
package main
import (
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
rehearse := func(context.Context) error {
fmt.Println("validating only")
return nil
}
commit := func(context.Context) error {
fmt.Println("committing")
return nil
}
// Real run: commit executes, rehearse is ignored.
_ = ax.Perform(context.Background(), rehearse, commit)
// Dry-run: rehearse executes, commit is skipped.
dryRun := ax.WithDryRun(context.Background(), true)
_ = ax.Perform(dryRun, rehearse, commit)
}
Output: committing validating only
func ResolveVersion ¶
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 ¶
SpanIDFromContext returns the active W3C span ID or ZeroSpanID.
func TraceIDFromContext ¶
TraceIDFromContext returns the active W3C trace ID or ZeroTraceID.
func WithDryRun ¶
WithDryRun returns a context carrying the dry-run state.
func WithIdempotencyKey ¶
WithIdempotencyKey returns a context carrying the idempotency key for the run.
func WithNonDeterministicFields ¶ added in v0.4.0
WithNonDeterministicFields registers cmd as emitting the standard success envelope for T. It reflects T once and records its ax:"nondeterministic" fields for __schema output; a nil command is ignored.
func WriteError ¶
WriteError writes err as a strict minified JSON error envelope followed by a newline.
Types ¶
type CommandSchema ¶
type CommandSchema = isolatedschema.CommandSchema
CommandSchema describes a Cobra command and its direct children.
type Envelope ¶
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 ¶
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 ¶
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 WithRetryAfterSeconds ¶ added in v0.3.0
func WithRetryAfterSeconds(seconds int64) ErrorOption
WithRetryAfterSeconds sets a relative backoff hint, in whole seconds, before a retry.
func WithRetryable ¶ added in v0.3.0
func WithRetryable(retryable bool) ErrorOption
WithRetryable records whether a naive retry of the failed command is safe.
Example ¶
ExampleWithRetryable shows a producer attaching machine-readable recovery guidance to a failure: an explicit retry-safety signal plus a relative backoff hint, so an agent can decide to retry (and how long to wait) without parsing human-facing text. The envelope is rendered to a buffer here for illustration; in a real CLI it is written to stderr.
package main
import (
"bytes"
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
err := ax.NewError(
context.Background(),
"network_timeout",
"upstream timed out",
ax.WithErrorTool("app"),
ax.WithErrorVersion("v0.1.0"),
ax.WithErrorExitCode(ax.ExitNetwork),
ax.WithRetryable(true),
ax.WithRetryAfterSeconds(30),
)
var buf bytes.Buffer
_ = ax.WriteError(&buf, err)
fmt.Print(buf.String())
}
Output: {"error_code":"network_timeout","message":"upstream timed out","trace_id":"00000000000000000000000000000000","tool":"app","version":"v0.1.0","schema_version":"1.0.0","retryable":true,"retry_after_seconds":30}
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. When omitted or empty, Execute falls back to ResolveVersion(""), which resolves build metadata and is never empty.
type HTTPClientOption ¶ added in v0.4.0
type HTTPClientOption func(*httpClientConfig)
HTTPClientOption configures NewHTTPClient.
func WithHTTPTimeout ¶ added in v0.4.0
func WithHTTPTimeout(d time.Duration) HTTPClientOption
WithHTTPTimeout sets the total request timeout on the returned client.
Non-positive durations are ignored and fall back to DefaultHTTPTimeout: a zero http.Client.Timeout disables the timeout entirely, which is exactly the unbounded-request hazard the client constructors exist to prevent.
type Logger ¶
Logger is the canonical structured-logging surface, initially backed by zerolog. The single-backend guardrail (this interface is a migration seam, not a pluggable-backend selector) and the trace-correlation contract are governed by Constitution Principles VI and VIII.
Example ¶
ExampleLogger shows the Logger surface beyond construction: WithLabels derives a logger that stamps low-cardinality labels on every line while keeping trace correlation. Keep labels low-cardinality (environment, application, host, version) — they are indexed in Loki.
package main
import (
"bytes"
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
var buf bytes.Buffer
logger := ax.NewLogger(context.Background(), ax.WithLoggerWriter(&buf))
labeled := logger.WithLabels(ax.Labels{Application: "app", Version: "v1.2.3"})
labeled.Info(context.Background()).Msg("started")
fmt.Print(buf.String())
}
Output: {"level":"info","application":"app","version":"v1.2.3","trace_id":"00000000000000000000000000000000","span_id":"0000000000000000","message":"started"}
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.
Example ¶
ExampleNewLogger builds the canonical structured logger: zerolog-backed, writing to the configured writer (stderr by default), with trace_id and span_id stamped on every line for trace correlation. With no active span the IDs are the zero W3C values, so the output is deterministic.
package main
import (
"bytes"
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
var buf bytes.Buffer
logger := ax.NewLogger(context.Background(), ax.WithLoggerWriter(&buf))
logger.Info(context.Background()).Msg("hello")
fmt.Print(buf.String())
}
Output: {"level":"info","trace_id":"00000000000000000000000000000000","span_id":"0000000000000000","message":"hello"}
type LoggerOption ¶
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; failures are reported as bounded diagnostics on the logger's configured writer (fail-open, FR-005) and never affect the CLI exit code. The caller must invoke ax.Flush to drain buffered entries at shutdown.
Push diagnostics (connection failures, non-2xx responses) resolve the configured writer lazily, so LoggerOption order between WithLokiFromEnv and WithLoggerWriter does not matter for them. Construction-time URL warnings are emitted when the option runs and go to the writer configured at that point.
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")
}
Output:
type MCPSchema ¶
type MCPSchema = isolatedschema.MCPSchema
MCPSchema is the lightweight MCP-compatible adapter shape.
func BuildMCPSchema ¶
BuildMCPSchema adapts the command tree to a simple MCP tools list.
Example ¶
ExampleBuildMCPSchema adapts the command tree to the MCP tools-list shape emitted by __schema --as=mcp: each callable command becomes a tool whose inputSchema describes its flags as a JSON Schema object.
package main
import (
"fmt"
"github.com/spf13/cobra"
ax "github.com/rshade/ax-go"
)
func main() {
root := &cobra.Command{
Use: "app",
Short: "test app",
}
root.Flags().String("config", "", "config file")
mcpSchema := ax.BuildMCPSchema(root)
fmt.Println(mcpSchema.Tools[0].Name)
fmt.Println(mcpSchema.Tools[0].Description)
fmt.Println(mcpSchema.Tools[0].InputSchema["type"])
}
Output: app test app object
type MCPTool ¶
type MCPTool = isolatedschema.MCPTool
MCPTool describes one command as an MCP-compatible tool.
type 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 ¶
ModeFromContext returns the resolved output mode stored in ctx.
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.
Example ¶
ExampleSchema shows the ax-native reflective schema tree: tool identity, the build-injected version, the mode-detection rule, the command tree, and the error-envelope contract. The schema carries its own schema_version, so it can evolve independently of the tool version.
package main
import (
"fmt"
"github.com/spf13/cobra"
ax "github.com/rshade/ax-go"
)
func main() {
root := &cobra.Command{
Use: "app",
Short: "test app",
}
s := ax.BuildSchema(root, ax.WithSchemaVersion("v0.1.0"))
fmt.Println(s.SchemaVersion)
fmt.Println(s.Tool)
fmt.Println(s.Version)
fmt.Println(s.ModeDetection)
}
Output: 1.0.0 app v0.1.0 --format flag > AGENT_MODE env > TTY detection
func BuildSchema ¶
func BuildSchema(root *cobra.Command, opts ...SchemaOption) Schema
BuildSchema reflects a Cobra command tree into the ax-native schema.
Example ¶
ExampleBuildSchema reflects a Cobra command tree into the versioned, ax-native schema document the reserved __schema command emits on stdout. The output is strict minified JSON with no timestamps or generated IDs, so it is byte-identical across runs.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
ax "github.com/rshade/ax-go"
)
func main() {
root := &cobra.Command{
Use: "app",
Short: "test app",
Example: "app run",
}
root.Flags().String("config", "", "config file")
s := ax.BuildSchema(root, ax.WithSchemaVersion("v0.1.0"))
if err := ax.WriteJSON(os.Stdout, s); err != nil {
fmt.Println("error:", err)
}
}
Output: {"schema_version":"1.0.0","tool":"app","version":"v0.1.0","mode_detection":"--format flag \u003e AGENT_MODE env \u003e TTY detection","command":{"use":"app","short":"test app","example":"app run","flags":[{"name":"config","type":"string","usage":"config file"}],"non_deterministic_fields":[]},"error_envelope":{"schema_version":"1.0.0","required":["error_code","message","trace_id","tool","version","schema_version"],"optional":["actionable_fix","context","suggestions"],"non_deterministic_fields":["trace_id"]}}
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.
Example ¶
ExampleTelemetry shows the lifecycle guard on Telemetry.Shutdown: it is nil-safe, so a CLI can defer Shutdown unconditionally on the handle — including a zero handle when setup was skipped — without a nil-pointer panic.
package main
import (
"context"
"fmt"
ax "github.com/rshade/ax-go"
)
func main() {
var telemetry *ax.Telemetry
fmt.Println(telemetry.Shutdown(context.Background()))
}
Output: <nil>
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 unconditionally replaces the process-wide OTel globals: any TextMapPropagator or TracerProvider an embedding application installed via otel.SetTextMapPropagator or otel.SetTracerProvider before this call is discarded. That clobbering is deliberate for now — ax-go targets short-lived CLI processes that own their telemetry lifecycle; cooperating with a pre-installed provider in long-running embedding apps is a known, deferred design decision.
StartTelemetry is fail-open: telemetry setup failures are reported to stderr. The error return is reserved for future use and is always nil today.
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
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.
Source Files
¶
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
|
|
|
logging
command
Command logging is the minimal consumer of ax-go's import-isolated logging surface, and the artifact internal/cmd/sizecheck measures against the absolute binary-size ceiling.
|
Command logging is the minimal consumer of ax-go's import-isolated logging surface, and the artifact internal/cmd/sizecheck measures against the absolute binary-size ceiling. |
|
rootlogging
command
Command rootlogging is the root-facade counterpart of examples/logging, and the denominator of the binary-size reduction ratio internal/cmd/sizecheck enforces.
|
Command rootlogging is the root-facade counterpart of examples/logging, and the denominator of the binary-size reduction ratio internal/cmd/sizecheck enforces. |
|
Package id provides non-observability identifier helpers.
|
Package id provides non-observability identifier helpers. |
|
internal
|
|
|
cmd/apidiff-verdict
command
Command apidiff-verdict scopes a go-apidiff report to ax-go's public API surface and decides whether a pull request introduces a breaking change that must be acknowledged.
|
Command apidiff-verdict scopes a go-apidiff report to ax-go's public API surface and decides whether a pull request introduces a breaking change that must be acknowledged. |
|
cmd/benchcheck
command
Command benchcheck enforces a performance regression budget by comparing two `go test -bench=.
|
Command benchcheck enforces a performance regression budget by comparing two `go test -bench=. |
|
cmd/covercheck
command
Command covercheck enforces per-package and repo-wide test-coverage floors against a Go coverage profile (the output of `go test -coverprofile=coverage.out -covermode=atomic ./...`).
|
Command covercheck enforces per-package and repo-wide test-coverage floors against a Go coverage profile (the output of `go test -coverprofile=coverage.out -covermode=atomic ./...`). |
|
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. |
|
cmd/sizecheck
command
Command sizecheck enforces ax-go's binary-size guarantee for the import-isolated logging surface.
|
Command sizecheck enforces ax-go's binary-size guarantee for the import-isolated logging surface. |
|
cmd/surfacecheck
command
surfacecheck is internal maintainer/CI tooling: it scans the complete compiler-visible surface of every public package, under each supported build-tag configuration and target profile, compares the result against the reviewed live baseline, and cross-validates the permanent public-surface audit.
|
surfacecheck is internal maintainer/CI tooling: it scans the complete compiler-visible surface of every public package, under each supported build-tag configuration and target profile, compares the result against the reviewed live baseline, and cross-validates the permanent public-surface audit. |
|
logcore
Package logcore is the zerolog-backed logger implementation shared by the two public logging surfaces: the root package ax and the import-isolated package logging.
|
Package logcore is the zerolog-backed logger implementation shared by the two public logging surfaces: the root package ax and the import-isolated package logging. |
|
mcpserver
Package mcpserver is the internal MCP protocol engine behind the public mcp package.
|
Package mcpserver is the internal MCP protocol engine behind the public mcp package. |
|
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 logging is the import-isolated public logging surface of ax-go.
|
Package logging is the import-isolated public logging surface of ax-go. |
|
Package mcp exposes an ax-go CLI's command tree as a live Model Context Protocol (MCP) server with no per-tool work.
|
Package mcp exposes an ax-go CLI's command tree as a live Model Context Protocol (MCP) server with no per-tool work. |
|
Package schema provides import-isolated command discoverability contracts.
|
Package schema provides import-isolated command discoverability contracts. |
