Skills
Progressive disclosure of agent capabilities through discoverable SKILL.md files
Skills are modular instruction packages that extend an agent's capabilities. They follow Anthropic's progressive disclosure pattern: at startup only metadata (name and description) is loaded into context, and the LLM reads the full instructions from disk only when a skill is relevant.
How Progressive Disclosure Works
Startup:
skills/
deploy/SKILL.md → { name: "deploy", description: "Deploy to production" }
refactor/SKILL.md → { name: "refactor", description: "Refactor code" }
Only name + description injected into system prompt.
Runtime:
User says "deploy the app"
→ LLM matches "deploy" skill
→ LLM reads /skills/deploy/SKILL.md for full instructions
→ LLM follows the workflowThis keeps the system prompt small while making dozens of skills discoverable.
The SKILL.md Format
Each skill lives in its own directory and is defined by a SKILL.md file with YAML frontmatter:
---
name: deploy
description: Deploy services to production with zero-downtime rollouts
---
## Workflow
1. Run pre-deploy checks
2. Build the container image
3. Push to registry
4. Roll out with health checks
## Output
Return a summary of what was deployed and the health check results.The frontmatter requires exactly two fields:
| Field | Type | Description |
|---|---|---|
name | string | Unique skill identifier |
description | string | Short description shown to the LLM for matching |
The body after the frontmatter contains the full instructions the LLM reads at runtime.
Discovering Skills
discoverSkillsInDirectory() scans a directory for subdirectories containing SKILL.md files and returns their metadata:
import { discoverSkillsInDirectory } from '@deepagents/context';
const skills = discoverSkillsInDirectory('./skills');
// [
// { name: 'deploy', description: 'Deploy to production', path: './skills/deploy', skillMdPath: './skills/deploy/SKILL.md' },
// { name: 'refactor', description: 'Refactor code', path: './skills/refactor', skillMdPath: './skills/refactor/SKILL.md' },
// ]Tilde paths (~/) are expanded to the user's home directory. Directories without a SKILL.md are silently skipped. Invalid SKILL.md files log a warning and are excluded.
Loading Individual Skills
loadSkillMetadata() loads metadata from a single SKILL.md path. It parses only the frontmatter -- the body is never loaded into memory:
import { loadSkillMetadata } from '@deepagents/context';
const skill = loadSkillMetadata('./skills/deploy/SKILL.md');
// { name: 'deploy', description: 'Deploy to production', path: './skills/deploy', skillMdPath: './skills/deploy/SKILL.md' }Parsing Frontmatter
parseFrontmatter() parses a SKILL.md string into its frontmatter and body:
import { parseFrontmatter } from '@deepagents/context';
const content = `---
name: deploy
description: Deploy to production
---
## Workflow
1. Build
2. Push
3. Roll out`;
const { frontmatter, body } = parseFrontmatter(content);
// frontmatter: { name: 'deploy', description: 'Deploy to production' }
// body: '## Workflow\n1. Build\n2. Push\n3. Roll out'Throws if the frontmatter is missing or lacks the required name and description fields.
The skills() Fragment
The skills() function is the primary integration point. It scans directories, filters skills, and produces a context fragment with embedded LLM instructions:
import { ContextEngine, SqliteContextStore, role, skills } from '@deepagents/context';
const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({
store,
chatId: 'chat-001',
userId: 'user-001',
}).set(
role('You are a helpful assistant.'),
skills({
paths: ['./skills'],
}),
);SkillsFragmentOptions
| Option | Type | Default | Description |
|---|---|---|---|
paths | string[] | required | Directories to scan for skills |
exclude | string[] | undefined | Skill names to exclude |
include | string[] | undefined | If set, only these skill names are included |
Filtering
Use include for an allowlist or exclude for a blocklist:
skills({
paths: ['./skills'],
include: ['deploy', 'refactor'],
})
skills({
paths: ['./skills'],
exclude: ['dangerous-skill'],
})When both are set, include is applied first, then exclude filters the result.
Generated Fragment
The skills() function returns an available_skills fragment containing:
- An
instructionsfragment with guidance on how the LLM should discover and use skills - One
skillfragment per discovered skill withname,path, anddescription
The instructions tell the LLM to:
- Match user requests to skill names or descriptions
- Read the
SKILL.mdfile on demand (progressive disclosure) - Load only the specific reference files needed, not everything
- Prefer running existing scripts over rewriting code
- Announce which skills are being used and why
- Understand that there is no separate skill tool, invoke action, command, or API -- using a skill means reading its
SKILL.mdand following the workflow defined there
The instructions also embed correct/incorrect usage examples so the LLM avoids a common misunderstanding: if a user says "use the onboarding skill", the correct behavior is to read the onboarding SKILL.md and follow it, not to claim a separate skill tool needs to be called, invoked, or activated first.
Extracting Available Skills
ContextEngine.getAvailableSkills() returns the metadata of all skills discovered from the configured context. This is used by the agent framework to populate GuardrailContext.availableSkills, enabling guardrails to detect when the LLM confuses skills with tools:
const context = new ContextEngine({ store, chatId: 'chat-001', userId: 'user-001' })
.set(skills({ paths: ['./skills'] }));
const skills = context.getAvailableSkills();
// [
// {
// name: 'deploy',
// description: 'Deploy to production',
// path: './skills/deploy',
// skillMdPath: './skills/deploy/SKILL.md',
// },
// ]Each entry is a SkillMetadata:
| Field | Type | Description |
|---|---|---|
name | string | Skill name |
description | string | Skill description |
path | string | Directory containing the skill |
skillMdPath | string | Path to the SKILL.md file |
Skill Classifier
The skill classifier ranks skills against user messages using BM25 text similarity. Instead of relying on the LLM to pattern-match skill names at inference time, the classifier scores each skill's name and description against the user's input and surfaces the top matches as a soft nudge in the prompt.
BM25SkillClassifier
The default implementation indexes skill metadata (name + description) into a BM25 corpus and ranks matches by relevance score:
import { BM25SkillClassifier, discoverSkillsInDirectory } from '@deepagents/context';
const skills = discoverSkillsInDirectory('./skills');
const classifier = new BM25SkillClassifier(skills);
const matches = classifier.match('deploy my docker container', { topN: 3 });
// [
// { skill: { name: 'deploy-helper', ... }, score: 0.82 },
// { skill: { name: 'docker-expert', ... }, score: 0.71 },
// ]SkillClassifierOptions
| Option | Type | Default | Description |
|---|---|---|---|
topN | number | 5 | Maximum number of matches to return |
threshold | number | 0 | Minimum score to include a match |
skillsReminder()
skillsReminder() bridges the classifier with the user message reminder system. It returns a UserReminder with a factory function that runs the classifier against each user message at resolve time. When matches are found, a formatted list of relevant skills (with scores and file paths) is injected as a <system-reminder>. When nothing matches, no reminder is injected.
import { ContextEngine, SqliteContextStore, role, skills, user, skillsReminder, discoverSkillsInDirectory } from '@deepagents/context';
const discoveredSkills = discoverSkillsInDirectory('./skills');
const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({ store, chatId: 'chat-001', userId: 'user-001' })
.set(
role('You are a helpful assistant.'),
skills({ paths: ['./skills'] }),
user('deploy my app to production', skillsReminder(discoveredSkills, { topN: 3 })),
);You can also pass a pre-built classifier instance instead of a raw skill array:
import { BM25SkillClassifier, skillsReminder } from '@deepagents/context';
const classifier = new BM25SkillClassifier(skills);
const reminderFragment = skillsReminder(classifier, { topN: 3, threshold: 0.1 });Custom Classifiers
Implement ISkillClassifier to replace BM25 with your own ranking logic (embeddings, LLM-based, etc.):
import { type ISkillClassifier, type SkillMatch, type SkillClassifierOptions, skillsReminder } from '@deepagents/context';
const customClassifier: ISkillClassifier = {
match(query: string, options?: SkillClassifierOptions): SkillMatch[] {
const topN = options?.topN ?? 5;
return myEmbeddingSearch(query, topN);
},
};
const reminderFragment = skillsReminder(customClassifier);Factory Reminders
reminder() now accepts a factory function in addition to a static string. When a factory is provided, user() resolves it at build time by passing the message's plain text content. If the factory returns an empty string, the reminder is skipped entirely.
import { user, reminder } from '@deepagents/context';
user(
'deploy the app',
reminder('Always ask for confirmation'),
reminder((content) => {
if (content.includes('deploy')) {
return 'Remind user about staging checks';
}
return '';
}),
);This is the mechanism that skillsReminder() uses internally -- it returns a UserReminder whose text is a factory that runs the classifier.
Types
interface SkillMetadata {
name: string;
description: string;
path: string;
skillMdPath: string;
}
interface SkillsFragmentOptions {
paths: string[];
exclude?: string[];
include?: string[];
}
interface ParsedSkillMd {
frontmatter: { name: string; description: string; [key: string]: unknown };
body: string;
}
interface ISkillClassifier {
match(query: string, options?: SkillClassifierOptions): SkillMatch[];
}
interface SkillMatch {
skill: SkillMetadata;
score: number;
}
interface SkillClassifierOptions {
topN?: number;
threshold?: number;
}
type ReminderText = string | ((content: string) => string);Next Steps
- Fragments - How fragments work
- Context Engine - Managing fragments with ContextEngine
- Container Tool - Running agents in sandboxed containers