Tool Contracts for AI Agents
A tool is an authority boundary presented as a function. JSON validation can prove that arguments match a shape; it cannot prove that the action is intended, authorized, safe to retry, or correctly completed.
From a Model Request to a Tool Result
The model requests a function call; application code executes the function. The function-calling guide describes this as a conversation that includes a tool definition, a call, its result, and a subsequent model response. Consider a read-only search_notes tool:
- The application supplies its name, purpose, and argument schema: an object with one required string
query, length 1–200, and no additional properties. The implementation can search only the approved public-note index. - The model returns a tool-call item with a call identifier, name, and arguments. The application waits for the complete item rather than executing streamed argument fragments.
- The application parses the arguments, validates the schema, checks authority, and dispatches only an allowlisted function. Invalid or denied requests return an error without running the search.
- It associates the search result with the original call identifier and sends it back with the conversation state. The model can then answer using the result or request another permitted action.
These abbreviated JSON messages use Responses-style fields; they are illustrative, not a complete API request:
{"type":"function_call","call_id":"call_1","name":"search_notes","arguments":"{\"query\":\"KV cache\"}"}
{"type":"function_call_output","call_id":"call_1","output":"{\"status\":\"ok\",\"matches\":[]}"}
Anthropic's API uses different field names for the same flow: the call arrives as a tool_use block, and the result goes back in a tool_result block (tool use overview).
An empty result means this search found no matches, not that the topic does not exist. Ordinary text that merely resembles this JSON is not an authorized call. Provider adapters or local parsers must first recognize a protocol-level request; the harness still decides whether it may execute. The result is untrusted data, not a new instruction. Bounded Agent Loops govern the next action and stopping conditions.
Contract Dimensions
Worked Contract
name: replace_text
purpose: replace one exact block in one repository file
input:
path: repository-relative path, no traversal
old_text: non-empty and must match exactly once
new_text: string
preconditions:
- path is inside approved write scope
- working tree version equals observed version
effects: reversible local write
idempotency: not retryable after success without re-reading
result:
status: succeeded | conflict | no-match | multiple-matches | denied
changed_file: path or null
approval: required when path is outside task scope
This contract gives the loop a discriminating failure: no-match means re-read the file first, multiple-matches means old_text needs more context, and denied means the path should not be touched at all. A generic error: edit failed cannot tell these apart and invites blind retries.
Result Envelope
A stable result records two things separately: whether the request arrived and the service answered (transport), and how much of the requested work got done (domain outcome). In this batch result, transport is ok, so the request arrived; status is partial, because only 8 of the 10 items succeeded:
{
"request_id": "req-123",
"transport": "ok",
"status": "partial",
"completed": 8,
"failed": 2,
"retry_safe": false,
"artifact": "results/batch-123.json",
"errors": [{"code": "permission-denied", "item": "..."}]
}
Never turn one successful item into batch success.
An unknown outcome must also stay separate from failure. Picture a payment-transfer tool: the request goes out, then the connection times out, and you do not know whether the money left the account. Treating that as a failure and sending it again can charge twice. So on an unknown outcome the harness first queries the transfer's status instead of retrying; how to retry with the same idempotency key is covered in Long-Running Agents.
Getting the Model to Use the Schema Reliably
Once the contract is written down, whether the model calls it correctly still depends on a few settings. OpenAI's function-calling guide gives concrete advice:
- Turn on strict mode. The guide says "We recommend always enabling strict mode." With
strict: true, every object needsadditionalProperties: false, and every field inpropertiesmust be listed inrequired; optional fields are expressed with anulltype. - Start with few tools. The guide suggests "fewer than 20 functions" available at the start of a turn, and calls this a soft suggestion. Defer rarely used tools with
tool_search; to limit which tools a few turns may call, useallowed_tools, which leaves the tool list itself unchanged and so keeps the prompt cache intact. - Write the description like a manual. The
descriptionshould state what the tool is for, when not to use it, and the format and units of its parameters, with one example where it helps.
MCP's 2026-07-28 revision also loosened schemas: inputSchema and outputSchema may use any JSON Schema 2020-12 keyword (changelog).
Design rules
- Avoid shell interpolation. Pass a program and argument array directly when possible. This removes shell parsing from the path, but arguments can still be dangerous:
git, cloud CLIs, and package managers all have flags with destructive or code-executing effects. - Validate resolved paths. Check repository boundaries after normalization and symlink resolution; rejecting the literal string
../is not enough. Keep sensitive paths outside the tool's authority rather than relying only on a denylist. - Stop unproductive retries. Stop once the same call has failed a set number of times; how to set this breaker's threshold is covered in Bounded Agent Loops.
- Return structured failures. Distinguish malformed input, denied authority, transient transport failure, domain rejection, partial completion, and unknown outcome. The next action depends on the difference.
Injection and Untrusted Results
Typed responses make the meaning of each field clear, but they do not control what a field says: a perfectly valid "title" can still read "ignore previous instructions." Tool results can only be treated as data, and any next step the model proposes from them goes back through policy; the full threat model is in Agent Security.
Compact Model Trade-offs
Smaller models often emit shallow schemas more reliably, but removing schema detail can discard safety-critical information. To learn whether a small model is good enough, test it with the exact prompt templates and quantization you deploy (the model file whose weights were compressed to lower precision) and count two things: what share of its tool calls pass validation, and what share of tasks succeed. Do not weaken permissions silently to accommodate a weaker model; prefer simpler single-purpose tools, constrained adapters, or stronger models.