@skill-tools/router

BM25 skill selection middleware for Agent Skills. Uses full-text search to select which skills to inject into an agent's context window from large skill catalogs. Zero external dependencies — built-in Okapi BM25 engine.

npm install @skill-tools/router

Quick Start

import { SkillRouter } from '@skill-tools/router';

const router = new SkillRouter();

await router.indexSkills([
  { name: 'bap-browser', description: 'AI-powered browser automation via BAP...' },
  { name: 'run-tests', description: 'Execute test suites...' },
  { name: 'lint-code', description: 'Run ESLint or Biome...' },
]);

const results = await router.select('open a webpage and fill out a form');
// [{ skill: 'bap-browser', score: 1.0, metadata: {...} }]

API

SkillRouter

Main class. Constructor accepts optional SkillRouterOptions with embedding config and BM25 parameters.

indexSkills(skills)

Index skill descriptions into the BM25 inverted index. Tokenizes, removes stop words, and builds posting lists.

indexDirectory(dirPath)

Auto-discover and index SKILL.md files in a directory.

select(query, options?)

Find the top-K most relevant skills for a natural language query.

Option Default Description
topK 5 Number of results
threshold 0.0 Minimum BM25 score (normalized 0–1)
boost Skill names to boost by 1.2x
exclude Skill names or wildcard patterns to exclude

detectConflicts(threshold?)

Find skills with highly similar descriptions that may conflict.

save() / load(snapshot)

Serialize/restore the full index as JSON. Useful for instant startup persistence.

SkillRouter.fromSnapshot(snapshot)

Static factory to restore from a serialized snapshot.

BM25 Parameters

Parameter Default Description
k1 1.2 Term frequency saturation. Higher values give more weight to repeated terms.
b 0.75 Length normalization. 0 = ignore length, 1 = fully normalize.
const router = new SkillRouter({
  bm25: { k1: 1.5, b: 0.8 },
});

Contextual Enrichment (v0.2.2)

Before indexing, context terms are extracted deterministically from the skill body and prepended to the description. This improves recall for queries that match terms in the skill's instructions, headings, or inline code — without needing LLMs or embeddings.

Max 80 context tokens. Deduped against description tokens to avoid inflating term frequency. Disable with context: false:

const router = new SkillRouter({ context: false });

Embedding Providers (Advanced)

By default, the router uses BM25 full-text search (zero dependencies). For custom semantic search, you can provide your own embedding function:

const router = new SkillRouter({
  embedding: {
    provider: 'custom',
    dimensions: 1536,
    embed: async (texts) => {
      // Call your embedding API here
      return texts.map(t => myEmbedFunction(t));
    },
  },
});

Architecture

BM25 Scoring Formulas

IDF(t) = log((N - df(t) + 0.5) / (df(t) + 0.5) + 1)

score(q, d) = Σ IDF(t) × (tf(t,d) × (k1 + 1))
              / (tf(t,d) + k1 × (1 - b + b × |d| / avgdl))

normalized = score / max_score   // top result = 1.0

Where N = total documents, df(t) = documents containing term t, tf(t,d) = frequency of t in document d, |d| = document length in tokens, avgdl = average document length.

BM25Index (Direct Use)

The BM25 engine is also exported for standalone use:

import { BM25Index } from '@skill-tools/router';

const index = new BM25Index();
index.add('doc-1', 'AI-powered browser automation via BAP');
index.add('doc-2', 'Run unit tests with Vitest');

const results = index.search('browser', { topK: 5 });
// [{ id: 'doc-1', score: 1.0 }]

Test Coverage

64 tests across 6 suites. Run with npx vitest run from the package directory.

router.test.ts (18 tests)

bm25.test.ts (17 tests)

context-extractor.test.ts (11 tests)

contextual-routing.test.ts (10 tests)

memory-store.test.ts (10 tests)

local-embedding.test.ts (8 tests)