cb82fc51df
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
252 lines
8.7 KiB
Markdown
252 lines
8.7 KiB
Markdown
# 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 | ~50–100 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
|