Skip to main content

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_orders with no customer,
  • 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.

Schema does double duty

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.

Real projects use a library

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_error results, and it self-corrects.

Next up: permissions — deciding which tools are even allowed to run, and requiring human approval for the risky ones.