Skip to main content

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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\":[]}"}

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. The agent loop governs the next action and stopping conditions.

Contract Dimensions​

DimensionRequired question
selectionWhen should this tool be chosen over another?
inputWhich fields, bounds, enums, and path rules are valid?
authorityWhich identity acts, and what may it read or change?
effectIs it read-only, reversible, externally visible, or destructive?
concurrencyWhat happens if state changes between read and write?
idempotencyCan the same request be retried without duplication?
resultWhat proves full, partial, denied, or unknown completion?
approvalWhich argument/effect combinations require a person?
observabilityWhich redacted fields, IDs, timings, and artifacts are retained?

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. A generic error: edit failed invites blind retries.

Result Envelope​

A stable result should separate transport from domain outcome:

{
"request_id": "req-123",
"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. “Unknown outcome” is distinct from failure because repeating an externally visible action may duplicate it.

Design rules​

  1. 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.
  2. 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.
  3. Stop unproductive retries. Use a breaker based on the operation's risk and idempotency. A read may tolerate several retries; an externally visible write may tolerate none after an unknown outcome.
  4. 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​

Webpages, issues, documents, and tool metadata can contain text designed to redirect the model. These remain data. A safe harness tags untrusted provenance, denies tool outputs automatic execution authority, strips secrets from model-facing contexts, and re-applies policies to proposed next actions. Typed responses reduce ambiguity; they do not sanitize semantics.

Compact Model Trade-offs​

Smaller models often emit shallow schemas more reliably, but removing schema detail can discard safety-critical information. Measure accepted-call rate and task success with exact prompt templates and quantizations. Do not weaken permissions silently to accommodate a weaker model; prefer simpler single-purpose tools, constrained adapters, or stronger models.

Explore connectionsOpen network