AI Agents & Orchestration · 11 min read

Building Tool-Calling Agents

Design safe tool schemas and execution loops for agents that need to interact with software.

Tools are APIs

A tool is an API exposed to a model, so it deserves the same design discipline as any public endpoint. Give it a stable name, a precise description, typed arguments, validation rules, permission scope, timeout, and structured result. Describe what the tool does and does not do, including units, identifiers, and side effects. Keep the catalog small. Every extra tool increases the chance of confusion, prompt injection, an incorrect choice, and additional latency. A clear contract is more valuable than a clever tool description.

Separate proposal from execution

The model should propose a tool call; ordinary application code should validate, authorize, execute, and format the result. Never interpolate raw model text into a shell command, SQL statement, URL, or filesystem path. Parse arguments against a schema, normalize identifiers, check ownership, and apply policy before execution. The model can help choose a customer or action, but the server must decide whether that choice is allowed. This boundary remains necessary even when a model appears reliable in testing.

Validate and authorize a tool calltypescript
type RefundArgs = { orderId: string; amount: number };

function executeRefund(args: RefundArgs, userId: string) {
  if (!/^ORD-[0-9]+$/.test(args.orderId)) {
    throw new Error('Invalid order ID');
  }
  if (args.amount <= 0 || args.amount > 10000) {
    throw new Error('Refund amount is outside the allowed range');
  }
  if (!canRefund(userId, args.orderId)) {
    throw new Error('User is not authorized for this order');
  }
  return refundService.create({ ...args, idempotencyKey: crypto.randomUUID() });
}

// The model proposes { orderId, amount }; application code decides.

Design safe schemas

Use narrow fields and enumerations instead of accepting a free-form instruction. Prefer a `customer_id` over “the customer mentioned above,” and a `refund_amount` with currency and maximum constraints over an unrestricted sentence. Make dangerous operations explicit in names and arguments. Reject unknown fields, oversized strings, invalid dates, and ambiguous identifiers. A schema should make unsafe intent difficult to express and make a reviewer’s approval screen understandable without reading the entire conversation.

Run the execution loop

A robust loop loads state, asks the model for either a final response or one of the available actions, validates the selected action, checks authorization, runs it with a deadline, and returns a bounded structured result. Record an operation ID so retries can be correlated. Limit the number of iterations and tool calls. If a tool fails, return a useful error category such as validation, permission, timeout, or dependency failure. Do not let the model retry forever or reinterpret a security failure as permission to try harder.

Idempotency and retries

Retries are safe only when the operation is safe to repeat or has an idempotency key. Reading an order may be retried after a transient timeout; sending an email or charging a card should use a durable operation record and provider-supported idempotency. Store the proposed action, authorization decision, execution status, and result. If the process crashes after the side effect but before saving success, recovery must check the operation status rather than blindly repeat it. Reliability is about state, not just backoff.

Approval and audit

Add a human gate before destructive, financial, external-communication, or irreversible actions. Show the user the tool name, normalized arguments, affected resource, expected side effect, and any uncertainty. The approved action should be bound to those arguments so a later model response cannot silently change them. Audit logs should capture who requested the action, which policy allowed it, who approved it, when it ran, and the outcome. Redact secrets and sensitive payloads while keeping enough evidence to investigate.

Example: an expense assistant

An expense assistant can read a receipt, extract fields, look up a category, and draft a reimbursement request. Extraction may return uncertain values that require confirmation. Category lookup can be read-only, while submission should validate employee ownership, currency, policy limits, and duplicate receipt IDs. The assistant should never be given a generic accounting command tool. Its tools might be `get_expense_policy`, `find_employee_expense`, and `create_draft_reimbursement`, with final payment handled by a separate approved workflow.