ExAgent.Skills (ExAgent v2.2.1)

View Source

Skills that the model loads on demand (progressive disclosure).

An agent with many specialised procedures should not send all of them on every request. With skills, the model sees a short catalog (name + description) and loads a skill's full instructions only when a task needs it:

skills = ExAgent.Skills.from_dir!(Application.app_dir(:my_app, "priv/skills"))
agent = ExAgent.new(model: model, instructions: "You are...", skills: skills)

ExAgent.new/1 with :skills adds:

  • a load_skill tool whose description lists the catalog. Its result (the skill instructions, plus its file list and newly available tools) becomes an ordinary tool return in the history, so a loaded skill stays in context across steps, ExAgent.Server turns, snapshots and continuations;
  • a read_skill_file tool, when a skill has a directory, to read files such as references/forms.md relative to that directory, never outside it;
  • every skill's :tools and, when there are any, an ExAgent.Skills.Gate before the agent's capabilities that offers them only once their skill has been loaded;
  • ExAgent.Skills.Restore after the agent's capabilities, which restores loaded skills that a compaction capability removed from the request.

Keeping the catalog in the tool definition leaves :instructions and the history unchanged, and lets providers cache it with the other tool definitions. Changing the skill set changes that definition, like changing any tool. Without :skills, agents are unaffected.

Directory layout

priv/skills/
  release-notes/
    SKILL.md
    references/style.md
  incident-report/
    SKILL.md

A SKILL.md starts with YAML frontmatter:

---
name: release-notes
description: Write release notes. Use when asked to summarise changes for a release.
---
# Release notes
1. Read references/style.md ...

This is the format used by other agent harnesses, so the same skills work there. See ExAgent.Skill for fields and validation rules.

Tools gated by a skill

skills = ExAgent.Skills.from_dir!(dir, tools: %{"deploy" => DeployTools.tools()})

The deploy tools are omitted from requests until deploy is loaded. They stay in the agent inventory, so persisted continuations restore them as usual. A tool listed both in :tools and in a skill is gated. The gate controls what the model is offered; it is not an authority boundary.

Composing by hand

tools/2 and capabilities/1 return the pieces ExAgent.new/1 adds, for example to place a capability that measures the final request after :restore. Keep :gate before capabilities that restrict function_tools (so they keep the final say) and :restore after any compaction capability.

Skills only add instructions and tools; they do not grant permissions or execute skill scripts.

Summary

Functions

Build the capabilities added for skills

Read every immediate subdirectory of root that contains a SKILL.md.

Names of the skills successfully loaded in messages, each once, ordered by its most recent load.

Build the tools added for skills: load_skill, read_skill_file when any skill has a :path, then each skill's gated tools (deduplicated).

Validate a skill list for an agent: valid skills, unique names, and gated tools whose names are unique.

Functions

capabilities(skills)

@spec capabilities([ExAgent.Skill.t()]) :: %{
  gate: ExAgent.Skills.Gate.t() | nil,
  restore: module()
}

Build the capabilities added for skills:

from_dir(root, opts \\ [])

@spec from_dir(Path.t(), keyword()) :: {:ok, [ExAgent.Skill.t()]} | {:error, term()}

Read every immediate subdirectory of root that contains a SKILL.md.

Skills are returned sorted by name. Directories without SKILL.md and hidden directories are ignored. An invalid skill, a duplicate name or an unreadable root fails the whole call.

Options:

from_dir!(root, opts \\ [])

@spec from_dir!(Path.t(), keyword()) :: [ExAgent.Skill.t()]

Like from_dir/2, raising ArgumentError on error.

loaded(messages)

@spec loaded([ExAgent.Message.t()]) :: [String.t()]

Names of the skills successfully loaded in messages, each once, ordered by its most recent load.

Useful to show which skills a conversation used, for example ExAgent.Skills.loaded(ExAgent.Server.history(server)).

tools(skills, opts \\ [])

@spec tools([ExAgent.Skill.t()], keyword()) :: [ExAgent.Tool.t()]

Build the tools added for skills: load_skill, read_skill_file when any skill has a :path, then each skill's gated tools (deduplicated).

Options: :max_file_bytes (default 256 KiB) bounds both the skill instructions returned by load_skill and files read by read_skill_file.

validate(skills)

@spec validate([ExAgent.Skill.t()]) :: :ok | {:error, term()}

Validate a skill list for an agent: valid skills, unique names, and gated tools whose names are unique.