Patterns for agent tool calls you can trust in production
Most of what makes an agent safe to run happens at the tool call, not in the prompt: who the call acts for, how risky it is, what happens when it is retried, and whether anyone can see it afterwards.
The idea in brief
An agent becomes risky the moment it can do things, not when it says things. Every action it takes goes through a tool call: a ticket created, a refund issued, a file deleted, an email sent. That makes the tool call the most useful place to put controls.
Prompts help, but they are requests to a model that can be misled. The controls that hold up in production sit outside the model, at the boundary between what the agent proposes and what a system actually does. This post collects seven patterns for that boundary, drawn from public guidance and from older distributed-systems practice that already solved half the problem.
The short version: the model proposes, and the system decides. Each pattern is one question the system should be able to answer before, during or after a call.
1. Design tools that are hard to misuse
The first control is the shape of the tool itself. A tool with a vague name, loose arguments and a dozen modes invites the model to guess. A narrow tool with strict arguments leaves less to guess.
Anthropic's "Building effective agents" (2024) calls this the agent-computer interface and argues it deserves as much care as a human interface. It borrows the term poka-yoke, from lean manufacturing: change the arguments so mistakes are harder to make. Its example is concrete. In their SWE-bench agent, the model made mistakes with relative file paths after moving out of the root directory. Requiring absolute paths fixed it, and the team reports spending more time on tools than on the overall prompt.
In practice that means:
- One job per tool.
refund_orderandcancel_order, notmanage_order(action=...). A narrow tool can be granted, graded and audited on its own. - Types that rule out bad input. Enums instead of free text, amounts in minor units, identifiers that must exist. MCP lets a tool publish an input schema and, since the 2025-06-18 revision, an output schema that clients should validate.
- Descriptions that state the boundary. Say what the tool will not do, and which similar tool to use instead.
Good tool design does not make a tool safe. It removes the mistakes that never needed to happen, so the remaining controls deal with the ones that matter.
2. Know who the call is for
When an agent calls a tool, there are at least two parties: the agent, which is software with its own identity, and the person it is acting for. Most systems were built to see one of them. A service account token says "the support bot is calling" and nothing about which customer asked, or what that customer may do.
OAuth already has a shape for this. RFC 8693, OAuth 2.0 Token Exchange (2020), lets a service swap a user's token for a new one that names both the subject and the actor. The act claim records who is acting on whose behalf, and actors can be nested when one agent calls another. Microsoft's Entra Agent ID documentation describes the same idea as an on-behalf-of flow for agents.
With both parties on the request, the tool can apply the rule that matters: the action must be allowed for the user and for the agent. OWASP's entry on Excessive Agency (LLM06, 2025) recommends exactly this, executing in the user's context with the minimum privileges needed, and enforcing authorization in the downstream system rather than trusting the model to decide.
Two details are easy to miss. The scope delegated to the agent should be narrower than the user's own permissions, since "everything I can do" is rarely what someone means. And the delegation should expire, so an agent that runs for weeks is not still holding last month's consent.
3. Grade every tool by risk
Not every tool needs the same treatment. Looking up an order is not issuing a refund. If every call needs approval, people stop reading the prompts and click yes. If none do, the first mistake is the expensive kind.
MCP gives tools a vocabulary for this. Since the 2025-03-26 revision, a tool can carry four annotations: readOnlyHint, destructiveHint, idempotentHint and openWorldHint. The defaults are deliberately cautious. A tool with no annotations is assumed to write, possibly destructively, not to be safe to repeat, and to reach outside systems. A March 2026 post on the MCP blog describes the annotations as a risk vocabulary that clients can use to decide when to ask for confirmation.
The same post, and the specification, are clear that annotations are hints. A server can describe its own tools wrongly, so clients must treat annotations as untrusted unless the server is trusted. The grade that drives approvals should belong to the platform, not to whoever wrote the tool.
| Tier | What it can do | Default handling | Example |
|---|---|---|---|
| Read | Returns data, changes nothing | Run, within the user's own access | Look up an order |
| Reversible write | Changes state that can be undone | Run, with limits and a record | Draft a reply, add a tag |
| Irreversible or external | Moves money, deletes, or leaves the system | Ask a person, showing the arguments | Issue a refund, send an email |
The MCP specification adds one more client duty here: show the tool's inputs to the user before calling it. An approval prompt that says "Allow refund?" is not a decision. One that says "Refund RM240 to customer 4821 for order 9917" is.
4. Make retries safe
Agents retry. Networks drop responses, tools time out, and runtimes restart mid-task. A timed-out call may still have completed, so a blind retry can create a second ticket or a second payment.
This is the oldest problem in the list, and it has a settled answer: idempotency keys. The caller attaches a unique key to each logical action and reuses it on every retry. The service stores the key with the result and returns the stored result when it sees the key again. Stripe's API documents the details well: keys of up to 255 characters, results saved once execution begins, keys that may be pruned after at least 24 hours, and an error if a key is reused with different parameters. AWS describes the same design across its own APIs.
The agent-specific part is where the key comes from. If the model generates the key, a retry may come back with a new one, because the model regenerated the whole call. The key belongs to the workflow: create it before the call, store it with the step, and reuse it on every attempt.
# illustrative only
step = workflow.step("refund", order_id=9917)
key = step.key or step.save_key(uuid4()) # made once, before the first attempt
for attempt in retries(max=3):
result = payments.refund(order_id=9917, amount=24000, idempotency_key=key)
if result.ok or not result.retryable:
break
Tools that cannot deduplicate need a different answer: check before retrying. Look up whether the refund exists, then decide. That is slower, and it is the reason the tier table above treats non-idempotent, irreversible tools with the most care.
5. Never give one agent the lethal trifecta
Some risks come from combinations, not single tools. Simon Willison's post "The lethal trifecta" (2025) names one that sits behind many published exploits. An agent becomes exploitable when it has all three of: access to private data, exposure to untrusted content, and a way to send data out.
The reason is prompt injection. Untrusted content can carry instructions, the model may follow them, and the instructions can say "send this to me". Willison's advice is blunt: avoid the combination entirely, because no reliable filter for injected instructions exists yet.
This turns into a design rule for the platform. Treat the three as capabilities, and refuse configurations that grant all three to one agent. Split the work instead. An agent that reads the web does not get the inbox. An agent with the inbox cannot make arbitrary HTTP calls. Where all three are needed, put a person or a narrow allow-list on the outbound path.
6. Leave a trace for every call
When something goes wrong, the first question is what the agent actually did. "The run succeeded" is not an answer if the run issued the wrong refund. Each tool call needs its own record: which tool, which arguments, for whom, with what result, and as part of which run.
OpenTelemetry's GenAI semantic conventions give this a shared shape. A tool call becomes an execute_tool span with attributes such as gen_ai.tool.name and gen_ai.tool.call.id, nested under the agent's run. Using the standard, rather than a home-grown log format, means traces from different frameworks and vendors can sit in the same backend. The conventions are still marked as in development, so expect some names to move.
Two practical points. Propagate trace context into the tool's own service, so the agent's span and the API's span join up. And decide early what not to record: arguments and results can hold personal data, and a trace store is a poor place to discover that.
7. Return errors the agent can act on
An agent reads errors the way a developer does, except faster and more literally. "Request failed" leads to a retry loop. "Refund exceeds the delegated limit of RM50; ask the user to approve" leads to the right next step.
RFC 9457, Problem Details for HTTP APIs (2023), gives errors a stable structure: a type, a title, a status, a detail and an instance. A typed error lets the agent, and the code around it, tell a retryable failure from a permanent one, and a policy refusal from a broken tool. MCP makes a similar split between protocol errors and tool execution errors, which are returned to the model as results with isError set.
The usual caution applies. A useful error says what happened and what to try next. It does not reveal the exact policy rule, internal hostnames or stack traces, which are as useful to an attacker steering the agent as they are to the agent.
Putting it together
None of these patterns is new on its own. Narrow interfaces, delegated tokens, risk tiers, idempotency keys, separation of duties, tracing and typed errors are all ordinary engineering. What is new is the caller: a component that is capable, fast, and occasionally wrong in ways that are hard to predict. That makes the boundary more important, not less.
A useful test for any tool an agent can call is whether the platform can answer these questions about it without reading the prompt:
| Question | Pattern | What answers it |
|---|---|---|
| Could the model call this wrongly by accident? | Tool design | Narrow tool, strict schema |
| Who is this call for, and may they ask for it? | Identity | Token with subject and actor |
| Does a person need to see it first? | Risk tier | Platform-owned grade, approval with arguments |
| What happens if it runs twice? | Retries | Workflow-owned idempotency key |
| Can it combine with other tools to leak data? | Trifecta | Capability rules per agent |
| Can we see what it did? | Tracing | execute_tool spans with call IDs |
| Can the agent recover when it fails? | Errors | Typed problem details |
Where to start
For a team with agents already running, the order that pays back fastest is usually:
- Trace every tool call. It is cheap, it changes nothing about behaviour, and it shows which tools are actually used and how.
- Grade the tools. Most will be reads. The few irreversible ones are where approvals and idempotency keys are worth the effort.
- Fix identity on the risky tools. Start with the ones that move money or touch personal data, and pass the user through.
- Check each agent for the trifecta. It is a configuration review, not a project, and it closes the worst class of leak.
The rest, better tool design and better errors, tends to follow once the traces show where agents get stuck. If you want to see three of these failures happen step by step, the Agent Gateway Playground runs them in the browser: a timeout and retry, a delegated permission check, and a 200 that was not a success.
Limitations
Patterns, not a framework.
These are design habits drawn from public sources and distributed-systems practice. Each team still has to decide which tools need which controls.
Some of the standards are young.
MCP tool annotations and the OpenTelemetry GenAI conventions are still changing. Field names here may move.
Controls cost something.
Approvals slow people down and a gateway is another service to run. Grade tools by risk so the cost lands where it is worth paying.
Prompt injection is not solved.
The patterns limit what an injected instruction can do. They do not stop the model from being misled.
Sources
Cite this post
Adam, Ammar. (Oct 2026). Patterns for agent tool calls you can trust in production. www.ammaradam.dev. https://www.ammaradam.dev/posts/patterns-for-safe-tool-calls/.
BibTeX
@article{adam2026patternsforsafetoolcalls,
title = {Patterns for agent tool calls you can trust in production},
author = {Adam, Ammar},
journal = {www.ammaradam.dev},
year = {2026},
month = {Oct},
url = {https://www.ammaradam.dev/posts/patterns-for-safe-tool-calls/}
}
Comments
Sign in with GitHub to like this post or join the discussion.