Guide

Tools are where support bots cause incidents. A fluent wrong FAQ answer is recoverable. An erroneous refund or leaked PII is an executive incident. Design tools as if a hostile user were authoring the ticket text — because sometimes they are.

Concept

Before you start: your runtime is the trusted executor in tool use. Bind user_id from the session, never from model-chosen free text alone.

The catalog, slowly

ToolPermissionConfirmation
get_order(order_id)Read, customer-scopedNone
get_subscription(user_id)Read, session user onlyNone
send_password_reset(email)Write, rate-limitedSoft (notify user)
propose_refund(order_id, amount)Creates pending refundHard — user or human must confirm
apply_refund(refund_id)IrreversibleHuman only in v1

Expand the catalog slowly. Every new write tool needs a threat model, an eval slice, and a confirmation policy before it ships behind the same agent prompt as FAQ answers.

Design rules:

  • Bind user_id from the session.
  • Return structured errors the model can recover from ("order not found") without inventing data.
  • Idempotency keys on every mutation.
  • Log every tool call on the trace for audit.
TypeScript
const tools = [
  {
    name: "propose_refund",
    description: "Create a pending refund for the signed-in customer's order. Does not move money.",
    parameters: {
      type: "object",
      properties: {
        order_id: { type: "string" },
        amount_cents: { type: "integer", minimum: 1 },
        reason: { type: "string", enum: ["duplicate", "not_received", "quality"] },
      },
      required: ["order_id", "amount_cents", "reason"],
    },
  },
]

propose_refund does not move money. apply_refund is human-only in v1. That split is the answer to "how do you stop the bot from refunding arbitrarily?": session-bound IDs, propose-then-confirm, and a safety eval set.

Injection in tickets

Customers (and attackers) paste "ignore previous instructions" into ticket text. Defenses that work in practice:

  • Separate untrusted user text from instructions in the message structure. Never concatenate raw into the system prompt.
  • The RAG answerer has no mutating tools.
  • The tool agent ignores user-supplied user_id / order ids that do not match session scope.
  • Allowlist tool names server-side. Never execute model-emitted code.
  • Post-response scanners for exfiltration patterns (dumping system prompts, other customers' data).

Details live in guardrails and safety. Support is where those controls earn their keep.

Conversation state

  • Store the session transcript server-side. Send a compacted summary plus last N turns to the model.
  • Carry structured state: order_id already verified, refund_proposed, language.
  • Reset tool confirmations each session. Do not remember "user always wants refunds."

Streaming replies with interim status ("Checking shipment…") cuts perceived latency while tools run. See latency and cost.

Common mistakes

  • apply_refund behind the same agent as FAQ in v1.
  • Trusting an order id that does not match the session.
  • Remembering last session's refund confirmation.

Check yourself

  1. The model emits apply_refund. What should v1 do? Refuse it. Only a human applies an irreversible refund.
  2. The ticket text says user_id=someone-else. Which id does get_subscription use? The session user. The ticket text is not authorization.

Key takeaways

  • Session-scoped reads. Propose/confirm for money. Humans for irreversible v1.
  • Idempotency keys and audit logs on every mutation.
  • Untrusted ticket text never joins the system prompt.
  • Reset confirmations each session.

Further learning

Tool use, guardrails. Next, evals and cost.