Why architecture matters here

Architecture matters here because the alternative — edit the schema, redeploy, hope — has a failure signature that looks like model regression. An agent that used to book meetings correctly starts putting the attendee list in the description field. The obvious suspect is the model: someone changed the temperature, someone shipped a new checkpoint, the provider must have updated something. The actual cause is that three weeks ago a backend engineer reordered two parameters and tightened a description, and the model's argument-filling habit was tuned to the old order. Without versioning you cannot even test the hypothesis, because the old schema no longer exists anywhere.

The second reason is fleet heterogeneity. In any system with more than one agent, tools are shared. A search tool is called by the research agent, the support agent, and a batch pipeline. Those three consumers have different prompts, different few-shot examples, and different regression suites. A schema change that improves the research agent's accuracy can degrade the support agent's, and with a single global schema you cannot ship the improvement without taking the regression. Versioning turns a fleet-wide coupled decision into per-agent independent ones.

Third, there is the evaluation problem. Agent quality is measured by replaying recorded trajectories, and a trajectory records the arguments the model emitted. Change the schema and every recorded trajectory becomes untestable — the assertions reference fields that no longer exist. Teams respond by rewriting the fixtures, which quietly destroys the baseline they were meant to protect. A versioned registry lets old trajectories replay against the schema they were recorded under, so a regression is a regression and not a fixture-drift artifact.

Finally, versioning is what makes deprecation survivable. Without it, removing a field is a coordinated flag day across every agent, every prompt, and every cached few-shot example. With it, removal is a measured decline: watch calls against v1 fall, shim the stragglers, reject with a helpful error, delete when the counter is zero. The same discipline that makes public APIs evolvable makes tool surfaces evolvable, but only if you build the counters first.

Advertisement

The architecture: every piece explained

The tool registry is the source of truth, and its unit of storage is not a schema but a versioned schema set: search_docs maps to {v1: {...}, v2: {...}, v3: {...}}, each entry immutable once published. Immutability is the whole point. If v1 can be edited, versioning is theatre — the thing an agent pinned to and the thing it gets can diverge. Publishing a change means minting a new version, always, even for a typo fix in a description, because a description edit is exactly the kind of change that moves model behaviour.

The compatibility classifier is the piece that most teams skip and later wish they had. Given two versions it decides whether the change is additive (a new optional field — old arguments still validate), breaking (a removed or renamed field, a narrowed enum, a newly required argument), or cosmetic-but-behavioural (a reworded description, a reordered property list). That third class is the one API tooling has no concept of and agents care about most. The classifier's job is to force the label to be explicit, so that a reworded description cannot ride to production disguised as a patch bump.

The schema resolver sits in the request path and answers one question: for this agent, on this turn, which version of each tool do we inject? Agents carry pins — search_docs@v2 — set in their configuration and changed deliberately. The resolver reads pins, fetches immutable schemas, and hands them to prompt assembly. Because the output is deterministic per agent version, the assembled prefix is byte-stable, which is what keeps the prompt cache warm. Resolve dynamically per user and you have thrown that away.

The argument validator and adapter chain handle the return path. The model emits arguments shaped like the version it saw. The validator checks them against that version's schema — not the newest one — and then walks the adapter chain: a v1-to-v2 function, a v2-to-v3 function, composed. Each adapter is small, pure, and independently testable, and the chain's output is always the newest shape. This is the design decision that keeps the system tractable: exactly one live handler exists, speaking only the current shape, and all historical compatibility lives in a linear chain of transformations rather than in branching conditionals inside business logic.

The deprecation telemetry and sunset controller close the loop. Every resolved call increments a counter keyed by tool, version, and calling agent. The sunset controller reads those counters and drives a state machine per version: active, warned (logged, dashboards flag it), shimmed (adapter still runs, but the resolver refuses new pins), rejecting (calls fail with an error naming the replacement field), and finally deleted. Transitions are gated on the counter, not on the calendar, which is the difference between a deprecation that completes and one that lives forever in a spreadsheet.

Tool schema versioning — the contract between a model's learned habits and your APIschemas are prompts, and prompts are frozen in the model's expectationsTool registryname -> versioned schema setCompatibility classifieradditive / breaking / cosmeticSchema resolverpicks version per agent pinPrompt assemblyinjects resolved schemaModel callemits tool_use w/ argumentsArgument validatorreject / coerce / migrateVersion adapter chainv1 args -> v2 args -> v3 handlerTool handler (single live impl)speaks only the newest shapeDeprecation telemetrycalls per version per agentSunset controllerwarn -> shim -> reject -> deletevalid args
Schema versioning is a resolver plus an adapter chain: agents pin a version, the resolver injects that shape into the prompt, the validator migrates emitted arguments forward, and one live handler speaks only the newest shape.
Advertisement

End-to-end flow

Follow a real change. The docs team wants search_docs to accept a structured filter object instead of a free-text filter string, because the model keeps inventing filter syntax. An engineer edits the schema in the registry. The registry refuses to overwrite v2 and mints v3. The compatibility classifier diffs the two, sees a field whose type changed from string to object, and labels the change breaking. That label matters immediately: it means v3 cannot be auto-adopted, and it means an adapter is mandatory before v3 can be published at all.

The engineer writes the v2-to-v3 adapter — a function that parses the old free-text filter with the same tolerant parser the handler used to contain, and emits the structured object. This is the moment the design pays off: that parser was going to exist somewhere regardless. Putting it in an adapter means the handler gets to delete it, and the messy compatibility logic sits in a file whose name announces exactly what it is and when it can be deleted.

v3 is published. Nothing changes yet, because no agent is pinned to it. The research agent's owner flips its pin to v3 in a config change, which is a normal deploy with a normal rollback. On the next turn, the resolver injects the v3 schema, the prompt prefix changes, the prompt cache misses once and then re-warms. The model — now seeing an object-shaped filter with a clear enum of fields — emits a structured filter. The validator checks it against v3, finds the chain empty (v3 is current), and calls the handler directly.

Meanwhile the support agent is still pinned to v2. Its model still emits a free-text filter string. The validator checks it against v2, passes, and runs the v2-to-v3 adapter, which parses the string and produces the object. The handler cannot tell the two callers apart, and does not need to. Two agents, two schema realities, one implementation — that is the property the adapter chain buys, and it is why the handler never grows a version parameter.

Over the next weeks, telemetry shows v2 calls declining as agent owners migrate on their own schedules, each validating against their own eval suite. When the support agent's owner sees the research agent's accuracy numbers improve, they migrate too. The v2 counter reaches zero, sits there for a full retention window, and the sunset controller advances v2 to rejecting. Nothing breaks, because nothing was calling it. A release later, v2 and its adapter are deleted, and the chain shortens by one link.

Notice what the flow never required. At no point did anyone diff two live schemas in the handler, branch on a version string inside business logic, or coordinate a flag day across the fleet. The research and support agents moved independently, each gated on its own evaluation suite, and the handler stayed ignorant of both. The compatibility classifier did the one genuinely hard piece of thinking — recognising that a string-to-object change was breaking and therefore adapter-mandatory — at publish time, in one place, rather than leaving it to be rediscovered as a production incident weeks later. That is the shape a good versioning architecture produces: the intelligence concentrates at the registry and the classifier, the compatibility work concentrates in a linear chain of small pure functions, and everything downstream of that — the handler, the eval harness, the agents — gets to reason about exactly one shape at a time. The alternative, where each of those components carries its own partial knowledge of the version history, is how a tool surface becomes the thing no one is willing to touch.