ISSUES CLOSED: #176
13 KiB
Skill Registry
The Skill Registry provides persistent storage for skill definitions in
CleverAgents. Skills are namespaced, reusable collections of tools
registered via YAML config and persisted to the skills and
skill_items database tables.
Registration Behavior
Skills are registered with a namespaced name (namespace/short_name)
as the unique identifier. The registry enforces:
- Name uniqueness: Duplicate names are rejected with
DuplicateSkillError. - Name validation: Names must match
^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$. - Cascading deletes: Removing a skill cascades to all child
skill_itemsrows.
Skill Items
Each skill stores its components as ordered items in the skill_items
table. The item_type discriminator distinguishes:
| item_type | Description |
|---|---|
tool_ref |
Reference to a named tool in the registry |
include |
Recursive inclusion of another skill |
inline_tool |
Anonymous tool defined inline |
mcp_source |
MCP server tool source |
agent_source |
Agent Skills Standard folder source |
Items are stored with a stable item_order to preserve definition
ordering across round-trips.
Filters
The SkillRegistryService.list_skills() method supports:
- Namespace filter: Pass
namespace="local"to list only skills in thelocalnamespace.
Service API
from cleveragents.application.services import SkillRegistryService
from cleveragents.infrastructure.database.repositories import SkillRepository
svc = SkillRegistryService(skill_repo=SkillRepository(session_factory))
# Register
svc.add_skill(skill)
# Retrieve
skill = svc.get_skill("local/code-tools")
# List with filter
skills = svc.list_skills(namespace="local")
# Update
svc.update_skill(updated_skill)
# Remove
svc.remove_skill("local/code-tools")
Agent Skills Discovery
The Skill Registry integrates with the Agent Skills Standard —
filesystem-based tool bundles identified by a SKILL.md file with
YAML front-matter.
Configuration
The skills.agent_skills_paths config key controls which directories
are scanned. Multiple paths are separated by commas:
# ~/.cleveragents/config.toml
[skills]
agent_skills_paths = "/home/user/.cleveragents/agent_skills,/opt/skills"
The default path is ~/.cleveragents/agent_skills.
Environment variable override:
CLEVERAGENTS_SKILLS_AGENT_SKILLS_PATHS.
Discovery Algorithm
- Parse the comma-separated
skills.agent_skills_pathsvalue. - For each directory, find immediate subdirectories containing a
SKILL.mdfile. - Parse the YAML front-matter (between
---fences) to extract the toolname,description, and optionalinput_schema/output_schema. - Construct
ToolSpecinstances withsource="agent_skills"andsource_metadatacontaining the filesystem path. - Register each tool in the
ToolRegistryunder theagent_skills/<name>namespace.
SKILL.md Format
---
name: my-tool
description: A custom agent skill
input_schema:
type: object
properties:
query:
type: string
---
# My Tool
Additional documentation here.
Conflict Handling
When an Agent Skill name collides with an existing tool in the
ToolRegistry, the discovery process records a DiscoveryConflict.
The caller decides the strategy:
| Strategy | Behavior |
|---|---|
skip |
Keep existing tool, log warning (default) |
error |
Raise ValueError with collision details |
replace |
Remove existing tool and register the new one |
Refresh Hook
The SkillRegistryService.refresh_agent_skills() method re-scans all
configured paths. It first removes previously registered agent skill
tools, then re-discovers and re-registers. The CLI agents skill tools
command accepts a --refresh flag to trigger this.
CLI Output
The agents skill tools command includes source metadata when tools
originate from Agent Skills:
agents skill tools local/devops-toolkit --format json
Each entry in the output includes a "source" field indicating the
tool's origin (builtin, agent_skills, mcp, or custom).
Service API (Discovery)
from cleveragents.application.services import SkillRegistryService
from cleveragents.infrastructure.database.repositories import SkillRepository
from cleveragents.tool.registry import ToolRegistry
tool_registry = ToolRegistry()
svc = SkillRegistryService(
skill_repo=SkillRepository(session_factory),
tool_registry=tool_registry,
)
# Initial discovery
result = svc.discover_and_register(
"~/.cleveragents/agent_skills,/opt/skills"
)
print(f"Discovered: {len(result.discovered)} skills")
print(f"Conflicts: {len(result.conflicts)}")
# Refresh (re-scan)
result = svc.refresh_agent_skills(
"~/.cleveragents/agent_skills,/opt/skills"
)
Database Schema
skills
| Column | Type | Description |
|---|---|---|
| name | String(255) | PK, namespaced name |
| namespace | String(100) | Extracted namespace |
| short_name | String(150) | Extracted short name |
| description | Text | Skill description |
| version | String(50) | Optional version string |
| metadata_json | Text | JSON metadata (overrides, etc.) |
| created_at | String(30) | ISO-8601 creation timestamp |
| updated_at | String(30) | ISO-8601 update timestamp |
skill_items
| Column | Type | Description |
|---|---|---|
| id | Integer | Auto-increment PK |
| skill_name | String(255) | FK to skills.name (CASCADE) |
| item_type | String(30) | Discriminator (tool_ref, etc.) |
| item_name | String(500) | Item identifier or name |
| item_config | Text | Optional JSON configuration |
| item_order | Integer | Stable ordering index |
| created_at | String(30) | ISO-8601 creation timestamp |
Flattening Rules
The SkillResolver flattens a skill's tool set using the following
deterministic algorithm:
Include Order
Tools are collected via depth-first traversal of includes:
- For each include (left-to-right), recurse into the included skill
first, processing its includes before its own
tool_refs. - After all includes are resolved, add the current skill's
tool_refsin definition order. - Add inline tools as
{skill_name}/_anon_{index}. - Add MCP tools as
mcp:{server}/{tool_name}. - Add agent skills as
agent_skill:{path}.
De-Duplication
When the same tool name appears multiple times (from overlapping includes), the first occurrence determines the position in the output list, but the last occurrence's entry metadata wins (last-wins semantics).
Override Merging
Overrides are applied at two levels:
- Skill-level overrides (
skill.overridesdict): Applied to matchingtool_refsduring resolution. - Per-include overrides (
SkillInclude.overrides): Applied to all tool entries contributed by the included skill. Uses shallow merge -- include overrides are merged on top of any existing entry overrides.
Non-Overridable Fields
The following ResolvedToolEntry fields cannot be overridden:
namesource_skillis_inline
Attempting to override these raises ValueError with the skill name
and include path for traceability.
Capability Summary
The SkillCapabilitySummary aggregates capability metadata across all
resolved tools:
| Field | Description |
|---|---|
total_tools |
Total number of resolved tool entries |
read_only_tools |
Count of inline tools with read_only=True |
write_tools |
Count of inline tools with writes=True |
checkpointable_tools |
Count of inline tools with checkpointable=True |
has_side_effects |
True if any inline tool has side effects |
mcp_sources |
Number of MCP server sources on the root skill |
agent_skill_sources |
Number of agent skill sources on the root skill |
validate_plan() Usage
The SkillRegistry.validate_plan() method checks that a plan's skill
references are satisfiable:
from cleveragents.skills.registry import SkillRegistry
registry = SkillRegistry()
# ... register skills ...
errors = registry.validate_plan({"skills": ["local/code-tools", "local/deploy"]})
if errors:
for err in errors:
print(f"Plan validation error: {err}")
Checks Performed
- All referenced skills exist in the registry.
- All includes are resolvable (no missing skills in include chains).
- No cycles in include chains.
- Tool refs are valid (if a tool registry is configured).
Error Messages
| Condition | Error message pattern |
|---|---|
| Missing skill | Skill '{name}' referenced in plan is not registered |
| Missing include | Skill '{name}': Included skill '{inc}' not found ... |
| Cycle detected | Skill '{name}': Cycle detected in skill includes: A -> B |
| Non-overridable field | Non-overridable field(s) ... in overrides from skill ... |
tools() Method
The SkillRegistry.tools() method combines resolve_tools() and
compute_capability_summary() into a single call:
entries, summary = registry.tools("local/code-tools")
print(f"Total tools: {summary.total_tools}")
print(f"Has writes: {summary.write_tools > 0}")
Returns a 2-tuple of (list[ResolvedToolEntry], SkillCapabilitySummary).
Testing
Behave BDD Tests
The Behave feature file features/skill_registry.feature contains 23
scenarios covering the full SkillRegistryService and SkillRepository
persistence layer:
| Area | Scenarios | Description |
|---|---|---|
| Registration | 6 | Each skill item type (tool_ref, include, inline_tool, mcp_source, agent_source) plus basic round-trip |
| Duplicate rejection | 1 | DuplicateSkillError on name collision |
| Retrieval | 2 | Get by name, non-existent returns None |
| Listing | 2 | List all, list with namespace filter |
| Update | 2 | Description change, non-existent raises SkillNotFoundError |
| Deletion | 2 | Remove existing (cascades items), remove non-existent returns False |
| Overrides | 1 | Override metadata survives persistence round-trip |
| Item ordering | 1 | Mixed item types maintain stable order across round-trip |
| Name validation | 3 | Empty name, no-slash name, special characters rejected at persistence layer |
| Large payload | 1 | 50 tool refs persist correctly |
| Domain objects | 1 | Listed skills are Skill domain instances |
Step definitions: features/steps/skill_registry_steps.py
Session management note: Each scenario creates a fresh in-memory
SQLite database with a single shared Session that is reused across all
repository calls within the scenario. This mirrors the production
UnitOfWork pattern (ADR-019) and avoids transaction-scoping issues
inherent to multiple ephemeral sessions on a single StaticPool
connection.
Run the Behave suite via:
nox -s unit_tests -- features/skill_registry.feature
Robot Smoke Tests
A Robot Framework smoke suite validates core registry operations:
robot/skill_registry.robot
robot/helper_skill_registry.py
| Test Case | What it validates |
|---|---|
| Register And Retrieve A Skill | Round-trip: register then get by name |
| List Skills With Namespace Filter | Register multiple skills, filter by namespace |
| Update A Skill | Change description after registration |
| Reject Duplicate Skill Name | Duplicate name produces error |
| Remove A Skill | Remove and verify absence |
nox -s integration_tests -- robot/skill_registry.robot
ASV Benchmarks
Performance benchmarks live in benchmarks/skill_registry_bench.py:
SkillModelConstruction--time_from_domain(domain-to-ORM mapping)SkillModelReconstruction--time_to_domain(ORM-to-domain mapping)SkillRepositoryCRUD--time_create_skill,time_get_skill,time_list_all,time_list_namespaceSkillRegistryListPerformance--time_list_100_skills,time_list_100_skills_filtered
nox -s benchmark