The fifteenth agent was committed by a machine
On June 26 a new agent landed on the restaurant-analytics platform I run at a large payments company. It reads uploaded underwriting documents, enriches them with live merchant data, and produces a recommendation: approve, conditional, decline, or escalate for manual review. The commit that shipped it changed 3 files and added 276 lines. A 133-line manifest. Five registration lines. A 138-line verification script that uploads a mock application, asks the running agent about it over a live websocket, and checks the reply proves the agent actually read the file.
It did not touch the websocket runtime. It did not touch the frontend. Nobody edited the chat interface, the auth layer, or the event stream. And I did not write it. My autonomous delivery system authored the commit end to end, pressure-tested it against a running backend, and noted in its own commit message the one thing that bit it along the way (a composed graph requires an explicit graph definition; it added one).
For contrast: this repo's first commit is December 6. By February 4 it held exactly two agents, both wired into the runtime by hand, by me, with the websocket handler knowing each of them by name. Two agents in two months, then thirteen more at a fraction of the cost. The distance between those numbers is a conversion story, and the honest ledger of that conversion is written in deletions.
What hardwired actually looks like from the inside
Before March 8 the websocket handler was 1,284 lines, and it knew things no runtime should know. Grep found 4 separate sites branching on the agent type string. The handler imported each agent's graph builder directly, assembled one hand-built kwargs dict for the customer agent and a different one for the admin agent, and derived its own cache key by hashing a frozenset of those kwargs. Which auth token each agent required lived in a hardcoded dict near the bottom of the file, four entries long, maintained by hand.
With two agents this worked, and that is the trap. Bespoke wiring is genuinely faster for the first agent and roughly break-even for the second. Everything after that is a tax. Each new agent adds a branch to a file every other agent shares, so every agent's change becomes every agent's regression risk. Identity, auth, context loading, caching, prompt assembly, streaming: six concerns, multiplied by the agent count, interleaved in one handler that everything flows through.
Here is what the tax cost me, specifically. When I finally converted, the audit found 4 places where the two agents' wirings had silently drifted. The worst one: the map that resolves a spoken location name to a location id was wired in one path and defaulted to an empty dict elsewhere. No exception. No log line. Queries that named a store just quietly failed to resolve it. That gap sat in production until the platform work forced a line-by-line reconciliation of the two paths. Silent divergence is the real cost of bespoke agents, and it starts at two, not at ten.
What a manifest declares
The conversion replaced all of that with a declaration. Every agent is now a manifest: one Python file stating what the agent is, and a compiler that turns the statement into a running graph. Here is the June agent's manifest, lightly redacted:
UNDERWRITING_ADVISOR_MANIFEST = AgentManifest(
agent_type="underwriting_advisor",
display_name="Underwriting Advisor",
graph_strategy="composed",
graph_config=GraphStrategyConfig(
max_iterations=14,
synthesis_fallback=True,
replan_on_stuck=False,
strategy_params=StrategyParams(graph_definition=_UNDERWRITING_GRAPH),
),
model_use_case="REASONING",
invocation_key="underwriting_advisor",
tool_groups=["knowledge_base", "merchant_recall"],
context_slots=[],
prompt=PromptConfig(
fallback_module="...manifests.underwriting_advisor",
fallback_function="get_underwriting_advisor_system_prompt",
),
capabilities=["chart_artifact", "table_artifact", "report_artifact"],
access=AgentAccess(allowed_realms=["tenant", "platform"]),
)Every field is a contract with the compiler. graph_strategy picks one of three topologies the platform knows how to build: a planner-routed ReAct loop, a pure ReAct loop, or a composed graph declared as typed nodes and edges. This agent uses the third; its whole topology, a two-node read-documents-then-decide loop, is declared right above the manifest in the same file. invocation_key points at a central model registry, so which LLM this agent runs on is a config lookup an admin can override at runtime, never a constant. tool_groups names groups in a shared catalog; the manifest imports no tool code. And access is Gate 1: which realms are allowed to compile this agent at all.
Notice what is missing. No websocket code. No auth branching. No frontend registration. The discovery API reads the registry, and the agent picker populates itself, filtered to what each user is authorized to see. The runtime stopped knowing agents by name on March 8 and has not learned a new name since.
The compiler, and gates that fail closed
When a session connects, the platform compiler runs a ten-step pipeline: check agent access (Gate 1), collect tools from the catalog, filter those tools by user permission (Gate 2), collect tool metadata, load the manifest's declared context slots, build the prompt from that loaded context, create the model, pack the strategy parameters, build the graph, and compile it with a checkpointer plus a tool-context factory. One pipeline. Every agent. No exceptions to route around.
The gates are the part I would defend in front of any security team. Gate 1 runs before anything is built; a user who fails it gets an access-denied exception, never a degraded agent. Gate 2 takes the tools the manifest requested and intersects them with what this specific user is authorized to hold, and the compiler logs the arithmetic on every compile, authorized over collected. A later addition, Gate 3, drops tools whose owning domain has not finished onboarding, and it carries my favorite invariant in the codebase: if a manifest explicitly overrides a tool that a gate has since removed, the compile raises an error rather than silently honoring the override.
The rule underneath all three: a compile that cannot satisfy its manifest must produce no agent instead of a weaker agent. That rule exists because of the empty-dict bug from the bespoke era. Silence was the failure mode I paid for. The platform's failure mode is a loud exception carrying the agent type and the reason.
The cutover, and the deletion ledger
The conversion itself was fast because the planning was slow. On March 8 at 10:07 the platform landed: manifest models, a registry, an agent picker driven by a discovery API. At 14:19 the same day, a second commit cut the runtime over to the compiler as the sole entry point. That commit message contains the sentence I still consider the most important in the repo: no feature flag, no legacy fallback. The plan had been through 5 external review passes before I executed it, and the cutover shipped with a 305-line verification script that checked 40 of 40 platform contracts green before I called it done.
The websocket handler changed by 203 lines added and 207 removed. Out went the 4 agent-type branches, the hardcoded auth dict, the hand-rolled cache keys, and the direct imports of both agents' graph builders. The handler kept its job, streaming and persistence and reconnection, and lost its opinions about who it was streaming for.
The deletions kept coming after the cutover, which is how I knew the seam was real. In June my autonomous delivery system, which had been running its own private graph compiler, cut itself over to the same platform compiler and deleted its bespoke 345-line one; its orchestration core dropped 239 lines against 96 added. And in April I ran the largest deliberate deletion of my career: forked the repo and stripped the entire restaurant domain out in a single commit. 340 files changed, 68,555 lines deleted, 1,979 added. The chassis booted. That fork now runs 16 registered agents in a completely different vertical, on the same compiler, the same gates, the same runtime.
| Concern | Hardwired (before) | Manifest platform (after) |
|---|---|---|
| Agent identity | 4 agent_type branch sites in a 1,284-line WS handler | Registry lookup; unknown types rejected at connect |
| Auth mode | Hand-maintained dict, four entries | Declared on the manifest, enforced by Gate 1 |
| Graph construction | Per-agent builders imported inside the runtime | One compiler, three declared strategies |
| Tool exposure | Whatever the builder happened to wire | Catalog groups filtered per user, logged as authorized/collected |
| Frontend | Hardcoded picker entries | Discovery API; zero frontend edits per agent |
| Adding an agent | 2 agents in ~60 days, each a runtime surgery | 1 manifest + registration lines; 276 lines total in June |
What it did to delivery
The delivery numbers before and after do not belong on the same chart. Pre-platform: 2 agents in roughly 60 days. Thirteen days after the cutover: 7 new agent manifests landed in a single commit, taking the registry from 2 to 9. Today the origin platform runs 15 manifests and the fork runs 16, and the marginal agent costs what the June agent cost. The runtime has since grown one more resolution path, a database fallback for agents authored in a builder UI, so a user-created agent now loads with no code commit at all. That extension took a fallback lookup, because the seam already existed.
In board terms: at bespoke pace an agent cost about an engineer-month, so a ten-agent roadmap was a hiring conversation. At manifest pace a ten-agent roadmap is a sprint plus a review queue, and the review itself got cheaper, because reviewing a new agent now means reading one declarative file instead of auditing edits to a shared 1,284-line runtime. The cutover cost one working day on top of the planning. Thirteen days later it paid for itself seven times over, in one commit.
There are also wins every agent inherits without asking. Per-intent tool filtering sends roughly 70% fewer tool tokens per call. The system prompt splits into a static section of about 3,000 tokens that the provider caches at a 90% discount and a dynamic tail of about 200. Tools bind in alphabetical order so the cache key stays stable across turns. None of the 15 agents implemented any of that. They got it by being compiled.
Convert at agent two
The standard advice says wait for three to five concrete examples before you build the abstraction. For agent runtimes I now think that advice is wrong, and I hold the receipts from following it. Agents look like independent programs and behave like clones. They share streaming, persistence, reconnection, auth, checkpointing, and observability by construction; on my platform that shared surface is the overwhelming majority of what an agent is. The part worth keeping bespoke is exactly manifest-sized: tools, prompt, topology, access.
My two bespoke agents had already diverged in 4 places by the time I converted, and one divergence was a shipped bug that failed silently in production. A third example would have meant a third wiring to reconcile and a third place for the empty dict to hide. The cost of converting at two was real too, and I will name it: I got the abstraction partly wrong. Two graph strategies were not enough, and I later had to build a third for typed composed graphs. Extending the platform was still cheaper than a fifth bespoke agent would have been.
So the stance I will defend: the moment your second agent copies your first agent's wiring, that copy is the platform asking to exist, and every month you delay, the eventual deletion grows. Mine grew to 68,555 lines. I got to delete them on my own schedule. Most teams get the schedule picked for them.