Design tools for agents, not for people
The tools you give an agent shape its behavior as much as the prompt does. Seven habits for tools a model can use reliably.
evanbyte3 min read

When an agent misbehaves, the prompt gets blamed first. Often the tools are the problem. A tool is an interface whose user is a model: it reads the name and description, guesses the arguments, and acts on whatever comes back. Interfaces built for human developers don't always survive that.
Seven habits
1. Fewer, sharper tools
Ten overlapping tools force the model to choose between near-identical options. Prefer a small set where each tool has one clear job, and merge the ones that always run together.
2. Names and descriptions are prompts
The model decides what to call from the name and description alone. Say what the tool does, when to use it, and when not to. 'Look up one order by ID. For searching by customer, use orders.search' beats 'orders'.
3. Strict, boring schemas
Use typed arguments, enums instead of free text, and explicit units. Reject bad input with a clear message instead of guessing what the model meant.
4. Errors that say what to do next
'Error 400' teaches nothing. 'No order A-229. IDs look like A-2291. Try orders.search with the customer's email' lets the agent recover on its own.
const getOrder = {
name: "orders.get",
description:
"Look up one order by its ID (format A-1234). " +
"To find an order by customer email, use orders.search instead.",
input: {
order_id: { type: "string", pattern: "^A-\\d{4}$" },
},
// Errors are written for the model, not for a log file.
onNotFound: (id: string) =>
`No order ${id}. IDs look like A-2291. Try orders.search with the customer's email.`,
};5. Keep reads and writes apart
Make read tools freely available and put write tools behind policy. Name them so the difference is obvious, and give every write a dry-run option so the agent, and an approver, can see the effect before it happens.
6. Make writes safe to repeat
Agents retry, and networks fail. A write that charges a card twice when called twice is a bug waiting for a bad day. Accept an idempotency key and return the original result on repeats.
7. Return less
A tool that returns forty pages of JSON burns the context window and distracts the model. Return what the next step needs, paginate the rest, and include identifiers so the agent can ask for more.
A redesign, step by step
A common starting point is one tool that does everything: a query tool that accepts raw SQL. It's flexible, and it's a trap. The model has to write correct queries against a schema it can't see, and one bad call can read or change almost anything.
crm.query({ sql: string })Three narrow tools usually serve better. Each one is easier to describe, to test, and to permission.
crm.find_account({ email?: string, name?: string })
// read · returns up to 5 matches
crm.get_account({ account_id: string })
// read · returns the fields the agent needs
crm.log_note({ account_id: string, note: string, dry_run?: boolean })
// write · idempotent · needs approvalThe agent loses nothing it needs, and the guardrails come for free: reads are always allowed, and the one write is easy to gate.
What to return
Think of a tool result as the agent's working memory. Return identifiers and the fields the next step needs, not the whole record. When there's more, say so, and tell the agent how to ask for it.
{
"matches": [
{ "account_id": "acc_3141", "name": "Acme Supply", "owner": "Priya" }
],
"total": 12,
"shown": 1,
"hint": "12 matches. Narrow the search with an email address, or call crm.get_account for details."
}Anti-patterns to avoid
- Tools that accept free-form query languages or shell commands.
- Descriptions that explain how a tool works instead of when to use it.
- Silent failures that return an empty list when something actually went wrong.
- Write tools with no way to preview or undo the change.
- Results whose shape changes from one call to the next.
Test the tools on their own
Before an agent uses a tool, test it like any other API: valid input, invalid input, timeouts, and duplicates. Then include tool-use cases in your evals, so you notice when a change to a description sends the model down a different path. Descriptions are code that the model reads, and they deserve the same review as any other change.

