How do you stop a model from calling a tool with an argument it invented?
Every tool argument is defined by a strict JSON schema — enums for known value sets, validated patterns for IDs, required fields wherever the tool genuinely needs them — with no free-text catch-all surface. A call that fails validation is rejected with a structured, actionable error the model can act on, never silently coerced or passed through to execution.
Also known as schema-validated tools, strict tool schemas, validate-reject-repair
What problem does this solve?
A tool schema that accepts any string for an ID field and marks every field optional never forces the model to justify where a value came from. It can generate a plausible-looking ID from pattern alone, the same way it would complete plausible code, and if the schema does not distinguish that from a value that actually came from a prior lookup, both look identical to the calling code.
If the server also skips validating that a generated value corresponds to something real before acting on it, a fabricated argument sails through to execution unchecked. This is a schema and validation gap more than a model-quality gap — a model asked to fill an unconstrained field will complete the pattern it was trained to produce, and a plausible-shaped ID is exactly that pattern.
How does it work?
Every tool argument gets the tightest schema the domain allows: a known enum where the value set is small and fixed, a validated regex pattern where it is not, and `required` marked accurately rather than defaulting everything to optional. Free-text fields are reserved for genuinely free-text content, such as a message body, never for identifiers or structured values a lookup could supply instead.
The server validates every call against its schema before executing anything, and treats validation failure as a distinct, measured category from execution failure, not lumped into one generic error bucket. A validation failure returns a structured, specific reason, such as "no matching record for X", the model can act on, rather than a silent no-op or a generic 500.
Where practical, mutating tools accept only IDs that trace back to a value returned by an earlier read or lookup call in the same run — enforced at the orchestration layer, not left to the model's discretion — so an argument the model wrote from scratch, rather than copied from real data, cannot reach anything that changes state.
What are the moving parts?
- Constrain every ID/enum field to the tightest schema available
Use an enum or a validated pattern, never a bare free-form string, wherever the domain allows it.
- Mark fields required based on actual need
Set required accurately instead of defaulting every field to optional.
- Validate every call server-side before execution
Treat validation failure as a category distinct from execution failure.
- Return a structured, specific error on validation failure
Give the model something it can act on, not a silent no-op or a generic error.
- Give the agent a lookup tool instead of expecting recall
Let it search for a real ID rather than infer or remember one from context.
- Trace mutating arguments back to a prior read result
Enforce, where practical, that a value passed to a mutating tool came from an earlier lookup in the same run, not from scratch.
When does it fit, and when doesn't it?
Use it when
- Tool calls fail at a rate noticeably above expected legitimate user error, especially with plausible-shaped but nonexistent IDs
- Any tool in the agent's reach can mutate state — write, delete, send, refund — rather than only read
- The agent is expected to reference existing records by ID across multiple turns
- You're onboarding a new tool and want its contract to fail safely from day one rather than after an incident
Don't use it when
- The tool is genuinely read-only, low-stakes, and idempotent (e.g. a public search) where a bad argument just returns an empty result with no downstream harm
- The value space is inherently open-ended free text (a user's message, a search query) — forcing an enum or pattern there breaks the tool's actual purpose
- You're validating something the caller already validated one layer up and duplicating it buys nothing but latency — check whether the guarantee already holds before adding a redundant layer
What does it actually cost?
Fabricated IDs get rejected before they reach anything real
Tighter schemas take real domain modeling work up front — you have to know the actual enum or pattern, not just accept anything
Validation failures become a measured, distinct metric instead of hiding inside a generic error rate
More rejected calls in the short term as legitimate edge cases the schema didn't anticipate get caught too — expect a tuning period
A model given a lookup tool stops needing to recall or infer IDs at all
An extra tool call (the lookup) on every task that references an existing record, adding a step and some latency
Tracing mutating arguments back to a prior read call closes off invented IDs at the orchestration layer, not just the schema layer
That tracing has to be built and maintained in the harness itself — it's not something a schema alone can enforce
How do you know it's working?
- Tool call rejection rate, split explicitly into validation failures vs. execution failures
- Rate of rejected calls where the invalid argument does not appear in any prior tool result the agent received
- Hallucination rate on free-form ID fields vs. enum/pattern-constrained ID fields, compared directly
- Share of mutating-tool calls whose argument traces to a prior read-tool result
What failure modes does this prevent?
A Ship Audit checks which of these patterns your system actually needs, prioritized against what's most likely to break first.