Validating arguments
Handling tool errors taught the harness to survive a tool that fails while running. But there's an earlier moment worth guarding: before the tool runs at all. The model fills in the arguments for a tool call — and sometimes it gets them wrong. This lesson checks those arguments against the schema first, and refuses to run the tool if they don't fit.
It's the difference between "let it crash and catch it" and "don't let the bad call happen in the first place."
Why the model's arguments can be wrong
Remember from What is a tool?: the schema tells the model what a tool expects. But the model fills in the values, and it's working from a conversation, not a contract. So it can:
- leave out a required field — call
look_up_orderswith nocustomer, - send the wrong type — pass a number where a string was expected,
- invent a field — add an argument the tool never declared.
Most of the time the model gets it right. But "most of the time" isn't good enough when the next step is running real code against your database.
The schema is already a contract — so use it
We wrote an input_schema for every tool back in What is a tool?. That schema says
exactly what's allowed: which fields exist, their types, and which are required.
We handed it to the model — but we never checked the model's reply against it.
Validation closes that gap: take the arguments the model sent, compare them to the schema, and reject anything that doesn't match — before calling the function.
The same schema both teaches the model what to send and lets the harness check what it got back. One definition, two jobs. That's the payoff for writing it carefully in What is a tool?.
A small validator
Here's the checker we add to the harness. No new libraries — it just reads the schema we already have:
def validate_arguments(name: str, arguments: dict) -> None:
"""Check the model's arguments against the tool's input schema."""
schema = SCHEMAS_BY_NAME[name]["input_schema"]
properties = schema.get("properties", {})
# 1. Every required field must be present.
for field in schema.get("required", []):
if field not in arguments:
raise ValueError(f"Missing required argument: {field!r}")
# 2. Every field sent must exist and have the right type.
for field, value in arguments.items():
spec = properties.get(field)
if spec is None:
raise ValueError(f"Unexpected argument: {field!r}")
expected = JSON_TYPES.get(spec.get("type"))
if expected and not isinstance(value, expected):
raise ValueError(f"Argument {field!r} must be a {spec['type']}")
It does two passes: first that nothing required is missing, then that
everything present is expected and the right type. JSON_TYPES is just a
small table mapping schema type names ("string", "integer", …) to Python
types.
Our validator handles the common cases to keep the idea clear. Production
harnesses usually lean on a library like jsonschema (or a typed model such as
Pydantic) to cover the full spec — nested objects, enums, number ranges, and
more. The principle is identical; the coverage is wider.
Wiring it in
We call the validator inside run_tool, right after the unknown-tool check and
right before actually calling the function:
def run_tool(name: str, arguments: dict) -> dict:
print(f" [harness] running {name}({arguments})")
if name not in TOOL_FUNCTIONS:
raise ValueError(f"Unknown tool: {name!r}")
validate_arguments(name, arguments) # stop bad calls before they run
function = TOOL_FUNCTIONS[name]
return function(**arguments)
Here's the elegant part: validate_arguments raises a ValueError on a bad
call — which is exactly the kind of exception the Handling tool errors
try/except already catches. So we wrote no new loop code. A validation failure
flows back to the
model as a tool_result with is_error=True, just like any other tool error,
and the model gets a chance to fix its arguments and try again.
So the two lessons fit together:
- Handling tool errors catches failures that happen during the tool.
- This lesson catches bad calls before the tool — and routes them through the same recovery path.
What it looks like
If the model somehow called look_up_orders with no customer, instead of a
confusing crash deep in your database code, the harness now says:
[harness] Tool failed: Missing required argument: 'customer'
…and the model, seeing that error on the next round, simply re-issues the call correctly. The bad request never reached your real code.
Recap
- The model fills in tool arguments and can get them wrong — missing fields, wrong types, made-up fields.
- The schema from What is a tool? is a contract; validation checks the model's arguments against it before running the tool.
- A small validator (required fields + types) catches the common mistakes with no new dependencies; real projects use a fuller library.
- Validation raises, so it reuses the Handling tool errors
error path — bad calls come back to the model as
is_errorresults, and it self-corrects.
Next up: permissions — deciding which tools are even allowed to run, and requiring human approval for the risky ones.