·4 min read·← All posts
Genie Multi-Agent AI Retrospective Go

The shape of the year

Genie started as a reference implementation of the MARA pattern in Go. Twelve months in, it has:

Here’s what held up and what didn’t.

What survived unchanged

The protocol package. pkg/protocol defines Message and the role constants. It hasn’t changed in 12 months because the contract is the only stable thing in a multi-agent system. Every agent depends on it; every package speaks it. Changing it would be a cross-cutting refactor; not changing it has been the right call.

The orchestrator’s hook surface. OnPolicyDeny, OnAgentError. Two hooks. We were tempted to add more (OnRetry, OnTimeout, OnFallback). Each time we resisted because the existing hooks composed to express the same thing. Twelve months later, two hooks still suffices.

The risk class taxonomy. RiskLow, RiskMedium, RiskHigh. Three levels; clear semantics; never temptingly extended. Compare to projects that drift to 7-tier or 10-tier risk schemes.

The fallback pattern. orchestrator.SetFallback(primary, fallback). One-line API; production-tested via make bcp-drill. Boring; works.

What we rewrote

The first RAG implementation. Single embedding model; one vector store; naive retrieval. Replaced with hybrid (vector + BM25 + RRF + rerank). The first version worked but quality plateaued; the second version generalised across customer use cases.

The first prompt-injection policy. Hand-rolled regex against a list of suspicious patterns. Replaced with a small classifier behind the pkg/safety plugin chain. The classifier handles paraphrased attacks the regex missed.

The first audit log. Wrote to a flat table; no chain; no tampering detection. Replaced with the hash-chained pkg/compliance/audit.go. The old log was useful for debugging; not useful for compliance. The new one serves both.

The first observability layer. Logs only. Replaced with OTel spans + metrics + structured logs (slog). Three signals; same shape across services; standard tooling reads them.

The HTTP middleware stack. First version was custom for each route. Replaced with chi router + composable middleware. The diff was bigger than it sounded; the consistency was worth it.

What we deleted

A custom workflow DSL. Built one to express agent orchestration declaratively. Used by no one because Go was already expressive enough. Deleted six months in.

A graph database integration. Added Neo4j support for a hypothetical use case. No customer asked for it; the code drifted; deleted.

A configuration hot-reload subsystem. Built for an ops scenario we never actually hit. The complexity outweighed the rare use case. Deleted; restart on config change is fine.

Three half-finished agents. Speculative agents (a “supply chain optimiser,” a “regulatory news scanner,” a “customer-satisfaction predictor”) that we built without a clear customer. Each had ~500 LoC. Deleted; the patterns we’d shipped were transferable.

What I’d carry to the next project

Three meta-patterns that crystallised:

  1. Be ruthless about deletion. Code that doesn’t serve a real use case is liability. Twelve months in, the codebase is smaller in some areas than at month 6 — that’s a feature, not a bug.

  2. Two layers of defence beat one layer of cleverness. RLS + bus tenant policy beat any single “smarter” tenant check. Hash-chained audit + verifier beat any single “more tamper-resistant” log format. The boring layered approach holds up.

  3. The hook surface should be minimal. Every time we resisted adding a hook, we found the existing hooks composed to cover the case. Every time we added one anyway, we regretted it within a quarter.

What still nags

Roadmap items, all of them. Not blockers for the current use cases.

The big takeaway

The thing that survived 12 months of production was the discipline, not any specific code. The discipline:

The code is replaceable. The discipline isn’t. For year two, the plan is more discipline, less code.

← Back to all posts