Production and case studies
Support tools and permissions
Session-scoped reads, propose-then-confirm for money, humans for irreversible actions. Bind identity from the session.
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
| Tool | Permission | Confirmation |
|---|---|---|
get_order(order_id) | Read, customer-scoped | None |
get_subscription(user_id) | Read, session user only | None |
send_password_reset(email) | Write, rate-limited | Soft (notify user) |
propose_refund(order_id, amount) | Creates pending refund | Hard — user or human must confirm |
apply_refund(refund_id) | Irreversible | Human 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_idfrom 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.
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_idalready 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_refundbehind 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
- The model emits
apply_refund. What should v1 do? Refuse it. Only a human applies an irreversible refund. - The ticket text says
user_id=someone-else. Which id doesget_subscriptionuse? 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.
Related
Case study: customer support agent
End-to-end: RAG over help docs, account tools, escalation to humans, evals and cost.
Tool use and function calling
Letting models call your code: tool schemas, the tool loop, and designing tools models use well.
Guardrails, security and safety
Prompt injection is unsolved. Cut blast radius with architecture, not a classifier that claims to detect it.