← All packs

Tool contract drift — what happens when the API underneath an agent's tool changes shape

A tool definition is a promise: call it with these arguments, and it will behave this way. An agent builds its plan on that promise without re-verifying it on every call — it can't afford to, and shouldn't have to. The promise usually holds. But the tool definition an agent sees is not the same thing as the code that actually executes when the tool is called, and that code often wraps an external API the tool's author doesn't control. When that API adds a required field, renames a parameter, changes a response shape, or silently starts returning a new enum value, the tool's own definition hasn't changed at all — it still says exactly what it said yesterday. The agent has no way to know the ground moved, and neither does anyone watching the agent, until the mismatch surfaces as behavior that's subtly wrong rather than a call that visibly fails.

Why this isn't tool description design or tool error messages

Tool description design is about writing a schema and wording clearly enough that an agent picks the right tool and fills in the right arguments in the first place (see writing tool descriptions agents actually use correctly) — it's a problem about the initial contract being well-specified. Tool error messages are about what a single failed call reports back so the agent can recover in that turn (see designing tool error messages agents can actually recover from) — it assumes the call fails loudly enough to be noticed as a failure. Contract drift is neither: the schema was fine when written, and the call frequently doesn't fail at all — it succeeds, returns a 200, and hands back a response that no longer means what the agent's code assumes it means. The bug isn't in how the tool was described or how errors are worded; it's that a contract everyone stopped watching quietly stopped being true.

The shapes drift actually takes

ChangeWhy it's dangerous
A response field is renamed or movedCode reading the old field name gets null/undefined instead of an error, and often treats that as a legitimate empty result rather than a broken read
A new required request parameter is added upstreamOld calls either get rejected (loud, at least noticeable) or, worse, silently defaulted to a value the caller never chose
An enum gains a new valueCode with an exhaustive-looking switch/if-chain over the old values falls through to a default branch or an unhandled case, for every record carrying the new value
A field's type or units change (string → number, cents → dollars, UTC → local)The call still succeeds and the field still parses — arithmetic and comparisons just become wrong by a consistent, hard-to-notice factor
Pagination behavior changes (page size, cursor format, default sort)An agent iterating pages can silently skip or duplicate records without any single call reporting an error

The common thread is that none of these require the tool's own definition to change, and most of them don't require any call to fail. That's precisely what makes drift a different category of problem from a broken tool: broken tools get noticed because something errors; drifted tools get noticed weeks later because a downstream number was wrong, if they get noticed at all.

Pin what "correct" looks like, not just what "success" looks like

Most integrations check that a call succeeded — status 200, no exception — and stop there. Contract drift needs a second check: that the response still has the shape the code assumes. A lightweight schema assertion on every response (the fields the code actually reads exist, with the expected types) turns a silent drift into a loud, immediate failure at the first call after the upstream change, instead of a slow accumulation of quietly wrong results. This is cheap precisely because it doesn't need to validate the whole response — only the handful of fields the agent's code actually depends on, which is usually a short, stable list even for a large API.

Contract tests catch drift before an agent ever sees it

A scheduled test that calls the real upstream API on a fixed cadence — independent of any agent run — and asserts the response shape matches what the tool's wrapper code expects, turns drift detection from "whenever an agent happens to notice something's off" into "a failing test at 3am, before any real task depended on it." This is the same idea as a contract test between two internal services, applied to a third-party dependency the team doesn't control and can't get advance notice from. It's a small, recurring cost that replaces an unbounded, unpredictable one.

When drift is caught mid-run, degrade — don't guess

If a response-shape assertion fails partway through a task, the tempting move is to have the agent work around it — reach for a similarly-named field, coerce a type, guess at what the new shape probably means. That's exactly the wrong instinct: a guess made once, under time pressure, inside a single run, is how a workaround for a schema change becomes a silent, wrong assumption baked into that run's output. The safer response is to treat a failed shape assertion the same as a hard tool failure — stop using that tool, report what changed, and let a human or a deliberate patch update the contract, rather than letting the agent's best guess stand in for a decision nobody actually made.

The test to apply: find the tool in the agent's kit that wraps a third-party API neither your team nor the agent controls, and ask when its response shape was last actually re-verified against reality — not "when did a call last succeed," but "when did anyone last check that a success still means what the code assumes it means." If the honest answer is "whenever it was first built" and that was months ago, contract drift already has an open window to walk through unnoticed.

Drift is what happens when nobody meant to change the contract but the ground moved anyway. There's also the deliberate version — a team choosing to remove or rename a tool on purpose — which needs its own safeguards precisely because it's plannable in a way accidental drift isn't; see deprecating a tool without breaking sessions already mid-task.