What is Progressive Disclosure?
Progressive Disclosure is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Show the model only a summary of each skill and load the details when it asks.
Revelação Progressiva
Progressive Disclosure is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Show the model only a summary of each skill and load the details when it asks.
Show the model only a short summary of each available skill, tool, or document, and load the full details when it asks. It is for agents with many capabilities, where loading everything on every call fills the context and muddles the choice.
A book’s table of contents: you read the chapter titles and open only what interests you.
Gabriel maintains a back-office agent that can do many things: reconcile statements, issue invoices (including Brazil’s NF-e electronic invoice), generate commission reports, open tickets with carriers, update supplier records. Each capability has detailed instructions (step by step, exceptions, formats), examples, and its own tools.
With everything in the prompt, each task’s initial context exceeds 40,000 tokens. Most of it is irrelevant to the task at hand. The agent mixes up reconciliation rules with invoicing rules, and every call is expensive before it does anything.
That shows up in the trace as Prompt Bloat: pages of procedure that will not be used. It also shows up as the wrong tool pick, a sibling of Tool Sprawl.
A router that picks the group at the start helps, but some tasks discover midway that they need another capability. “Reconcile the card charges and, if an invoice is missing, issue the NF-e” starts in one topic and ends in another.
The question this pattern answers: how does an agent have access to many capabilities without carrying all of them in its context all the time?
Organize capabilities in two levels:
The agent receives the index and a tool to load the detail (load_skill(name)). When it decides it needs a capability, it loads it and follows it. Details can have more levels: the main file points to reference files, loaded only if needed.
The same applies to tools: the initial list carries names and short descriptions, and full definitions load on demand. Some providers offer this natively, with tool search.
The index Gabriel puts in the system prompt fits on one screen:
Skills (load with load_skill before you run them):
- card-reconciliation: use to match card receipts to the statement and settle the receivable.
- invoice-issuing: use to issue, cancel, or look up NF-e invoices.
- commission: use to calculate and export salesperson commission.
- logistics: use to open a carrier ticket or look up tracking.
The task arrives: “Reconcile yesterday’s card receipts”. The agent calls load_skill("card-reconciliation") and only then sees the step by step, the acquirer’s occurrence codes, and the settle_receivable tool. The invoice skill never enters.
An illustrative account. Twelve skills at 3,000 tokens each add up to 36,000 tokens on every turn. The twelve-line index takes about 400 tokens. Loading one skill adds 3,000 once. On a six-turn loop, you go from about 216,000 tokens of instruction to about 22,000. The price is one extra turn, the load_skill turn.
The short description is the contract. If every line says “useful in back office”, the agent loads all twelve. If each line states the task’s verb (“match card receipts”), it picks right.
flowchart TD
T(["1. Task"]):::io --> A(["2. Agent with the index"]):::llm
IDX[("Index: name and when to use")]:::store --> A
A -->|"I need reconciliation"| L["3. load_skill"]:::tool
SK[("Full skills")]:::store --> L
L --> A
A --> TL["4. The skill's tools"]:::tool
TL --> A
A --> OUT(["5. Result"]):::io
schema SkillIndexEntry { name: string, when_to_use: string }
INDEX = skills.index() # a few lines per skill
tool load_skill(name) -> { instructions: string, tools: list[Tool] }
function run(task):
state.system = BASE_PROMPT + render(INDEX) # 2
tools = [load_skill]
while budget.check():
reply = llm_with_tools(state.system, tools, state)
if reply.is_final: return reply.text # 5
for call in reply.tool_calls:
if call.name == "load_skill":
skill = skills.load(call.args.name) # 3
state.add(observation(call, skill.instructions))
tools += skill.tools # 4
trace.span("skill_loaded", call.args.name)
else:
state.add(execute(call))
| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Context | Small at the start, grows only with what is loaded | input tokens on the first step |
| Calls | One extra turn to load each skill | skill_loaded spans |
| Cache | Loading appends at the end; the prefix stays cached | stable cache_read_input_tokens |
| Failure: skill not loaded | The agent tries without the instructions | a capability’s typical error with no prior skill_loaded |
| Failure: loads everything | Vague descriptions lead the agent to load several | high skills per task |
| Failure: stale index | A new skill with no index description | a capability that is never used |
{ "task": "Reconcile yesterday's card receipts", "expected_skills": ["card-reconciliation"], "unexpected_skills": ["invoice-issuing"] }
Include tasks that cross skills on purpose (“reconcile and, if needed, issue the invoice”). The right agent loads the second one midway, not both on the first step.
| Pros | Cons |
|---|---|
| A small, cheap initial context | One extra turn per load |
| Many capabilities without confusion | Depends on good index descriptions |
| Skills versioned and testable separately | The agent may not load when it should |
| A stable prefix for the cache | More structure to organize |
The Python tab builds the skill index from files and gives the agent a tool to load the details; the TypeScript tab uses the provider’s tool search for large tool sets.
Python. Use this when skills live in folders in the repository, each with a main file.
from pathlib import Path
import yaml
SKILLS_DIR = Path("skills") # skills/card-reconciliation/SKILL.md, skills/invoice-issuing/SKILL.md, ...
def read_skill(path: Path) -> tuple[dict, str]:
raw = path.read_text(encoding="utf-8")
_, front, body = raw.split("---", 2) # YAML header with name and when_to_use
return yaml.safe_load(front), body.strip()
def skill_index() -> str: # 2. only name and when to use
lines = []
for f in sorted(SKILLS_DIR.glob("*/SKILL.md")):
meta, _ = read_skill(f)
lines.append(f"- {meta['name']}: {meta['when_to_use']}")
return "Available skills (load with load_skill before using):\n" + "\n".join(lines)
LOAD_SKILL = {
"name": "load_skill",
"description": "Loads the full instructions of a skill from the index. Use it before doing that skill's task.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]},
}
def load_skill(name: str) -> str: # 3
path = SKILLS_DIR / name / "SKILL.md"
if not path.exists():
return f"Skill '{name}' does not exist. Check the index."
_, body = read_skill(path)
return body # the skill's tools are added to the list by the loop
SYSTEM = "You are the back-office agent.\n\n" + skill_index()What you gained: skills as versioned files, with the index generated automatically. What you lost: adding the skill’s tools to the list is up to your loop.
TypeScript. Use this when there are many tool definitions and you want the provider to do the search.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Dozens of tools: only the search tool is visible from the start;
// the rest load when the model finds them through search.
const tools = [
{ type: "tool_search_tool_bm25_20251119", name: "tool_search_tool_bm25" },
...ALL_BACKOFFICE_TOOLS.map((t) => ({ ...t, defer_loading: true })),
];
export async function step(messages: Anthropic.MessageParam[]) {
return client.messages.create({
model: "claude-opus-5-5",
max_tokens: 8000,
system: "You are the back-office agent. Find the right tool before acting.",
tools: tools as Anthropic.Messages.ToolUnion[],
messages,
});
}What you gained: the provider handles searching and loading definitions. What you lost: search covers only tools; long per-skill instructions still need the file scheme.
Progressive Disclosure is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Show the model only a summary of each skill and load the details when it asks.
Tool Sprawl and Unused Tools.
In the real-world scenarios Coding agent and Back-office automation with durable execution.