Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

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 workflow

This 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:

skills/deploy/SKILL.md
---
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.
SKILL.md
SKILL.md

The frontmatter requires exactly two fields:

FieldTypeDescription
namestringUnique skill identifier
descriptionstringShort 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

OptionTypeDefaultDescription
pathsstring[]requiredDirectories to scan for skills
excludestring[]undefinedSkill names to exclude
includestring[]undefinedIf 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:

  1. An instructions fragment with guidance on how the LLM should discover and use skills
  2. One skill fragment per discovered skill with name, path, and description

The instructions tell the LLM to:

  • Match user requests to skill names or descriptions
  • Read the SKILL.md file 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.md and 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:

FieldTypeDescription
namestringSkill name
descriptionstringSkill description
pathstringDirectory containing the skill
skillMdPathstringPath 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

OptionTypeDefaultDescription
topNnumber5Maximum number of matches to return
thresholdnumber0Minimum 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