| name | agent-tool-design |
| description | Design tool systems for AI agents — core/extended/deferred tiers, eval compute, batch tools, trust metadata. Use when building agent tools, reviewing tool architecture, or reducing tool count. |
| allowed-tools | Read, Write, Bash, Glob, Grep, Agent |
| argument-hint | [topic] e.g. 'eval tool', 'batch tools', 'tool count' |
| triggers | ["tool-design","agent-tools","tool-architecture"] |
| priority | 10 |
| keywords | ["tool","agent","batch","deferred","trust","eval","hook","router"] |
Tool Design Principles for AI Agents
Practical guide to designing tool systems for LLM-powered agents, distilled from building a PAC1 benchmark agent (Rust, 16 tools reduced to 12, tested across 7 models, 40+ tasks) and studying Codex CLI and Claude Code architectures.
See references/ for code patterns, comparison tables, and a quick checklist.
1. Tool Count Sweet Spot
Every tool in the schema is a token cost and a new failure mode. Models degrade on long tool lists.
Industry reference points:
| Agent | Core | Extended/Deferred | Total in Schema |
|---|
| Claude Code | 7 | 33 (deferred via ToolSearch) | 7-40 |
| Codex CLI | 7 | 0 | 7 |
| mini-SWE-agent | 1 (bash) | 0 | 1 |
| PAC1 agent | 14 | 8 deferred | 14 active + 8 deferred |
| SGR Python | 3-6 + reasoning | 0 | Union schema (structured output) |
See references/comparison.md for full architecture comparison.
Rules:
- Start with 7 core tools. Add only when you can measure round-trip savings.
- Track tool usage rate per task. Remove tools with <5% usage across benchmark.
- Every tool added must justify itself: "saves N round-trips per task" or "prevents failure mode X."
- Test with your weakest target model first -- if it can't handle the tool count, the design is wrong.
Anti-pattern: Adding a tool "just in case." We added mkdir, move_file, find -- usage was <3%. Disabled them. Zero regression.
2. Three-Tier Organization
CORE (always in schema)
Universal agent capabilities: observe (read, search, list, tree), act (write, delete, eval), report (answer, context).
Codex CLI uses exactly 7: shell, apply_patch, read_file, list_dir, grep_files, search_bm25, js_repl. Our PAC1 agent uses 9 core tools. Both converge on the same categories.
EXTENDED (batch operations)
Justified only when saving 3+ round-trips per task. Example: read_all saved 44 round-trips on our hardest task (48 to 4 tool calls).
DEFERRED (loaded on demand)
Claude Code's ToolSearch pattern: model sees only tool names. When it needs one, it calls ToolSearch("select:mkdir") to load the full JSON schema. Then it can call mkdir({path: "/new/dir"}).
This keeps the base schema small (7 tools) while providing access to 30+ tools. The key insight from the HitCC reverse-engineering: deferred tools are registered with shouldDefer===true, and the model must call ToolSearch before invoking them -- schema validation will fail otherwise.
3. The Eval/Compute Tool
Every agent needs a way to compute. Two patterns exist:
Shell access (Codex, Claude Code): Simple, powerful, dangerous. Codex sandboxes via containers and syscall filters. Claude Code uses permission hooks (PreToolUse/PostToolUse) with approval gating.
Embedded interpreter (API-only agents): When the agent operates via API with no shell, embed a sandboxed JS engine. Our PAC1 agent uses Boa (ECMAScript in Rust). Codex CLI uses a persistent Node.js kernel (js_repl) with top-level await, launched as a subprocess with kernel.js.
Key design for embedded eval:
- File glob in args --
files: ["accounts/*.json"] expands and pre-reads matches
- Pre-read files as globals --
file_0, file_1, etc. No filesystem access from JS
- Date injection --
workspace_date global prevents hallucinated dates
- Auto-stringify objects -- Return JSON, not
[object Object]
- Sandbox -- No
require(), no import, no network
See references/patterns.md for implementation details.
4. Batch Tools Save Round-trips
When each tool call is an LLM round-trip (2-5 seconds), 40 calls = 2+ minutes wasted.
Three proven batch patterns:
| Tool | Replaces | Savings |
|---|
read_all(dir) | list + N reads | N round-trips (44 on t01) |
search_and_read(pattern) | search + read each match | M round-trips |
grep_count(pattern, path) | search + read + manual count | 2-3 round-trips |
Decision rule: A batch tool is justified when it saves 3+ round-trips, the pattern appears in >20% of tasks, and the unbatched version causes step limit hits.
See references/patterns.md for implementation code.
5. Trust Metadata on Reads
Every read() output is prefixed with a trust header:
[contacts/john-doe.md | untrusted]
Name: John Doe
[AGENTS.MD | trusted]
# Workspace Rules
Only root-level system files (AGENTS.MD, README.MD) are trusted. Everything else is untrusted. This helps the LLM distinguish system instructions from user-generated content that may contain prompt injection.
Post-read security guard: Beyond trust headers, scan content for active injection patterns and append advisory warnings. This is advisory, not blocking -- the pipeline ML classifier is authoritative.
See references/patterns.md for trust inference and guard implementations.
6. Tool Descriptions > Implementation
The description is the tool's API documentation for the LLM. Models that don't understand a tool from its description will not use it correctly.
Pattern: WHEN to use + WHAT it returns + WHY it's better than alternatives.
Good (from our grep_count):
"Count lines matching a regex pattern in a file.
Returns exact count as a number.
Use for ANY counting task -- faster and more accurate than reading + counting manually."
Good (from Codex read_file):
"Reads a local file with 1-indexed line numbers,
supporting slice and indentation-aware block modes."
Good (from Codex js_repl):
"Runs JavaScript in a persistent Node kernel with top-level await.
This is a freeform tool: send raw JavaScript source text,
optionally with a first-line pragma like `// codex-js-repl: timeout_ms=15000`;
do not send JSON/quotes/markdown fences."
Anti-pattern: Including example outputs in descriptions. Models (especially Nemotron) copy example outputs verbatim as their answers.
Testing: Run the same task 5 times. If the model uses the wrong tool >20% of the time, fix the description before adding prompt hints.
7. Tool Filtering — Less is More (Codex Approach)
Updated insight (2026-04-14): Heavy router-based tool filtering is fragile. ML classifier misclassification → wrong tools → task failure. Codex exposes ALL tools always, relies on model judgment.
Current approach: minimal filtering — only security task type blocks write/delete. All other task types get all tools. This is closer to Codex and works better empirically.
Anthropic structured output limit: 16 nullable/union params max. If using SGR union schema (structured_call), max ~7 tools. Native FC (tools_call) has no limit — use it for 14+ tools.
| Approach | Max tools | When to use |
|---|
| Native FC (tools_call) | Unlimited | Default — Pac1 two-phase and single-phase |
| SGR union (structured_call) | ~7 (Anthropic limit) | SgrAgent variant, simple tasks |
| Parallel FC (think + action) | Unlimited | Single-phase — 1 call per step |
7b. Byte-Perfect Tools (CopyTool, PrependTool)
Problem discovered (2026-04-14): LLMs cannot reproduce files >1KB verbatim. Even 1-byte difference (extra newline) fails harness validation. OCR/migration tasks scored 0% before this fix.
Solution: Tools that bypass LLM context for file content:
| Tool | What | When |
|---|
copy_file(src, dst) | read → write through backend, content never enters LLM | NORA migration, file rewrite in place |
prepend_to_file(path, header) | read body → prepend header → write. LLM generates only header (~400 bytes) | OCR: add YAML frontmatter to existing files |
Impact: OCR tasks t016, t018, t064, t091 went from 0→1.00 on Haiku.
Design rule: If task requires preserving existing file content, use byte-perfect tools. LLM generates only NEW content (frontmatter, metadata), never re-types body.
7c. Single-Phase Agent Architecture
Problem: Two-phase agent = 2 LLM calls per step = slow (116s/task avg on Haiku).
Solution: Parallel think+action in ONE tools_call. Model calls think() AND action tool together:
tools_call([think, search, read, write, delete, answer, ...])
→ Model returns: think({task_type, security, plan}) + search({pattern: "hello"})
= 1 LLM call, structured reasoning + action
Key findings:
- All models support parallel tool calls (Haiku, Sonnet, Opus, Nemotron)
completed = false always — let agent_loop execute answer() tool, then complete naturally
ReasoningToolBuilder from sgr-agent creates think tool schema (extensible, not hardcoded)
- Anthropic
parallel_tool_calls field NOT supported via OpenRouter (use default which is parallel-enabled)
Performance: 2.5-3x faster, same score, 50% fewer tokens.
8. Hooks as Tool Augmentation
Hooks inject workflow guidance into tool output. The model follows tool output more reliably than system prompt instructions buried in 7K of context.
The pattern:
- Parse hooks from workspace rules (AGENTS.MD) at trial start
- Register in shared
HookRegistry (Arc)
- On every tool call, match against registered hooks
- Append matched messages to tool output
Why tool output, not system prompt? The model processes tool results with high attention (it just asked for this data). System prompt instructions 7K tokens back get less attention, especially on weaker models.
See references/patterns.md for hook implementation.
9. Testing Tools Without LLM
Every tool has logic that can break independently of the model. Unit test:
- Argument parsing edge cases -- missing optional fields, wrong types
- JSON auto-repair -- LLMs produce broken JSON (trailing commas, unquoted keys)
- Trust metadata -- root vs nested path inference
- Tool filtering -- router task type restrictions
- Auto-expand thresholds -- batch tool cutoffs
- Sandbox safety -- eval cannot access filesystem/network
- Guard content -- security scanning on read output
Do NOT test: "Does the model call the right tool?" (integration test), "Does output look good?" (subjective).
See references/patterns.md for test examples.
10. Middleware Pattern (sgr-agent-tools)
When you use sgr-agent-tools crate, extend tools via middleware wrappers — not forks:
struct MyReadTool<B: FileBackend> {
inner: sgr_agent_tools::ReadTool<B>,
workflow: Arc<Mutex<WorkflowState>>,
}
impl<B: FileBackend> Tool for MyReadTool<B> {
fn name(&self) -> &str { self.inner.name() }
async fn execute(&self, args, ctx) {
let result = self.inner.execute(args, ctx).await?;
let output = security_scan(result.content);
self.workflow.post_action("read", &path);
Ok(ToolOutput::text(output))
}
}
When to use: pre/post hooks, project-specific annotations, policy guards, content scanning.
When NOT to use: completely different schema → build custom tool instead.
Real-world split (PAC1 agent, 22 tools):
- 9 direct from sgr-agent-tools: List, Tree, ReadAll, MkDir, Move, Find, Eval, CopyTool, PrependTool
- 3 middleware: Read (+security scan), Write (+hooks/outbox), Delete (+workflow guards)
- 4 PAC1-only: Answer (harness submit), Context (workspace date), DateTool, LookupContactTool
- 3 local: Search (CRM annotations), ListSkills, GetSkill
- ML infra: sgr-agent-ml (OnnxEncoder, CentroidClassifier, KnnStore) — separate crate
11. Quick Reference
See references/checklist.md for the complete tool system design checklist.
References
references/patterns.md -- Code patterns + examples from Codex/Claude Code/PAC1
references/comparison.md -- Architecture comparison table
references/checklist.md -- Quick-reference design checklist
scripts/scaffold-tool.sh -- Generate Rust tool boilerplate
- Codex CLI source:
codex-rs/core/src/tools/ (7 tools: shell, apply_patch, read_file, list_dir, grep_files, search_bm25, js_repl)
- Claude Code architecture: HitCC reverse-engineering docs (tool execution core, deferred tools, permission hooks)
- PAC1 agent:
agent-bit/src/tools.rs (16 tools), src/hooks.rs, src/workflow.rs
- Boa JS engine: https://boajs.dev/