In one sentence
A tool definition that describes its arguments but not its results is half a contract, and a model will fill in the other half by guessing field names that sound right.
Why this matters
Here is the failure, exactly as it happened. We pointed a small model at our data operations for the first time. It asked one of them for a reachability result, and then, to use that result, wrote this:
count. The operation returns
rows_total.That is the entire bug. No such field exists, so at run time the reference resolved to nothing and the step collapsed — and every step after it. Across 22 tasks, the share that executed successfully was 0%.
The part worth sitting with is that every one of those plans passed validation. Nothing was malformed. The model had a schema for each operation's arguments and honoured all of them. It invented a name on the way back out, where nobody was checking.
The guess is not even unreasonable. A model has read a great many APIs in
which the thing that counts returns count, so absent any statement
to the contrary it emits the name the rest of the world uses. Nothing in a
standard tool-calling stack contradicts it: the tool definition ships an input
schema, the call succeeds, and the invention surfaces later as a dangling
reference, an empty value, or — worst — a plausible answer computed from
nothing.
The idea in plain language
First, the word this post needs. A plan here is just a short list of tool calls where later calls use the results of earlier ones: fetch the messages in this window, then count what that returned, then report the number. Because step three refers to step two, the steps have to agree about what step two hands over. That agreement is the thing that was missing.
Written out, the asymmetry is stark. One direction of the boundary is governed; the other is not.
And here is the same plan before and after the contract existed — one field name is the whole difference between nothing running and everything running:
How TGMS handles it
Every data operation in the TGMS registry now declares the fields it returns, beside its arguments — and that registry generates the tool definitions the model is given, so the manual and the enforcement cannot drift apart. Before a plan runs, the verifier walks every cross-step reference and rejects any whose field the producing operation does not emit.
The rejection is not a bare error. It carries the real field list back to the planner, which is what makes the next attempt succeed rather than guess again:
E_SCHEMA: temporal_reachability outputs no field 'count' (available: rows, rows_total, truncated, cursor)
A readable manual would not have done this. The model already had a readable manual; what it lacked was a checkable one. Documentation is a prompt. A contract is a gate.
- schema — every argument against its JSON Schema;
- acyclicity — steps must form a directed acyclic graph, and a reference may only point backwards;
- grounding — no identifier the task did not supply and no earlier operation resolved;
- temporal sanity — every window and belief time well-formed and ordered;
- cost — an estimate, with work predicted to be too expensive refused rather than started.
What we measured
- Question
- Does declaring and enforcing result fields change whether plans execute?
- Workload
- The 22-task development split over a message-log dataset, including 3 correction-probe tasks.
- Compared
- One system, one model, the check disabled versus enabled — a controlled re-run, not a model comparison.
- Metric
- Execution success: the share of tasks whose plan ran to completion and produced a result. Higher is better. This is not answer correctness — a plan can execute and still answer wrongly.
- Held constant
- Qwen2.5-7B-Instruct, temperature 0, same prompts, same tasks, same repair budget.
- Run
- Single deterministic run per condition.
What we found
On the three correction-probe tasks the change is total: none of them ran before, all of them ran after. Across the full 22-task split the same check moves execution success from nothing to just over half. Both numbers describe the same check; they differ because the rest of the split contains tasks a 7B model fails to plan for other reasons.
With the check in place the bottleneck moved from “can the model address a result” to “can the model plan”, which is the better problem to have — and one that model capacity does affect. On this split, the share of first-attempt plans that pass validation rises from 23% at 7B to 50% at 14B.
What this means in practice
If you ship tools for models to call — MCP servers included — the transferable rule is that result schemas deserve the same rigour as argument schemas. Concretely:
- Declare your result fields where you declare your arguments, and generate the model-facing tool definition from that one source so the two cannot diverge.
- Reject unknown field references before execution, not after — a dangling reference discovered at run time has already cost you the call.
- Put the real field list in the error. “Unknown field” makes a model
guess again; “no field
count; available:rows,rows_total, …” makes it correct itself. - Do not name a field something the model will assume. If your counting
endpoint returns
rows_total, expectcountto be attempted, and make that attempt fail loudly. - Test with a small model. Large models paper over an absent contract often enough that you may never see the gap; a 7B model finds it on the first run.
What this result does not show
The check is narrower than “validating the shape of a result”. It verifies the first component of a reference path against the producing operation's declared fields. A reference to a field that exists is accepted whatever lies deeper inside it — nested shapes, element types, and whether a row really carries the sub-field a later step assumes all remain unchecked at plan time. This closed one specific hole completely; it did not make results statically typed.
The measurement is also small and single-condition: one model, one dataset, one deterministic run per condition, and a probe subset of 3 tasks behind the 100% figure. Execution success says a plan ran, not that its answer was correct — the accuracy of what those plans returned is a separate measurement.
Finally, no experiment here compares how often models of different sizes invent result field names. The effect holds at both 7B and 14B, but that is a statement about the mechanism, not a measured law about model scale.
Takeaway
Input validation tells you a model called your tool correctly. Only an output contract tells you the model is entitled to believe what it thinks your tool said — and the gap between those two is where a perfectly valid plan quietly stops working.
Evidence and reproduction
The check is a short helper in tgms/agent/verifier.py, called from the plan walker and from answer-spec resolution; the ablation flag that produced the OFF condition is ablate_output_contracts. The result-field declarations live with the operation definitions in the registry. The finding is written up in §6 of the paper. Every number on this page is quoted from docs/site_facts.json and checked in CI.
Continue reading
Prerequisite: why TGMS stores two kinds of time — the design these operations exist to serve.