Module 7 · Tools, Skills & Plugins ⏱ 17 min

Skills and Progressive Disclosure

By the end of this lesson you will be able to:
  • Model a skill as three parts — a short description, longer instructions, and tool references — that stay off-stage until they are needed
  • Implement progressive disclosure: a listSkills step that returns cheap cards every turn and a loadSkill step that injects one full payload on demand
  • Add the two guards that keep the design honest: load each skill at most once, and cap how many a single turn can load

A model that can do anything is a model that does nothing well. Wire up a hundred tools and a shelf of how-to documents, dump them all into the prompt on every turn, and three things break at once: you pay for tokens the conversation does not need, the model's attention spreads thin across options it will never use, and the one instruction that mattered drowns in irrelevant noise. This is context pollution, and it worsens as you add capabilities.

A skill is the antidote. It bundles a capability — its instructions, the tools it needs, and a short description of when to reach for it — into one unit that stays off-stage until the conversation calls for it. The trick is progressive disclosure: keep a cheap card for every skill in context, and load a skill's full payload only when it is selected.

flowchart LR
  C["context window"] --> T1["skill cards<br/>always loaded<br/>cheap"]
  T1 -.->|"model picks one"| P["skill payload<br/>instructions + tools<br/>loaded on demand"]
  style T1 fill:#166534,color:#fff
  style P fill:#7c3aed,color:#fff
Two tiers: cheap skill cards stay in context; a full payload loads only when the model picks that skill.

What a skill contains

Three parts make up a skill. The metadata is a name plus a one-sentence description of when to use it — the only text that lives in context permanently, so it earns its words. The instructions are a longer prose block, a fragment of system prompt that teaches the model how to carry out the skill once it is chosen. The tools are the concrete actions the skill can perform, referenced by id and resolved to full schemas only on load.

The point is that the second and third parts are large and the first is tiny. A skill that triages bug reports might carry forty lines of instructions and three tool schemas — a thousand tokens — but its card is a single line. Progressive disclosure is the discipline of paying for the card always and the payload only when needed.

A skills registry and a listSkills handler that returns only the cheap cards. Press Run.
const skills = {
  triage: {
    description: 'Triage incoming bug reports into priority buckets',
    instructions: 'Read each report, label it P0 to P3 by severity and scope...',
    toolIds: ['search_issues', 'post_comment']
  },
  summarize: {
    description: 'Summarise a long document into bullet points',
    instructions: 'Produce at most five bullets, ordered by importance...',
    toolIds: ['read_doc']
  }
};

function listSkills() {
  return Object.keys(skills).map(function (id) {
    return { id: id, description: skills[id].description };
  });
}

console.log(listSkills().length);
console.log(listSkills()[0].id + ': ' + listSkills()[0].description);

The description does real work

Because the card is the only part of a skill the model ever sees before choosing, the quality of that one line decides whether the skill gets used at all. A vague entry like helps with stuff describes nothing the model can match against a task, so the skill sits unused no matter how good its instructions are. A precise entry — triage incoming bug reports into priority buckets — names the trigger and the output, so the model reaches for it the moment a user mentions bug reports.

Contrast a skill with a plain tool. A tool is one action the model can call; a skill is a bundle of instructions plus the tools that action needs, loaded together. You reach for a skill when a task needs coaching, not just a single function.

The two-step mechanism

Progressive disclosure is a loop with two steps. First, on every turn, the host calls listSkills() and shows the model only the cards — name and description, nothing more. The model reads those cards and decides whether any skill fits the current task. Second, when a skill is chosen, the host calls loadSkill(id), which returns the full payload — the instructions and the resolved tool schemas — and injects just that one into context for the turn.

Nothing about the other skills ever enters the prompt. A catalogue of fifty skills costs fifty lines on every turn; only the one or two that fire cost their full weight, and only for the turns they fire on. That is the whole reason the structure exists.

flowchart TD
  T["every turn"] --> L["listSkills<br/>returns cards only"]
  L --> M["model reads cards<br/>picks a skill or none"]
  M -->|"picks triage"| LOAD["loadSkill<br/>instructions + resolved tools"]
  LOAD --> CTX["one payload added to context"]
  style L fill:#1d4ed8,color:#fff
  style LOAD fill:#7c3aed,color:#fff
listSkills returns cards every turn; loadSkill resolves and injects one full payload only when a skill is chosen.
loadSkill returns the instructions and resolves tool ids to full schemas. Press Run.
const tools = {
  search_issues: { name: 'search_issues', inputSchema: { type: 'object' } },
  post_comment: { name: 'post_comment', inputSchema: { type: 'object' } },
  read_doc: { name: 'read_doc', inputSchema: { type: 'object' } }
};
const skills = {
  triage: { description: 'Triage bug reports', instructions: 'Label by severity...', toolIds: ['search_issues', 'post_comment'] }
};

function loadSkill(id) {
  const s = skills[id];
  return {
    instructions: s.instructions,
    tools: s.toolIds.map(function (tid) { return tools[tid]; })
  };
}

const payload = loadSkill('triage');
console.log(payload.tools.length);
console.log(payload.tools[0].name);

Why a budget caps the payload

Even with on-demand loading, a turn can select several skills at once, and each payload is large. Without a limit the model can cheerfully load six skills, blow past the context window, and push the user's actual question off the page. A token budget fixes this: estimate the cost of each candidate payload, add skills in priority order, and stop before the running total exceeds the budget.

The estimate need not be exact — a rough token count is enough to make the decision, because you are choosing whether to include a skill, not grading it. The discipline is what matters: treat context as a finite resource, measure what you put into it, and prefer the cheapest skill that covers the task.

A budgeted loader: include matching skills until the token budget would be exceeded. Press Run.
const skills = [
  { id: 'triage', description: 'Triage bug reports by severity', tokens: 600 },
  { id: 'search', description: 'Search the codebase for symbols', tokens: 400 },
  { id: 'docs', description: 'Search the documentation site', tokens: 500 }
];

function loadWithinBudget(all, keywords, budget) {
  const kw = keywords.map(function (k) { return k.toLowerCase(); });
  const picked = [];
  let used = 0;
  for (const s of all) {
    const d = s.description.toLowerCase();
    if (!kw.some(function (k) { return d.indexOf(k) !== -1; })) continue;
    if (used + s.tokens > budget) break;
    picked.push(s.id);
    used += s.tokens;
  }
  return picked;
}

console.log(loadWithinBudget(skills, ['search'], 1000).join(','));
console.log(loadWithinBudget(skills, ['bug', 'search'], 900).join(','));
flowchart LR
  B["token budget"] --> F["fill with matching skills<br/>in order"]
  F --> S["stop before exceeding"]
  S --> R["only what fits<br/>enters context"]
  style B fill:#b45309,color:#fff
  style R fill:#166534,color:#fff
A token budget fills with matching skills in order and stops before exceeding the cap.
Exercise

A skill's description field is the only part that stays in context on every turn. What does that imply about how you should write it?

Exercise

What does this print? listSkills builds a card per skill; .map(s => s.id) pulls each id and join stitches them into one string.

const skills = {
  calc: { description: 'Run a calculator', instructions: 'add numbers', toolIds: ['add'] },
  notes: { description: 'Take notes', instructions: 'capture text', toolIds: ['write'] }
};
function listSkills() {
  return Object.keys(skills).map(function (id) { return { id: id, description: skills[id].description }; });
}
console.log(listSkills().map(function (s) { return s.id; }).join(','));
Exercise

Write listSkills() that returns an array of { id, description } cards — one per skill in the skills object (the key is the id). Do NOT include instructions or toolIds in the cards.

const skills = {
  calc: { description: 'Run a calculator', instructions: 'add numbers', toolIds: ['add'] },
  draw: { description: 'Draw a shape', instructions: 'pick a shape', toolIds: ['render'] }
};
function listSkills() {
  // return [{id, description}, ...] one card per skill
}
Exercise

This injectSkill adds a skill's payload to the context. It has a bug: it appends the payload on EVERY call, even when that skill is already in context — so a skill used on five turns appears five times. Fix it so a skill already in the context (matched by skillId) is never added a second time.

function injectSkill(context, skillId, loader) {
  const payload = loader(skillId);
  context.push({ skillId: skillId, payload: payload });
  return context;
}
Exercise

Write loadWithinBudget(skills, keywords, budget) that picks which skills to load. Include a skill only if its description contains any of the keywords (case-insensitive). Walk the skills array in order, and stop — WITHOUT adding the current skill — as soon as adding it would push the cumulative tokens over budget. Return the array of chosen skill ids. (SKILLS is already defined for you.)

const SKILLS = [
  { id: 'cal', description: 'calendar and events', tokens: 300 },
  { id: 'mail', description: 'email inbox', tokens: 400 },
  { id: 'doc', description: 'document editor', tokens: 500 }
];

function loadWithinBudget(skills, keywords, budget) {
  // include a skill only if its description contains a keyword;
  // stop (without adding) when the next skill would exceed budget
}

Recap

  • A skill bundles three parts: a short description (always in context), longer instructions, and toolIds. It stays off-stage until it is needed.
  • Progressive disclosure is a two-step loop: listSkills() shows only cheap cards every turn; loadSkill(id) injects one full payload only when that skill is chosen.
  • The description does real work: it is the sole text the model sees when deciding, so a vague card produces a skill that is never called.
  • loadSkill resolves tool ids to full schemas; the catalogue stores ids, not whole tool definitions.
  • Guard against re-injection: append a payload once, not on every turn, or context bloats the way the design was meant to prevent.
  • A token budget caps how many skills a single turn can load.

Next you will make a tool safe to run — least privilege and the allowlist that stops a tool call from becoming an incident.

Checkpoint quiz

Under progressive disclosure, what does the model see for a skill it has NOT yet chosen?

A skill's payload keeps reappearing on every turn it is used, growing the context each time. What was forgotten?

Go deeper — technical resources