The architecture, schemas, error handling, and safety patterns behind reliable agent tool use
AI agents become useful when they can do more than generate text.
They need to search databases, read documents, call APIs, create records, execute code, send messages, update systems, and sometimes recover from failures.
That means an agent is only as capable as the tools it can use.
But there is an important engineering distinction:
A tool API designed for humans is not necessarily a good tool API for an AI agent.
Traditional APIs are usually designed around predictable software clients. The client developer knows the API contract, understands the parameter names, validates inputs, handles errors, and writes the control flow.
An AI agent is different.
The model has to decide whether to call a tool, which tool to call, what arguments to provide, and how to interpret the result.
That makes tool design part of the agent's reasoning architecture.
Modern agent frameworks expose tools through structured schemas. For example, MCP tools define names, descriptions, input schemas, and optionally output schemas. MCP implementations can also expose behavioral hints such as read-only or destructive behavior. ([Model Context Protocol][1])
So the question is no longer:
"How do I expose my API to an AI?"
The better question is:
"How do I design an API that an AI can reliably reason about?"
1. A Tool Is an Interface Between Reasoning and Software
Consider a simple agent:
User
↓
AI Agent
↓
Model decides what to do
↓
Tool
↓
Application / Database / API
↓
Tool result
↓
Model continues reasoning
The tool sits directly between the model and your software.
Suppose a user says:
"Find my latest invoice and tell me whether it has been paid."
The agent may need to:
- Identify the user.
- Search invoices.
- Select the relevant invoice.
- Inspect its payment status.
- Explain the result.
You could expose one enormous function:
get_invoice(
user_id,
invoice_id,
customer_name,
email,
status,
date_from,
date_to,
include_payment,
include_items,
include_customer,
...
)
Technically, this might work.
For an AI agent, it creates a much harder decision problem.
A better tool surface might be:
search_invoices
get_invoice
get_invoice_payment
Each tool has a narrower responsibility.
This leads to the first principle:
Design Tools Around Decisions, Not Database Operations
A tool should represent a meaningful capability the agent can reason about.
Bad:
execute_sql
call_api
update_database
Better:
search_invoices
create_invoice
cancel_invoice
get_payment_status
The second set gives the model a semantic vocabulary.
The model does not need to understand your internal database structure.
It only needs to understand:
"When should I use this capability?"
2. Tool Names Are Part of the Interface
Developers sometimes treat tool names as implementation details.
For agents, they are part of the model-facing API.
Compare:
get_data
with:
search_customer_orders
The second immediately communicates intent.
https://goodoff.co/
A good tool name should answer:
What capability does this tool provide?
Examples:
search_documents
get_document
create_presentation
generate_chart
send_email
schedule_meeting
get_weather
Avoid names that require internal knowledge:
process_v2
execute_operation
handler_7
data_service
run_query
The agent should not need to inspect your source code to understand the purpose of a tool.
3. Tool Descriptions Are Instructions for the Model
This is one of the most overlooked parts of agent engineering.
A tool description is not just API documentation.
It becomes part of the model's decision context.
Google's Agent Development Kit documentation, for example, notes that a tool's Python docstring becomes part of what the model sees and recommends writing it clearly because it tells the model when and how to use the tool. ([Google GitHub][2])
Consider:
def search_documents(query: str):
"""Search documents."""
This is technically valid.
But it leaves important questions unanswered:
- What should
querycontain? - Is this semantic search?
- Should the agent use it for exact matches?
- What does it return?
- When should the agent prefer another tool?
A better description:
def search_documents(query: str, limit: int = 10):
"""
Search the document library using semantic and keyword matching.
Use this when the user asks about information that may exist
inside uploaded documents.
Do not use this tool for exact document IDs.
Returns matching documents with their titles, IDs,
relevance scores, and short excerpts.
"""
Now the model has decision guidance.
4. Treat the Tool Schema as a Contract
A tool should have a strict input contract.
For example:
{
"name": "search_documents",
"description": "Search the document library for relevant content.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The information or concept to search for."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
}
},
"required": ["query"]
}
}
This is significantly better than:
{
"query": "string"
}
because the schema communicates constraints.
Modern MCP tooling uses JSON Schema for tool inputs and can also define structured output schemas. Current MCP SDK documentation shows schemas being used both to describe what arguments a tool accepts and to validate those arguments before the handler executes. ([MCP TypeScript SDK][3])
That gives you an important architecture:
Model
↓
Tool schema
↓
Validation
↓
Tool implementation
Do not rely on the model alone for validation.
5. Keep Parameters Small and Semantic
One common mistake is exposing every possible API parameter to the model.
Imagine:
{
"customer_id": "...",
"organization_id": "...",
"region": "...",
"locale": "...",
"timezone": "...",
"include_deleted": false,
"include_archived": false,
"include_metadata": true,
"include_permissions": false,
"sort_field": "...",
"sort_direction": "...",
"page": 1,
"page_size": 50,
"cursor": "...",
"debug": false
}
This may be appropriate for a low-level backend API.
It is usually a poor model-facing tool.
The model has to reason about too many choices.
Instead, create an agent-oriented interface:
{
"query": "customer invoices from March",
"limit": 10
}
The backend can translate that into the complex internal API call.
This creates a useful separation:
Agent-facing API
↓
Simple tool contract
↓
Adapter layer
↓
Internal APIs
↓
Database
Your internal architecture can remain complicated.
The model-facing interface should remain understandable.
6. Avoid Ambiguous Parameters
Names matter.
Compare:
{
"id": "123"
}
with:
{
"invoice_id": "123"
}
The second is safer because the semantic meaning is explicit.
Similarly:
date
is ambiguous.
Prefer:
start_date
end_date
created_after
created_before
Instead of:
type
consider:
document_type
Instead of:
name
consider:
customer_name
AI systems operate through semantic interpretation.
Reducing ambiguity reduces unnecessary reasoning.
7. Make Invalid States Difficult to Express
Suppose your API expects a date range.
Instead of allowing:
{
"start_date": "tomorrow",
"end_date": "yesterday"
}
and hoping the backend handles it, define explicit validation.
For example:
{
"type": "object",
"properties": {
"start_date": {
"type": "string",
"format": "date"
},
"end_date": {
"type": "string",
"format": "date"
}
},
"required": ["start_date", "end_date"]
}
Then validate the relationship server-side:
if start_date > end_date:
raise InvalidDateRange()
Schema validation catches structural problems.
Business validation catches semantic problems.
You need both.
8. Design Tool Outputs for the Next Decision
Tool design is not only about inputs.
The output is equally important.
Suppose a search tool returns:
{
"data": [
{
"id": "123",
"text": "...",
"metadata": "..."
}
]
}
The model has to figure out what each field means.
Instead:
{
"results": [
{
"document_id": "123",
"title": "Product Strategy",
"relevance": 0.91,
"excerpt": "The product strategy focuses on..."
}
],
"total_results": 18
}
Now the model has useful information for the next step.
The output should help answer:
What should the agent do next?
This is especially important for multi-step agents.
9. Use Structured Outputs When the Next Step Needs Structure
Suppose a tool returns:
The customer has three unpaid invoices. The oldest
was created on January 12 and is currently overdue.
A human can understand it.
Another model call has to interpret the text.
A structured result is easier to consume:
{
"customer_id": "cus_123",
"unpaid_invoices": 3,
"oldest_invoice": {
"invoice_id": "inv_456",
"created_at": "2026-01-12",
"status": "overdue"
}
}
MCP currently supports optional outputSchema definitions for structured tool results, and its SDK can validate structured content against that schema. ([MCP TypeScript SDK][3])
Structured output becomes particularly valuable when tools are chained:
Search customer
↓
Get invoice
↓
Check payment
↓
Generate response
Each step should produce information that the next step can reliably consume.
10. Do Not Hide Side Effects
There is a major difference between:
search_orders
and:
cancel_order
The first reads information.
The second changes state.
Your tool interface should make that distinction obvious.
For example:
get_customer
search_orders
get_invoice
versus:
create_customer
update_customer
cancel_order
send_email
delete_document
MCP tool metadata can include behavioral hints such as readOnlyHint, destructiveHint, and idempotentHint. These are useful signals, although they should not be treated as security controls. ([Model Context Protocol][1])
A practical architecture is:
Read operation
↓
Can often execute automatically
Write operation
↓
Validate
↓
Check permissions
↓
Potential approval
↓
Execute
Destructive operation
↓
Validate
↓
Explicit confirmation
↓
Execute
The tool itself should enforce authorization.
Never assume that because the model selected a tool, the action is authorized.
11. Idempotency Matters More With Agents
Agents retry.
Networks fail.
Models repeat actions.
A user may say:
"Send the report to Sarah."
The agent calls:
send_report()
The server processes it.
The response is lost.
The agent does not know whether the operation succeeded.
It might call the tool again.
Now Sarah receives two reports.
For side-effecting tools, consider idempotency keys:
{
"recipient": "sarah@example.com",
"report_id": "report_123",
"idempotency_key": "agent-run-789-send-report"
}
The backend can recognize that the operation has already been performed.
This turns retries from a dangerous behavior into a manageable one.
12. Design Errors for Recovery
Traditional APIs often return:
{
"error": "Bad Request"
}
That is not very useful to an agent.
Consider:
{
"error": {
"code": "INSUFFICIENT_PERMISSION",
"message": "The current user cannot access this project.",
"retryable": false,
"action": "Ask the user to select a project they have access to."
}
}
Now the model has information for the next decision.
Useful error categories include:
INVALID_ARGUMENT
NOT_FOUND
PERMISSION_DENIED
RATE_LIMITED
TEMPORARY_FAILURE
CONFLICT
REQUIRES_CONFIRMATION
The model should be able to distinguish:
Try again
from:
Change the request
from:
Ask the user
from:
Stop
13. Separate Retryable and Non-Retryable Errors
This distinction becomes critical in autonomous systems.
For example:
{
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 10
}
versus:
{
"code": "INVALID_CUSTOMER_ID",
"retryable": false
}
The agent can then make a more informed decision.
A simple retry policy might look like:
if error.retryable:
retry_with_backoff()
elif error.code == "INVALID_ARGUMENT":
reconsider_arguments()
elif error.code == "PERMISSION_DENIED":
ask_user()
else:
stop_and_report()
This is much safer than blindly retrying every failure.
14. Keep Tools Focused
A tool should generally have one clear responsibility.
Consider:
manage_customer
which can:
create
read
update
delete
search
merge
archive
restore
This gives the model a large decision surface.
Instead:
search_customers
get_customer
create_customer
update_customer
archive_customer
Each tool has a more precise meaning.
Google's MCP security guidance similarly recommends keeping MCP tools focused on a single responsibility. ([Google GitHub][4])
The principle is simple:
Fewer decisions per tool usually means clearer decisions for the agent.
15. But Do Not Create Hundreds of Tiny Tools
There is another failure mode.
You can overcorrect.
Imagine:
get_user_name
get_user_email
get_user_timezone
get_user_language
get_user_company
get_user_role
get_user_status
Now the model has to discover seven tools just to understand one user.
Tool granularity should match meaningful agent actions.
A useful test is:
Would an agent naturally think of this as a distinct capability?
If yes, it may deserve its own tool.
If not, it may belong inside another operation.
16. Tool Descriptions Should Explain When Not to Use Them
This is an advanced but powerful pattern.
Instead of:
Search documents.
use:
Search the document library for information contained
in uploaded documents.
Use this when the answer may exist inside user documents.
Do not use this for general web research.
Do not use this when the user provides an exact document ID.
The negative guidance reduces tool confusion.
This becomes increasingly important when an agent has many tools with overlapping capabilities.
17. Build an Agent-Friendly API Layer
Your existing backend probably looks something like:
Frontend
↓
REST API
↓
Services
↓
Database
Adding agents does not mean the model should receive unrestricted access to that REST API.
Instead:
┌──────────────┐
│ Agent │
└──────┬───────┘
↓
┌─────────────────┐
│ Tool Layer │
│ │
│ search_docs │
│ create_report │
│ send_report │
└────────┬────────┘
↓
┌─────────────────┐
│ Adapter Layer │
└────────┬────────┘
↓
┌─────────────────┐
│ Internal APIs │
└────────┬────────┘
↓
Database
This adapter layer is valuable because your internal APIs can evolve independently from the agent interface.
18. Example: Building a Document Tool
Let's build a simple Python tool.
from typing import TypedDict
class DocumentResult(TypedDict):
document_id: str
title: str
excerpt: str
relevance: float
def search_documents(
query: str,
limit: int = 5
) -> list[DocumentResult]:
"""
Search uploaded documents for information relevant to the query.
Use this when the user asks about information that may exist
inside uploaded documents.
Do not use this for general web research.
Args:
query: Natural-language description of the information to find.
limit: Maximum number of results. Must be between 1 and 10.
Returns:
Matching documents with titles, excerpts, and relevance scores.
"""
if not query.strip():
raise ValueError("query cannot be empty")
if not 1 <= limit <= 10:
raise ValueError("limit must be between 1 and 10")
results = document_search_engine.search(
query=query,
limit=limit
)
return [
{
"document_id": result.id,
"title": result.title,
"excerpt": result.excerpt,
"relevance": result.score
}
for result in results
]
Notice what this tool does not expose:
database connection
SQL query
embedding model
vector database
chunk size
index name
internal storage path
Those are implementation details.
The agent needs a capability, not your infrastructure.
19. Tool APIs Need Authentication Too
A common mistake is to think:
"The model is inside our application, so the tool is trusted."
The model is not an authorization system.
Every tool invocation should still pass through normal security controls.
For example:
Agent
↓
Tool
↓
Authenticated identity
↓
Authorization
↓
Validation
↓
Business logic
↓
Database
Never let a model-generated argument bypass authorization.
Bad:
def get_customer(customer_id):
return database.get_customer(customer_id)
Better:
def get_customer(customer_id, user):
authorize(
user=user,
resource=customer_id,
action="read"
)
return database.get_customer(customer_id)
The model chooses what it wants to do.
Your application decides whether it is allowed to do it.
20. Add Observability to Every Tool Call
When an agent makes a wrong decision, you need to know why.
Log at least:
agent_run_id
tool_name
tool_version
arguments
user_id
timestamp
latency
result_status
error_code
retry_count
For sensitive systems, carefully control what arguments and results are logged.
A useful trace might look like:
Run: agent_9821
09:41:02
Tool: search_documents
Arguments:
query = "Q3 pricing strategy"
09:41:03
Result:
3 documents
09:41:04
Tool: get_document
Arguments:
document_id = "doc_918"
09:41:04
Result:
success
09:41:07
Tool: create_summary
Arguments:
document_id = "doc_918"
09:41:09
Result:
success
Now debugging becomes possible.
Without tool-level observability, an agent failure often looks like:
User: Why did you give me the wrong answer?
Agent: Sorry.
That is not an engineering strategy.
21. Version Your Tool Contracts
Tool APIs evolve.
Today:
{
"query": "..."
}
Tomorrow:
{
"query": "...",
"filters": {}
}
Later:
{
"query": "...",
"filters": {},
"ranking": "semantic"
}
Changing semantics without considering existing agents can cause subtle failures.
Treat tools like public APIs.
Consider:
search_documents.v1
search_documents.v2
or maintain backward-compatible schemas where possible.
This matters especially when multiple agents or external clients consume the same tool.
22. Test the Tool, Not Just the Model
An agent can fail for two completely different reasons:
Model failure
or:
Tool failure
You need to test both.
Tool-level tests
def test_search_documents_rejects_empty_query():
with pytest.raises(ValueError):
search_documents("")
def test_search_documents_rejects_invalid_limit():
with pytest.raises(ValueError):
search_documents("pricing", limit=100)
Agent-level tests
Test whether the model chooses the right tool:
User:
"Find the pricing information in my uploaded files."
Expected:
search_documents
And:
User:
"Search the internet for today's AI news."
Expected:
web_search
Tool selection is part of agent behavior and should be evaluated as such.
Modern agent development workflows increasingly include structured evaluation alongside unit and integration tests. Google's current agent tooling, for example, includes evaluation datasets and grading workflows as part of the development lifecycle. ([Google GitHub][2])
23. A Practical Tool Design Checklist
Before exposing a function to an AI agent, ask:
Identity
- Is the tool name unambiguous?
- Does it describe a meaningful capability?
Description
- Does the description explain what the tool does?
- Does it explain when to use it?
- Does it explain when not to use it?
Inputs
- Are parameters semantic?
- Are types explicit?
- Are required fields defined?
- Are constraints validated?
- Are ambiguous parameters avoided?
Outputs
- Is the result structured?
- Can the agent easily understand what happened?
- Does the output provide enough information for the next decision?
Errors
- Are errors machine-readable?
- Can the agent distinguish retryable from permanent failures?
- Does the error explain what the agent can do next?
Safety
- Does the backend enforce authorization?
- Are destructive operations clearly identified?
- Are side effects explicit?
- Are confirmation requirements enforced outside the model?
Reliability
- Is the operation idempotent where appropriate?
- Can requests safely be retried?
- Are timeouts defined?
Observability
- Can you trace every tool invocation?
- Can you identify latency and failures?
- Can you reproduce an agent run?
Evolution
- Can the tool contract change safely?
- Do you have a versioning strategy?
24. The Architecture to Aim For
A production agent should not look like this:
LLM
↓
Random API calls
↓
Database
A better architecture is:
┌───────────────┐
│ User │
└───────┬───────┘
↓
┌───────────────┐
│ Agent │
└───────┬───────┘
↓
┌─────────────────────┐
│ Tool Registry │
└─────────┬───────────┘
↓
┌────────────────────────┐
│ Agent-Friendly Tool API │
└────────────┬───────────┘
↓
┌────────────────────┐
│ Validation │
│ Authorization │
│ Rate Limits │
│ Idempotency │
│ Observability │
└──────────┬─────────┘
↓
┌──────────────┐
│ Adapter Layer│
└──────┬───────┘
↓
┌──────────────────────┐
│ Internal APIs / DBs │
└──────────────────────┘
The model controls the reasoning loop.
Your application controls the execution boundary.
That separation is fundamental.
The Real API Is the Model's Mental Model
The biggest mistake in agent tool design is thinking of tools as simple wrappers around existing functions.
They are not.
A tool is a reasoning interface.
The model needs to understand:
What can I do?
When should I do it?
What information do I need?
What will happen?
What will I get back?
What should I do if it fails?
A well-designed tool API answers all six questions.
This is why tool descriptions, schemas, structured outputs, error contracts, permissions, and observability matter so much.
Protocols such as MCP are formalizing many of these concepts. Current MCP tooling supports tool descriptions, input schemas, output schemas, and behavioral annotations, while newer SDKs also provide schema-based validation for tool arguments and structured results. ([Model Context Protocol][1])
The future of agent engineering is not just about giving models more tools.
It is about giving them better interfaces to reason through.
And the difference between an unreliable agent and a production-grade agent may come down to something as small as this:
Bad tool:
"execute_action"
Good tool:
"cancel_subscription"
The second one gives the model something it can reason about.
That is the real job of an agent tool API.
Final Takeaway
When designing tools for AI agents:
Design for decisions.
Use explicit schemas.
Keep tools focused.
Write descriptions for the model.
Return structured results.
Make errors recoverable.
Separate reads from side effects.
Enforce authorization outside the model.
Support safe retries.
Instrument every invocation.
Evaluate tool selection.
Version important contracts.
The best tool API is not necessarily the one with the most capabilities.
It is the one that gives the agent clear, constrained, predictable actions.
That is how you turn tool calling from a demo feature into an engineering system.
Top comments (0)