Register skills
Register Markdown playbooks in a SkillCatalog and surface them through search_capabilities alongside tools.
Skills are Markdown playbooks (a deploy runbook, a debugging checklist) ranked by a separate BM25 corpus from tools. What a skill is and when to reach for one is on Tools vs MCP vs 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
import asyncio
from ratel_ai import Skill, SkillCatalog, get_skill_content_tool, search_capabilities_tool
skills = SkillCatalog()
asyncio.run(skills.register(
Skill(
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_tool
)
))
search = search_capabilities_tool(catalog, skills) # 2nd arg → result gains a populated `skills` bucket
load = get_skill_content_tool(skills) # id == "get_skill_content"Only id, name, and description are required; tags, tools, metadata, and body are optional. get_skill_content({skillId}) returns {"body": ...}, or {"error": ..., "isError": True} for an unknown id.
How skills surface
Pass a SkillCatalog as the second argument to search_capabilities_tool and search returns the skills bucket alongside tools, each with its own result budget, so a relevant skill is never starved by matching tools. The agent pulls a skill's full body into context on demand via get_skill_content_tool (id get_skill_content, constant GET_SKILL_CONTENT_ID).
A skill can also 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.
The SkillCatalog surface
SkillCatalog(trace=None, method="bm25", embedding=None) takes the same constructor keywords as ToolCatalog, and its method surface mirrors it over the skill corpus:
skills.search(query, top_k) # → list[SkillHit] (.skill_id, .score, .rank, .fused); synchronous, BM25-only
skills.search_async(query, top_k) # awaitable; any method; origin=, method= as on ToolCatalog
skills.has(skill_id) # → bool
skills.get(skill_id) # → Skill | None
skills.size() # → int
skills.invoke(skill_id) # → str: the body; raises ValueError on an unknown id
skills.replace_all(iterable) # → PendingReplace: the batch becomes the whole catalogOrder 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.
record_event() and drain_trace_events() work exactly as on ToolCatalog. Note that invoke is synchronous here, no await: loading a skill body is a lookup, not an executor call. It records a skill_invoke trace event.
Replace the whole catalog
replace_all 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:
reload = skills.replace_all([*local_skills, *remote_skills])
reload.added, reload.removed, reload.updated, reload.unchanged # final before awaiting
await reload # drives the embedding pass — always awaitTwo-phase, exactly like register: the corpus swap lands synchronously, and the returned PendingReplace already carries the final counts — awaiting drives only the embedding pass and resolves to a plain ReplaceOutcome. Always await the result so an embedding failure raises instead of being swallowed.
On a failed pass the new corpus is already live and BM25 ranks it; semantic search raises embeddings-not-built until a later pass succeeds. A reload racing an in-flight operation — dense work, but also a plain search_async — is rejected with RuntimeError: registry busy rather than blended; 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. 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): construct SkillCatalog with method="semantic" or "hybrid" so await skills.register(...) embeds each skill; a BM25 catalog cannot be retrofitted. The two caches are independent: skills can rank hybrid while tools stay on BM25, or run a different embedding model.
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
Register MCP servers
Ingest an upstream MCP server's tools with register_mcp_server: namespaced ids, the session-owning handle, error propagation, one ranked surface.
Use the discovery tools
Hand the agent search_capabilities, invoke_tool, and get_skill_content, then assemble each turn's toolset with a top-K pre-filter.