3.0. Packaging
In one glance
- You will: Build the agent as one static binary, watch it refuse a composition that does not exist, and learn where each capability in this chapter lands.
- You need:
mise run installdone. No model, key, container, or network. Assumed, not required: the composition from 2.1. First Agent. - Time: about 20 minutes, hands-on.
Why the agent ships as one binary, not one per surface
Packaging is the decision about how many executables an agent ships as, and which artifacts travel with them: seed data, runtime state, a container image. Every extra executable is a second configuration path and a second build pipeline, and the one that breaks in production is the one nobody wired into the test suite. Whoever debugs it cannot tell which configuration path the failing process read.
This chapter adds four ways to reach the agent — console, web, MCP, and A2A — each a plausible excuse for that second executable. There is one binary instead, and configuration selects the composition it runs as. This page builds it, watches it refuse a composition that does not exist, and maps where a capability lands. Build it and look:
cd agents/go
mise run build
./bin/agent version{
"build_timestamp": "2026-08-10T20:12:50+02:00",
"mode": "development",
"version": "development",
"source_identity": "f72b98b025ae35f644c865cd097b66155b3b0d04",
"revision": "f72b98b025ae35f644c865cd097b66155b3b0d04",
"tree_digest": "sha256:7c103f338448acc491d90f73de4908d76cb1e16d45614842a97353c7dd66ebe4",
"dirty": false
}Your hashes will differ: they are this checkout’s revision and a digest of its whole tree, stamped in at link time so a running process can name its source. mode and version read development because AGENT_BUILD_MODE is unset; a release build reads the VERSION file instead.
The build sets CGO_ENABLED=0, an invariant the rest of the course depends on rather than an optimization. It lets the same file run inside the distroless image of Chapter 6 — the binary and its data, no shell, package manager, or compiler — and keeps one pure-Go SQLite implementation in the binary instead of a C library that must be present on every host that runs it.
You just built the whole agent — every composition, every transport, every tool — as a single self-contained file with no runtime dependencies.
How AGENT_ENTRYPOINT picks a composition, and what it refuses
That one file has to be told which agent to be. It parses its environment and refuses bad settings before constructing a runtime, so ask what it resolved:
cd agents/go
./bin/agent config:checkAgent configuration is valid. Resolved settings (secrets masked):
- AGENT_A2A_MAX_LLM_CALLS = 12
- AGENT_A2A_PORT = 8080
- AGENT_DATA_DIR = ../data
- AGENT_ENTRYPOINT = agent
- AGENT_MCP_URL = (unset)
- AGENT_MODEL = qwen3:4b-instruct
- AGENT_STATE_DIR = .state
- AGENT_WRITES_DISABLED = false
- OPENAI_API_KEY = **********That is nine lines out of forty-eight settings; the real listing names every setting the runtime read, sorted, with secrets replaced by asterisks. It answers “which value is this process using” without printing a credential.
Configuration is parsed once, at startup, and every problem found is reported together: a five-second fix instead of a debugging session inside a half-built agent. Predict what that does to a composition that does not exist — a typo in AGENT_ENTRYPOINT could plausibly fall back to the default, or fail deep in the first turn:
cd agents/go
AGENT_ENTRYPOINT=supervisor ./bin/agent consoleagent: loading the configuration (run `config:check` for the resolved settings): agent configuration is invalid (1 problems)
- AGENT_ENTRYPOINT: must be one of agent, workflow, coordinator; got "supervisor"Those are the first two of eighty-one lines: the binary exits non-zero before a model client exists, names the three legal values, and then — because the subcommand never parsed — ADK dumps its whole argument reference underneath. Read the top two lines and ignore the rest.
cmd/agent parses a subcommand, loads configuration once, and composes the requested surface. Every task below runs that same file:
| Task | Exact task expansion | Composition or transport |
|---|---|---|
mise run run | AGENT_ENTRYPOINT=agent go run ./cmd/agent console | Conversational agent in the terminal |
mise run workflow | AGENT_ENTRYPOINT=workflow go run ./cmd/agent console | Bounded read-only workflow |
mise run coordinator | AGENT_ENTRYPOINT=coordinator go run ./cmd/agent console | Least-privilege coordinator |
mise run web | AGENT_ENTRYPOINT=agent go run ./cmd/agent web -port 8002 webui -api_server_address http://localhost:8002/api api -webui_address localhost:8002 -sse-write-timeout 60m | ADK development web and REST surface |
mise run mcp | go run ./cmd/agent mcp | MCP over standard input/output |
mise run mcp:http | MCP_HOST=127.0.0.1 MCP_PORT=8000 MCP_TRANSPORT=streamable-http go run ./cmd/agent mcp | MCP over loopback HTTP |
mise run a2a | AGENT_ENTRYPOINT=agent go run ./cmd/agent a2a | Persistent A2A server |
mise run data:reset | rm -rf .state | Delete disposable runtime state; the next A2A start republishes it |
Each expansion shows that task’s own environment. A process that builds a model client needs credentials; a process that only reads a database must not hold any. So five tasks — run, workflow, coordinator, web, and a2a — additionally load the repository-root .env through mise’s redacted dotenv loader, and the two MCP tasks deliberately do not. Step outside mise and the loader goes with it, which is why 3.4. Memory writes set -a && . ../.env && set +a by hand before running the retrieval command from evals.
The first three rows differ by one environment variable: the composition is data, not a build target, so a behavior you see in mise run workflow reproduces in a container shipping one binary.
Two directories complete the picture, and confusing them is the packaging mistake that hurts most. agents/data is committed, immutable seed: incidents, service logs, runbooks, and skills. Runtime writes go to agents/go/.state on a host, or a shared volume in Kubernetes. Startup copies or migrates a runtime generation, a writable copy published from that seed, and only the write-owning boundary — the one process allowed to publish it — does so. Read tools and probes never touch the seed. Both locations are explicit settings, AGENT_DATA_DIR and AGENT_STATE_DIR, so nothing depends on which directory you are standing in.
The container follows the same rule: the multi-stage agents/go/Dockerfile compiles this exact package with CGO_ENABLED=0, then copies the binary and the seed into that distroless runtime. Chapter 6 verifies the non-root user, read-only filesystem, probes, scan, and artifact identity.
Deeper: the packaging mistakes that break the course contract
Each has a specific failure behind it, not a style preference:
- Adding agent dependencies to the root theme module, which makes the documentation build resolve the agent’s graph.
- Reading ambient credentials inside a package instead of passing validated configuration, which makes
config:checka lie. - Hiding runtime state beside immutable seed data, which makes
mise run data:resetdestructive. - Building a second executable per composition, which lets two of them drift and only one get tested.
- Importing
agents/gofrom the black-boxevalsmodule, which turns protocol observation into unit tests of your own implementation. - Relying on cgo, which breaks the static distroless image contract and forces a second SQLite engine into the build.
Where each package lives, and what go.mod and go.sum own
The module is deliberately flat: packages are grouped by responsibility, not by framework layer, so a capability has one obvious home:
agents/go/
├── cmd/agent/ executable and subcommands
├── compose/ conversational, workflow, and coordinator wiring
├── config/ typed environment boundary
├── data/ immutable seed and the runtime generation published from it
├── domain/ normalized ids, enums, and shared records
├── tools/ incident reads and guarded actions
├── memory/ runbooks, notes, and semantic retrieval
├── policy/ budget, compaction, redaction, and injection controls
├── resilience/ deadlines, retries, and circuit breaking
├── a2aserver/ persistent A2A transport and task store
├── mcpserver/ least-privilege MCP server
├── telemetry/ logs, traces, and metrics
├── go.mod direct, indirect, and tool dependencies
└── go.sum resolved module checksumsgo list ./... prints twenty-two packages, and the map stops at twelve because those twelve are where a capability lands. The others are buildinfo, kagentinterop, model, piiwebhook, platformdrill, principal, state, a second small command, and two internal/ helpers. Dependencies point inward toward typed domain values and outward only at explicit adapters, which is why compose can swap a local tool for a remote one without domain noticing.
go.mod gives the module its import path, language version, and direct requirements; go.sum authenticates what was downloaded. Here is the requirement block, quoted from the file the build reads:
require (
github.com/a2aproject/a2a-go/v2 v2.4.0
github.com/caarlos0/env/v11 v11.4.1
// Three module names, one SQLite engine. github.com/glebarez/go-sqlite is the
// database/sql driver (registered as "sqlite") that a2aserver, data, memory, and
// state open directly; github.com/glebarez/sqlite is the GORM dialector ADK's
// session store needs, and is a thin layer over that same driver; modernc.org/sqlite
// is the transpiled-C engine under both and stays indirect. All three are pure Go,
// which is what lets CGO_ENABLED=0 hold and keeps exactly one SQLite implementation
// in the binary. Never add a cgo driver such as github.com/mattn/go-sqlite3 beside them.
github.com/glebarez/go-sqlite v1.23.0
github.com/glebarez/sqlite v1.11.0
github.com/google/jsonschema-go v0.4.3
// The second session backend, and the only non-SQLite database in the binary.
// gorm.io/driver/postgres is the GORM dialector ADK's session store needs;
// github.com/jackc/pgx/v5 is the pure-Go driver under it, imported for its
// database/sql registration so cmd/agent can own and bound the pool itself.
// Both stay cgo-free, so CGO_ENABLED=0 still holds. Sessions are the only
// state that moves here — the incident, task, memory, and vector databases
// remain SQLite files owned by one writer (Ch. 6.9).
github.com/jackc/pgx/v5 v5.10.0
github.com/modelcontextprotocol/go-sdk v1.7.0
// ADK Go v2.2.0 owns this generated-client pair and requires openai-go v3.49.0.
// Bump it only with ADK, so the adapter and the generated client stay in step.
github.com/openai/openai-go/v3 v3.49.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v3.49.0 validator=agents/go mise run check and test
// ADK Go v2.2.0 still uses the OTel log.Value and log.KeyValue APIs that the
// 1.45/0.21 release set removed, so 1.44 with log 0.20 is the highest compiling family.
go.opentelemetry.io/otel v1.44.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v1.44.0 validator=agents/go mise run check and test
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/log v0.20.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v0.20.0 validator=agents/go mise run check and test
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/text v0.40.0
google.golang.org/adk/v2 v2.2.0
google.golang.org/genai v1.66.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v1.66.0 validator=agents/go mise run check and test
gorm.io/driver/postgres v1.6.2
)Read it for two things, not the version numbers. Three of those module names — glebarez/go-sqlite, glebarez/sqlite, and the modernc.org/sqlite the comment names — are one pure-Go SQLite engine seen from three angles, which is the requirement behind the CGO_ENABLED=0 invariant above. Four entries carry a // compatibility hold: marker naming ADK as their owner: openai-go, otel, otel/log, and genai are pinned to whatever the installed ADK compiles against, so nobody bumps one alone and discovers at runtime that the adapter no longer matches.
Two commands confirm the graph resolves and matches its checksums:
cd agents/go
go mod verify
go list -deps ./... >/dev/nullall modules verifiedThat answer is narrow: the bytes you compiled are the bytes the module authors published. It says nothing about licences, known vulnerabilities, or whether the code is any good — separate checks own each of those claims, and 0.2. Evidence explains why the course keeps them apart.
go.mod also carries a tool directive listing goimports, govulncheck, and gofumpt. Each is then part of the module graph — versioned and checksummed like any dependency, runnable as go tool gofumpt — without importing a line of it into the production binary. Tools that span modules, such as golangci-lint and gotestsum, are pinned in the root mise.toml instead: one authority per tool, no hidden global install.
Deeper: when vendoring is worth its cost
The learner path uses the module cache and committed checksums. Vendoring writes a large derived tree into the repository, and earns it only when a release or restricted build environment forbids fetching modules at build time:
cd agents/go
go mod vendor
go build -mod=vendor ./cmd/agentIf you do it, review vendor/modules.txt and make the module mode explicit in CI and release builds: a vendored tree that silently disagrees with go.mod is worse than none. This course commits no vendor tree: reproducibility comes from versioned requirements, checksums, pinned build tools, and a digest-pinned container.
Your turn: prove the composition is configuration, not a build target
That claim is easy to state and easy to get wrong later, so check it by hand now.
- Mode:
inspect— every step is a read or a refusal, so there is nothing to revert. - Goal: show that the composition is configuration rather than a build target, and that an unknown composition never reaches a runtime.
- Files to touch: none.
- Preflight:
cd agents/go && mise run build, then confirm./bin/agent versionreports"mode": "development". - Steps: record
sha256sum bin/agent. Run./bin/agent config:check, thenAGENT_ENTRYPOINT=workflow ./bin/agent config:check, thenAGENT_ENTRYPOINT=coordinator ./bin/agent config:check, and read theAGENT_ENTRYPOINTline each time. Finish withAGENT_ENTRYPOINT=supervisor ./bin/agent console, and read only its first two lines — the seventy-nine that follow are ADK’s argument reference, as above. - Gate that proves completion: the three valid runs report three different entrypoints,
sha256sum bin/agentis identical after all of them, and the fourth exits non-zero namingagent, workflow, coordinator. - Final state: no files changed. Write down which of the eight tasks in the table above you would reach for to reproduce a colleague’s bug report about the workflow.
What you can do now
- You can say why one static, cgo-free binary serves three compositions, selected by
AGENT_ENTRYPOINTrather than a build target. - You can read the resolved configuration with secrets masked, and say why
AGENT_ENTRYPOINT=supervisorexits before a runtime exists. - You can say what
go.mod,go.sum, thetooldirective, and vendoring each own, and whatgo mod verifydoes not claim. - You can name where a new capability belongs, and which directory is immutable seed rather than disposable state.
You now have somewhere to put the next seven pages’ worth of capability, and a build that stays one file when they are in it.
Continue to 3.1. Tools, which fills tools with the reads the agent answers from and the writes that change a live system.