
Tools as contracts: designing tools the agent won't misuse
In the previous post I listed seven questions that come before the prompt. Question 3 — tools — I deliberately saved for a post of its own. Here it is.
A tool is what turns a model that only predicts text into a system that executes. It is also where most agents break. Not because the model is dumb — because the tool was handed over poorly defined. And an agent with a poorly defined tool is a confused operator with too much access.
The central idea of this post is a single one: treat every tool as a contract, not as an endpoint.
A tool is not an endpoint
The reflex of anyone coming from backend work is to think of a tool as an API route. But there is one difference that changes everything: nobody on the other side read your documentation.
The model does not call your function because it understood the code. It reads a description in natural language — name, what it does, which parameters — and decides, on the spot, whether that is the right tool and what to pass to it.
In other words: the tool description is not a comment. It is the interface. It is the only context the model has to choose between getting it right and doing something stupid. If it is ambiguous, the agent will get it wrong — and it will get it wrong confidently.
The contract: six fields every tool declares
In the anchor post I said I like to think of tools as contracts. A tool contract answers six things, and all of them matter:
| Field | The question it answers |
|---|---|
| What it does | which action, in one unambiguous sentence |
| Inputs | what data it takes, with what type and format |
| Outputs | what it returns — and what that means |
| Errors | what can fail, and how the agent finds out |
| Reversible? | can it be undone, or is it final |
| Approval? | does it run alone or does it need a human |
Notice that only the first two are "API" fields. The last four are agent design — and they are exactly the ones almost nobody writes down.
The description is a prompt, not a comment
The tool description is read by the model on every call. Write for the model, not for the dev who will maintain it six months from now.
Bad:
buscar_dados(id)— fetches data.
Good:
buscar_pedido(pedido_id)— returns the status, items, and address of one order by ID. It is not for searching by customer or listing several; uselistar_pedidosfor that.
The second version does two things the first does not: it says what the tool is, and it says what it is not. "What it is not" prevents half the wrong uses — the model tends to force the closest tool when none really fits. Closing the door matters as much as opening it.
Typed inputs, validated on the server
The model will pass the wrong input. It will send a string where an enum was expected, a date in a creative format, an ID that does not exist. It is not "if", it is "when".
That is why a tool's input needs two layers. The first is the schema the model
sees: types, enums, required fields. status: "pago" | "pendente" | "cancelado"
guides the model far better than status: string — an enum closes off the error
space before it happens.
The second layer is the one that actually protects: server-side validation. The schema in the prompt is a suggestion the model can ignore — the same way a UI lock is decorative once the client talks straight to the API. The tool runs on your backend; that is where the input gets checked, not in the hope that the model obeys the schema.
Outputs the model can actually use
A good tool also returns well. Two common output mistakes:
Dumping the database. Returning the raw row with thirty columns makes the agent burn tokens and lose track of what is relevant. Return what the next decision needs, not everything you have.
Hiding the failure. If the tool failed, the model has to know — as data, not
as an exception that dies in your log. A return of { ok: false, motivo: "pedido não encontrado" } lets the agent correct course. An error the model never sees is
an agent that carries on as if it had worked.
The three levels, now per tool
In the anchor post and in the seven-questions one I split actions into three levels. When designing a tool, that becomes a label per tool:
| Level | Example tool | Rule |
|---|---|---|
| Read | buscar_pedido, listar_opções | runs directly |
| Preparation | simular_troca, montar_proposta | prepares, does not commit |
| Write | cancelar_pedido, enviar_mensagem | hard rule or approval |
The practical point: you do not treat every tool with the same care. A wrong
buscar_pedido produces a bad answer. A wrong cancelar_pedido produces a real
problem, in a real system, with a real customer. The level decides where a
confirmation point goes — and where it is not needed.
Idempotency: the agent will repeat
There is one detail that separates people who have run an agent in production from people who only built a demo: the agent repeats. It retries when the response is slow, it loops when the termination criterion is loose, it calls the tool again when it reinterprets the step.
If cobrar_cliente is not idempotent, "try again" becomes charging twice.
enviar_mensagem without idempotency becomes the customer receiving the same
thing three times.
The defense is old and well known: an idempotency key. The tool accepts an operation identifier and, if it already processed that identifier, returns the previous result instead of running again. Every write tool needs this. It is not sophistication — it is the minimum for letting an agent anywhere near an action that touches money or messaging.
Fewer tools, better defined
The temptation is to give the agent more tools "just in case". That is the opposite of what works. Each extra tool is one more decision that can go wrong, more description competing for the model's attention, more surface to audit.
An agent with five well-defined tools solves more, and fails less, than one with twenty poorly defined ones. A tool is not a feature you accumulate. It is a permission you grant — and extra permission is extra risk.
The contract breaks in production — now what?
A contract that is good on paper still breaks in production. The tool is defined, the levels are right, idempotency is in place — and something will still fail in a way you did not predict.
In production, the difference between finding a break from a log or from an angry customer is the execution trail: input, tool call, output, retry, and failure. A tool contract does not replace operations; it makes operations auditable.
Next step
If there is a workflow in your operation that needs to query data, act within limits, and leave an audit trail, bring it to a 30-minute diagnostic.
Related posts
A pentester read another customer's conversation — and I learned where the boundary lives
Two months after launch we hired an ethical hacker. He found two critical flaws. The worst was a conversation ID exposed in the front-end that gave access to another customer's data. The lesson is not about IDOR.
The mental architecture of an agent: 7 questions before the first prompt
Before you write a single prompt, an agent demands 7 design decisions — the same 7 that separate a reliable agent from a good-looking demo.
A chatbot is not an agent: the difference that actually matters
The practical difference between chatbots and AI agents, and why that distinction changes architecture, product, risk, and operations.


