How do you design the tools an agent calls?
Give each tool one narrow purpose with a self-describing schema, return failures as structured results the model can act on rather than raising, take an idempotency key on anything that mutates, and put validation and authorisation in the executor because arguments produced by a model are untrusted input.
What the interviewer is scoring
- Does the candidate design for a caller that cannot ask clarifying questions
- Whether errors are returned as data with a recovery hint rather than thrown
- That model-produced arguments are treated as untrusted, including under prompt injection
- Can the candidate explain why identity must be bound server-side rather than passed as a parameter
- Whether idempotency is tied to a key the caller supplies rather than to hopeful retry logic
Answer
A tool is an API for a caller that cannot ask you anything
The consumer of a tool schema reads it once, in a context window, alongside everything else it is thinking about, and then commits to arguments without the option of a clarifying question. Every ambiguity you leave is resolved by guessing. That single constraint drives most of the design.
So the name says the intent, the description says when to use it and when not to, and every parameter documents its format, its units and its bounds. Prefer an enumeration over a free string wherever the set is known, because an enum is a constraint the schema can enforce and a sentence in a description is only a suggestion. Give optional parameters real defaults rather than expecting the caller to know your conventions. Avoid the flag parameter that changes what the tool does, because you have then built two tools and hidden one of them. And avoid the free-form object of options entirely: it is where a schema stops being a contract.
Narrow tools, and not too many of them
run_sql(query) looks like leverage and behaves like a trap. It moves all of the difficulty into constructing the argument, which is where models are least reliable and where you have the least ability to validate. find_orders(customer_id, status, placed_after) constrains the space of possible mistakes to values you can check, and its failures are legible.
The tension is that specificity multiplies tool count, and a large set with overlapping responsibilities produces a different failure: the model picks a plausible neighbour of the tool you intended. When two tools could both reasonably answer the same request, that is a design defect rather than a prompting problem, and the fix is to merge them or to make the descriptions state the discriminating condition explicitly. Test selection directly — hold a set of requests with the correct tool labelled, and measure how often the right one is chosen — because selection accuracy is a property of your schema surface, and it is the cheapest thing to measure and the easiest to regress when someone adds a tool.
Errors are results
An exception that propagates out of the executor either kills the run or arrives back as an opaque framework message, and in both cases the model has learned nothing it can use. Return failure as a normal result with a structure: what went wrong, whether retrying could help, and what a corrected call would look like.
The difference is one turn versus a wasted run. "Invalid input" produces another invalid call. "placed_after must be an ISO-8601 date; received last tuesday" produces a correct call immediately. Distinguish the retryable from the terminal, because a model that cannot tell them apart will hammer a permanently failing tool until its budget is gone. And write the message for the model without leaking your internals — no stack traces, no internal identifiers, no upstream error text you have not read, because that text becomes part of a prompt and anything in a prompt is potentially instruction.
Arguments from a model are untrusted input
This is the part that separates a design answer from an engineering one. The arguments arriving at your executor were produced by a model whose context contains retrieved documents, tool results, web pages and user text — all of which may be adversarial. Prompt injection means an attacker who controls any of that content is, in effect, a client of your tool. So the executor validates and authorises exactly as an internet-facing endpoint does, and it does so on the server side of the boundary, never in the prompt.
Concretely: identity and tenancy are bound from the session, not accepted as parameters. If your tool signature contains user_id or tenant_id and the model fills it in, you have handed the model the authority to act as anyone, and one persuasive document is all it takes. Authorise against the end user's permissions rather than the agent's service credentials, or you have built a confused deputy that will happily read anything the service can read. Scope the credentials the run holds to the minimum for the task and for the duration of the run. Validate paths, identifiers and URLs against allow-lists rather than sanitising them.
# The model supplies only what it could legitimately know. Identity comes from
# the session; scope comes from the caller's own permissions.
def issue_refund(args, ctx):
order_id = validate_uuid(args["order_id"]) # reject, don't coerce
amount = validate_money(args["amount_minor"], max_minor=50_00)
# Authorise as the human, not as the agent's service account.
if not ctx.principal.can("refund", order_id):
return {"ok": False, "retryable": False,
"error": "not permitted to refund this order"}
# The key makes duplicate delivery harmless. Derived from the run and step,
# so a framework retry and a model retry collapse onto the same key.
key = f"{ctx.run_id}:{ctx.step}:refund:{order_id}"
return payments.refund(order_id, amount, idempotency_key=key)
Idempotency, because retries are invisible to you
There are at least three sources of retry in an agent stack: the model deciding it is unsure whether the call succeeded, the orchestration framework retrying a timed-out call, and the user resubmitting. None of them coordinate. Any tool with a side effect therefore takes an idempotency key, the server records it against the result, and a repeat with the same key returns the original outcome rather than performing the action again. Derive the key from the run identifier and step so it is stable across retries of the same intent and distinct across genuinely new intents, and never let the model choose it freely.
A timeout is the case to reason about aloud. A payment tool that times out has an unknown outcome, and without a key the only safe recovery is a read-back to check state, which many APIs make awkward. With a key, recovery is simply retrying. Where the underlying system offers no idempotency of its own, put a claim record in front of it in your own store and make the tool consult that record first.
Return shapes decide how many turns you spend
A tool result enters a context window, so its size is a cost and a distraction. Return the fields the next decision needs, not the entity in full. Include stable identifiers so a follow-up call can reference something precisely instead of matching on a name. Normalise units and formats so the model never has to convert. When a result is large, truncate deliberately, say so in the payload, and return a cursor — a silently truncated list is a correctness bug, because the model will reason as if it saw everything.
Finally, treat the schema as a published interface. Changing a parameter's meaning without changing its name breaks every saved plan, every evaluation case and every in-flight run. Add rather than repurpose, keep the old shape accepted for a deprecation window, and run your tool-selection evaluation on the change before it ships.
Likely follow-ups
- You have thirty tools and the agent keeps choosing the wrong one. What do you change?
- How do you keep a tool result from consuming half the context window?
- A retrieved document instructs the model to call your refund tool. What stops it?
- How would you version a tool whose schema must change for existing running agents?
Related questions
- When is an agent the wrong shape for a problem?hardAlso on agents5 min
- How do you enforce per-user permissions in a retrieval system?hardAlso on authorisation6 min
- How do you turn a training script that works on your laptop into a scheduled pipeline?mediumAlso on idempotency5 min
- What is indirect prompt injection, and how would you defend an assistant that reads user-supplied documents?hardAlso on agents5 min
- Walk me through what happens between a customer tapping a card and the merchant getting paid.hardAlso on authorisation6 min
- Walk me through an order's journey from placement to delivery. How would you model its status?hardAlso on idempotency6 min
- A client times out and retries your POST /payments call. How do you make that endpoint idempotent?hardAlso on idempotency5 min
- We sell to trade customers who each negotiate their own prices, and those prices change. Design the schema.hardAlso on schema-design4 min