Skip to main content

Crate turul_agent_skills

Crate turul_agent_skills 

Source
Expand description

§turul-agent-skills

Parse, validate, render and catalog Agent Skills.

An Agent Skill is a directory holding a SKILL.md file: YAML frontmatter that names and describes a capability, then Markdown instructions for an LLM agent, optionally with bundled scripts and references. An agent shows the model a catalog of names and descriptions, and loads a skill’s full instructions only when the task calls for it. See the specification.

This crate follows the Agent Skills specification and agrees with the skills-ref reference implementation (version 0.1) on validation. It has no async runtime, no network and no LLM dependency. Where it differs from skills-ref, the differences are listed at the end.

§Install

cargo add turul-agent-skills                 # the library
cargo install turul-agent-skills-cli         # the `turul-skills` command

§Quick start

use turul_agent_skills::prelude::*; imports the types and functions below.

§Validate

validate checks a skill directory against the specification and returns every problem found. An empty list means the skill is valid.

let errors = turul_agent_skills::validate(&dir);
assert!(errors.is_empty());
for error in &errors {
    println!("{}: {error}", error.code().as_str());
}

§Load and read properties

load is lenient: a skill that is usable but imperfect loads with diagnostics, and only a skill that cannot be used is skipped.

use turul_agent_skills::load;

match load(&dir) {
    Ok(loaded) => {
        let skill = &loaded.skill;
        assert_eq!(skill.properties.name.as_str(), "greet");
        assert_eq!(skill.properties.description, "Greet a user.");
        assert_eq!(skill.properties.license.as_deref(), Some("MIT"));
        assert_eq!(skill.body, "Say hello.\n");
        assert!(loaded.diagnostics.is_empty());
    }
    Err(skipped) => {
        for error in &skipped.errors {
            eprintln!("{error}");
        }
    }
}

A skill embedded in a binary (include_str!) loads with load_str(dir_name, text): the same rules, with dir_name standing in for the directory and no location.

§Create and write a skill

SkillBuilder parses the name and trims the description, so a Skill always has a legal identity and a non-empty description. build returns BuildError otherwise. write_to creates <parent>/<name>/SKILL.md.

use turul_agent_skills::SkillBuilder;

let skill = SkillBuilder::new("summarize", "Summarize a document in three bullets.")
    .license("MIT")
    .body("Read the document, then answer in three bullets.\n")
    .build()
    .expect("a legal name and a non-empty description");

let file = skill.write_to(parent.path())?;
assert!(file.ends_with("summarize/SKILL.md"));
assert!(turul_agent_skills::validate(file.parent().unwrap()).is_empty());

§Modify an existing skill

Load, change a field, write back. Frontmatter keys outside the specification and the body are preserved.

use turul_agent_skills::load;

let mut skill = load(&dir).expect("loads").skill;
skill.properties.description = "Greet a user warmly.".to_string();
skill.write_to(parent.path())?;

let reloaded = load(&dir).expect("loads").skill;
assert_eq!(reloaded.properties.description, "Greet a user warmly.");
assert_eq!(reloaded.body, "Say hello.\n");

§Catalog prompt

to_prompt renders the <available_skills> block a model sees at the start of a session. It returns None when there are no skills, because an empty catalog is better omitted.

use turul_agent_skills::{load, to_prompt};

let skills = vec![load(&dir).expect("loads").skill];
let catalog = to_prompt(&skills).expect("one skill");
assert!(catalog.starts_with("<available_skills>"));
assert!(catalog.contains("<name>\ngreet\n</name>"));
assert_eq!(to_prompt(&[]), None);

§Authoring advisories

advise reports the two measurable recommendations of the specification’s progressive-disclosure guidance: a SKILL.md over 500 lines, and file references nested deeper than one level. It is separate from validate and load, which it never affects. Judging scope, gotchas or description quality is review work, not the library’s.

use turul_agent_skills::{ErrorCode, Skill, advise};

let long = Skill::builder("long", "A long skill.")
    .body("line\n".repeat(600))
    .build()
    .expect("a legal name and a non-empty description");

let advisories = advise(&long);
assert_eq!(advisories[0].code(), ErrorCode::SkillMdTooLong);
assert!(advisories[0].message().contains("500"));

§Activation

When the model asks for a skill, activation returns its instructions, its directory and the names of the files bundled with it. Bundled files are listed, never read.

use turul_agent_skills::{activation, load};

let skill = load(&dir).expect("loads").skill;
let text = activation(&skill);
assert!(text.starts_with("<skill_content name=\"greet\">"));
assert!(text.contains("Say hello."));
assert!(text.contains("<file>scripts/hello.sh</file>"));

§Discover skills

discover scans the directories you give it, loads every skill it finds and resolves name collisions: a project skill beats a user skill, and each skill that loses is reported as a diagnostic. Which directories to scan, and whether a project is trusted enough to scan, is the caller’s decision.

use turul_agent_skills::{Scope, ScopeKind, discover};

let scopes = Scope::conventional(ScopeKind::Project, project.path());
let found = discover(&scopes);
assert_eq!(found.skills.len(), 1);
for diagnostic in &found.diagnostics {
    eprintln!("{}: {}", diagnostic.path.display(), diagnostic.error);
}

§Command line

turul-agent-skills-cli installs turul-skills, with the same validate, read-properties and to-prompt commands, output and exit codes as skills-ref.

$ turul-skills validate greet
Valid skill: greet

$ turul-skills read-properties greet
{
  "name": "greet",
  "description": "Greet a user.",
  "license": "MIT"
}

$ turul-skills validate bad
Validation failed for bad:
  - Skill name 'Bad' must be lowercase
  - Directory name 'bad' must match skill name 'Bad'
  - Missing required field in frontmatter: description
$ echo $?
1

to-prompt greet prints the same <available_skills> block as the library’s to_prompt.

§Examples with an LLM

examples/skill-llm uses turul-llm to let a model pick a skill from the catalog and use it (catalog), and to draft, validate, write and modify a skill (author); smoke asserts every public capability step by step. They run offline against a stub model by default:

cargo run -p skill-llm --bin catalog
cargo run -p skill-llm --bin author
cargo run -p skill-llm --bin smoke
RUN_OLLAMA_SMOKE=1 cargo run -p skill-llm --bin catalog   # a live local Ollama model
RUN_OLLAMA_SMOKE=1 cargo run -p skill-llm --bin smoke

§Spec conformance and divergences

The specification is followed; where skills-ref 0.1 and the specification differ, or skills-ref has a defect, the choice is recorded in ADR-001 and pinned by a test. All 40 skills-ref test cases are ported as fixtures and run as a conformance suite. Beyond the topics below, the CLI matches skills-ref output and exit codes; usage and --help text come from clap.

Topicskills-ref 0.1This crateDecided in
Opening delimiterAnything starting with --- passesLine 1 is exactly ---ADR-003
Closing delimiterA --- inside a value ends the blockThe first later line that is exactly ---ADR-003
Optional field typeslicense, allowed-tools, metadata not type-checkedStrings, string, and a string-to-string mappingADR-004
Empty compatibilityAcceptedRejected (the specification requires 1-500 characters)ADR-004
Unreadable or non-UTF-8 fileUncaught exceptionA read-failed errorADR-004
Unquoted : in a valueAn errorRepaired by the library’s load; validate and the CLI report it, as skills-ref doesADR-003, ADR-007
Invalid names when loadingPrinted uncheckedSkipped with the rule’s message (the client guide relaxes this)ADR-002, ADR-004
Printed nameAs writtenNFKC-normalizedADR-002
read-properties metadata orderSource orderSorted by keyADR-003, ADR-005
Empty catalogAn empty wrapper elementNone (the CLI still exits 2 without paths)ADR-006, ADR-007
DiscoveryNonediscover (project before user, bounded, symlink-contained)ADR-009

Licensed under MIT OR Apache-2.0.

Modules§

prelude
The types and functions most programs need, for one glob import.

Structs§

Diagnostic
A problem found while discovering, with the path it concerns.
Discovered
The outcome of discover: the winning skills and every problem met on the way.
LoadSkip
A skill that could not be loaded, with every error found.
Loaded
A skill that loaded, with the problems that did not stop it.
NameError
Why a candidate name is not a SkillName, with every rule it broke.
RawSkill
A SKILL.md split into its top-level frontmatter entries and body.
Scope
A directory tree to scan for skills.
Skill
A loaded or constructed skill: spec fields, extension keys and the body.
SkillBuilder
Builds a new Skill from its fields; the one way to construct a skill that was not loaded from disk.
SkillName
The identity of a skill: trimmed, NFKC-normalized, and checked against the name rules of the Agent Skills specification.
SkillProperties
The fields the Agent Skills specification defines, as validated values.
ValidationError
One problem found in a skill, with a stable ErrorCode and a readable message.

Enums§

BuildError
Why SkillBuilder::build produced no skill.
ErrorCode
A stable, machine-readable problem kind.
NameViolation
One identity rule a candidate name broke.
ParseError
Why a document could not be parsed.
RawValue
A YAML value of the frontmatter subset. Every scalar is a string.
ScopeKind
Where a scope’s skills come from; a project skill beats a user skill of the same name.

Constants§

MAX_RECOMMENDED_LINES
Lines in SKILL.md above which advise reports ErrorCode::SkillMdTooLong.
MAX_RESOURCE_FILES
Most resource files activation lists before truncating.
MAX_SCAN_DEPTH
Deepest directory level scanned below a root; directories deeper are not entered. discover and activation both honor it.
MAX_SCAN_DIRS
Most directories scanned in one scope before it is cut short, which discover reports as ErrorCode::ScanLimitReached.

Functions§

activation
The text a client returns when a skill is activated: the trimmed body, the skill directory and a capped list of bundled files. Files are listed by name and never opened.
advise
Reports the two measurable authoring recommendations of the specification’s progressive-disclosure guidance. Advisories are not validation: they never make validate or load fail, and judging scope, gotchas or description quality is left to review.
discover
Scans scopes in the order given and returns the skills found.
display_path
Formats a path the way messages and the command line print it: /-joined, . components dropped, and an empty path as ..
load
Loads the skill in dir leniently, keeping what is usable.
load_str
Loads a skill from SKILL.md text leniently, for skills embedded in a binary.
parse_skill_md
Splits content into frontmatter entries and body.
to_prompt
The <available_skills> catalog for a model, skills in input order, or None when there are none (an empty catalog is better omitted).
validate
Checks the skill directory dir strictly and returns every problem found.