aimu.skills¶
Filesystem-discovered agent skills, plus runtime skill authoring. See how-to: build a personal assistant for the self-improvement pattern.
aimu.skills.AgentSkill
dataclass
¶
AgentSkill(name: str, description: str, path: Path, compatibility: str = '', license_info: str = '', metadata: dict = dict(), allowed_tools: tuple[str, ...] = ())
A single discovered Agent Skill from the filesystem.
The fields mirror the Agent Skills specification's frontmatter, with two renames where the
spec's key is not a usable Python attribute: license becomes license_info and
allowed-tools becomes allowed_tools.
script_tool_names ¶
Return the :func:script_tool_name for every .py / .sh in scripts/.
Scripts whose names collapse to the same tool name are listed once, first sorted path
winning, matching the skills-server registration. That covers foo.py / foo.sh
(.py sorts first) and foo-bar.py / foo_bar.py (the hyphen sorts first).
aimu.skills.SkillManager ¶
Discovers and manages Agent Skills from the filesystem.
With no skill_dirs argument, scans the standard search paths at project and
user scope: .agents/skills/, .claude/skills/, ~/.agents/skills/,
~/.claude/skills/. Project-level paths win on name collision. Pass explicit
skill_dirs to override all defaults.
Discovery logs (at INFO) the number of skills found and the paths searched, so a
missing skill directory is easy to spot. Malformed SKILL.md files, and files that
violate the Agent Skills specification, raise :class:SkillLoadError rather than
being silently skipped.
include narrows discovery to the named skills, so a host giving one agent a subset
does not have to filter in three places: :meth:catalog_prompt and the skills server
both read :attr:skills. A name in include that no search path provides raises,
because the alternative is an agent quietly holding fewer skills than it asked for.
Usage::
manager = SkillManager() # auto-discover
manager = SkillManager(skill_dirs=["/path/to/skills"]) # explicit
manager = SkillManager(include=["pdf-processing"]) # only these
print(manager.catalog_prompt())
body = manager.get_skill_body("pdf-processing")
refresh ¶
Invalidate the cache and re-discover skills, returning the new map.
Lets a skill authored at runtime (see :func:aimu.skills.write_skill) become
visible mid-run without constructing a fresh manager.
catalog_prompt ¶
Return an XML skill catalog suitable for injection into a system prompt.
Each entry lists the skill name, description, and any script-derived tool names
the model can call directly (without first calling activate_skill).
get_skill_body ¶
Return the full instructions body of a named skill.
Raises :class:SkillNotFoundError if the skill doesn't exist.
aimu.skills.SkillLoadError ¶
Bases: ValueError
Raised when a SKILL.md file is malformed and cannot be parsed.
aimu.skills.SkillNotFoundError ¶
Bases: KeyError
Raised when a requested skill name does not exist.
aimu.skills.build_skills_server ¶
Build an in-process FastMCP server from a SkillManager.
Registered tools
- activate_skill(name): returns the full SKILL.md body for the named skill
- {skill}__{stem}(args=""): runs a Python or shell script from a skill's scripts/ dir, named by aimu.skills.skill.script_tool_name (both halves slugified)
The returned FastMCP instance can be passed directly to MCPClient(server=...).
aimu.skills.script_tool_name ¶
Return the tool name a skill's script is registered under: {skill}__{stem}, slugified.
The single source of this name. Three surfaces need it and must agree, because the catalogue
is what the model reads: :meth:AgentSkill.script_tool_names advertises it,
:func:aimu.skills.mcp.build_skills_server registers it, and add_skill_script reports it
back after writing a script. A name advertised but not registered is a tool call that cannot
succeed, so they are built here rather than formatted in three places.
Because both halves are slugified, two stems that differ only in their separator
(backup-db and backup_db) map to one tool name; callers dedupe on the result.
Authoring¶
aimu.skills.write_skill ¶
write_skill(name: str, description: str, body: str, *, skills_dir: Union[str, Path], overwrite: bool = False, metadata: Optional[dict] = None, scripts: Optional[dict[str, str]] = None) -> Path
Write a new SKILL.md under skills_dir/<name>/ and return its path.
The file carries YAML frontmatter (name, description, optional metadata)
followed by the markdown body, matching the format
:class:~aimu.skills.manager.SkillManager discovers.
Validates that name is a slug (lowercase-with-hyphens, no path separators, which
also prevents traversal) and that description is non-empty. Refuses to overwrite an
existing skill unless overwrite=True. The written file is round-tripped through the
manager parser, so an authored skill is guaranteed discoverable (a parse failure raises
:class:~aimu.skills.manager.SkillLoadError).
scripts maps "<slug>.py" / "<slug>.sh" filenames to source, written into
scripts/ (each becomes a {skill}__{stem} tool). .sh files are marked executable.
aimu.skills.make_skill_authoring_tool ¶
Return an async @tool that authors a skill and refreshes manager.
The tool writes a new SKILL.md under skills_dir via :func:write_skill, then
calls :meth:SkillManager.refresh so the skill is discoverable in the same run. Both
manager and skills_dir are captured by closure (no module globals).
Note: after refresh, activate_skill (and any fresh-conversation catalog rebuild) will
surface the new skill, but a skill catalog already injected into an in-flight system
prompt is not retroactively updated. See :class:~aimu.aio.SkillAgent.