What this system is, and its 5 parts
Think of this as designing the toolbox before the agent picks it up. What tools to give, how to describe them, what guard-rails to put around them. The trap is to give the agent fifteen tools and hope; the discipline is to give it four or five really well-described tools, write each description in the same four-line pattern (what / when / edge cases / ordering), wrap risky calls in PreToolUse hooks, normalize the results in PostToolUse hooks, and label every error with one of four buckets so retry logic can reason about it. The whole point is that tool design IS agent design. Get the toolbox right and the rest of the agent works.
Tool Registry (4-5 Tools)
the optimum, not the maximumThe agent's toolbox. Empirically, 4-5 tools is the routing-accuracy sweet spot; past that, accuracy drops ~8% per added tool because descriptions overlap and the model alternates. If the use case needs more tools, split into specialist sub-agents. Each with its own 4-5-tool registry. And route between them with a triage classifier.
ConfigurationCap: 4-5 tools. Beyond: split agents. Don't cram a customer-support tool, a refund tool, a sentiment tool, and 11 admin tools into one agent. That's three agents pretending to be one.Concept: tool-calling →
4-Line Description Pattern
what · when · edge cases · orderingThe canonical tool description shape. Line 1: what the tool does. Line 2: when to call it. Line 3: edge cases (returns, failure modes). Line 4: ordering (which tools must come before / after). This pattern makes routing structural. The model reads the pattern in every description and learns the shape. Vague one-liners produce ~12% wrong-tool selection; the 4-line pattern drops it below 3%.
Configurationdescription: "Look up a customer by customer_id and confirm they are active.\nUse this BEFORE any other tool that mentions the customer.\nEdge cases: returns 'not_found' if customer_id is missing.\nAlways run before lookup_order or process_refund."Concept: tool-calling →
PreToolUse Hook (Policy Gate)
deterministic, before execSits between the model's tool_use request and the actual tool execution. Reads tool_input (e.g., refund_amount), compares to policy (amount <= cap), exits 0 (allow) or 2 (deny with stderr message). Deny routes the model back with the policy reason, and the agent re-plans. The single-most-effective lever for converting probabilistic prompt-only policies into 100%-deterministic gates.
Configurationmatcher: "process_refund". Hook reads stdin JSON: {tool_name, tool_input}. Exits 0 to allow, exits 2 with stderr to deny. SDK forwards stderr back to the model as a tool_result with is_error=true.Concept: hooks →PostToolUse Hook (Normalization + Audit)
after exec, before next turnFires AFTER the tool runs but BEFORE the result is fed to the model. Normalizes raw outputs (timestamps to ISO-8601, status codes to enum names, field renames), captures side-effect signals, and writes the canonical audit log entry. Without it, the model sees inconsistent output shapes across calls; with it, every call has a predictable contract and an audit trail.
Configurationmatcher: '*'. Hook reads stdin: {tool_name, tool_input, tool_result}. Transforms tool_result into normalized shape. Writes audit row to durable log. Always exits 0 (does not deny. That's PreToolUse's job).Concept: hooks →4-Bucket Structured Error Contract
Transient · Permission · Data · BusinessEvery tool that can fail emits an error in one of four explicit buckets, not a free-form string. The harness reads is_error: true + error.bucket, then routes: Transient → retry; Permission → escalate (don't retry, won't fix itself); Data → surface to user; Business → block + log + escalate. Without this contract, the agent retries permission errors forever and surfaces transient ones as catastrophes.
Configurationtool_result on failure: { is_error: true, content: { bucket: "Transient"|"Permission"|"Data"|"Business", code, detail, retryable: bool } }. Agent reads bucket and retryable; never branches on detail text.Concept: structured-outputs →The problem
- A tool registry the agent routes accurately. The right tool fires on the first try ≥ 95% of the time.
- Risky operations gated structurally. Refund cap, destructive Bash, write access policed by hooks not prompts.
- Errors that the agent can reason about. A permission-denied looks different from a transient timeout, and the agent retries accordingly.
- 15-tool registry → routing accuracy drops 8% per tool past 5; the agent alternates and misses obvious matches.
- Vague one-line tool descriptions → agent picks the wrong tool ~12% of the time.
- Prompt-only policy enforcement ('never refund > $500') → leaks 3-5% in production despite emphatic phrasing.
- ✓ Tool count per agent ≤ 5; rare tools moved to specialist sub-agents
- ✓ Every tool description follows the 4-line pattern (what / when / edge cases / ordering)
- ✓ PreToolUse hook gates every policy-bearing tool; exit 2 on violation
- ✓ PostToolUse hook normalizes outputs and logs every call to the audit trail
- ✓ Tool errors emit one of the 4 structured buckets (Transient · Permission · Data · Business)
- ✓ MCP servers used for cross-agent tool sharing. No inline duplication
One run, traced end to end
8 steps to production
Cap the registry at 4-5 tools (split otherwise)
Audit your current tool list. Past 5, you're guaranteed losing routing accuracy. The fix is structural: identify the use cases that actually share state vs those that don't, then split into specialist agents with their own 4-5-tool registries. Use a triage classifier (or a top-level coordinator agent) to route the user request to the right specialist.
Concept: tool-calling →# AUDIT: count + classify tools
SUPPORT_TOOLS = ["verify_customer", "lookup_order", "process_refund",
"escalate_to_human", "audit_log"] # 5. At the optimum
ADMIN_TOOLS = ["create_user", "delete_user", "reset_password",
"lock_account", "unlock_account", "audit_admin"] # 6. Split
# WRONG: cram all 11 into one agent
# tools = SUPPORT_TOOLS + ADMIN_TOOLS # 11. Routing accuracy drops ~32%
# RIGHT: two specialist agents, triage routes between them
def triage(user_request: str) -> str:
"""Tiny classifier. Pick the specialist agent."""
if any(w in user_request.lower() for w in ["refund", "order", "ticket"]):
return "support"
if any(w in user_request.lower() for w in ["password", "account", "user"]):
return "admin"
return "support" # default
def route(user_request: str) -> dict:
specialist = triage(user_request)
tools = SUPPORT_TOOLS if specialist == "support" else ADMIN_TOOLS
return run_agent(tools=tools, message=user_request)Write every tool description in the 4-line pattern
Line 1: what (one sentence). Line 2: when (which user intent triggers this). Line 3: edge cases (what happens on failure, missing args). Line 4: ordering (which tools must come before / after). This pattern is the model's structural cue. It reads the pattern across all 5 tool descriptions and routes accordingly. Vague one-liners produce ~12% wrong-tool selection; this pattern drops it below 3%.
Concept: tool-calling →Wire the PreToolUse hook on policy-bearing tools
For every tool that touches money, identity, or destructive state, the PreToolUse hook is the architectural gate. It reads tool_input from stdin JSON, applies the policy check in code (not in a prompt), and exits 0 or 2. Exit 2's stderr message is fed back to the model as a tool_result with is_error: true. The model re-plans with the policy in view.
Concept: hooks →Wire the PostToolUse hook for normalization + audit
PostToolUse fires AFTER the tool runs, BEFORE the model sees the result. Two jobs: normalize the output shape (timestamps to ISO-8601, status codes to enum names, ms to seconds, etc.) so the model sees a consistent contract across calls; and write a canonical audit row capturing tool_name, tool_input, normalized_output, latency, and stop_reason context. The audit log is the replay tool when production breaks at turn 18.
Concept: hooks →Emit errors in 4 structured buckets
Every tool that can fail returns an error tagged with one of four buckets: Transient (network blip, retry), Permission (403/401, escalate. Don't retry, won't fix itself), Data (input malformed, surface to user), Business (policy violation, log + escalate). The agent reads bucket and retryable, and routes accordingly. Without this contract, the agent retries permission errors forever and surfaces transient blips as catastrophes.
Concept: structured-outputs →Use tool_choice 'auto' for specialists; 'forced' only for mandatory extraction
tool_choice: 'auto' is the right default. The model decides whether to call any tool, and which one, based on the request. tool_choice: 'any' forces the model to call SOME tool (rarely useful). tool_choice: { type: 'tool', name: ... } forces a specific tool. Only correct for extraction pipelines where the tool is mandatory. Forced tool_choice on a conversational specialist agent removes the agent's reasoning capacity.
Concept: tool-choice →Share tools across agents via MCP servers
When two agents both need lookup_order or verify_customer, don't duplicate the tool inline. Expose it through an MCP server. Each agent connects to the MCP server, advertises it as a tool, and gets a single source of truth. Updating the tool's behavior is a single deploy; the agents pick it up automatically. MCP also abstracts auth, observability, and rate-limiting away from each agent.
Concept: mcp →Test routing accuracy with a 50-intent eval set
Tool design is empirical. Build a 50-intent eval set where each intent has a known correct tool. Run the agent over it, count first-call accuracy. Below 95% routing accuracy means the descriptions need work; below 90% likely means too many tools. Re-run the eval after every tool addition or description tweak.
Concept: evaluation →9 decisions the exam turns into distractors
15 tools 'because the model is smart enough'
4-5 tools (the optimum); split into specialist sub-agents past 5
One vague sentence ('verifies a customer')
4-line pattern: what / when / edge cases / ordering
System prompt: 'never refund more than $500'
PreToolUse hook reads tool_input.amount, exits 2 on violation
Free-form error messages parsed by the agent
4 buckets (Transient · Permission · Data · Business) + retryable boolean
15-tool agent with overlapping descriptions. Routing accuracy at first-call drops to ~65%; the agent alternates between similar tools, sometimes calls 3 tools before settling on the right one. Latency up; cost up; quality down.
Cap at 4-5 tools per agent. Move rare tools (used <10% of conversations) to specialist sub-agents. Use a triage classifier to route requests to the right sub-agent. Each sub-agent stays at 4-5 tools.
Tool descriptions are one-liners ('verifies the customer', 'looks up an order'). Agent misroutes ~12% of calls because it can't tell when each tool applies.
Anthropic 4-line pattern: what / when / edge cases / ordering. Each line targets a specific routing decision the model has to make. Wrong-tool rate drops below 3%.
Refund cap enforced via system-prompt language. Production logs show 3-5% of refunds violate the cap. Audit fails; finance rolls back; trust in the agent drops.
PreToolUse hook reads tool_input.amount, compares to policy, exits 2 with stderr message on violation. Deterministic, not probabilistic. Policy violations drop to 0.
Tool returns 403; agent retries indefinitely. Tool returns 500; agent crashes. Tool returns 400 with malformed input; agent gives up without surfacing the input issue to the user.
4-bucket structured error contract (Transient · Permission · Data · Business) with retryable boolean. Agent reads bucket, branches: Transient → retry with backoff; Permission → escalate; Data → surface to user; Business → block + log + escalate.
Agent calls lookup_order before verify_customer. Wrong record returned 12% of the time because the customer_id wasn't validated first. Bad data pollutes downstream decisions.
Tool descriptions explicitly state ordering ('Always run BEFORE process_refund'; 'Use ONLY after verify_customer has confirmed the customer is active'). The 4-line pattern's last line is for ordering precisely because ordering is so often the routing failure.
Cost & latency
Hooks run as subprocesses reading stdin JSON. No LLM call. Pure local Python/TS. The latency is below the noise floor of a typical tool API call.
50 messages × ~500 tokens input + ~50 tokens output at Sonnet 4.5 prices. Cheap insurance against routing regressions; run on every tool registry change and weekly in CI.
Tools array is stable; mark with cache_control: ephemeral. Schema-cache hit rate ≥ 70% drops effective per-call cost ~90% on the tools array.
MCP runs as a separate process or service; network round-trip adds latency. Worth the cost when 2+ agents share the tool. Single source of truth beats inline duplication.
Append-only JSONL write in the PostToolUse hook. At 1000 calls/day, 1MB/day, 30MB/month. Negligible storage. Indispensable for production debugging.
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 agent has 6 tools. Routing accuracy drops from 95 percent (with 5 tools) to 87 percent (with 6). What is the cause and the architectural fix?
Frequently asked
Why does the 4-line description pattern matter so much?
It gives the model structural cues across the registry. Each tool has the same 4 lines in the same order. After reading 5 such descriptions, the model has a stable mental model: 'when I want to know WHAT, I read line 1; WHEN, line 2; ORDERING, line 4'. Vague one-liners force the model to infer shape every time. The pattern cuts wrong-tool selection from ~12% to <3%.
Can I use the 4-line pattern in MCP tool descriptions?
Yes. Same pattern, same effect. MCP tools surface to the agent through the same tools[] array as inline tools; the description format is identical. If you ship an MCP server, write the 4-line pattern into the server's tool definitions. Downstream agents inherit the routing accuracy without doing anything.
What if the policy is too complex for a hook?
Then the hook calls a policy service. PreToolUse hooks are subprocesses, not pure functions. They can hit a Convex action, a feature flag service, a rules engine. The point is the gate is OUTSIDE the prompt: deterministic code makes the deny decision, not the model. Complex policies live in the service the hook calls; simple bounds live in the hook itself.
How do I add a new tool to the registry without breaking routing?
Three steps: (1) add the tool with a full 4-line description; (2) re-run the 50-intent routing-accuracy eval. If accuracy drops below 95%, the new tool's description overlaps with an existing one, fix the descriptions; (3) gate the deploy on the eval threshold. Adding tools blindly is the #1 way registries degrade.
Should every tool emit the 4-bucket error contract?
Every tool that can fail. Read-only lookups can mostly emit Transient or Data; write tools add Business; auth-protected tools add Permission. The contract is uniform: { is_error: true, error: { bucket, code, detail, retryable } }. The agent's retry logic relies on it; without uniformity, you'd write per-tool retry code and miss bugs.
When do I split into specialist sub-agents vs add tools to the existing one?
At 5 tools is the rule of thumb. Tools that share state (e.g. verify_customer + lookup_order + process_refund) can stay in one agent. Tools that don't share state (admin functions vs support functions vs analytics) belong in different agents. Split, route with a triage classifier, each agent stays at 4-5 tools.
Is the 4-bucket model Anthropic's or community-derived?
Community-derived but architecturally consistent with Anthropic's tool-use guidance. The buckets formalize the patterns Anthropic's docs hint at (transient retry; permission escalate; etc.). The scenario catalog marks this scenario as 🟡 OP-claimed (Reddit thread 1s34iyl) but architecturally well-grounded. Drilling it benefits real exam prep.
How does this scenario compose with MCP server security?
Tightly. When tools are exposed via MCP rather than inline, the 4-line description pattern, the PreToolUse policy gate, and the 4-bucket structured-error contract all live on the MCP server side. The MCP-security checklist (secrets via ${ENV_VAR}, every parameter untrusted, binary allowlist, HTTPS-only transport, audit-log every tool_use) is the operational hardening; the patterns on this page are the design contract. Cross-link: P3.4 (developer-productivity-agent) FAQ covers the MCP-security checklist in depth. Pair both pages when designing a new MCP server. Tagged related: mcp-security cluster.
