🧨Why tool calling is the fragile layer
Everything else in an agent system fails softly — a weak summary, a drifted plan — and a human or a later step can catch it. Tool calls fail hard: a payment is issued, a row is deleted, an email is sent. The blast radius of a model mistake is exactly the blast radius of the tools you handed it, which is why tool design is security design and not merely API ergonomics.
The compounding point deserves emphasis because it drives the whole design philosophy. At 95 percent per-call reliability, a ten-call task succeeds about 60 percent of the time if failures are independent and fatal. The engineering response is not to demand a better model; it is to make failures recoverable, retries safe, and dangerous actions gated — so a failed call is an event the system handles, not a corpse in the transcript.
That framing — assume failure, design for recovery — is the through-line of every pattern below.
📐Tool schema design: narrow beats broad
The dominant design error is building tools that mirror your internal APIs. Internal APIs are broad because they serve many callers with client-side logic; a model has no client-side logic — it is the caller, and every degree of freedom in the schema is a way for it to be wrong. A generic query_database(sql: string) tool can express anything and will eventually express something catastrophic. A set of narrow tools — get_customer_by_email, list_open_orders(customer_id) — constrains the model to the operations you have actually made safe.
Narrow tools also give the model a better decision surface. Tool selection accuracy degrades as the number of similar-looking tools grows, but it degrades faster when tools are parameterized clones of each other than when each tool has a distinct, nameable purpose. The sweet spot for most agents is a small catalogue of single-purpose tools with names that read like the user intent: refund_order, not post_to_billing_api with an action field.
Parameter design is its own craft. Prefer enums over free strings wherever the value space is closed — an enum cannot be hallucinated into an invalid value. Prefer IDs the agent obtained from earlier tool results over IDs it must construct or recall. Give every parameter a description that states format and units, because "date" will be interpreted three ways by the same model in the same week. And keep required parameters few: every required field the model must source is a failure opportunity.
| Design choice | Broad style | Narrow style | Winner for agents |
|---|---|---|---|
| Endpoint shape | One generic endpoint, action parameter | One tool per operation | Narrow — selection and validation both improve |
| Parameters | Free-form strings, flexible payloads | Enums, typed scalars, constrained formats | Narrow — closes hallucination surface |
| Catalogue size | Few tools, many modes | Many single-purpose tools | Narrow up to ~dozens, then add tool routing |
| Errors | HTTP status + generic body | Structured, model-recoverable messages | Narrow — see error contracts below |
| Safety | Caller-side checks (absent) | Safety baked into each tool | Narrow — the tool is the guardrail |
Every degree of freedom in a tool schema is a way for the model to be wrong. Narrow tools are not less powerful; they are power with the sharp edges pre-removed.
🛡️Argument validation: treat model output as hostile input
The correct security posture for tool arguments is the one you already use for user-submitted form data: validate everything, trust nothing. The model will, with enough calls, produce arguments that are malformed, out of range, semantically absurd ("refund -400 dollars"), or — in adversarial settings — deliberately injected through content the agent read. Schema-level validation (types, enums, ranges, formats) is the floor, not the ceiling.
Semantic validation is the layer most systems skip. Schema says amount is a number; business logic says a refund cannot exceed the order total, the order must be in a refundable state, and the order must belong to the customer in the conversation. These checks live in the tool implementation or a policy layer in front of it — never in the prompt, because prompt-level rules are instructions the model follows when it remembers to, not constraints.
Validation failures should feed back, not throw. When arguments fail validation, return the failure as a structured, correctable error to the model (see the error-contract section) and let it retry — a large share of argument errors are one-shot slips the model fixes immediately when told precisely what was wrong. Reserve hard failures for validation problems that are unrecoverable or repeated, and log every rejected call: your validation log is the highest-signal dataset you have for how the model actually uses your tools.
🔁Idempotency: because retries are not optional
Agents retry. Timeouts fire, the orchestrator re-attempts a step, the model itself re-issues a call it is unsure completed. If your mutation tools are not idempotent, retries are how customers get charged twice and tickets get created in triplicate. Idempotency is not a nice-to-have in an agent system; it is the property that makes the entire reliability architecture — timeouts, retries, at-least-once execution — safe to use.
The mechanism is standard and borrowed straight from payments engineering: the orchestrator generates an idempotency key per logical operation (not per call attempt), the tool stores the result keyed by it, and repeat executions return the stored result instead of re-executing. The subtlety is key ownership: the model should not generate the key — it will generate a fresh one on retry, defeating the purpose. Keys come from the deterministic layer that wraps the model.
Design tools to be checkable as well as idempotent where you can. A dry_run or confirm parameter, or a paired check_* read tool, lets the agent verify state before and after a mutation — which converts the model own uncertainty ("did that refund go through?") into a tool call instead of a guess. Agents that can check their work are dramatically easier to make reliable than agents that must act on faith.
| Tool class | Retry safety | Pattern |
|---|---|---|
| Reads (get, list, search) | Naturally safe | Nothing needed — but cache and rate-limit |
| Idempotent writes (set, update-by-key) | Safe with care | Upsert semantics; last-write policy documented |
| Mutations (charge, refund, create) | Unsafe by default | Orchestrator-issued idempotency keys + stored results |
| External side effects (email, third-party API) | Unsafe and uncontrollable | Transactional outbox + executor with dedupe |
| Destructive (delete, cancel) | Unsafe and irreversible | Soft-delete + confirmation gate + audit trail |
📨Error contracts the model can recover from
A tool error is a message to the model, and most tool errors are written for humans or machines that parse status codes — neither of which is your consumer. "500 Internal Server Error" teaches the model nothing; it will retry the identical call or hallucinate a workaround. A recoverable error contract answers three questions in the response body: what went wrong, whether retrying helps, and what to change. invalid_parameter: date must be ISO-8601, got "next Friday" is an instruction the model can act on, and it will.
Classify errors by recoverability and say which class they are. Retryable-as-is (rate limited, transient downstream failure — include retry timing). Retryable-with-changes (validation failure — include exactly what to change). Not retryable with this tool (permission denied, resource does not exist — tell the model to escalate or choose another path rather than hammering). And never-retry red lines (policy violation) that should also trip your monitoring, because repeated policy-violating attempts are a signal about the task or an attack, not bad luck.
The payoff shows up in run-level reliability more than anywhere else. Most multi-step task failures are not unrecoverable errors — they are recoverable errors presented in a form the model could not recover from. Fixing the error contract is routinely the highest-ROI reliability change in an existing agent system, ahead of model upgrades, ahead of prompt work.
The model is the parser of your error messages. Write errors as instructions — what failed, whether retry helps, what to change — and watch run-level reliability move further than any model upgrade would move it.
🔐Permission tiers: least privilege for models
Not every tool deserves the same trust level, and the permission structure should reflect blast radius, not convenience. A workable tiering: read-only tools available by default; reversible mutations behind a policy check; irreversible or financial mutations behind a confirmer or human approval; and administrative actions outside the agent reach entirely, reachable only through a human-operated path. The tier of a tool is a property of the action, and it should be enforced in code the model cannot talk its way around.
Permissions must be scoped to the task context, not just the agent. A support agent handling customer X conversation should hold credentials that can touch customer X records — so a prompt injection that hijacks the agent hits a wall at the authorization layer. Per-task or per-session scoped credentials are more operational work than one god-mode service account, and they are the difference between an injection that is embarrassing and one that is a breach. This connects directly to memory and retrieval: anything the agent reads can attempt to steer it, so the write surface must assume the read surface is compromised.
Human-in-the-loop approval is a tier, not a philosophy. It belongs on the narrow set of actions that are irreversible and high-value — and it must be engineered to be usable (clear summary of the proposed action, diff or amount, one-click approve) or operators will rubber-stamp, at which point the control is theatre. If everything requires approval, nothing does.
Audit everything, structurally. Every tool call with its arguments, its tier, its policy decision, and its outcome, keyed to the run and the principal. When something goes wrong — it will — the audit trail is how you distinguish model error from injection from design flaw, and it is also the evidence your security review and your customers will ask for.
Securing agents end to end: injection to sandboxing
| Tier | Examples | Gate | Approval latency cost |
|---|---|---|---|
| Tier 0 — read-only | Search, fetch records, report data | None beyond auth | None |
| Tier 1 — reversible mutations | Draft a message, stage a config change | Policy checks, scoped credentials | None |
| Tier 2 — bounded irreversible | Refund under a limit, send to a known contact | Confirmer step + hard caps | One round-trip |
| Tier 3 — high-blast-radius | Payments above limits, deletions, external announcements | Human approval with a usable UI | Human-time — use sparingly |
| Tier 4 — administrative | Credential rotation, permission changes | Not available to the agent at all | Out of band |
⏱️Rate limiting and the loop problem
Agents discover rate limits the hard way: a tool call fails, the model retries immediately, the failure feeds the loop, and a confused agent can generate a self-inflicted denial-of-service against your own infrastructure or a third-party quota in seconds. Tool-level rate limiting is therefore not just about protecting downstream systems; it is about making the failure mode survivable. Limits should return retryable errors with explicit timing — information the model can plan around rather than a wall it walks into repeatedly.
Budget the loop, not just the tool. Every agent run should carry hard caps: maximum steps, maximum wall-clock, maximum tokens, and maximum spend — enforced by the orchestrator, with a defined terminal behaviour (return partial results with an explicit incomplete status) when a cap fires. An agent without step caps is an infinite loop with a billing meter attached, and every production team has the incident story to prove it.
Third-party quota strategy deserves design thought. When the agent uses external APIs with their own limits, the wrapper should implement queuing and backoff in the tool layer, so the model sees latency rather than failures. Where quotas are scarce, surface them in tool results ("3 of 50 daily lookups remaining") so the model can ration — models respond surprisingly well to explicit scarcity signals.
🧪Mocking tools for evals
You cannot evaluate an agent against live tools: the world changes under the eval, side effects are unacceptable, and third-party flakiness gets scored as agent failure. Every eval harness needs a tool virtualization layer — recorded or simulated tool implementations behind the same schemas the agent sees in production. If your tools were designed narrow and structured (the earlier sections), this layer is straightforward to build; if your tools are broad and side-effecting, the eval problem is telling you something about the design.
Three mock flavours cover the need. Replays serve recorded responses from real interactions — highest fidelity, best for regression suites, with the maintenance cost that recordings age as APIs evolve. Scripted mocks implement per-scenario behaviour (this customer exists, this order is already refunded) — the workhorse for scenario evals. Fault injectors return calibrated failures — timeouts, validation errors, rate limits — because the behaviours you most need to evaluate are the recovery behaviours, and happy-path mocks cannot surface them.
Score the tool use, not just the outcome. An eval that only checks the final answer credits an agent that got lucky with wrong calls. Grade call sequences against scenario expectations: right tools, valid arguments, no policy-violating attempts, recovery behaviour after injected faults. Tool-call-level scoring is also what makes evals diagnostic — "fails 40 percent of refund scenarios by skipping the eligibility check" is actionable; "score dropped" is not.
The full agent eval methodology
Replay mocks
Recorded real responses for regression suites; refresh them on API change or they become tests of a world that no longer exists.
Scripted scenario mocks
Per-scenario state and behaviour; the backbone of task-level evals.
Fault injectors
Timeouts, validation errors, rate limits on demand — because recovery behaviour is the thing you most need to measure.
Realistic mess
Duplicates, stale fields, truncation, noise. A tidy mock environment evaluates an agent production will never meet.
📊Honest failure rates and what to do with them
You should assume your tool-calling layer fails regularly and design the system so that regular failure is survivable. Wrong tool selection, malformed arguments, hallucinated parameter values, and premature give-ups after recoverable errors are all routine events in production traces, for every model, as of writing. Anyone selling you an agent architecture that does not discuss its failure rate per call and its recovery rate per failure is selling you the demo.
Measure four numbers on your own traces: per-call argument-validity rate, tool-selection accuracy on multi-tool tasks, recovery rate after first failure (does call two fix it), and end-to-end task success. The first three are leading indicators you can improve with the patterns above; the fourth is the number your users feel. Improve error contracts and watch recovery rate move. Narrow the schemas and watch argument validity move. The measurements tell you which pattern to apply next — which is why the instrumentation comes before the cleverness.
Know also when the answer is less agency. If a workflow tool-calling reliability cannot reach the bar the business needs, the correct engineering decision is often to demote the flow from agent-chosen tools to a deterministic chain with model steps inside it. Reliability you cannot achieve with autonomy you do not strictly need is a bad trade, and making it deliberately is the mark of a team that will still be running the system next year.
How to build an MCP server with well-designed toolsTalk to us about production agent reliability