Solving tips
- The model produces arguments as untrusted text; validate them before you ever call the tool.
- Check three things in order: no missing required args, no unknown args, and every value has the right type.
- Return a machine-usable result — (ok, error) — so the loop can feed a clear error back to the model instead of crashing.
Before an agent runs a tool, it has to trust the arguments the model produced — but those arguments arrive as untrusted, model-generated data. A hallucinated field, a missing required parameter, or a string where an integer belongs will make the underlying function crash or, worse, do the wrong thing silently. The guard that sits between the model’s proposed call and the real function is one of the most common things interviewers ask you to build in an agent round.
Task
Complete validate_tool_args(schema, required, args):
schema maps each valid argument name to its expected Python type.
required is the set of argument names that must be present.
args is the dict the model produced.
Check for problems in this exact order and return the first one you find:
- Missing required arg — a name in
required is absent from args.
- Unknown arg — a key in
args is not present in schema.
- Wrong type — a value is not an instance of the type declared in
schema.
Return a tuple (ok, error_message): (True, "") when everything passes, otherwise (False, "<reason>") describing the first failure.
Example
schema = {"city": str, "days": int}
required = {"city"}
validate_tool_args(schema, required, {"city": "Paris", "days": 3})
# -> (True, "")
validate_tool_args(schema, required, {"days": 3})
# -> (False, "missing required arg: city")
validate_tool_args(schema, required, {"city": "Paris", "unit": "C"})
# -> (False, "unknown arg: unit")
validate_tool_args(schema, required, {"city": "Paris", "days": "3"})
# -> (False, "type error: days expected int, got str")
Constraints
- Enforce the ordering: a missing required arg is reported before an unknown arg, which is reported before a type error.
args may contain extra keys, be missing keys, or hold wrong-typed values — never assume it is well-formed.
- Do not call the tool; only validate. No network or LLM calls.
- Watch the
bool/int subtype trap: in Python isinstance(True, int) is True, so a declared-int field will accept True unless you guard it. Handle it however you justify in the interview, but be ready to explain the choice.
Approach
Run three sequential passes and return on the first failure so the ordering contract holds: required-presence, then unknown-key detection, then per-value type checks. Each check is a simple set or membership test over the schema, and the type check uses isinstance with an explicit bool/int guard so a boolean does not sneak into an integer field. Only if all three passes are clean do we return (True, "").
Solution
from typing import Any
def validate_tool_args(
schema: dict[str, type],
required: set[str],
args: dict[str, Any],
) -> tuple[bool, str]:
# 1. missing required args
for name in required:
if name not in args:
return (False, f"missing required arg: {name}")
# 2. unknown args
for name in args:
if name not in schema:
return (False, f"unknown arg: {name}")
# 3. type check every provided value against the schema
for name, value in args.items():
expected = schema[name]
# bool is a subclass of int, so isinstance(True, int) is True;
# reject a bool where a non-bool type was declared.
if expected is not bool and isinstance(value, bool):
return (False, f"type error: {name} expected {expected.__name__}, got bool")
if not isinstance(value, expected):
return (False, f"type error: {name} expected {expected.__name__}, got {type(value).__name__}")
return (True, "")
Walkthrough
On the four example calls:
{"city": "Paris", "days": 3} — every required name (city) is present, no key is outside the schema, "Paris" is a str and 3 is an int. All passes clean, so (True, "").
{"days": 3} — pass 1 finds city in required but not in args, returning (False, "missing required arg: city") before any other check runs.
{"city": "Paris", "unit": "C"} — required is satisfied, so pass 2 runs and finds unit is not a key in schema, returning (False, "unknown arg: unit").
{"city": "Paris", "days": "3"} — passes 1 and 2 are clean; pass 3 checks days and sees "3" is a str, not the declared int, returning (False, "type error: days expected int, got str").
Complexity & notes
- Time is O(R + A) where R is the number of required names and A is the number of provided args — each of the three passes is linear, and dict/set membership is O(1). Space is O(1) beyond the inputs.
- The ordering is a deliberate contract: a missing required arg is the most fundamental failure, an unknown key is next, and a type mismatch is checked last only on keys we know are valid. Interviewers often probe why you check in that order.
- The
bool/int guard is the classic trap. Because bool subclasses int, a naive isinstance(True, int) passes and a hallucinated True slips into a numeric field. Reject it explicitly unless the declared type actually is bool.
- Returning
(ok, error) instead of raising lets the agent loop feed the error string back to the model as a tool result, so the model can correct its arguments on the next turn — the same reflect-and-retry pattern that makes tool use robust.
- This deliberately handles only flat, single-type schemas. Real systems layer on JSON Schema (nested objects, enums, ranges, unions), but the guard-before-execute shape is identical.