ax-go 0.2 and 0.3: contracts worth pinning
v0.2.0 split ax-go into contract packages you can import without the runtime; v0.3.0 added the coverage floors and compatibility matrix that keep them safe to pin.
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__schemacommand.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
The root ax package still works unchanged. It becomes a thin facade that
re-exports every moved type:
type Error = contract.Error
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.