Be Strict in What Your Tools Accept: Postel's Law Fails Agent Systems
"Be conservative in what you send, be liberal in what you accept." Postel's law is arguably the most successful design principle in the history of networking — it's how TCP implementations from different vendors managed to interoperate in the 1980s, and it shaped four decades of protocol and API design. It is also, in the judgment of the IETF itself, a principle that curdles over time: RFC 9413 grew out of a draft bluntly titled "The Harmful Consequences of the Robustness Principle," arguing that liberal acceptance helps interoperability in the short term while quietly rotting the ecosystem in the long term.
Agent systems compress that long-term rot into weeks. When the "sender" is a language model emitting tool calls, every act of liberal acceptance — casting "5" to 5, dropping an unknown field, fuzzy-matching an enum typo — destroys the exact signal you need to keep the system healthy. The tool boundary is the one place in an agent architecture where failing loudly is the reliability feature, and most teams get it backwards.
The instinct is understandable. Your agent calls a search tool with {"limit": "10"} instead of {"limit": 10}, the run crashes, and a user files a ticket. The obvious fix is a coercion layer: numbers-as-strings get parsed, unknown fields get dropped, "asending" gets corrected to "ascending". The crash disappears, the demo works, and everyone moves on. What you've actually built is a system that converts schema violations into semantic corruption — and semantic corruption is invisible to every monitor you have.
Coercion Doesn't Fix Errors, It Launders Them
A malformed tool argument is not noise. It is the model telling you something about its state: it misread the schema, the tool description is ambiguous, the context window has drifted, or the provider silently updated the model underneath you. Each of those causes has a different fix. Coercion erases the evidence for all of them at once.
Consider what the common "robust" behaviors actually do:
- Casting
"5"to5hides that the model has started emitting stringly-typed numbers — often the first visible symptom after a model version bump or a prompt change that pushed the schema out of the model's attention. - Dropping unknown fields hides that the model is hallucinating parameters, which usually means it's pattern-matching against a different tool's signature or an older version of yours. The extra field you dropped may have carried the user's actual intent.
- Guessing at enum typos turns a legible failure into a silent wrong answer.
"decending"→"descending"feels safe;"deleted"→"delete"on a status filter is a data-corruption bug that will take days to trace back.
The insidious part is where the damage lands. A crash lands in your error dashboard. A coerced argument lands in your business data — the wrong rows fetched, the wrong record updated, the subtly wrong answer returned to the user. Your evals never see it, because the tool call "succeeded." Your traces show a green checkmark. The failure surfaces weeks later as an inexplicable customer complaint, and nothing in your telemetry connects it to the day your coercion layer started firing ten times more often than usual.
This is the agent-era version of what protocol engineers learned the hard way. RFC 9413's core argument is that tolerating deviations doesn't make deviations go away — it makes them permanent, because senders never receive the feedback that would correct them. Every workaround becomes load-bearing. The difference is that in protocol land, the feedback loop back to the sender's implementers took years. In an agent loop, the sender is sitting right there, one turn away, ready to be corrected in milliseconds — if you bother to tell it.
The Retry Loop Is Where Postel's Assumptions Invert
Postel's law was written for a world where the sender was a remote implementation you couldn't change. Rejecting a slightly malformed TCP segment accomplished nothing: the other stack wasn't going to fix itself, and the user just saw a failed connection. Liberal acceptance was the only lever available.
An agent system inverts every one of those assumptions:
- The sender is correctable, immediately. A structured validation error returned to the model becomes context for the very next attempt. Models are genuinely good at this: given the field name, the expected type, and the received value, current frontier models fix their own tool calls on the first retry the vast majority of the time. You are not accommodating a foreign implementation; you are training-in-context a collaborator.
- Rejection is cheap. A failed tool call costs one round trip — milliseconds of latency and a few hundred tokens. A silently corrupted one costs an unbounded amount: wrong actions taken against real systems, wrong data shown to real users, and engineering hours spent reconstructing what happened without any log entry marking the corruption point.
- The deviation rate is a vital sign. Validation failures per thousand tool calls is one of the most useful metrics an agent platform can track. It spikes when a provider ships a stealth model update, when a prompt refactor buries the schema, when a new tool's description confuses the model. Coercion sets this metric to zero permanently — not by fixing the problem, but by unplugging the smoke detector.
Strict rejection with a good error message is what makes the loop self-correcting. The pattern that works in production is consistent across frameworks: validate at the boundary (Pydantic, Zod, JSON Schema — the library matters less than the strictness setting), and on failure, return a structured, model-legible error naming the exact field, the constraint it violated, and the value received. Not an HTTP 500. Not a stack trace. Not "invalid input". Something the model can act on: limit must be an integer, received the string "10"; valid range is 1–100.
The distinction between error audiences matters more than most teams realize. A stack trace is written for a human with access to the source code. A model-legible error is written for a reader that has only the conversation context and the tool schema.
Teams building on LangChain and MCP consistently report self-correction rates climbing dramatically when generic errors are replaced with structured ones — a model can't fix what it can't parse. If your validation errors are illegible to the model, strictness degrades into a crash loop, which is how strictness gets its bad reputation.
Fail the Whole Call, Not the Field
A tempting middle ground is partial acceptance: validate what you can, execute with the fields that passed, warn about the rest. This is worse than either extreme.
Partial acceptance means the tool executed something other than what the model asked for, and the model doesn't know it. The model's mental state now says "I searched with three filters"; reality says two filters ran. Every subsequent step in the plan reasons from a false premise, and the eventual failure — if anyone notices at all — surfaces several turns downstream, in a step that was innocent. Debugging agent trajectories is hard precisely because errors propagate through the model's beliefs, not just through the data. Atomic rejection keeps the model's beliefs and the world synchronized: either the call happened as stated, or it didn't happen and the model knows why.
The same logic applies to unknown fields, which deserve special hostility. JSON Schema's default of ignoring extra properties is exactly wrong for tool calls — set additionalProperties: false everywhere. An extra field is never harmless: it's either hallucination (the model is confused about your API), intent that has nowhere to go (the model wanted behavior your tool doesn't offer, and dropping the field means silently not doing what it intended), or drift from a schema version mismatch. All three are things you want surfaced on the turn they occur.
Strictness at the Boundary, Constrained Decoding at the Source
None of this argues for tolerating a high failure rate. It argues for putting the robustness where it doesn't destroy information — and the industry's trajectory here is telling.
Both major providers now ship exactly this: OpenAI's structured outputs with strict: true constrains decoding so function arguments match the supplied schema — their launch evals showed 100% schema compliance, versus under 40% for prompting alone on complex schemas — and Anthropic offers strict tool use that guarantees tool names and inputs match the declared schema. Constrained decoding makes malformed syntax nearly impossible at the source: the model literally cannot emit a token sequence that violates the grammar.
Notice what this is: strictness so early it never fails, not liberalism. The provider didn't build a smarter coercion layer; they moved the schema constraint into token sampling. That is the correct direction to push — generate strictly, validate strictly, and let nothing in between "help."
Constrained decoding doesn't eliminate the validation layer, though, and treating it as if it does is its own trap. It guarantees syntactic validity — types, required fields, enum membership — but not semantic validity: a date range where end precedes start, an ID that references nothing, a limit of 100 on an endpoint that caps at 25, a syntactically perfect call to the wrong tool entirely. Cross-field constraints, referential checks, and business rules still live in your validation layer, and they should fail with the same structured, model-legible errors. The two mechanisms are complements: constrained decoding handles the grammar, boundary validation handles the meaning.
There's also a boring, load-bearing engineering rule hiding here: exactly one schema definition, with validators and the model-facing tool description generated from the same source. Taxonomies of real-world MCP faults keep finding the same root cause — the schema the model sees and the schema the runtime enforces drifting apart, so breakage appears at runtime instead of integration time. In that failure mode the model is "wrong" against one schema and correct against the other, and no amount of retry fixes a disagreement between your own artifacts.
The Checklist
For teams building or auditing an agent's tool layer, the principle compresses to a few decisions:
- Validate every tool call against a strict schema before execution.
additionalProperties: false, no implicit type coercion, enums matched exactly. - Reject atomically. Never execute a partially valid call; never drop fields to make a call valid.
- Return structured, model-legible errors: field, constraint, received value, and valid range or options. Write them for a reader with no source access.
- Bound the retry loop. Two or three validation retries, then escalate — to a human, a fallback, or a clean failure. Retry format errors; don't retry semantic failures that need different handling.
- Track validation-failure rate per tool as a first-class metric. It's your earliest warning of model drift, prompt regressions, and schema-description ambiguity.
- Use provider strict modes (constrained decoding) to kill syntactic failures at the source, and keep semantic validation at the boundary.
- Generate the model-facing schema and the runtime validator from one definition, so they cannot drift apart.
Postel's law earned its place in history by solving a real problem: interoperability between implementations that couldn't talk to each other's authors. Agent systems don't have that problem — the "implementation" on the other side of the boundary is a model that reads your error message and corrects itself on the next turn. In that world, liberal acceptance isn't generosity; it's the deliberate destruction of your only feedback channel. Be conservative in what your agents send — constrained decoding gets you most of the way — and be strict, precise, and articulate in what your tools accept. The reliability of the whole loop depends on the boundary refusing to be polite.
- https://datatracker.ietf.org/doc/html/rfc9413
- https://en.wikipedia.org/wiki/Robustness_principle
- https://nordicapis.com/meet-hyrum-and-postel/
- https://openai.com/index/introducing-structured-outputs-in-the-api/
- https://platform.claude.com/docs/en/build-with-claude/structured-outputs
- https://apxml.com/courses/building-advanced-llm-agent-tools/chapter-1-llm-agent-tooling-foundations/tool-error-handling
- https://dev.to/ethan_thunderbit/designing-reliable-tool-schemas-with-zod-for-llm-agents-21ha
- https://arxiv.org/html/2603.05637v1
