Progressive Disclosure: Give the Agent Tools As It Needs Them
The tool list is a per-call parameter, not a fixed property. Withhold the write tools until acting is authorized and a compromised context has nothing dangerous to reach for — enforcement, not instruction.
The default is a fixed tool list: every tool the agent might need, offered on every turn. It's simple, and it degrades in two ways as the system grows. Selection accuracy falls as the list lengthens, and every dangerous tool is reachable at every moment regardless of whether the run has any business using it.
Varying the tool list by phase fixes both, and it's a change in your loop rather than in your prompts.
The mechanism
The tool list is a parameter of each model call, so it can differ per turn:
tools = tools_for(phase, run_context)
reply = model(messages, tools=tools)
phase can come from an explicit state machine, from what the agent has done so far, or from a lightweight classification at run start. The list is assembled by your code, which means the agent cannot expand its own capability.
Three ways to phase it
By workflow stage. An investigate-then-act agent holds read tools during investigation and gains write tools only after a policy check or an approval. → The most valuable version: most of a run's duration doesn't need its most dangerous permission, and this makes that literal rather than aspirational.
By intent. After classifying the request, offer only that route's tools. A billing question doesn't need deployment tools in context.
By progressive discovery. Start with a small set including one that reveals more:
list_available_data_sources() -> ["orders", "shipping", "billing"]
# calling this adds query_orders, query_shipping, query_billing to the list
Useful when the full surface is genuinely large — the agent navigates to what it needs instead of reading everything up front.
What it buys
Better selection. Fewer candidates, less overlap, fewer near-miss choices.
Smaller context. Tool specs are tokens, re-sent every turn. A large surface trimmed to a relevant subset is a real saving on every call.
A structural safety boundary. A tool that isn't offered cannot be called, regardless of what the context contains. This is the strongest available answer to injection targeting a specific action — and it's enforcement rather than instruction.
Clearer traces. Knowing which phase a run was in when it made a choice makes debugging faster.
The costs
Cache invalidation. Tool definitions usually sit in the cached prefix. Changing them mid-run invalidates it from that point. ⚠️ Mitigate by changing the list at a small number of phase boundaries rather than continuously — a stable list within a phase preserves caching where it matters.
A wrong phase strands the agent. If it needs a tool that isn't offered, it can't proceed and may not be able to say why. Always include a way out: request_tool(name, reason) that either grants it under policy or escalates. Without that, phase misjudgments become silent failures.
More moving parts. Phase transitions are logic that can be wrong, and they need testing.
✅ Practical guidance
- Keep a stable core. Tools needed in every phase stay always-available, so only the variable part changes.
- Transition at few, clear boundaries — ideally driven by an explicit state rather than inferred each turn.
- Log the offered list per turn. Debugging "why didn't it call X" needs to answer "was X offered?" first, and that question is otherwise unanswerable.
- Test the stranded case. A run that needs an unoffered tool should escalate cleanly, and there should be a test proving it.
- Start with two phases. Read-only, then acting. That captures most of the benefit; finer phasing has diminishing returns and rising complexity.
🔍 When to bother
Worth it when: your tool list exceeds roughly a dozen; you have destructive tools that only matter in one phase; selection errors are a measured problem; or your tool specs are a significant share of context.
Not worth it when: you have five tools and they're all read-only. The complexity buys nothing, and a fixed list is one less thing to be wrong.
The takeaway
The tool list is a per-call parameter, not a property of the agent. Vary it by phase — read-only until acting is authorized, route-specific after classification, discovered progressively when the surface is large. Keep transitions few so caching survives, always provide an escape for a mis-phased run, and log what was offered on every turn. Two phases and an escape hatch capture most of the benefit for a small amount of code.