Files
cleveragents-core/robot/helper_agent_skills_loader.py
aditya cb82fc51df feat(skill): add agent skills loader
Implemented AgentSkillSpec loader that parses SKILL.md frontmatter and
progressive disclosure sections (discover/activate/deactivate) into
structured SkillStep objects with stable 1-based ordering.

Mapped Agent Skills to AgentSkillToolDescriptor with namespaced naming
(namespace/short_name), source="agent_skill", read-only defaults, and
AgentSkillResourceSlot bindings for scripts/, references/, and assets/
directories. All resource slots are unconditionally read_only.

Added explicit validation for missing frontmatter fields (name,
description) and invalid namespace format with actionable error messages.

Added docs/reference/agent_skills.md covering folder layout, SKILL.md
parsing rules, progressive disclosure model, and tool mapping.

Added Behave scenarios covering valid/invalid SKILL.md parsing,
namespaced naming, step ordering, missing frontmatter errors, progressive
disclosure lifecycle, tool mapping, and resource binding slots.

Added Robot Framework integration tests using the deploy-to-staging
example skill folder (robot/agent_skills_loader.robot).

Added ASV benchmarks for parsing throughput, folder load, progressive
disclosure lifecycle, and resource listing
(benchmarks/agent_skills_loader_bench.py).

ISSUES CLOSED: #160
2026-02-26 09:10:35 +00:00

221 lines
6.6 KiB
Python

"""Robot Framework helper for Agent Skills Loader integration tests.
Provides a CLI-style interface for Robot to invoke Agent Skills loader
operations and verify the results. Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_agent_skills_loader.py load-skill <folder_path>
python robot/helper_agent_skills_loader.py discover <folder_path>
python robot/helper_agent_skills_loader.py activate <folder_path>
python robot/helper_agent_skills_loader.py tool-descriptor <folder_path>
python robot/helper_agent_skills_loader.py invalid-skill <folder_path>
python robot/helper_agent_skills_loader.py list-resources <folder_path>
"""
from __future__ import annotations
import sys
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.skills.agent_skills_loader import AgentSkillLoader # noqa: E402
def _cmd_load_skill(folder: str) -> int:
"""Load a skill folder and verify the spec is populated."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
if not loader.spec.name:
print("agent-skills-fail: name is empty")
return 1
if not loader.spec.description:
print("agent-skills-fail: description is empty")
return 1
print(f"agent-skills-load-ok: {loader.spec.name}")
return 0
def _cmd_discover(folder: str) -> int:
"""Call discover() and verify metadata-only descriptor."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
descriptor = loader.discover()
if descriptor.source != "agent_skill":
print(
f"agent-skills-fail: expected source=agent_skill, got {descriptor.source}"
)
return 1
if descriptor.body:
print(
f"agent-skills-fail: body should be empty at discover, "
f"got: {descriptor.body!r}"
)
return 1
if not descriptor.name:
print("agent-skills-fail: name is empty")
return 1
print(f"agent-skills-discover-ok: {descriptor.name}")
return 0
def _cmd_activate(folder: str) -> int:
"""Call discover() then activate() and verify body is loaded."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
loader.discover()
activated = loader.activate()
if not activated.body:
print("agent-skills-fail: body is empty after activate")
return 1
print(f"agent-skills-activate-ok: body_len={len(activated.body)}")
return 0
def _cmd_tool_descriptor(folder: str) -> int:
"""Convert loaded skill to tool descriptor and verify fields."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
td = loader.to_tool_descriptor()
if td.source != "agent_skill":
print(f"agent-skills-fail: expected source=agent_skill, got {td.source}")
return 1
if not td.read_only:
print("agent-skills-fail: expected read_only=True")
return 1
if td.writes:
print("agent-skills-fail: expected writes=False")
return 1
if not td.agent_skill_path:
print("agent-skills-fail: agent_skill_path is empty")
return 1
if not td.name:
print("agent-skills-fail: tool descriptor name is empty")
return 1
print(f"agent-skills-tool-descriptor-ok: {td.name}")
print(f" resource_slots={len(td.resource_slots)}")
for slot in td.resource_slots:
print(f" slot: name={slot.name} access={slot.access}")
return 0
def _cmd_invalid_skill(folder: str) -> int:
"""Attempt to load an invalid skills folder and verify error."""
try:
AgentSkillLoader.from_folder(Path(folder))
print("agent-skills-fail: expected error but none was raised")
return 1
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-invalid-ok: {exc}")
return 0
def _cmd_list_resources(folder: str) -> int:
"""List resources from a skill folder."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
resources = loader.list_resources()
print(f"agent-skills-list-resources-ok: count={len(resources)}")
for p in resources:
print(f" resource: {p.name}")
return 0
def _cmd_steps(folder: str) -> int:
"""Load a skill folder and print structured steps from the spec."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
steps = loader.spec.steps
print(f"agent-skills-steps-ok: count={len(steps)}")
for step in steps:
print(f" step[{step.index}]: {step.content}")
return 0
def _cmd_resource_slots(folder: str) -> int:
"""Load a skill folder and print resource binding slots."""
try:
loader = AgentSkillLoader.from_folder(Path(folder))
except (ValueError, FileNotFoundError) as exc:
print(f"agent-skills-fail: {exc}")
return 1
slots = loader.resource_slots
print(f"agent-skills-resource-slots-ok: count={len(slots)}")
for slot in slots:
print(
f" slot: name={slot.name} type={slot.resource_type} access={slot.access}"
)
return 0
_COMMANDS = {
"load-skill": _cmd_load_skill,
"discover": _cmd_discover,
"activate": _cmd_activate,
"tool-descriptor": _cmd_tool_descriptor,
"invalid-skill": _cmd_invalid_skill,
"list-resources": _cmd_list_resources,
"steps": _cmd_steps,
"resource-slots": _cmd_resource_slots,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 3:
print(
"Usage: helper_agent_skills_loader.py "
"<load-skill|discover|activate|tool-descriptor"
"|invalid-skill|list-resources> "
"<folder_path>"
)
return 1
command = sys.argv[1]
folder = sys.argv[2]
handler = _COMMANDS.get(command)
if handler is None:
print(f"Unknown command: {command}")
return 1
return handler(folder)
if __name__ == "__main__":
sys.exit(main())