Skip to content

Skills

AgentSkills.io-compatible packaged instruction bundles (SKILL.md files) that agents load on demand via progressive disclosure:

  • L1: the skill catalog (names + descriptions) appears in the system prompt.
  • L2: the agent activates a skill — full instructions are loaded.
  • L3: the agent reads supporting resource files (scripts/, references/, assets/).

Attach via AgentConfig.skills=[skill_or_path, ...].

Skill dataclass

Skill(name: str, description: str, instructions: str = '', path: Path | None = None, allowed_tools: list[str] | None = None, metadata: dict[str, Any] = dict(), license: str | None = None, compatibility: str | None = None)

A skill — packaged instructions for an agent.

Skills follow the AgentSkills.io specification.

Example

skill = Skill( ... name="code-review", ... description="Review code for quality and security issues.", ... instructions="# Code Review\n1. Check error handling...", ... )

Load from filesystem

skill = Skill.from_file(Path("./skills/code-review"))

from_file classmethod

from_file(path: Path | str) -> Skill

Load a skill from a directory containing SKILL.md.

Parameters:

Name Type Description Default
path Path | str

Path to skill directory or SKILL.md file.

required

Returns:

Type Description
Skill

Loaded Skill instance.

Raises:

Type Description
FileNotFoundError

If SKILL.md not found.

ValueError

If required fields missing.

Source code in src/locus/skills/models.py
@classmethod
def from_file(cls, path: Path | str) -> Skill:
    """Load a skill from a directory containing SKILL.md.

    Args:
        path: Path to skill directory or SKILL.md file.

    Returns:
        Loaded Skill instance.

    Raises:
        FileNotFoundError: If SKILL.md not found.
        ValueError: If required fields missing.
    """
    path = Path(path)

    if path.name == "SKILL.md":
        skill_file = path
        skill_dir = path.parent
    elif path.is_dir():
        skill_file = path / "SKILL.md"
        skill_dir = path
    else:
        msg = f"Expected directory or SKILL.md file, got: {path}"
        raise FileNotFoundError(msg)

    if not skill_file.exists():
        msg = f"SKILL.md not found in {skill_dir}"
        raise FileNotFoundError(msg)

    content = skill_file.read_text(encoding="utf-8")
    return cls.from_content(content, path=skill_dir)

from_content classmethod

from_content(content: str, path: Path | None = None) -> Skill

Parse a skill from raw SKILL.md content.

Parameters:

Name Type Description Default
content str

Raw SKILL.md file content.

required
path Path | None

Optional filesystem path for resource resolution.

None

Returns:

Type Description
Skill

Parsed Skill instance.

Raises:

Type Description
ValueError

If required fields (name, description) missing.

Source code in src/locus/skills/models.py
@classmethod
def from_content(cls, content: str, path: Path | None = None) -> Skill:
    """Parse a skill from raw SKILL.md content.

    Args:
        content: Raw SKILL.md file content.
        path: Optional filesystem path for resource resolution.

    Returns:
        Parsed Skill instance.

    Raises:
        ValueError: If required fields (name, description) missing.
    """
    frontmatter, body = _parse_frontmatter(content)

    name = frontmatter.get("name", "")
    description = frontmatter.get("description", "")

    if not name:
        msg = "SKILL.md missing required field: name"
        raise ValueError(msg)
    if not description:
        msg = "SKILL.md missing required field: description"
        raise ValueError(msg)

    # Parse allowed-tools (space-delimited string or list)
    allowed_tools_raw = frontmatter.get("allowed-tools")
    allowed_tools: list[str] | None = None
    if isinstance(allowed_tools_raw, str):
        allowed_tools = allowed_tools_raw.split()
    elif isinstance(allowed_tools_raw, list):
        allowed_tools = [str(t) for t in allowed_tools_raw]

    return cls(
        name=name,
        description=description,
        instructions=body,
        path=path,
        allowed_tools=allowed_tools,
        metadata=frontmatter.get("metadata", {}),
        license=frontmatter.get("license"),
        compatibility=frontmatter.get("compatibility"),
    )

from_directory classmethod

from_directory(path: Path | str) -> list[Skill]

Load all skills from a parent directory.

Scans subdirectories for SKILL.md files.

Parameters:

Name Type Description Default
path Path | str

Parent directory containing skill subdirectories.

required

Returns:

Type Description
list[Skill]

List of loaded skills.

Source code in src/locus/skills/models.py
@classmethod
def from_directory(cls, path: Path | str) -> list[Skill]:
    """Load all skills from a parent directory.

    Scans subdirectories for SKILL.md files.

    Args:
        path: Parent directory containing skill subdirectories.

    Returns:
        List of loaded skills.
    """
    path = Path(path)
    skills: list[Skill] = []

    if not path.is_dir():
        msg = f"Not a directory: {path}"
        raise FileNotFoundError(msg)

    for child in sorted(path.iterdir()):
        if child.is_dir() and (child / "SKILL.md").exists():
            try:
                skills.append(cls.from_file(child))
            except (ValueError, FileNotFoundError):
                continue  # Skip invalid skills

    return skills

list_resources

list_resources(max_files: int = 20) -> list[str]

List resource files from scripts/, references/, assets/ directories.

Parameters:

Name Type Description Default
max_files int

Maximum number of files to list.

20

Returns:

Type Description
list[str]

List of relative file paths.

Source code in src/locus/skills/models.py
def list_resources(self, max_files: int = 20) -> list[str]:
    """List resource files from scripts/, references/, assets/ directories.

    Args:
        max_files: Maximum number of files to list.

    Returns:
        List of relative file paths.
    """
    if self.path is None:
        return []

    resources: list[str] = []
    for dir_name in _RESOURCE_DIRS:
        resource_dir = self.path / dir_name
        if resource_dir.is_dir():
            for f in sorted(resource_dir.iterdir()):
                if f.is_file() and not f.name.startswith("."):
                    resources.append(f"{dir_name}/{f.name}")
                    if len(resources) >= max_files:
                        return resources

    return resources

SkillsPlugin

SkillsPlugin(skills: list[Skill | str | Path], max_resource_files: int = 20)

Bases: Plugin

Plugin that provides AgentSkills.io skill discovery and activation.

Injects a compact XML catalog of available skills into the system prompt. Registers a skills tool that the agent calls to load full instructions.

Example

from locus.skills import Skill, SkillsPlugin

plugin = SkillsPlugin( ... skills=[ ... Skill.from_file("./skills/code-review"), ... Skill( ... name="summarize", description="Summarize text", instructions="..." ... ), ... ] ... )

agent = Agent( ... config=AgentConfig( ... model=model, ... plugins=[plugin], ... ) ... )

Initialize with skill sources.

Parameters:

Name Type Description Default
skills list[Skill | str | Path]

List of Skill instances, paths to skill directories, or paths to parent directories containing skills.

required
max_resource_files int

Max resource files to list per skill.

20
Source code in src/locus/skills/plugin.py
def __init__(
    self,
    skills: list[Skill | str | Path],
    max_resource_files: int = 20,
) -> None:
    """Initialize with skill sources.

    Args:
        skills: List of Skill instances, paths to skill directories,
               or paths to parent directories containing skills.
        max_resource_files: Max resource files to list per skill.
    """
    self._skills: dict[str, Skill] = {}
    self._max_resource_files = max_resource_files
    self._activated: list[str] = []

    for source in skills:
        if isinstance(source, Skill):
            self._skills[source.name] = source
        elif isinstance(source, (str, Path)):
            path = Path(source)
            if (path / "SKILL.md").exists():
                skill = Skill.from_file(path)
                self._skills[skill.name] = skill
            elif path.is_dir():
                for skill in Skill.from_directory(path):
                    self._skills[skill.name] = skill

activated_skills property

activated_skills: list[str]

Get list of activated skill names (most recent last).

available_skills property

available_skills: list[str]

Get list of available skill names.

on_before_model_call async

on_before_model_call(event: Any) -> None

Inject skills catalog XML into messages before model call.

Source code in src/locus/skills/plugin.py
@hook
async def on_before_model_call(self, event: Any) -> None:
    """Inject skills catalog XML into messages before model call."""
    catalog = self._generate_catalog_xml()
    if not catalog:
        return

    from locus.core.messages import Message

    # Inject catalog as a system message at the beginning
    catalog_msg = Message.system(
        "The following skills are available. To activate a skill, "
        "call the `skills` tool with the skill name.\n\n" + catalog
    )

    # Insert after the first system message (if any)
    messages = list(event.messages)
    insert_idx = 1 if messages and messages[0].role.value == "system" else 0
    messages.insert(insert_idx, catalog_msg)
    event.messages = messages

get_activation_tool

get_activation_tool() -> Any

Create the skills activation tool.

Returns a Tool that the agent calls to load skill instructions.

Source code in src/locus/skills/plugin.py
def get_activation_tool(self) -> Any:
    """Create the skills activation tool.

    Returns a Tool that the agent calls to load skill instructions.
    """
    skills_dict = self._skills
    plugin = self

    @tool_decorator(
        name="skills",
        description="Activate a skill to load its instructions. "
        "Call with the skill name from the available_skills catalog.",
    )
    def skills(skill_name: str) -> str:  # noqa: ARG001
        """Load a skill's full instructions.

        Args:
            skill_name: Name of the skill to activate.
        """
        if not skill_name:
            return "Error: skill_name is required."

        skill = skills_dict.get(skill_name)
        if skill is None:
            available = ", ".join(sorted(skills_dict.keys()))
            return f"Unknown skill: '{skill_name}'. Available: {available}"

        # Track activation
        if skill_name in plugin._activated:
            plugin._activated.remove(skill_name)
        plugin._activated.append(skill_name)

        # Telemetry — opt-in. ``emit_sync`` no-ops outside an
        # active run_context, so SDK users who never enter one
        # pay nothing for this line.
        try:
            from locus.observability.emit import EV_SKILL_ACTIVATED, emit_sync  # noqa: PLC0415

            emit_sync(
                EV_SKILL_ACTIVATED,
                skill_name=skill_name,
                has_resources=bool(skill.list_resources(max_files=1)),
                instructions_length=len(skill.instructions or ""),
            )
        except Exception:  # noqa: BLE001 — telemetry must never break the SDK
            pass

        return plugin._format_skill_response(skill)

    return skills

get_hooks

get_hooks() -> dict[str, Any]

Discover all @hook decorated methods.

Returns:

Type Description
dict[str, Any]

Dict mapping hook method names to bound methods.

Source code in src/locus/hooks/plugin.py
def get_hooks(self) -> dict[str, Any]:
    """Discover all @hook decorated methods.

    Returns:
        Dict mapping hook method names to bound methods.
    """
    hooks: dict[str, Any] = {}
    for attr_name in dir(self):
        if attr_name.startswith("_"):
            continue
        attr = getattr(self, attr_name, None)
        if attr is not None and callable(attr) and getattr(attr, "_is_hook", False):
            hooks[attr_name] = attr
    return hooks

get_tools

get_tools() -> list[Any]

Discover all @tool decorated methods.

Returns:

Type Description
list[Any]

List of Tool instances found on the plugin.

Source code in src/locus/hooks/plugin.py
def get_tools(self) -> list[Any]:
    """Discover all @tool decorated methods.

    Returns:
        List of Tool instances found on the plugin.
    """
    from locus.tools.decorator import Tool

    tools: list[Any] = []
    for attr_name in dir(self):
        if attr_name.startswith("_"):
            continue
        attr = getattr(self, attr_name, None)
        if isinstance(attr, Tool):
            tools.append(attr)
    return tools

init_agent

init_agent(agent: Any) -> None

Called when plugin is attached to an agent.

Override to perform setup that requires the agent instance.

Parameters:

Name Type Description Default
agent Any

The agent this plugin is being attached to.

required
Source code in src/locus/hooks/plugin.py
def init_agent(self, agent: Any) -> None:
    """Called when plugin is attached to an agent.

    Override to perform setup that requires the agent instance.

    Args:
        agent: The agent this plugin is being attached to.
    """