Ratel Docs
TypeScript SDK

Register skills

r.skills: register Markdown playbooks that rank in their own corpus and surface next to tools — the core's SkillCatalog, exposed raw.

Skills are Markdown playbooks (a deploy runbook, a debugging checklist) ranked in a separate corpus from tools, so a relevant skill is never crowded out by matching tools. The concept is covered in Tools, MCP servers & skills; Ratel's own coding-agent skills (including ratel-decompose-prompt, which extracts skills from a long system prompt) are on the Skillset.

Register a skill

A skill can declare the tools its instructions call: when the skill matches a query, those tools are pulled into the tools bucket (additively, deduped) so the agent gets the playbook and the tools it needs in one turn instead of a second search.

import { ratel } from "@ratel-ai/sdk";

const r = ratel();
await r.skills.register({
  id: "vercel-deploy",
  name: "vercel-deploy",
  description: "How to deploy to Vercel: env vars, preview vs production, rollbacks.",
  tags: ["deploy", "ship to production"],     // indexed for ranking
  tools: ["vercel__deploy", "fs__read_file"], // surfaced alongside the skill when it matches
  metadata: { stacks: ["next", "vercel"] },   // non-indexed context for higher-layer ranking
  body: "## Deploying to Vercel\n1. ...",      // returned by get_skill_content
});

const tools = r.modelTools(); // search_capabilities now returns a populated `skills` bucket

Only id, name, and description are required; tags, tools, metadata, and body are optional (parity with the Python SDK). Ranking indexes a skill's name, description, and tags; the body never enters the index, so a long playbook cannot drown out its own description.

How skills surface

Nothing to wire: search_capabilities from r.modelTools() already searches r.skills, so results carry a skills bucket alongside tools, each with its own result budget. The agent pulls a skill's full body into context on demand via get_skill_content (constant GET_SKILL_CONTENT_ID).

get_skill_content({ skillId }) returns { body }, or { error, isError: true } for an unknown id.

Assembling the funnel piecemeal instead? Pass the SkillCatalog as the second argument to searchCapabilitiesTool to populate the skills bucket, and build the loader with getSkillContentTool(skills) (id get_skill_content).

The r.skills surface

r.skills is the shared SkillCatalog itself, exposed raw — no guarded wrapper (unlike r.tools), so the whole surface is on the handle and search top-K is not clamped:

r.skills.search(query, topK);        // → SkillHit[] ({ skillId, score, rank, fused }); synchronous, BM25-only
await r.skills.searchAsync(query, topK); // → Promise<SkillHit[]>; any method, (query, topK, origin?, method?)
r.skills.has(skillId);          // → boolean
r.skills.get(skillId);          // → Skill | undefined
r.skills.size();                // → number of registered skills
r.skills.invoke(skillId);       // → string: the Markdown body; throws on unknown id
r.skills.replaceAll(skills);    // → PendingReplace: the batch becomes the whole catalog
r.skills.recordEvent(event);    // inject a trace event into the active sink
r.skills.drainTraceEvents();    // → unknown[]; memory sink only

Order hits by rank (0-based, stable across methods) and branch on fused: true means score is an RRF value (hybrid search, or the experimental adaptive usage arm), not a raw BM25 or cosine score.

invoke returns the body directly (body ?? ""), recording a skill_invoke trace event, and throws unknown skillId: <id> for an unknown id; the get_skill_content capability tool is the boundary that translates that throw into the structured { error, isError: true } the agent sees.

The catalog's options (SkillCatalogOptions = { trace?, method?, embedding? }, the same as ToolCatalog's) come from RatelConfig: ratel({ method, embedding, trace }) forwards all three to both catalogs. Constructing standalone remains the piecemeal path — new SkillCatalog(options) takes them directly.

Replace the whole catalog

replaceAll makes the batch the entire catalog (ADR 0015): ids absent from it are removed, including skills registered in-process. It exists for a source that fetches the full catalog rather than pushing deltas; a host that keeps local skills alongside a remote source composes the batch itself:

const reload = r.skills.replaceAll([...localSkills, ...remoteSkills]);
reload.added; // counts (added / removed / updated / unchanged) are final before awaiting
await reload; // drives the embedding pass — always await (or .catch())

It mutates the one shared SkillCatalog in place, so r.skills, every adapted view, and the capability tools from modelTools() all observe the reload — and modelTools() pins the search_capabilities description, so a reload cannot bust the prompt cache.

Two-phase, exactly like register: the corpus swap commits synchronously; only the embedding pass rides the returned PendingReplace (Promise<ReplaceOutcome> & ReplaceOutcome). The counts stay readable even when that pass fails — but it is a real promise, so leaving it unhandled turns an embedding failure into an unhandled rejection.

On a failed pass the new corpus is already live and BM25 ranks it; semantic search reports EmbeddingsNotBuilt until a later pass succeeds. A reload racing an in-flight operation — dense work, but also a plain searchAsync — throws registry busy at the call site rather than blending; retry it. Only new or re-worded skills are embedded, so reloading an unchanged catalog costs zero embedding calls.

PendingReplace and ReplaceOutcome are exported from @ratel-ai/sdk. The tool corpus has no equivalent: register stays the only way into a ToolCatalog.

Embeddings for skills

The skill corpus has its own embedding cache, maintained exactly like the tool one (The embedding cache): create the core with ratel({ method: "semantic" }) or "hybrid" so await r.skills.register(...) embeds each skill; a BM25 catalog cannot be retrofitted. The two caches stay independent, but ratel() gives both catalogs the same method and embedding model — to mix (say, hybrid skills over BM25 tools), construct new SkillCatalog(...) piecemeal.

Embeddings cover the same projection the ranking indexes (name, description, tags), never the body, so a long playbook costs one small embedding. When the engines are worth it is on Semantic & hybrid search.

Next steps

On this page