DEV Community

Richard Shade
Richard Shade

Posted on • Originally published at rshade.github.io

ax-go 0.2 and 0.3: contracts worth pinning

Two ax-go releases landed close together, and they're really one idea in two halves. v0.2.0 let you pin just the contract an agent depends on, without the runtime behind it. v0.3.0 is the work that keeps that contract from moving once you've pinned it.

If you're new here: ax-go is the agentic-experience foundation under my Go CLIs. stdout is data, traces cross the plugin boundary, every human table has a JSON twin. The contract is the whole point, so the contract is what both releases are about.

v0.2.0: import-isolated contract packages

Go imports are all-or-nothing. There's no tree-shaking: import a package, and you compile in its entire transitive dependency graph, whether you call into it or not.

So until v0.2.0, reusing any ax-go contract (the error envelope, the exit codes, the __schema shapes) meant importing the root ax package. And ax is the full runtime: the OpenTelemetry SDK, zerolog and Loki, HTTP and gRPC instrumentation. A thin orchestrator that only wanted to emit a well-formed error envelope was paying for a telemetry stack it never started.

v0.2.0 carves four narrow packages out of the root:

  • contract: exit codes, mode resolution, context metadata, the success/error envelope, the strict JSON/NDJSON writers.
  • config: bounded Hujson reads and comment-preserving RFC 6902 patches.
  • schema: the ax-native and MCP-compatible schema builders, plus the __schema command.
  • id: UUID v4 idempotency keys and UUID v7 entity IDs.

Each depends on the standard library and not much else: never on the root facade, never on a runtime adapter. So the import you reach for matches the weight you take on:

import "github.com/rshade/ax-go/contract"  // just the shapes
import ax "github.com/rshade/ax-go"          // the shapes plus the runtime
Enter fullscreen mode Exit fullscreen mode

The root ax package still works unchanged. It becomes a thin facade that re-exports every moved type:

type Error = contract.Error
Enter fullscreen mode Exit fullscreen mode

That's a type alias, not a new type, and the distinction does real work: ax.Error and contract.Error are the same type. So errors.As(err, *contract.Error) still matches an error built deep inside the runtime. Identity survives the split, and upgrading from v0.1.0 is a go get -u and nothing else.

The boundary is a test, not a promise. Each contract package ships an import-isolation test that runs go list -deps and fails the build if a forbidden runtime import (the root facade, the OTel SDK, Loki, the HTTP or gRPC instrumentation) shows up in its graph. CI enforces the isolation; I don't have to remember to.

v0.3.0: keeping the contract from drifting

Splitting the contract out is half the promise. The other half is that the shapes you pin don't move underneath you. v0.3.0 is the guardrails.

Coverage floors. Every contract package now has its own coverage floor enforced in CI, alongside a repo-wide one. The packages a thin consumer pins are exactly the packages that shouldn't quietly lose test coverage.

A compatibility matrix and a CONTRIBUTING guide. The README now spells out which Go versions and which ax-go versions are supported, so "can I depend on this" has a written answer instead of a guess. CONTRIBUTING documents how the contract surface is allowed to change, and how it isn't.

A gate on breaking changes. Public-API changes now run through go-apidiff in CI, so a breaking change to an exported contract can't merge without tripping a gate. The pin you depend on can't move by accident.

the docs site

Both releases ship against a real home now: rshade.github.io/ax-go, an Astro Starlight site built on the shared rshade-theme. Same tokens as finfocus, because they came from the same shop.

where it's at

Still pre-1.0, still pinnable. v0.2.0 let you pin just the contract; v0.3.0 is the work that keeps pinning it safe. An agent shouldn't have to compile a telemetry stack to read an exit code, and it shouldn't wake up to find the code moved underneath it either.

Top comments (0)