Skills
- In one line
- Loads reusable
SKILL.mdinstructions and resolvesskill,skill_readandskill_searchtool calls against them. - You need
- The
plugin-skillsfeature and either a skills directory or inlineSkillvalues. - Read this if
- You are registering
SkillsPluginor authoring aSKILL.mdfile.
Feature and entry type
| Item | Value |
|---|---|
| Feature flag | plugin-skills (no dependencies) |
| Entry type | SkillsPlugin |
| Supporting types | SkillsConfig, Skill |
| Plugin name | "skills" |
| Attachment | register_plugin |
| Peer features | None |
| Needs | A skills directory, inline Skill values, or both |
Build the plugin with SkillsPlugin::new(config), SkillsPlugin::from_dir(path) (default config, scanning path) or SkillsPlugin::inline(skills) (default config, no directory). Skill::inline(name, description, instructions) builds a skill without references. The accessors are skill(name), list_skills(), search(query, limit) and read_reference(skill, file).
Hooks
SkillsPlugin overrides on_request and on_stream_chunk.
on_requestappends one system message to the request wheninject_catalogis true and at least one skill is loaded. The message names the three tools, tells the model to useskill_searchfirst when unsure which skill applies, and lists each skill as- <name>: <description>.on_stream_chunkreplaces askill,skill_readorskill_searchToolCallwith aToolResult. Any other block passes through untouched.
Tool arguments and results:
skilltakes a non-blankname. The output is a JSON object{name, description, instructions, references}, wherereferenceslists the file names.skill_readtakes non-blankskillandfile. The output is the content of that reference file.skill_searchtakes a non-blankquery. The output is a JSON array of{name, description}objects, at mostmax_search_resultslong.
Search is case-insensitive. Each whitespace-separated query term scores 3 points per occurrence in the skill name, 2 in the description and 1 in the instructions. Skills with a positive score are ranked by score descending, then name ascending. SkillsPlugin::search called directly with a blank query returns every skill.
Failures never fail the hook. The error text becomes the ToolResult output, so the model can react to it in the conversation.
Configuration
SkillsConfig defaults:
| Key | Type | Default | Meaning |
|---|---|---|---|
skills_dir | Option<PathBuf> | None | Directory whose direct subdirectories each hold a SKILL.md. |
inline_skills | Vec<Skill> | empty | Skills supplied in code. |
inject_catalog | bool | true | Whether on_request injects the catalog message. |
max_search_results | usize | 5 | Cap on skill_search results. |
skills_dirsubdirectories without aSKILL.mdare skipped silently.inline_skillswin over directory skills on a name conflict. Among duplicates, the later entry wins, and the final list is sorted by name.
SKILL.md format
A SKILL.md file has this shape:
- The first line is
---, followed bykey: valuelines, then a closing---line. name:anddescription:are required and must not be blank. Values may be unquoted or double-quoted.- Other keys, such as
licenseorallowed-tools, are read and ignored. - Everything after the closing line, trimmed, is the instructions.
- Every regular file in an optional
references/directory next toSKILL.mdbecomes a reference keyed by file name. Content is read as UTF-8, lossy.
The skill name is the name: value. The directory name does not matter.
Caps and at-cap policy
No cap: the skill list is fixed at construction, deduplicated by name and sorted, so nothing grows with traffic. The usage gauge is none.
max_search_resultsbounds oneskill_searchresult, not the plugin.- The plugin loads every skill and every reference file into memory at construction and sets no size limit on them.
- The catalog message grows with the number of loaded skills and is added to every request while
inject_catalogis true.
Errors
Construction returns PluginError::Io, from SkillsPlugin::new and from_dir, when:
skills_dircannot be read.- A
SKILL.mdor a reference file is unreadable. - A
SKILL.mdhas no leading---line or no closing---line. - A
SKILL.mdlacks a non-blankname:ordescription:.
The message names the offending file. Inline skills are never rejected.
At call time the hook returns no error. Each variant below becomes ToolResult output text:
PluginError::Validationwhen a required tool argument is missing, not a string, or blank.PluginError::Internalfor an unknown skill name (the text points toskill_search), an unknown skill inskill_read, or a skill with no such reference file.
Example
examples/skills.rs writes a SKILL.md and one references/factors.md into a temporary directory and loads them with SkillsPlugin::from_dir. It takes three turns: the model calls skill and gets the instructions back, then calls skill_read for the bundled file, then answers. The directory is removed when the program ends. It needs a llama-server on port 1234 with the demo model loaded. CUCA_BASE_URL and CUCA_MODEL retarget it at any OpenAI-compatible server.
---
name: unit-convert
description: Convert between metric and imperial units using exact factors
license: Apache-2.0
---
# Unit conversion
Read `factors.md` for the exact factor, multiply, and answer with the number
followed by the unit and nothing else.let skills = Arc::new(SkillsPlugin::from_dir(dir.path())?);
let client = CucaClient::builder()
.with_provider(ProviderEndpoint::LlamaCpp)
.with_base_url(base_url.clone())
.register_plugin(Arc::clone(&skills) as Arc<dyn CucaPlugin>)
.build()?;cargo run --example skills --features "provider-llamacpp plugin-skills"Loaded from /tmp/cuca-skills-3284142
unit-convert: Convert between metric and imperial units using exact factors (1 reference file(s))
The model reaches for the skill out of the injected catalog
catalog in the outbound request, last line: - unit-convert: Convert between metric and imperial units using exact factors
skill returned: {"description":"Convert between metric and imperial units using exact factors","instructions":"# Unit conversion\n\nRead `factors.md` for the exact factor, multiply, and answer with the number\nfollowed by the unit and nothing else.","name":"unit-convert","references":["factors.md"]}
thinking blocks: 138
The model reaches for the reference file
catalog in the outbound request, last line: - unit-convert: Convert between metric and imperial units using exact factors
skill_read returned: 1 mile = 1.609344 kilometers
1 pound = 0.45359237 kilograms
thinking blocks: 122
The answer, from the instructions and the factor
reply: 19.312128 kilometers
thinking blocks: 369
The temporary directory path depends on the platform. Which tool the model calls, its wording and the thinking-block counts depend on the model; this output came from google/gemma-4-12b-qat. The full program also registers a recorder plugin around SkillsPlugin to print the catalog and the model's tool call.
Related pages
- Plugins lists every plugin, its hooks and its caps.
- Write a custom plugin walks through the hook contract.
- Error types lists
PluginError.