Files
cleveragents-core/docs/reference/agent_skills.md
T
aditya cb82fc51df feat(skill): add agent skills loader
Implemented AgentSkillSpec loader that parses SKILL.md frontmatter and
progressive disclosure sections (discover/activate/deactivate) into
structured SkillStep objects with stable 1-based ordering.

Mapped Agent Skills to AgentSkillToolDescriptor with namespaced naming
(namespace/short_name), source="agent_skill", read-only defaults, and
AgentSkillResourceSlot bindings for scripts/, references/, and assets/
directories. All resource slots are unconditionally read_only.

Added explicit validation for missing frontmatter fields (name,
description) and invalid namespace format with actionable error messages.

Added docs/reference/agent_skills.md covering folder layout, SKILL.md
parsing rules, progressive disclosure model, and tool mapping.

Added Behave scenarios covering valid/invalid SKILL.md parsing,
namespaced naming, step ordering, missing frontmatter errors, progressive
disclosure lifecycle, tool mapping, and resource binding slots.

Added Robot Framework integration tests using the deploy-to-staging
example skill folder (robot/agent_skills_loader.robot).

Added ASV benchmarks for parsing throughput, folder load, progressive
disclosure lifecycle, and resource listing
(benchmarks/agent_skills_loader_bench.py).

ISSUES CLOSED: #160
2026-02-26 09:10:35 +00:00

252 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent Skills Standard Loader
The **Agent Skills loader** (`cleveragents.skills.agent_skills_loader`) implements the
[AgentSkills.io](https://AgentSkills.io) standard for packaging instruction-driven,
multi-step workflows as `SKILL.md` files with optional resource sub-directories.
Agent Skills differ from MCP tools: instead of a single function call, they load
procedural *instructions* that an LLM agent follows—potentially invoking multiple
tools in sequence.
---
## Folder Layout
```
my-skill/
├── SKILL.md ← required — frontmatter + instructions
├── scripts/ ← optional — Python/shell scripts the agent may run
│ └── deploy.py
├── references/ ← optional — supplementary Markdown documents
│ └── runbook.md
└── assets/ ← optional — any binary or static resources
└── config.json
```
Only `SKILL.md` is required. The three resource sub-directories are discovered
automatically when present and made available on demand during Tier 3 disclosure.
---
## SKILL.md Format
`SKILL.md` files follow Markdown with a YAML frontmatter block:
```markdown
---
name: local/deploy-to-staging
description: Deploy the current branch to the staging environment.
steps: # optional — structured steps with stable ordering
- Verify all tests pass
- Push the branch to the remote
- Run the CI pipeline
- Execute scripts/deploy.py to deploy
- Check the health endpoint
version: 1.0.0 # optional
compatibility: # optional
min_agent_version: "3.0.0"
metadata: # optional — arbitrary key/value pairs
author: team
environment: staging
allowed-tools: # optional — restrict which tools the skill may call
- builtin/shell
- builtin/git-status
---
# Deployment Details
Additional prose context for each step, loaded on activation.
```
### Required Frontmatter Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Namespaced name: `namespace/short-name` (e.g. `local/deploy-staging`) |
| `description` | `string` | Short human-readable description of the skill |
### Optional Frontmatter Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `steps` | `list[string]` | `[]` | Ordered step instructions parsed into `SkillStep` objects with stable 1-based indexing |
| `version` | `string` | `"0.0.0"` | Semantic version of the skill |
| `compatibility` | `mapping` | `{}` | Compatibility constraints (e.g. `min_agent_version`) |
| `metadata` | `mapping` | `{}` | Arbitrary key/value pairs (author, tags, etc.) |
| `allowed-tools` | `list[string]` | `[]` | Tools the skill is permitted to invoke |
### Structured Steps
When the `steps` key is present, each list entry becomes a `SkillStep` with a
stable 1-based index and non-empty content string:
```python
spec = AgentSkillSpec.from_file(Path("my-skill/SKILL.md"))
for step in spec.steps:
print(f"{step.index}. {step.content}")
# 1. Verify all tests pass
# 2. Push the branch to the remote
# ...
```
Steps are stored in insertion order and never reordered — `step.index` is
always equal to the step's position in the original list.
---
## Namespaced Naming
Skill names follow the same `namespace/short-name` convention as tools and actors
(see [Tool Model](tool_model.md)):
```
local/deploy-to-staging ← user-defined, default namespace
devops/code-review-pipeline ← custom namespace
team/onboarding-workflow ← team namespace
```
Rules:
- Both `namespace` and `short-name` must start with an alphanumeric character.
- Allowed characters: letters, digits, hyphens (`-`), underscores (`_`).
- No spaces or special characters.
---
## Three-Tier Progressive Disclosure
The loader implements the three-tier model defined in the specification
(`docs/specification.md` § AgentSkillAdapter):
| Tier | Method | What loads | Token cost |
|------|--------|-----------|------------|
| **1 — Metadata** | `discover()` | `name` + `description` only | ~50100 tokens per skill |
| **2 — Instructions** | `activate()` | Full `SKILL.md` Markdown body | Recommended < 5000 tokens |
| **3 — Resources** | `list_resources()` | Paths from `scripts/`, `references/`, `assets/` | Variable, on demand |
This means an actor can have dozens of Agent Skills available but only pay the
token cost for their metadata at startup. Full instructions load only when the
LLM determines a task matches the skill.
---
## Tool Mapping
Agent Skills are registered in the Tool Registry with `source="agent_skill"`.
The `AgentSkillToolDescriptor` carries:
| Field | Value | Notes |
|-------|-------|-------|
| `source` | `"agent_skill"` | Fixed |
| `name` | From frontmatter `name` | e.g. `local/deploy-to-staging` |
| `description` | From frontmatter `description` | Used in system prompt |
| `read_only` | `True` | Default — skill itself does not write |
| `writes` | `False` | Default |
| `agent_skill_path` | Absolute folder path | Runtime uses this to locate `SKILL.md` |
| `allowed_tools` | From frontmatter `allowed-tools` | Optional restrict list |
| `body` | Empty at discover; full body after activate | Progressive disclosure |
| `resource_slots` | List of `AgentSkillResourceSlot` | One slot per discovered resource directory |
### Resource Binding Slots
Each discovered sub-directory (`scripts/`, `references/`, `assets/`) becomes a
**read-only resource binding slot** in the tool descriptor:
```python
td = loader.to_tool_descriptor()
for slot in td.resource_slots:
print(slot.name) # "scripts", "references", or "assets"
print(slot.resource_type) # "agent_skill_scripts", etc.
print(slot.access) # "read_only"
print(slot.path) # absolute path on disk
```
Slots are only created for directories that exist — if a skill has no
`references/` folder, no `references` slot is emitted. All slots are
unconditionally `access="read_only"`.
### Read-Only Default Rationale
The skill descriptor is **read-only by default** because the skill's instructions
do not themselves perform writes — the LLM agent decides which (potentially
write-capable) tools to invoke during execution. Individual tools in `allowed-tools`
may write; the skill descriptor's own capability metadata does not.
---
## Python API
```python
from pathlib import Path
from cleveragents.skills.agent_skills_loader import AgentSkillLoader, AgentSkillSpec
# Parse a SKILL.md string directly
spec = AgentSkillSpec.from_string("""
---
name: local/my-skill
description: Does something useful.
---
Step 1: Do this.
""")
# Load a full folder
loader = AgentSkillLoader.from_folder(Path("./skills/my-skill"))
# Tier 1 — discover (metadata only)
descriptor = loader.discover()
print(descriptor.name) # "local/my-skill"
print(descriptor.body) # "" (empty)
# Tier 2 — activate (load instructions)
activated = loader.activate()
print(activated.body) # "Step 1: Do this."
# Tier 3 — list resources on demand
resources = loader.list_resources()
for path in resources:
print(path.name)
# Deactivate to free token budget
loader.deactivate()
print(loader.active_body) # None
# Convert to Tool Registry descriptor
tool_descriptor = loader.to_tool_descriptor()
print(tool_descriptor.source) # "agent_skill"
print(tool_descriptor.read_only) # True
print(tool_descriptor.agent_skill_path) # "/abs/path/to/my-skill"
```
---
## Path Safety
All file paths returned by `list_resources()`, `script_paths`, `reference_paths`,
and `asset_paths` are **absolute paths** that have been verified to reside within
the skill folder. The loader uses `Path.resolve()` + `rglob()` to prevent path
traversal outside the skill boundary.
---
## Error Handling
| Condition | Exception | Message pattern |
|-----------|-----------|-----------------|
| `SKILL.md` not found in folder | `ValueError` | mentions `"SKILL.md"` |
| Folder path does not exist | `FileNotFoundError` | mentions `"not found"` |
| Missing `---` frontmatter | `ValueError` | mentions `"frontmatter"` |
| Missing required `name` field | `ValueError` | mentions `"name"` |
| Missing required `description` field | `ValueError` | mentions `"description"` |
| Invalid `namespace/name` format | `ValueError` | mentions `"namespace"` |
| Malformed YAML in frontmatter | `ValueError` | mentions `"YAML"` |
---
## Related
- [Skills Protocol](skills_protocol.md) — `SkillDefinition`, `SkillMetadata`
- [Skill CLI](skill_cli.md) — `agents skill` commands
- [Tool Model](tool_model.md) — `Tool`, `ToolSource.AGENT_SKILL`
- [MCP Adapter](mcp_adapter.md) — MCP tool source (counterpart)
- `docs/specification.md` § AgentSkillAdapter — authoritative spec
- `ADR-028` — Agent Skills Standard adoption decision