Skills¶
pkg/agent/skills/ and pkg/agent/skilltoolset/ implement
agentskills.io-spec progressive-disclosure
skills — folder-shaped capability bundles that an agent can discover
and load on demand. Each skill is a SKILL.md (YAML frontmatter +
Markdown body) plus optional references/, assets/, and scripts/
directories. The agent sees only the L1 metadata (name +
description) up front; when the LLM judges a skill relevant, it
calls load_skill to read the full instructions,
list_skill_resources to enumerate the skill's files, and
load_skill_resource to fetch a specific one. This keeps system
prompts small while giving the agent access to a much larger set of
specialised playbooks.
What it does¶
A skill on disk looks like:
skills/
pdf-extraction/
SKILL.md # frontmatter + instructions
references/
formats.md
assets/
template.json
scripts/
run.sh
SKILL.md opens with YAML frontmatter:
---
name: pdf-extraction
description: Extract structured tables from scanned PDF financial reports.
license: MIT
compatibility: helix-1.x
allowed-tools:
- fs.read
- shell.exec
metadata:
owner: search-team
---
# PDF extraction skill
Step 1 — call `fs.read` on the input path…
A skills.Source provides storage abstraction: list frontmatters,
load instructions, list and load resources. The SkillToolset
(pkg/agent/skilltoolset) wraps a Source as three auto-generated
tools:
list_skills— returns L1 metadata (names + descriptions) for every skill in the source. (At Run start, the agent also emits askills_registeredevent carrying the same metadata so Studio shows the catalogue upfront.)load_skill— loads a skill's full SKILL.md body. Activates the skill in the per-runActiveSkillsTracker, granting anyallowed-toolsin its frontmatter pre-approval for the rest of the run. Its result also carries aresourcesmanifest — the exact file paths in the skill — so the common case needs no separate discovery call.list_skill_resources— enumerates a skill's files (references//assets//scripts/…) so the LLM loads real paths instead of guessing. Takesskill_nameand an optionalsubpath(e.g.references) to scope the listing;.or an omitted subpath lists everything. WrapsSource.ListResources.load_skill_resource— reads a specific file under the skill'sreferences//assets//scripts/subtree.
The system prompt picks up a fixed instruction block telling the
LLM how to use these four tools — including that it must discover
real paths via list_skill_resources (or the load_skill
manifest) before reading a file, and that load_skill_resource is
the ONLY way to view skill files (not the regular file tools).
Implementations¶
Sources¶
FileSystemSource¶
NewFileSystemSource(fs.FS) — reads skills from an fs.FS (a real
filesystem rooted at skills/, an embedded embed.FS, or any
in-memory FS). Validates the frontmatter on every list / load and
rejects skills with the wrong layout (missing SKILL.md, bad name).
Resource paths are sandboxed: .. traversal is rejected, and only
paths under references/, assets/, and scripts/ are reachable.
InlineSource¶
NewInlineSource([]InlineSkill) — same Source surface but reads
from in-memory data. Use this when skills are declared inline in
YAML or generated at startup. Validates frontmatter and resource
paths at construction, so misconfigured inline skills fail fast at
workspace load rather than at first use.
MergedSource¶
NewMergedSource(s1, s2, …) — composes multiple Sources into one.
Duplicate skill names error on first list. Useful for mixing a
filesystem source with an inline source (the workspace's checked-in
skills + ones generated at start).
ActiveSkillsTracker¶
Per-run state held on the context. load_skill activates a skill;
allowed-tools from its frontmatter then pre-approve those tool
names — they skip human approval even when the agent's
approval_policy: always or tagged would otherwise require it.
The tracker is fresh on every agent.Run, so concurrent runs don't
bleed permissions into each other.
Public methods:
Activate(name, allowedTools)— register a successful load.IsToolAllowedByActiveSkill(toolName)— runtime check the agent uses insiderequiresApprovalCtx.ActiveSkills()— names of currently active skills.SkillsAllowingTool(toolName)— which active skills granted a given tool (for "approved by X" trace events).
Frontmatter schema¶
type Frontmatter struct {
Name string `yaml:"name"` // required, kebab-case, 1-64 chars
Description string `yaml:"description"` // required, 1-1024 chars
License string `yaml:"license,omitempty"`
Compatibility string `yaml:"compatibility,omitempty"`
Metadata map[string]string `yaml:"metadata,omitempty"`
AllowedTools []string `yaml:"allowed-tools,omitempty"`
}
Validate(fm) checks name shape, description bounds, etc.
Parse(r) reads the leading frontmatter from a bufio.Reader and
leaves it positioned at the start of the markdown body.
Sentinel errors¶
ErrInvalidSkillName, ErrInvalidFrontmatter, ErrSkillNotFound,
ErrDuplicateSkill, ErrInvalidResourcePath, ErrResourceNotFound
— use errors.Is to distinguish source error classes.
skilltoolset.SkillToolset¶
ts, err := skilltoolset.New(ctx, skilltoolset.Config{
Source: fsSource,
SystemInstruction: "", // empty = use the default instruction block
})
for _, t := range ts.Tools() { agentTools.Register(t) }
agentSystemPrompt += ts.SystemInstruction()
agentSystemPrompt += "\n" + ts.SkillsCatalogXML(ctx)
SystemInstruction() returns the default block telling the LLM:
If a skill seems relevant, you MUST use
load_skillwithname="<SKILL_NAME>"to read its full instructions before proceeding. Useload_skill_resourceto view files within a skill's directory.
SkillsCatalogXML(ctx) returns L1 metadata as an XML block that
the reifier appends to the system prompt at construction time. Without
it, the LLM would have to call list_skills on every turn just to
discover what's available. SkillsCatalogJSON(ctx) is the same data
shaped for machine consumption (debug events, IDE rendering).
Agent integration¶
When AgentDefinition.Skills is non-empty, the reifier:
- Resolves each skill ID against the registered Sources.
- Constructs a merged
Sourcecovering all matched skills. - Wires a
SkillToolsetand registers its three tools on the agent'stool.Registry. - Appends the
SystemInstructionandSkillsCatalogXMLto the agent's system prompt. - Forwards
SkillsCatalogJSONto the agent so it can emit askills_registeredevent at the start of eachRun.
At runtime:
load_skill→ activates the skill in the tracker → agent emits a typedskill_loadedevent.load_skill_resource→ emits askill_resource_loadedevent with both skill ID and resource path.- Subsequent tool calls consult
IsToolAllowedByActiveSkillto bypass approval for tools the skill pre-approved.
Configuration¶
YAML reference:
agents:
- id: doc-processor
type: react
skills:
- pdf-extraction
- invoice-parsing
- email-summarisation
approval_policy: tagged
approval_categories: [external_write]
skills: is a list of skill IDs. The reifier resolves these
against the workspace's skills/ directory (plus any registered
inline sources). When the agent runs:
- All four skill tools (
list_skills,load_skill,list_skill_resources,load_skill_resource) are auto-registered alongside any other tools you've declared. - The system prompt is auto-augmented with the catalogue and the usage instructions.
- If a loaded skill's frontmatter declares
allowed-tools: [shell.exec]and the agent hasapproval_policy: taggedwithexternal_writematching shell, calls toshell.execafterload_skillproceed WITHOUT approval — the skill has vouched for them.
Behavior notes¶
allowed-toolspre-approval is per-RUN, not per-AGENT. Each call toagent.Runstarts with an empty tracker; the LLM must re-load_skillfor the next run if it needs the same tool bypass.- Skill names must be kebab-case (
^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$) and unique within the merged Source. Duplicates fail at construction. - Resource paths are scoped —
load_skill_resourcerejects absolute paths,..traversal, and anything outsidereferences/,assets/,scripts/. Path validation runs insafeRelativePath. - The LLM should NOT use generic file tools (
fs.read) on skill files — the toolset's system instruction explicitly tells it to useload_skill_resourceso the tracking and sandboxing apply. SkillsCatalogonAgentConfigis the JSON form of the L1 catalogue. When the reifier wires a SkillToolset, it sets this field; the agent emits it as askills_registereddebug event upfront so Studio's debugger can render the catalogue before the LLM has had a chance to calllist_skills.
Example¶
A workspace with two filesystem skills:
agents:
- id: extractor
type: react
model: { provider: anthropic, model: claude-sonnet-4-7 }
system_prompt: "You extract structured data from documents."
skills: [pdf-extraction, csv-cleanup]
tools:
- { name: read-file, type: function, function: fs.read }
- { name: shell-exec, type: function, function: shell.exec,
categories: [external_write] }
approval_policy: tagged
approval_categories: [external_write]
When the agent receives "Pull the totals from invoices.pdf", the
LLM sees the L1 catalogue, decides pdf-extraction is relevant,
calls load_skill name="pdf-extraction", reads the instructions
(which say to call shell.exec for pdftotext), and proceeds —
the shell call is auto-approved because the skill listed
shell.exec under allowed-tools.
Source¶
pkg/agent/skills/source.go—Sourceinterface.pkg/agent/skills/frontmatter.go—Frontmatter,Parse/Validate/Build, sentinel errors.pkg/agent/skills/filesystem_source.go—FileSystemSource,safeRelativePath.pkg/agent/skills/inline_source.go—InlineSource,InlineSkill.pkg/agent/skills/merged_source.go—MergedSource.pkg/agent/skills/active.go—ActiveSkillsTracker,WithActiveSkillsTracker.pkg/agent/skilltoolset/toolset.go—SkillToolset,SkillsCatalogXML,SkillsCatalogJSON.pkg/agent/skilltoolset/internal/skilltool/— the three tool factoriesListSkills,LoadSkill,LoadSkillResource, plusSkillsToXML.pkg/agent/adk/agent.go—emitSkillsRegistered,emitSkillLoaded,emitSkillResourceLoaded, therequiresApprovalCtxhook.pkg/dsl/reifier.go— Skill resolution + toolset wiring.
See also¶
- tools — the underlying tool framework; skills
register as ordinary
tool.Tools. - react — the loop that emits
skill_loadedetc. - guardrails — skills'
allowed-toolspre-approve calls without weakening guardrails.