Tools as contracts
Trace a model-requested tool call through schema validation, execution and a returned observation.
THE IDEA
A tool is a contract
A tool is a function the application offers to the model. The model never sees your code. It sees a name, a description and an input schema, and it decides when to ask for a call. Everything else (what the tool really does, which arguments are acceptable, what happens on failure) is enforced by your code, the host. A good contract states the purpose, the inputs, the outputs and the errors, and stays narrow: get_weather(city, unit) is easy to validate, while run(anything) is not.
The model sees only a tool's name, description and schema. The host decides what the schema allows and checks every call against it before running anything.
A narrow contract (one city, two units) is easy to validate and to evaluate. An execute-anything tool such as run(command) has no useful schema: almost every string would be "valid".
Predict first. Is a descriptive tool name sufficient to validate arguments?
Chapter context
A tool call is a structured request for the application to perform an operation. The model proposes arguments; the surrounding application decides whether the operation is valid and permitted and is responsible for executing it.
TRY IT YOURSELF
Live lab
The model never sees your code. It sees only the tool's name, description and schema. Start with the vague description, run it, then rewrite it as a clear contract.
TRY IT YOURSELF
Write the contract
The model never sees your code. It sees only the tool's name, description and schema. Start with the vague description, run it, then rewrite it as a clear contract.
Make the model call get_weather with city Oslo, have the host run it, and finish with an answer that uses the result.
Task given to the model: Do I need an umbrella in Oslo today?
This loop as code
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
TOOLS = [ # name, description and input_schema are the "Tool definition"
{"name": t["name"], "description": t["description"], "input_schema": t["schema"]}
for t in [
{
"name": "get_weather",
"description": "Gets info.",
"schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"city"
]
}
}
]
]
messages = [{"role": "user", "content": "Do I need an umbrella in Oslo today?"}]
for step in range(4): # Step budget
response = client.messages.create(model="claude-haiku-4-5", max_tokens=600, tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": response.content})
calls = [b for b in response.content if b.type == "tool_use"]
if not calls:
break # final answer
results = []
for call in calls: # the model only PROPOSES these
problems = validate(call.input, schema_for(call.name)) # your host-side check
if problems:
output, is_error = "Invalid arguments: " + "; ".join(problems), True
else:
output, is_error = run_tool(call.name, call.input) # your code runs it
results.append({"type": "tool_result", "tool_use_id": call.id, "content": output, "is_error": is_error})
messages.append({"role": "user", "content": results})Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.Exercises
Part 1. Do it hands-on
Check three calls against a schema by hand, then write the validator.
The model sees only a tool's name, description and schema. The host decides what the schema allows and checks every call against it before running anything.
A narrow contract (one city, two units) is easy to validate and to evaluate. An execute-anything tool such as run(command) has no useful schema: almost every string would be "valid".
- To do: Schema for get_weather: city (string, required), unit (celsius or fahrenheit, optional), no other fields. Call {"city": "Oslo"}: type valid or invalid
- To do: Call {"city": "Oslo", "unit": "kelvin"}: valid or invalid?
- To do: Call {"town": "Oslo"}: how many problems does the validator report (missing required fields plus unexpected fields)?
- To do: Is a descriptive tool name such as get_weather sufficient to validate its arguments? Type yes or no
Part 2. Do it in code
- validate_args(schema, args): return a list of problem codes. schema has properties (name → {type, enum}), required (list) and additionalProperties (bool). First one "missing:<name>" for each required name that is absent (in the order of required); then for each argument in order: "unexpected:<name>" if it is not a declared property and additionalProperties is False; "type:<name>" if its value has the wrong type; otherwise "enum:<name>" if the property has an enum and the value is not in it.
- Types: string, integer, number, boolean, array, object. A bool is NOT an integer or a number. A property with no type accepts anything.
- is_valid(schema, args): True if there are no problems.
- contract_issues(tool): tool is a dict with name, description and schema. Return "no_description" if the description is missing or blank, "vague_description" if it has fewer than 4 words, "no_schema" if the schema is missing (in that order; a blank description gets only no_description).
Both the live lab and the exercises must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Is a descriptive tool name sufficient to validate arguments?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
A useful execution model is: requested operation + validated arguments + permission decision → execution result. Schema validation establishes a data-shape contract; authorization establishes whether this caller may perform this operation. Neither implies the other.
Worked example
A calculator tool requires numeric a and b and returns a+b. For a=2,b=3, its result is 5. If the request instead asks for an unknown field or omits b, reject or repair the request according to the contract before executing. A retry of this pure calculation is harmless; that does not establish the same property for a side-effecting tool.
Reference code
The following example uses the Python standard library and does not call a remote service.
def add(a,b):
if type(a) not in (int,float) or type(b) not in (int,float):
raise ValueError('numeric inputs required')
return a+b
assert add(2,3) == 5
try:
add('2',3)
except ValueError:
pass
else:
raise AssertionError('invalid input accepted')
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
A model proposal is not permission. JSON schema shape validation is not business-rule validation. Tool outputs must not silently become trusted instructions.