Skill Loading
Progressive skill loading via the view_skill tool — two-phase discovery and on-demand content loading following the agentskills.io specification
Metadata in the prompt, content on demand
The view_skill(name, path?) tool implements progressive skill loading — a two-phase pattern that keeps the agent’s
baseline prompt small while making arbitrarily deep domain knowledge available on demand. It follows the
agentskills.io specification.
When path is omitted, the tool loads SKILL.md (full L2 instructions); when present, it reads the resource at that
path (L3 reference material). One tool covers both “load skill” and “read referenced file” actions, avoiding the L1
token cost of a separate view_skill + read_skill_resource pair. Design rationale: see
Integration Guide.
The tool resides in @zapvol/backend/src/tools/tools/view-skill.tool.ts, with the skill registry in
@zapvol/backend/src/agent/skill/skill-registry.ts. Content flows through an injected SkillStorage port (R2 on
the server, FS on Desktop) — decoupled from the task sandbox.
Two-Phase Loading
Phase 1 — Metadata in Prompt (L1)
At agent startup, the skill registry scans the skills directory and injects a lightweight listing into the system prompt (L1 metadata layer):
### Skills: `view_skill`
Available Skills:
- **market-research**: Systematic market analysis with competitive intelligence
- **code-review**: Structured code review with security and performance checks
- **data-analysis**: Statistical analysis and visualization workflows
Each entry costs ~20–30 tokens. Even with 50 skills, the total L1 overhead stays under 1,500 tokens.
Phase 2 — Full Content on Demand
When the agent recognizes a user task matching a skill’s domain, it calls view_skill(name="market-research") (path
omitted). The tool reads the full SKILL.md content via SkillStorage and returns it as the tool result. The agent then
follows the skill’s instructions; if the skill body references references/x.md, the agent calls
view_skill(name="market-research", path="references/x.md") to fetch it on demand.
This deferred loading means only activated skills consume context window space.
Skill Directory Structure
Each skill is a {name}/ entry in the skill storage (the FS backend lays it out as a directory):
skills/
market-research/
SKILL.md ← Entry point (frontmatter + instructions)
references/
frameworks.md ← Additional reference files
templates.md
code-review/
SKILL.md
SKILL.md Format
---
name: market-research
description: Systematic market analysis with competitive intelligence
description_zh: 系统化市场分析与竞争情报
compatibility: ">=1.0"
allowed-tools: tavily_search exa_company_search filesystem
min_tier: lite
metadata:
zapvol:
dependencies:
python:
- pandas
- matplotlib
---
## Instructions
Step-by-step skill instructions here...
Frontmatter Fields
| Field | Required | Purpose |
|---|---|---|
name | Yes | Must match directory name; kebab-case, max 64 chars |
description | Yes | English description shown in L1 listing |
description_zh | No | Chinese description variant |
compatibility | No | Version compatibility range |
allowed-tools | No | Space-separated tool names this skill may use |
min_tier | No | Minimum model tier required ("lite" or "ultra") |
metadata.zapvol.dependencies | No | Declared package deps (python / node arrays) — parsed into metadata, not auto-installed |
SkillRegistry
The skillRegistry singleton manages skill discovery, caching, and loading:
Discovery (Phase 1)
On first access to a scope, listMetas() / listMetasAcross() call storage.listSkills(scope), parse SKILL.md
frontmatter via gray-matter, validate the metadata (name format, length limits, name-directory match), and cache the
results. Subsequent access returns from cache.
Loading (Phase 2)
readFile(scope, name, relPath) reads through SkillStorage, returning { content, path } | null. When relPath is
omitted it loads SKILL.md and strips the frontmatter, returning only the body; with a relPath it returns the raw
resource content.
Path traversal is rejected as a SkillStorage contract invariant — each storage backend enforces it, so normalized
paths containing .. or absolute paths never resolve.
Compaction
When context compression occurs, the output of view_skill path-omitted calls (which can be large — full skill
instructions) is compacted to metadata only:
| Before | After |
|---|---|
| Full skill content (500–2,000 tokens) | { skillName, path, totalChars, totalLines } (~20 tokens) |
The compacted form tells the LLM what skill was loaded without preserving the full instructions. If the agent needs the
instructions again, it can call view_skill(name) once more.
Calls with a path argument (L3 resource reads) are out of scope for this mechanism — they are treated as ordinary
file-read tool results and handled by the generic compaction strategy.
Client Output Reduction
For the client stream, the full skill content is replaced with a simple confirmation:
toClientOutput: (output) => ({
skillName: output.skillName,
path: output.path,
content: "Skill activated.",
});
This prevents large skill files from bloating the client-side message payload.
Integration Points
| System | How view_skill integrates |
|---|---|
| Prompt (L1) | instructions() injects the skill listing (no longer gated by sandbox.skillsDir) |
| Tool Registry | Dynamic input schema: name enum from the listed skills, path optional |
| Compaction | compact() reduces path-omitted call output to metadata summary |
| Client Stream | toClientOutput() returns “Skill activated” stub |
| SkillStorage | Files read through SkillStorage.readFile(scope, name, relPath) — R2 / FS backends |