What this system is, and its 5 parts
Think of this as the AI teammate that sits behind your support inbox. When a customer writes in about a refund, a tech glitch, or an account question, this agent reads the message, looks up who the customer is, decides what they actually need, and either solves it on the spot or hands the case to a human with all the context already prepared. It exists because most support questions follow the same handful of patterns, and answering them in seconds (instead of hours) is the difference between a customer who stays and a customer who churns. Everything below is how that simple idea is wired up safely in production.
Tool Registry
verify · lookup · process · escalateHolds the 4-5 tools the specialist agent can call. Each tool has a clear description and JSON schema. Tool count stays low to keep routing accurate.
Configurationtools: [verify_customer, lookup_order, process_refund, escalate_to_human]. tool_choice: auto. Each description is 4 lines: what it does, when to use, edge cases, ordering with peers.Concept: tool-calling →
PreToolUse Hook
policy gate · deterministicSits between Claude's tool_use request and actual tool execution. Enforces refund caps, escalation triggers, and time-of-day limits. Exits 2 (deny) on violation.
ConfigurationHook fires before process_refund. Reads tool_input.amount, compares to policy.refund_cap. Exit 2 with stderr message routes Claude to retry with adjusted args or escalate.Concept: hooks →
Case-Facts Block
pinned customer statePinned at the top of every system-prompt iteration. Holds customer_id, order_id, refund_amount, policy_limit. Survives summarization. Re-read every turn.
Configurationsystem: f"CASE_FACTS: {customer_id} · {order_id} · ${amount} · cap=${cap}". Updated by hooks after state-changing tool calls.Concept: case-facts-block →Specialist Agent
the agentic loopRuns the messages.create() loop. Reads stop_reason after every response: end_turn → exit, tool_use → execute + append result + continue, max_tokens → save partial.
Configurationwhile True: resp = client.messages.create(...). if resp.stop_reason "end_turn": break. if resp.stop_reason "tool_use": execute_tools(...).Concept: agentic-loops →
Escalation Queue
structured handoffReceives blocked calls from PreToolUse hook + low-confidence + sentiment-triggered escalations. Each entry has a structured context block (cus_id, reason, partial_status, recommended_action).
Configurationqueue.push({customer_id, intent, partial_state, blocked_tool, reason, recommended_action}). Human triages in ~10s vs 5min for transcript review.Concept: escalation →The problem
- Resolve the request in one turn without multiple agent transfers.
- Get identity verified before any account-modifying action.
- See a clear path to a human when the agent can't help.
- Single-agent chatbots forget the customer ID by turn 8 (no case-facts pinning).
- Prompt-only refund-cap policy leaks 3% of refunds above the limit (no deterministic hook).
- Sentiment-triggered escalation creates false positives: angry users with valid policy denials get escalated unnecessarily.
- ✓ p95 resolution latency < 12 seconds end-to-end
- ✓ Refund-cap violations = 0 (hook-enforced, not prompt-enforced)
- ✓ Audit log entry per ticket with case-facts snapshot
- ✓ CSAT ≥ 4.2/5 across resolved tickets
One run, traced end to end
8 steps to production
Define the system prompt with case-facts
Anchor the agent's role + constraints + the case-facts block at the very top of the system prompt. The case-facts block is the immutable truth about this customer + order + policy.
Concept: case-facts-block →from anthropic import Anthropic
client = Anthropic()
def build_system_prompt(case_facts: dict) -> str:
return f"""You are a customer support agent for ACME.
CASE_FACTS (immutable; re-read every turn):
- customer_id: {case_facts['customer_id']}
- order_id: {case_facts['order_id']}
- refund_amount: ${case_facts['amount']}
- policy_cap: ${case_facts['cap']}
Constraints:
- Verify customer before ANY account-modifying call.
- Refunds above policy_cap MUST escalate (a hook enforces this).
- Branch on stop_reason. Never on response text."""Define the 4-tool registry
Keep the tool count between 4-5. Each tool description is structured in 4 lines: what / when / edge cases / ordering. This is the primary lever for correct routing, fix descriptions, not the model.
Concept: tool-calling →Wire the PreToolUse policy hook
The hook is deterministic, prompt-only enforcement leaks 3% of cases past the cap. Exit 2 to deny; exit 0 to allow; the SDK reads stderr to route Claude back with feedback.
Concept: hooks →Run the agent loop on stop_reason
Branch on the structured field, never the response text. end_turn → exit. tool_use → execute, append, continue. max_tokens → save partial. stop_sequence → custom termination.
Concept: stop-reason →Add the structured escalation block
When the hook denies or the agent reaches stop_reason with low confidence, push a structured block, not the transcript, to the human queue. Triage time drops from 5 minutes to 10 seconds.
Concept: escalation →Wire the sentiment + confidence gates
Two final guards on the response: sentiment monitor (orthogonal to policy, distress alone never triggers a refund) + confidence threshold. Either gate can route to escalation.
Concept: escalation →Cache the system prompt for cost
The system prompt + tool definitions don't change between turns. Mark them with cache_control: ephemeral and pay roughly 90% less for those bytes on every turn.
Concept: prompt-caching →Audit log every resolution
Every closed ticket writes a row: customer_id, agent_path, tool_calls, escalation_reason (if any), elapsed_ms, CSAT. This is your replay tool when production breaks at turn 18.
Concept: evaluation →9 decisions the exam turns into distractors
"any" or {type:"tool",name:"X"}
"auto" (default)
parse response text for 'done'
branch on field; max_tokens = partial
progressive summarization of customer_id
case-facts block + threaded messages
no caching
ephemeral on system + tools
Code checks response.text.includes('done') to decide termination.
Branch on stop_reason 'end_turn'. Text + tool_use can co-exist in one response.
System prompt says 'never refund more than $500'. Production sees 3% violations.
PreToolUse hook checks tool_input.amount <= 500. Deterministic gate.
Customer raises voice → agent escalates regardless of policy.
Sentiment is orthogonal. Trigger only on policy exception, ambiguity, or explicit request.
By turn 8, agent has summarized cus_42 → 'a customer wanting a refund'.
Pin CASE_FACTS block in system prompt. Re-read every turn. Never paraphrased.
Agent calls lookup_order first; pulls wrong record 12% of the time.
Programmatic prerequisite: verify_customer is called via tool description ordering. Hook can also enforce.
Cost & latency
8 avg turns × (system + tools + accumulating history). Cache hits ~70% on system+tools.
Pre-cache: ~$0.04. With ephemeral cache on system+tools: ~$0.018. ~55% reduction.
Streaming first token in ~150ms. Tool round-trips 1.5-2s each. 4 tool calls × 2s + 800ms compose.
5-min TTL on ephemeral. Continuous traffic keeps cache warm.
Check every gate before release
5 exam-pattern questions
Work through one question at a time, check the architecture, then move through the set.
Your refund agent uses prompt-only enforcement with the rule 'never refund over $500'. Production logs show 3% of refunds violate the policy. What is the architectural fix?
Frequently asked
Why a separate hook for refund cap instead of putting it in the system prompt?
Prompt-only enforcement is probabilistic. Claude follows the rule ~95-97% of the time, leaving 3-5% leakage. Hooks are deterministic, they read structured tool_input fields and exit 2 to deny. For policy-bearing limits (refunds, escalation thresholds), determinism is required. Use prompts for tone and behavior; use hooks for hard policy.
How many tools should the registry have?
4-5 is the optimum per Anthropic's customer-support guide. Fewer means the agent has to compose multiple low-level calls into one task. More degrades selection accuracy, overlapping descriptions cause the model to alternate. If you need >5, split into specialist agents (e.g., refund agent + tech agent + account agent) and route between them with a triage classifier.
Should the system prompt include few-shot examples of past conversations?
Sparingly. 1-2 high-quality examples can lock tone and tool-use pattern. More than 3 starts crowding the cache and dilutes attention. Better leverage: pin a clear tool registry with detailed descriptions + a sharp CASE_FACTS block. Examples are for edge-case behavior; descriptions are for routing.
What's the difference between sentiment escalation and policy escalation?
Policy escalation: the agent hits a structural condition that requires a human (refund > cap, identity unverifiable, ambiguous request). Triggered by hooks or explicit conditions. Sentiment escalation: the customer shows distress. Sentiment is *orthogonal*, distress alone never warrants escalation. Combine them only as a tie-breaker (low confidence + distress = escalate).
How do I handle a customer who switches topics mid-conversation?
Re-route through triage. If the new intent maps to a different specialist (e.g., refund → tech), don't try to handle it inline. Push the original case-facts to the new specialist's task string + spawn (or context-switch) a new agent. Trying to handle multi-intent in one specialist agent erodes accuracy and pollutes the case-facts block.
What's a good escalation queue SLA?
5-10 minutes for customer-blocking flows; 2-4 hours for batch flows (overnight refund reconciliation). Mark each escalation with intent + urgency from the triage stage; route customer-blocking ones to the live queue, batch ones to a daily review. The structured block format is the same; only the SLA differs.
Should I cache the message history across turns?
No. The message list grows monotonically, caching it has marginal value (each turn changes the cache key). Cache the system prompt + tool definitions instead, those are stable across turns and account for 60-80% of token cost on long conversations. ~5-min TTL on ephemeral cache is sufficient for live chat traffic.
When should I use a sub-agent instead of expanding this one?
When (a) the new flow is parallelizable (e.g., research a customer's order history while another agent handles billing), (b) the new flow needs different tool scope (read-only research vs write-capable refund), or (c) the new flow generates verbose intermediate work that pollutes the main case-facts block. Use sub-agents for isolation; use this agent for inline reasoning.
How do I prevent infinite loops?
stop_reason is the primary control, branch on it, never on text. Iteration cap (max_iter=15) is a safety net, not the primary control. If you hit the cap regularly, the bug is upstream: missing tool_result append, ambiguous tool descriptions, or two tools alternating. Raising the cap masks the real issue.
What should the audit log capture?
Per closed ticket: customer_id, order_id, full tool_call sequence (just names + timestamps), stop_reason per turn, elapsed_ms, escalation_reason (if any), csat (if surveyed). Skip the full transcript, the structured trace is enough to replay any failure. Store for 90 days minimum.
