Core (agentskills-core)¶
Registry, provider contract, validation, and shared exceptions.
pip install agentskills-core
agentskills_core ¶
Core runtime model for the Agent Skills format.
This package provides the foundational abstractions for registering,
validating, and accessing Agent Skills <https://agentskills.io>_:
- :class:
SkillProvider-- abstract base class for skill backends. - :class:
Skill-- lightweight runtime handle to a single skill. - :class:
SkillRegistry-- unified index with explicit registration and built-in catalog builder. - :func:
validate_skill-- validates a skill against the specification. - :func:
validate_version-- validates an optional semverversionfield. - :func:
get_logger-- returns a logger in the sharedagentskills.*namespace. - :func:
redact_url-- strips credentials from a URL before it is logged or raised. - :func:
encode_resource_content-- safely encodes resource bytes as tool output. - :class:
SkillNotFoundError-- raised when a skill does not exist. - :class:
ResourceNotFoundError-- raised when a resource within a skill does not exist. - :class:
ResourceListingNotSupportedError-- raised when a provider cannot enumerate a skill's resources. - :class:
DiscoveryNotSupportedError-- raised when a provider cannot enumerate the skills it holds. - :class:
SkillUnavailableError-- raised when a backend is unreachable or fails transiently, as distinct from a skill that does not exist. - :class:
AgentSkillsError-- base class for all library exceptions. - :class:
AgentSkillsError-- base class for all library exceptions.
Install::
pip install agentskills-core
AgentSkillsError ¶
Bases: Exception
Base exception for all Agent Skills library errors.
DiscoveryNotSupportedError ¶
Bases: AgentSkillsError, NotImplementedError
The provider cannot enumerate the skills it holds.
Raised by the default :meth:SkillProvider.discover
<agentskills_core.SkillProvider.discover> implementation, and by
providers whose backend offers no index -- a static HTTP host with
no skill manifest, for instance.
Like :class:ResourceListingNotSupportedError, this is deliberately
not an empty list. A caller told a backend holds no skills
stops looking; a caller told the backend cannot be enumerated falls
back to explicit registration. Check
:attr:SkillProvider.supports_discovery
<agentskills_core.SkillProvider.supports_discovery> to avoid the
exception entirely.
Example::
if provider.supports_discovery:
await registry.register_all(provider)
FastPath
dataclass
¶
A resolved single-skill fast path.
Passed to an integration's fast_path= argument. Holding the
decision in a value rather than re-deriving it means the prompt an
integration injects and the tools it drops cannot disagree about
which skill was chosen.
tokens
instance-attribute
¶
tokens: int
Estimated cost of the body, by the counter the outline uses.
FileAccessNotSupportedError ¶
Bases: AgentSkillsError, NotImplementedError
The provider cannot enumerate and read a skill's original files losslessly.
ResourceListingNotSupportedError ¶
Bases: AgentSkillsError, NotImplementedError
The provider cannot enumerate a skill's resources.
Raised by the default
:meth:SkillProvider.list_resources
<agentskills_core.SkillProvider.list_resources> implementation, and
by providers whose backend offers no way to enumerate -- a static
HTTP host with no manifest, for instance.
This is deliberately not an empty result. "Cannot enumerate"
and "enumerated, found nothing" lead a caller to different
behaviour, so they must be distinguishable. Check
:attr:SkillProvider.supports_resource_listing
<agentskills_core.SkillProvider.supports_resource_listing> to avoid
the exception entirely.
Example::
if provider.supports_resource_listing:
resources = await provider.list_resources("incident-response")
ResourceMedia
dataclass
¶
What a resource is, and whether a model can be shown it.
is_text
instance-attribute
¶
is_text: bool
Whether this should be treated as text needing no special handling.
False for anything whose leading bytes identify a renderable image,
whatever those bytes happen to decode to -- a file that starts
GIF89a is a GIF, and a small one can decode as UTF-8 by
coincidence.
renderable
instance-attribute
¶
renderable: bool
Whether this should be delivered as a native content block.
False for text, for unrecognised binaries, and for images past
:data:DEFAULT_MAX_INLINE_IMAGE_BYTES -- in every case the JSON
envelope remains correct, so a caller can ignore this field and
lose nothing but the improvement.
ResourceNotFoundError ¶
Bases: AgentSkillsError, LookupError
A requested resource does not exist within a skill.
Resources include scripts, assets, and reference documents bundled with a skill.
Raised by get_script, get_asset, and get_reference on
both :class:~agentskills_core.SkillProvider and
:class:~agentskills_core.Skill when the named resource cannot be
found.
Example::
try:
data = await skill.get_reference("missing.md")
except ResourceNotFoundError:
print("Reference not found")
Section
dataclass
¶
One heading of a body, with the text charged to it.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
Stable, addressable identifier, unique within one body. |
title |
str
|
The heading text, or :data: |
level |
int
|
Number of leading |
text |
str
|
The heading line and everything up to the next heading. |
SectionNotFoundError ¶
Bases: AgentSkillsError, LookupError
A requested section does not exist within a skill's body.
Raised by :meth:Skill.get_section
<agentskills_core.Skill.get_section> and
:meth:SkillRegistry.get_skill_section
<agentskills_core.SkillRegistry.get_skill_section> when the given
key does not match any heading. Section keys are derived from the
body, so they change when the body does; the message lists the keys
that are currently valid.
Example::
try:
text = await skill.get_section("rollbck")
except SectionNotFoundError as exc:
print(exc) # names the keys that do exist
SectionRef
dataclass
¶
One line of an outline: what a section is, and what it costs.
Skill ¶
Runtime handle to a single Agent Skill.
All data access is delegated to the backing
:class:~agentskills_core.SkillProvider. The handle itself stores
only the skill name and a provider reference -- no content is
cached, and no execution logic is included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name (must match the |
required |
provider
|
SkillProvider
|
The :class: |
required |
Example::
skill = registry.get_skill("incident-response")
meta = await skill.get_metadata()
print(meta["description"])
body = await skill.get_body()
supports_file_access
property
¶
supports_file_access: bool
Whether the provider can enumerate and read all original files.
supports_resource_listing
property
¶
supports_resource_listing: bool
Whether the backing provider can enumerate this skill's resources.
get_asset
async
¶
get_asset(name: str) -> bytes
Return the raw content of a bundled asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Name of the asset to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the asset does not exist. |
get_body
async
¶
get_body() -> str
Return the markdown instruction body for this skill.
This represents the full skill instructions that an agent reads upon activation -- the content after the YAML frontmatter in the Agent Skills format.
Returns:
| Type | Description |
|---|---|
str
|
Markdown text. |
get_metadata
async
¶
get_metadata() -> dict[str, Any]
Return the parsed YAML frontmatter for this skill.
Always contains the required name and description keys.
May also include optional keys such as license,
compatibility, metadata, and allowed-tools.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary of frontmatter key-value pairs. |
get_outline
async
¶
get_outline() -> SkillOutline
Return the body's sections without returning the body.
Computed on top of :meth:get_body, which providers already
cache, so this costs no extra round-trip on a warm provider and
needs nothing from the :class:~agentskills_core.SkillProvider
interface.
Returns:
| Name | Type | Description |
|---|---|---|
A |
SkillOutline
|
class: |
SkillOutline
|
class: |
|
SkillOutline
|
whole body's estimated size, and whether fetching that |
|
SkillOutline
|
whole body is the cheaper move. |
get_reference
async
¶
get_reference(name: str) -> bytes
Return the raw content of a bundled reference document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Name of the reference to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the reference does not exist. |
get_script
async
¶
get_script(name: str) -> bytes
Return the raw content of a bundled script.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Name of the script to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the script does not exist. |
get_section
async
¶
get_section(key: str) -> str
Return one section of the body, addressed by key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A key from :meth: |
required |
Returns:
| Type | Description |
|---|---|
str
|
The heading line and everything up to the next heading of |
str
|
any level. Sections do not overlap, so a parent does not |
str
|
include its subsections. |
Raises:
| Type | Description |
|---|---|
SectionNotFoundError
|
If no section carries that key. |
list_files
async
¶
list_files() -> list[str]
Return every file path relative to this skill's root, including SKILL.md.
list_resources
async
¶
list_resources() -> dict[str, list[str]]
Return this skill's resource names, grouped by kind.
Check :attr:supports_resource_listing first -- not every
backend can enumerate.
Returns:
| Type | Description |
|---|---|
dict[str, list[str]]
|
Mapping of |
dict[str, list[str]]
|
sorted resource names. |
Raises:
| Type | Description |
|---|---|
ResourceListingNotSupportedError
|
If the provider cannot enumerate resources. |
read_file
async
¶
read_file(path: str) -> bytes
Return the original bytes of a file relative to this skill's root.
SkillFile
dataclass
¶
One captured file with a digest computed from its original bytes.
SkillNotFoundError ¶
Bases: AgentSkillsError, LookupError
A requested skill does not exist.
Raised by :class:~agentskills_core.SkillProvider methods when
the given skill_id cannot be resolved, and by
:meth:SkillRegistry.get_skill <agentskills_core.SkillRegistry.get_skill> when
the skill is not in the index.
Example::
try:
skill = await registry.get_skill("nonexistent")
except SkillNotFoundError:
print("Skill not found")
SkillOutline
dataclass
¶
A body's sections, their cost, and whether they are worth fetching.
A section fetch is not automatically cheaper than a body fetch: it costs a tool call, a model turn, and the tokens of the outline that preceded it. The outline therefore carries the whole-body figure and says plainly which is cheaper, because shipping the split without that guidance makes the common case worse to improve the rare one.
whole_body_is_cheaper
property
¶
whole_body_is_cheaper: bool
Whether the agent should skip sectioning and read the body.
render ¶
render() -> str
Return the outline as text for an agent to read.
One rendering, shared by every integration, so the three cannot drift into giving an agent three different cost estimates for the same skill.
SkillProvider ¶
Bases: ABC
Abstract base class that every skill backend must implement.
A :class:SkillProvider is a pure content accessor -- it serves
skill metadata, body text, and resources by skill ID. Enumerating
the skills a backend holds is an optional capability: see
:meth:discover. Without it, registration is explicit via
:meth:SkillRegistry.register <agentskills_core.SkillRegistry.register>.
Implementations must enforce progressive disclosure: expensive I/O (reading file bodies, fetching resources) should only happen when the corresponding method is called explicitly.
All methods are async to support non-blocking implementations.
Subclass this to back skills with any storage -- filesystem, database,
remote API, etc. Register skills with a
:class:~agentskills_core.SkillRegistry to create a unified skill
catalog.
Example::
class MyProvider(SkillProvider):
async def get_metadata(self, skill_id: str) -> dict: ...
async def get_body(self, skill_id: str) -> str: ...
# ... remaining abstract methods
discover
async
¶
discover() -> list[str]
Return the IDs of every skill this provider can serve.
Optional capability. The default implementation refuses.
Override it and set :attr:supports_discovery to True in
backends that can enumerate.
Implementations return sorted IDs, and return only skills that
appear to exist -- a directory without a SKILL.md is not a
skill. Discovery does not validate: an ID that comes back
here can still fail :func:~agentskills_core.validate_skill,
which is what :meth:SkillRegistry.register_all
<agentskills_core.SkillRegistry.register_all> reports on.
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted skill IDs. An empty list means the backend was |
list[str]
|
enumerated and holds nothing. |
Raises:
| Type | Description |
|---|---|
DiscoveryNotSupportedError
|
If this provider cannot enumerate its skills. Never returns an empty list to signal the same thing -- that would be indistinguishable from an empty backend, and would silently register nothing. |
get_asset
abstractmethod
async
¶
get_asset(skill_id: str, name: str) -> bytes
Return the raw bytes of a single asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name containing the asset. |
required |
name
|
str
|
Name of the asset to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content as bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the asset does not exist. |
get_body
abstractmethod
async
¶
get_body(skill_id: str) -> str
Return the markdown body (instructions) for a skill.
The body contains the skill's full instructions in Markdown format, corresponding to the content after the YAML frontmatter in the Agent Skills format. It should only be loaded when the agent decides to activate the skill (progressive disclosure).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name to look up. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The markdown instruction text. |
Raises:
| Type | Description |
|---|---|
SkillNotFoundError
|
If the skill does not exist. |
get_metadata
abstractmethod
async
¶
get_metadata(skill_id: str) -> dict[str, Any]
Return the parsed YAML frontmatter for a skill.
Per the specification the returned dict will always contain at least
the required name and description keys. Optional keys
include license, compatibility, metadata, and
allowed-tools.
This method should return only the frontmatter metadata -- never the full instruction body -- to keep context usage low during the discovery phase.
The returned dict must not be shared between calls: a caller that mutates it must not affect the next caller. Implementations that cache a parsed dict should return a copy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name to look up. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary of frontmatter key-value pairs. |
Raises:
| Type | Description |
|---|---|
SkillNotFoundError
|
If the skill does not exist. |
get_reference
abstractmethod
async
¶
get_reference(skill_id: str, name: str) -> bytes
Return the raw bytes of a single reference document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name containing the reference. |
required |
name
|
str
|
Name of the reference to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content as bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the reference does not exist. |
get_script
abstractmethod
async
¶
get_script(skill_id: str, name: str) -> bytes
Return the raw bytes of a single script.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name containing the script. |
required |
name
|
str
|
Name of the script to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Raw content as bytes. |
Raises:
| Type | Description |
|---|---|
ResourceNotFoundError
|
If the script does not exist. |
list_files
async
¶
list_files(skill_id: str) -> list[str]
List every original file, including SKILL.md, relative to the skill root.
Optional capability paired with :meth:read_file. Implementations set
supports_file_access and return sorted, unique POSIX relative paths.
The listing must include arbitrary supporting directories, not only the
standard resource kinds. Missing skills raise SkillNotFoundError.
Listing must not read supporting file contents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill whose files should be enumerated. |
required |
Raises:
| Type | Description |
|---|---|
FileAccessNotSupportedError
|
If lossless file access is unavailable. |
list_resources
async
¶
list_resources(skill_id: str) -> dict[str, list[str]]
Return the resource names a skill contains, grouped by kind.
Optional capability. The default implementation refuses.
Override it and set :attr:supports_resource_listing to
True in backends that can enumerate.
Implementations return every key in :data:RESOURCE_KINDS, using
an empty list for a category the skill does not use, so callers
never have to guard on key presence. Names are sorted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill name to enumerate. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, list[str]]
|
Mapping of |
dict[str, list[str]]
|
sorted resource names. |
Raises:
| Type | Description |
|---|---|
ResourceListingNotSupportedError
|
If this provider cannot enumerate resources. Never returns an empty mapping to signal the same thing -- that would be indistinguishable from a skill with no resources. |
SkillNotFoundError
|
If the skill does not exist. |
read_file
async
¶
read_file(skill_id: str, path: str) -> bytes
Read one original file without decoding, truncation, or reconstruction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
The skill containing the file. |
required |
path
|
str
|
A POSIX relative path returned by :meth: |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Original bytes, including frontmatter and line endings for SKILL.md. |
Raises:
| Type | Description |
|---|---|
FileAccessNotSupportedError
|
If lossless file access is unavailable. |
SkillNotFoundError
|
If the skill does not exist. |
ResourceNotFoundError
|
If the path is absent or escapes the skill root. |
SkillRegistry ¶
Unified index over explicitly registered skills.
Skills are added via :meth:register, either one at a time or as a
batch of (skill_id, provider) tuples, or via :meth:register_all
for a provider that can enumerate itself. The registry enforces a
flat namespace: each skill ID must be unique. A :exc:ValueError
is raised if a duplicate is detected.
:meth:register is async because it validates every skill
against the Agent Skills specification before storing it.
:meth:list_skills and :meth:get_skill are synchronous lookups.
:meth:list_skills returns :class:~agentskills_core.Skill
instances sorted by ID.
:meth:get_skills_catalog is async because it fetches metadata from
providers.
__init__ ¶
__init__(*, catalog_concurrency: int = DEFAULT_CATALOG_CONCURRENCY) -> None
Create an empty registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_concurrency
|
int
|
Maximum number of provider metadata
fetches issued in parallel by :meth: |
DEFAULT_CATALOG_CONCURRENCY
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If catalog_concurrency is less than 1. |
get_skill ¶
get_skill(skill_id: str) -> Skill
Return the :class:~agentskills_core.Skill handle by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
Skill name to look up. |
required |
Returns:
| Type | Description |
|---|---|
Skill
|
The registered :class: |
Raises:
| Type | Description |
|---|---|
SkillNotFoundError
|
If no skill with the given name is registered. |
get_skill_outline
async
¶
get_skill_outline(skill_id: str) -> SkillOutline
Return a registered skill's body outline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
Skill name to look up. |
required |
Returns:
| Type | Description |
|---|---|
SkillOutline
|
The skill's :class: |
Raises:
| Type | Description |
|---|---|
SkillNotFoundError
|
If no skill with the given name is registered. |
get_skill_section
async
¶
get_skill_section(skill_id: str, key: str) -> str
Return one section of a registered skill's body.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id
|
str
|
Skill name to look up. |
required |
key
|
str
|
A key from :meth: |
required |
Returns:
| Type | Description |
|---|---|
str
|
The section text. |
Raises:
| Type | Description |
|---|---|
SkillNotFoundError
|
If no skill with the given name is registered. |
SectionNotFoundError
|
If the skill has no section with that key. |
get_skills_catalog
async
¶
get_skills_catalog(*, format: Literal['xml', 'markdown'] = 'xml', tags: Iterable[str] | None = None, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None, max_chars: int | None = None, selection_hints: bool = True, total: int | None = None) -> str
Build a skill-catalog string for system-prompt injection.
Two output formats are supported:
"xml"
An <available_skills> XML block. This is the
recommended format when using Claude or other
Anthropic models.
"markdown"
A human-readable Markdown catalog listing every registered
skill's name and description.
Only name, description and the selection metadata are
extracted from each skill's metadata, keeping token usage low.
The catalog is injected into every system prompt on every turn, so its size is a fixed cost per request. The filters below narrow it, and max_chars caps it::
await registry.get_skills_catalog(
tags=["incident"],
exclude=["deprecated-runbook"],
max_chars=8000,
)
include and exclude match skill IDs and are applied before any metadata is fetched, so narrowing a large registry costs proportionally fewer provider round-trips. tags needs metadata and is applied after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
format
|
Literal['xml', 'markdown']
|
Output format — |
'xml'
|
tags
|
Iterable[str] | None
|
Keep only skills carrying at least one of these tags,
read from the spec's free-form |
None
|
include
|
Iterable[str] | None
|
Allow-list of skill IDs. Only these are considered. An ID that is not registered raises, since an allow-list naming a skill that does not exist silently costs the agent a capability. |
None
|
exclude
|
Iterable[str] | None
|
Deny-list of skill IDs, applied after include and winning over it — a deny-list that another argument can override is not a deny-list. Unregistered IDs are ignored here, because a deny-list is meant to outlive the thing it denies. |
None
|
max_chars
|
int | None
|
Hard ceiling on the length of the returned string, including the truncation note. Whole entries are dropped from the end until the result fits, so the output stays valid and the same arguments always produce the same catalog. Roughly four characters per token is the usual estimate. |
None
|
selection_hints
|
bool
|
Include each skill's |
True
|
total
|
int | None
|
The size of the set these skills were chosen from,
for a caller that has already narrowed the catalog
itself — a ranked subset, say. The catalog then reports
the shortfall the same way it reports truncation, so a
skill that was ranked out is not indistinguishable from
one that was never registered. Defaults to the number
of entries left after this call's own filters, which is
to say no shortfall is reported for them: a static
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
A string ready for insertion into a system prompt. When |
str
|
entries were dropped, the XML root carries |
str
|
|
str
|
closing note; nothing is ever dropped silently, because a |
str
|
catalog that varies without saying so makes agent behaviour |
str
|
non-reproducible. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If format is not |
SkillNotFoundError
|
If include names an unregistered skill. |
list_skills ¶
list_skills() -> list[Skill]
register
async
¶
register(skill_id: str, provider: SkillProvider) -> None
register(skills: list[tuple[str, SkillProvider]]) -> None
register(skill_id_or_skills: str | list[tuple[str, SkillProvider]], provider: SkillProvider | None = None) -> None
Register one or more skills with their providers.
Validates each skill against the Agent Skills specification
using :func:~agentskills_core.validate_skill. This catches
misconfiguration (missing SKILL.md, unreachable endpoint,
invalid metadata) at registration time rather than at first use.
Single skill::
await registry.register("incident-response", provider)
Batch registration::
await registry.register([
("incident-response", fs_provider),
("api-style-guide", http_provider),
])
Batch registration is atomic — if any skill fails validation, none of the skills in the batch are registered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill_id_or_skills
|
str | list[tuple[str, SkillProvider]]
|
Either a single skill ID |
required |
provider
|
SkillProvider | None
|
The :class: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a skill_id is already registered, if a skill fails validation, or if the arguments are invalid. |
register_all
async
¶
register_all(provider: SkillProvider) -> list[str]
Register every skill a provider holds.
Asks the provider to enumerate itself and registers the result, so a folder of thirty skills does not require thirty hard-coded identifiers::
provider = LocalFileSystemSkillProvider(Path("./skills"))
registered = await registry.register_all(provider)
Discovery is an optional capability. Providers that cannot
enumerate raise rather than returning an empty list, so a
misconfigured backend cannot look like an empty one -- check
:attr:SkillProvider.supports_discovery
<agentskills_core.SkillProvider.supports_discovery> first if
the provider may not support it.
Registration is atomic and reports every validation failure at once. Discovering thirty skills and being told only about the first broken one turns a single fix into thirty rounds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
SkillProvider
|
The backend to enumerate and register. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The registered skill IDs, sorted. |
Raises:
| Type | Description |
|---|---|
DiscoveryNotSupportedError
|
If provider cannot enumerate the skills it holds. |
ValueError
|
If any discovered ID is already registered, or if any discovered skill fails validation. Nothing is registered in either case. |
SkillSnapshot
dataclass
¶
An immutable file set captured from one skill.
The source must be trusted and immutable during capture. Repeated reads detect ordinary concurrent changes, but are not a filesystem transaction or a defense against a malicious provider. Later source changes cannot affect the captured files. Digests establish consistency, not publisher trust.
SkillUnavailableError ¶
Bases: AgentSkillsError
A skill backend could not be reached, or failed transiently.
Raised for server-side failures (5xx), rate limiting (429),
timeouts, and connection errors -- anything where the skill may
well exist and the same request could succeed later.
This is deliberately not :class:SkillNotFoundError. Mapping a
503 onto "not found" tells the caller a skill was deleted when
the truth is that a server had a bad minute, which turns a
retryable blip into a permanent-looking failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
Human-readable description. Must not contain
credential material -- see the |
required |
retry_after
|
float | None
|
Server-advised delay in seconds, taken from a
|
None
|
Example::
try:
body = await skill.get_body()
except SkillNotFoundError:
... # genuinely gone; stop asking
except SkillUnavailableError as exc:
... # try again later, optionally after exc.retry_after
build_skill_manifest ¶
build_skill_manifest(snapshot: SkillSnapshot, *, skill_path: str | None = None) -> dict[str, Any]
Build a complete JSON manifest without rewriting any captured bytes.
skill_path is an unescaped server-chosen path ending in the frontmatter
name. By default an alias becomes a prefix, preserving the declared name
as the final segment. This does not establish publisher trust.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot
|
SkillSnapshot
|
The complete original file capture to describe. |
required |
skill_path
|
str | None
|
Optional unescaped canonical path ending in the declared name. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A JSON-compatible skill entry with every author field and file digest. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Frontmatter, file paths, publication limits, or skill_path are invalid. |
capture_skill
async
¶
capture_skill(skill: Skill, *, max_files: int = DEFAULT_SNAPSHOT_MAX_FILES, max_total_bytes: int = DEFAULT_SNAPSHOT_MAX_BYTES) -> SkillSnapshot
Capture and verify a complete file set, failing without a partial result.
Defaults match the MCP Skills extension's per-skill delivery limits. Callers must also bound the number of retained snapshots. Provider reads must enforce their own per-file bounds, as bytes arrive already allocated.
Raises:
| Type | Description |
|---|---|
FileAccessNotSupportedError
|
The skill lacks lossless file access. |
ValueError
|
A limit, listing, or returned file is invalid. |
SkillUnavailableError
|
The source changed during capture. |
classify_resource ¶
classify_resource(name: str, data: bytes, *, max_inline_image_bytes: int = DEFAULT_MAX_INLINE_IMAGE_BYTES) -> ResourceMedia
Report what data is and whether it can be shown to a model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Resource file name. Used only when the bytes are not self-identifying, since a name is a claim and bytes are evidence. |
required |
data
|
bytes
|
Raw resource bytes as returned by a provider. |
required |
max_inline_image_bytes
|
int
|
Ceiling above which an image is
described rather than shown. Use |
DEFAULT_MAX_INLINE_IMAGE_BYTES
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
ResourceMedia
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_inline_image_bytes is negative. |
encode_resource_content ¶
encode_resource_content(name: str, data: bytes, *, max_inline_binary_bytes: int = DEFAULT_MAX_INLINE_BINARY_BYTES) -> str
Encode raw resource bytes as text an LLM can consume.
Valid UTF-8 is returned verbatim. Everything else is returned as a JSON envelope::
{
"name": "architecture.png",
"media_type": "image/png",
"size_bytes": 20481,
"encoding": "base64",
"content": "iVBORw0KGgo..."
}
Binaries larger than max_inline_binary_bytes use the same envelope
with "encoding": "none" and an explanatory note instead of
content, so the agent still learns the resource exists and why it
was withheld.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Resource file name, used to infer the media type. |
required |
data
|
bytes
|
Raw resource bytes as returned by a provider. |
required |
max_inline_binary_bytes
|
int
|
Size ceiling for inlining binary
content. Use |
DEFAULT_MAX_INLINE_BINARY_BYTES
|
Returns:
| Type | Description |
|---|---|
str
|
The decoded text, or a JSON envelope string for binary content. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_inline_binary_bytes is negative. |
Example::
text = encode_resource_content("sev.md", await skill.get_reference("sev.md"))
estimate_tokens ¶
estimate_tokens(text: str) -> int
Estimate the token cost of text at four characters per token.
get_logger ¶
get_logger(module: str) -> logging.Logger
Return the agentskills.* logger for a module.
Pass __name__. The distribution prefix is rewritten so that
agentskills_http.static logs as agentskills.http.static,
giving every package a common namespace root while keeping the
logger name tied to the module it lives in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module
|
str
|
The calling module's |
required |
Returns:
| Type | Description |
|---|---|
Logger
|
A configured :class: |
redact_url ¶
redact_url(url: str, *, relative_to: str | None = None) -> str
Strip credentials from url so it is safe to log or raise.
Query strings, fragments and userinfo are dropped, because that is where credentials actually live: SAS tokens, signed-URL signatures and basic-auth passwords. When relative_to is given the scheme and host are dropped too, leaving only the path beneath that base -- a host is recoverable from configuration, but a full path sitting in a shared log is not something the operator can take back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
The URL to sanitise. |
required |
relative_to
|
str | None
|
Base URL to express the result relative to. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
A string safe to place in a log record or an exception message. |
resolve_fast_path
async
¶
resolve_fast_path(registry: SkillRegistry, *, include: Iterable[str] | None = None, max_tokens: int = DEFAULT_FAST_PATH_MAX_TOKENS) -> FastPath | None
Decide whether registry qualifies for the single-skill fast path.
Returns None -- meaning "use the normal catalog path" -- unless
the effective skill set is exactly one and its body fits under
max_tokens.
include narrows the registry to an effective set, so a registry of
fifty skills that a selector has narrowed to one takes the same path
as a registry that only ever held one. That is the same situation
and the len(list_skills()) == 1 test would miss it.
Both refusals are logged, because an integration silently choosing between two prompt shapes is worse than a slightly slower one.
split_frontmatter ¶
split_frontmatter(raw: str) -> tuple[dict[str, Any], str]
Split SKILL.md content into YAML frontmatter and markdown body.
Frontmatter is the YAML block delimited by --- on its own line
at the very start of the file. If no valid frontmatter is detected
the entire content is returned as the body with an empty dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
str
|
Full text content of a |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A |
str
|
is |
Example::
meta, body = split_frontmatter(Path("SKILL.md").read_text())
print(meta.get("name"))
split_sections ¶
split_sections(body: str) -> list[Section]
Split body at its headings, without overlapping.
A heading owns its own line and everything up to the next heading of any level, so a nested section is not also counted inside its parent. The parts therefore sum to the whole, which is the only property that makes a cost breakdown checkable.
Text before the first heading becomes a level-0 section, and #
inside a fenced code block is a shell comment rather than a heading.
validate_skill
async
¶
validate_skill(skill: Skill) -> list[str]
Validate a single skill against the Agent Skills specification.
Checks that the skill's definition is well-formed and that its frontmatter satisfies all required and optional field constraints.
Validation rules:
- Skill body must be non-empty.
name(required) -- 1-64 characters, lowercase[a-z0-9-], must not start or end with a hyphen, must not contain consecutive hyphens, and must match the skill ID.description(required) -- 1-1024 characters.version(optional) -- valid semver when present. Not yet part of the upstream specification; see :func:validate_version.when_to_use/when_not_to_use(optional) -- lists of at most five non-empty strings of at most 200 characters each. They are charged on every turn, so they are bounded; see :data:SELECTION_FIELDS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skill
|
Skill
|
The :class: |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
A list of human-readable error messages. An empty list means |
list[str]
|
the skill is valid. |
validate_version ¶
validate_version(value: object) -> str | None
Check a frontmatter version value, returning an error or None.
version is not part of the upstream Agent Skills
specification. It is supported here as an optional extension so
that consumers can pin, compare, and detect drift; absence is always
valid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The raw value parsed from YAML frontmatter. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
A human-readable error message, or |
str | None
|
valid semver string. |