Skip to content

Migrating from v1

v2 is a clean break. There are no deprecation shims: the removed APIs are gone in 2.0.

Why

v1 implemented all of Agent Skills itself — discovery, SKILL.md parsing, validation, the catalog, instruction injection, and its own list_skills / load_skill tools. pydantic-ai-harness now ships a Skills capability that does the first five, and Pydantic AI's load_capability does the sixth. Keeping a parallel implementation meant guaranteed drift.

So v2 delegates all of it and keeps only what harness deliberately does not do: remote sources, bundled files, sandboxed script execution, and Python-defined skills. See pydantic-ai-skills and pydantic-ai-harness for the division of labour.

The short version

# v1
from pydantic_ai_skills import SkillsToolset

agent = Agent(model, toolsets=[SkillsToolset(directories=['./skills'])])

# v2
from pydantic_ai_skills import SkillsCapability

agent = Agent(model, capabilities=[SkillsCapability('./skills')])

Requirements

v1 v2
pydantic-ai-slim >=1.105 >=2.38
pydantic-ai-harness not used >=0.28, required
Python >=3.10 >=3.10

harness sets a pydantic-ai-slim>=2.37 floor of its own; this package needs 2.38 for AbstractCapability.visit_and_replace, which SkillsCapability overrides.

Removed, and what replaces it

v1 v2
SkillsToolset SkillsCapability, passed via capabilities=
list_skills tool The deferred-capability catalog, in the prompt
load_skill tool Pydantic AI's load_capability
SkillsDirectory Pass the directory path straight to SkillsCapability
discover_skills() harness discovers; packages.index_libraries() indexes bundled files
parse_skill_md() harness parses; _parsing.parse_skill_md remains internal
validate_skill_metadata() harness validates, always
Skill.from_file() harness loads directory-backed skills
instruction_template= harness renders # Skill: <name>
max_depth= Immediate children only, matching harness
validate= harness always validates
exclude_tools= resources=False / scripts=False
reload(), auto_reload= Construct a new SkillsCapability
defer_loading= on the capability Every skill is individually deferred
SkillRegistry.search/get/install/update SkillRegistry.sync()
SkillRegistry.get_skills() skill_infos() / skill_names()
script_executor= on a registry script_executor= on SkillsCapability
validate= on a registry harness validates

New in v2

LocalSkillsRegistry A local directory that can take part in composition
GitSkillsRegistry.revision() The commit SHA the local clone is on
S3SkillsRegistry.revision(name) The newest object modification time for a skill
require_loaded= Refuse file reads for a skill the model has not loaded
resolve_skill_dir= Substitute ${SKILL_DIR} / ${CLAUDE_SKILL_DIR} in instructions
list_bundled_files= Append the names of a skill's resources and scripts to its instructions
SkillRegistry.__or__ a \| b builds a CombinedRegistry

Walking through the changes

Toolset to capability

# v1
SkillsToolset(
    directories=['./skills'],
    registries=[registry],
    skills=[my_skill],
    include=['pdf'],
)

# v2 — same options, on a capability
SkillsCapability(
    './skills',
    registries=[registry],
    skills=[my_skill],
    include=['pdf'],
)

directories is now the first positional parameter and accepts a single path as well as a list.

Behavioural change: how a skill is loaded

In v1 the model called load_skill('pdf-processing'). In v2 it calls load_capability(id='pdf-processing') — Pydantic AI's own tool, injected automatically when any deferred capability is present.

Nothing needs changing in your skills, but if you have prompts or evals that name load_skill, update them.

Behavioural change: recursive discovery

v1 searched up to max_depth levels for SKILL.md. v2 looks only at a library's immediate children, matching harness. If you relied on nesting:

skills/                    skills/
└── vendor/          →     ├── vendor-pdf/
    └── pdf/               └── vendor-xlsx/
        └── SKILL.md

Or keep the layout and pass each subdirectory as its own library:

SkillsCapability(['./skills/vendor', './skills/internal'])

Behavioural change: name validation

harness validates skill names strictly and rejects a SKILL.md whose frontmatter name disagrees with its directory. v1 only warned. A skill that loaded with a warning in v1 may now raise at construction — the fix is to make the two agree, or drop the name key and let it be derived from the directory.

Registries

# v1
skills = registry.get_skills()
await registry.install('pdf', './installed')
results = await registry.search('pdf')

# v2
library = registry.sync()            # -> Path to a skill library
names = registry.skill_names()
infos = registry.skill_infos()       # name, description, directory

sync() subsumes install and update: it clones or pulls, downloads or re-syncs, and returns the directory. It is synchronous, and safe to call repeatedly.

Cloning is now lazy — v1 cloned in __init__ when auto_install=True; v2 clones on the first sync(). Constructing a registry no longer touches the network.

The composition wrappers changed shape too. They now stage real directories, and a filtered() predicate receives a SkillInfo rather than a full Skill:

# v1
registry.filtered(lambda skill: 'pdf' in skill.name.lower())

# v2
registry.filtered(lambda info: 'pdf' in info.name)

prefixed() and renamed() now rewrite each staged package's frontmatter name, because harness requires it to match the directory.

Script executors

The executor moved from the source to the capability, so one setting covers every skill regardless of where it came from:

# v1
GitSkillsRegistry(url, script_executor=OpenSandboxScriptExecutor())

# v2
SkillsCapability(
    registries=[GitSkillsRegistry(url)],
    script_executor=OpenSandboxScriptExecutor(),
)

Reloading

# v1
toolset = SkillsToolset(directories=['./skills'], auto_reload=True)
toolset.reload()

# v2 — rebuild
def build_agent() -> Agent:
    return Agent(model, capabilities=[SkillsCapability('./skills')])

For a long-lived server, rebuild on a redeploy or a schedule and swap the agent. See Snapshots.

Agent specs

# v1
capabilities:
  - SkillsCapability:
      directories: ['./skills']
      max_depth: 3
      validate: true
      defer_loading: true

# v2
capabilities:
  - SkillsCapability:
      directories: ['./skills']
      include: ['pdf-processing']

max_depth, validate, instruction_template, exclude_tools, auto_reload and defer_loading are no longer accepted. resources, scripts, require_loaded, resolve_skill_dir and list_bundled_files are.

If you were only using level 1 and 2

If your skills are instructions and nothing else — no references/, no scripts/, no remote sources — you do not need this package at all. Use harness directly:

from pydantic_ai_harness import Skills

agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[Skills('./skills')])

That is a smaller dependency and identical behaviour. This package is worth taking on only for what it adds.