diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py
index db1c43abfe..356051da3f 100644
--- a/python/packages/core/agent_framework/__init__.py
+++ b/python/packages/core/agent_framework/__init__.py
@@ -147,6 +147,7 @@
InlineSkillScript,
InMemorySkillsSource,
Skill,
+ SkillFrontmatter,
SkillResource,
SkillScript,
SkillScriptRunner,
@@ -432,6 +433,7 @@
"SessionContext",
"SingleEdgeGroup",
"Skill",
+ "SkillFrontmatter",
"SkillResource",
"SkillScript",
"SkillScriptRunner",
diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py
index 44755a2efd..c41ab67f6a 100644
--- a/python/packages/core/agent_framework/_skills.py
+++ b/python/packages/core/agent_framework/_skills.py
@@ -55,6 +55,8 @@
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, Any, ClassVar, Final, Protocol, TypeVar, cast, runtime_checkable
+import yaml
+
from ._feature_stage import ExperimentalFeature, experimental
from ._sessions import ContextProvider
from ._tools import FunctionTool
@@ -459,6 +461,67 @@ async def run(self, skill: Skill, args: dict[str, Any] | None = None, **kwargs:
return result
+@experimental(feature_id=ExperimentalFeature.SKILLS)
+class SkillFrontmatter:
+ """Parsed and validated YAML frontmatter from a SKILL.md file.
+
+ Frontmatter is the L1 (discovery) layer of the
+ `Agent Skills specification `_.
+ It contains the minimal metadata needed to advertise a skill in the system
+ prompt without loading the full skill content.
+
+ The constructor validates ``name``, ``description``, and ``compatibility``
+ and raises :exc:`ValueError` if any value violates the specification rules.
+
+ Attributes:
+ name: Skill name (lowercase letters, numbers, hyphens only;
+ max 64 characters; no leading/trailing/consecutive hyphens).
+ description: Human-readable description of the skill
+ (≤1024 characters).
+ license: Optional license name or SPDX identifier.
+ compatibility: Optional compatibility information (max 500 chars).
+ allowed_tools: Pre-approved tool names, parsed from the space-delimited
+ frontmatter field.
+ metadata: Arbitrary key-value metadata from the ``metadata:`` YAML block.
+ """
+
+ def __init__(
+ self,
+ *,
+ name: str,
+ description: str,
+ license: str | None = None,
+ compatibility: str | None = None,
+ allowed_tools: list[str] | None = None,
+ metadata: dict[str, Any] | None = None,
+ ) -> None:
+ """Initialize a SkillFrontmatter.
+
+ Args:
+ name: Skill name in kebab-case.
+ description: Skill description for discovery.
+ license: Optional license name or SPDX identifier.
+ compatibility: Optional compatibility information (max 500 chars).
+ allowed_tools: Pre-approved tool names.
+ metadata: Arbitrary key-value metadata.
+
+ Raises:
+ ValueError: If ``name``, ``description``, or ``compatibility``
+ violates the Agent Skills specification rules.
+ """
+ # Perform the validations
+ _validate_skill_name(name=name)
+ _validate_skill_description(name=name, description=description)
+ _validate_compatibility(name=name, compatibility=compatibility)
+
+ self.name = name
+ self.description = description
+ self.license = license
+ self.compatibility = compatibility
+ self.allowed_tools: list[str] = allowed_tools or []
+ self.metadata = metadata
+
+
@experimental(feature_id=ExperimentalFeature.SKILLS)
class Skill(ABC):
"""Abstract base class for all agent skills.
@@ -472,34 +535,32 @@ class Skill(ABC):
`Agent Skills specification `_.
Attributes:
- name: Skill name (lowercase letters, numbers, hyphens only).
- description: Human-readable description of the skill.
+ frontmatter: The parsed frontmatter metadata for this skill.
"""
def __init__(
self,
*,
- name: str,
- description: str,
+ frontmatter: SkillFrontmatter,
) -> None:
"""Initialize a Skill.
- Validates the skill name and description against specification rules.
-
Args:
- name: Skill name (lowercase letters, numbers, hyphens only;
- max 64 characters; no leading/trailing/consecutive hyphens).
- description: Human-readable description of the skill
- (≤1024 characters).
-
- Raises:
- ValueError: If the name or description is invalid.
+ frontmatter: Validated metadata for this skill. Contains the name,
+ description, and all optional fields defined in the Agent Skills
+ specification.
"""
- _validate_skill_name(name)
- _validate_skill_description(name, description)
+ self.frontmatter = frontmatter
- self.name = name
- self.description = description
+ @property
+ def name(self) -> str:
+ """Skill name, delegated from :attr:`frontmatter`."""
+ return self.frontmatter.name
+
+ @property
+ def description(self) -> str:
+ """Human-readable description, delegated from :attr:`frontmatter`."""
+ return self.frontmatter.description
@property
@abstractmethod
@@ -573,6 +634,18 @@ def _validate_skill_description(name: str, description: str) -> None:
)
+def _validate_compatibility(name: str, compatibility: str | None) -> None:
+ """Validate an optional skill compatibility value against specification rules.
+
+ Args:
+ name: The skill name (used in error messages).
+ compatibility: The compatibility value to validate.
+
+ """
+ if compatibility is not None and len(compatibility) > MAX_COMPATIBILITY_LENGTH:
+ raise ValueError(f"Skill '{name}' compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer.")
+
+
def _build_skill_content(
name: str,
description: str,
@@ -640,8 +713,7 @@ class InlineSkill(Skill):
registered with a :class:`SkillsProvider`.
Attributes:
- name: Skill name (lowercase letters, numbers, hyphens only).
- description: Human-readable description of the skill.
+ frontmatter: The parsed frontmatter metadata for this skill.
instructions: The skill instructions text.
Examples:
@@ -650,8 +722,7 @@ class InlineSkill(Skill):
.. code-block:: python
skill = InlineSkill(
- name="db-skill",
- description="Database operations",
+ SkillFrontmatter(name="db-skill", description="Database operations")
instructions="Use this skill for DB tasks.",
)
@@ -664,8 +735,7 @@ def get_schema() -> str:
def __init__(
self,
*,
- name: str,
- description: str,
+ frontmatter: SkillFrontmatter,
instructions: str,
resources: Sequence[SkillResource] | None = None,
scripts: Sequence[SkillScript] | None = None,
@@ -673,13 +743,12 @@ def __init__(
"""Initialize an InlineSkill.
Args:
- name: Skill name (lowercase letters, numbers, hyphens only).
- description: Human-readable description of the skill (≤1024 chars).
+ frontmatter: The parsed frontmatter metadata for this skill.
instructions: The skill instructions text.
resources: Pre-built resources to attach to this skill.
scripts: Pre-built scripts to attach to this skill.
"""
- super().__init__(name=name, description=description)
+ super().__init__(frontmatter=frontmatter)
self.instructions = instructions
self._resources: list[SkillResource] = list(resources) if resources is not None else []
@@ -944,8 +1013,10 @@ class ClassSkill(Skill, ABC):
class UnitConverterSkill(ClassSkill):
def __init__(self) -> None:
super().__init__(
- name="unit-converter",
- description="Convert between common units.",
+ frontmatter=SkillFrontmatter(
+ name="unit-converter",
+ description="Convert between common units.",
+ ),
)
@property
@@ -967,8 +1038,10 @@ def convert(self, value: float, factor: float) -> str:
class UnitConverterSkill(ClassSkill):
def __init__(self) -> None:
super().__init__(
- name="unit-converter",
- description="Convert between common units.",
+ frontmatter=SkillFrontmatter(
+ name="unit-converter",
+ description="Convert between common units.",
+ ),
)
@property
@@ -989,18 +1062,14 @@ def scripts(self) -> list[SkillScript]:
def __init__(
self,
*,
- name: str,
- description: str,
+ frontmatter: SkillFrontmatter,
) -> None:
"""Initialize a ClassSkill.
Args:
- name: Skill name (lowercase letters, numbers, hyphens only;
- max 64 characters).
- description: Human-readable description of the skill
- (≤1024 characters).
+ frontmatter: Validated metadata for this skill.
"""
- super().__init__(name=name, description=description)
+ super().__init__(frontmatter=frontmatter)
self._cached_content: str | None = None
self._cached_resources: list[SkillResource] | None = None
self._cached_scripts: list[SkillScript] | None = None
@@ -1250,16 +1319,14 @@ class FileSkill(Skill):
"""A :class:`Skill` discovered from a filesystem directory backed by a SKILL.md file.
Attributes:
- name: Skill name (lowercase letters, numbers, hyphens only).
- description: Human-readable description of the skill.
+ frontmatter: Parsed metadata for this skill.
path: Absolute path to the directory containing this skill.
"""
def __init__(
self,
*,
- name: str,
- description: str,
+ frontmatter: SkillFrontmatter,
content: str,
path: str,
resources: Sequence[SkillResource] | None = None,
@@ -1268,14 +1335,13 @@ def __init__(
"""Initialize a FileSkill.
Args:
- name: Skill name (lowercase letters, numbers, hyphens only).
- description: Human-readable description of the skill (≤1024 chars).
+ frontmatter: Validated metadata parsed from the SKILL.md frontmatter block.
content: The full raw SKILL.md file content including YAML frontmatter.
path: Absolute path to the skill directory on disk.
resources: Resources discovered for this skill.
scripts: Scripts discovered for this skill.
"""
- super().__init__(name=name, description=description)
+ super().__init__(frontmatter=frontmatter)
self._content = content
self.path = path
@@ -1346,6 +1412,7 @@ def __call__(self, skill: FileSkill, script: FileSkillScript, args: dict[str, An
MAX_SEARCH_DEPTH: Final[int] = 2
MAX_NAME_LENGTH: Final[int] = 64
MAX_DESCRIPTION_LENGTH: Final[int] = 1024
+MAX_COMPATIBILITY_LENGTH: Final[int] = 500
DEFAULT_RESOURCE_EXTENSIONS: Final[tuple[str, ...]] = (
".md",
".json",
@@ -1366,12 +1433,6 @@ def __call__(self, skill: FileSkill, script: FileSkillScript, args: dict[str, An
re.MULTILINE | re.DOTALL,
)
-# Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value,
-# Group 3 = unquoted value.
-YAML_KV_RE = re.compile(
- r"^\s*(\w+)\s*:\s*(?:[\"'](.+?)[\"']|(.+?))\s*$",
- re.MULTILINE,
-)
# Validates skill names: lowercase letters, numbers, hyphens only;
# must not start or end with a hyphen, and must not contain consecutive hyphens.
@@ -2179,19 +2240,18 @@ async def get_skills(self) -> list[Skill]:
if parsed is None:
continue
- name, description, content = parsed
+ frontmatter, content = parsed
- if name in skills:
+ if frontmatter.name in skills:
logger.warning(
"Duplicate skill name '%s': skill from '%s' skipped in favor of existing skill",
- name,
+ frontmatter.name,
skill_path,
)
continue
file_skill = FileSkill(
- name=name,
- description=description,
+ frontmatter=frontmatter,
content=content,
path=skill_path,
)
@@ -2433,52 +2493,11 @@ def _get_validated_resource_path(skill_dir: str, resource_name: str) -> str:
return resource_full_path
- @staticmethod
- def _validate_skill_metadata(
- name: str | None,
- description: str | None,
- source: str,
- ) -> str | None:
- """Validate a skill's name and description against naming rules.
-
- Enforces length limits, character-set restrictions, and non-emptiness
- for both file-based and code-defined skills.
-
- Args:
- name: Skill name to validate.
- description: Skill description to validate.
- source: Human-readable label for diagnostics (e.g. a file path
- or ``"code skill"``).
-
- Returns:
- A diagnostic error string if validation fails, or ``None`` if valid.
- """
- if not name or not name.strip():
- return f"Skill from '{source}' is missing a name."
-
- if len(name) > MAX_NAME_LENGTH or not VALID_NAME_RE.match(name):
- return (
- f"Skill from '{source}' has an invalid name '{name}': Must be {MAX_NAME_LENGTH} characters or fewer, "
- "using only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen "
- "or contain consecutive hyphens."
- )
-
- if not description or not description.strip():
- return f"Skill '{name}' from '{source}' is missing a description."
-
- if len(description) > MAX_DESCRIPTION_LENGTH:
- return (
- f"Skill '{name}' from '{source}' has an invalid description: "
- f"Must be {MAX_DESCRIPTION_LENGTH} characters or fewer."
- )
-
- return None
-
@staticmethod
def _extract_frontmatter(
content: str,
skill_file_path: str,
- ) -> tuple[str, str] | None:
+ ) -> SkillFrontmatter | None:
"""Extract and validate YAML frontmatter from a SKILL.md file.
Parses the ``---``-delimited frontmatter block for ``name`` and
@@ -2497,38 +2516,54 @@ def _extract_frontmatter(
logger.error("SKILL.md at '%s' does not contain valid YAML frontmatter delimited by '---'", skill_file_path)
return None
- yaml_content = match.group(1).strip()
- name: str | None = None
- description: str | None = None
+ yaml_text = match.group(1)
- for kv_match in YAML_KV_RE.finditer(yaml_content):
- key = kv_match.group(1)
- value = kv_match.group(2) if kv_match.group(2) is not None else kv_match.group(3)
-
- if key.lower() == "name":
- name = value
- elif key.lower() == "description":
- description = value
+ try:
+ raw_data = yaml.safe_load(yaml_text)
+ except yaml.YAMLError as e:
+ logger.error("Syntax YAML error '%s': %s", skill_file_path, e)
+ return None
- error = FileSkillsSource._validate_skill_metadata(name, description, skill_file_path)
- if error:
- logger.error(error)
+ if not isinstance(raw_data, dict):
+ logger.error("Frontmatter at '%s' is not a valid dict", skill_file_path)
return None
- # name and description are guaranteed non-None after validation
- return name, description # type: ignore[return-value]
+ # Normalize!
+ parsed = {k.lower(): v for k, v in raw_data.items()}
+
+ name: str | None = parsed.get("name")
+ description: str | None = parsed.get("description")
+ compatibility: str | None = parsed.get("compatibility")
+ allowed_tools_raw: str | None = parsed.get("allowed_tools")
+ allowed_tools = [t for t in allowed_tools_raw.split() if t] if allowed_tools_raw else []
+
+ metadata_raw = parsed.get("metadata")
+ metadata: dict[str, Any] | None = metadata_raw if isinstance(metadata_raw, dict) else None
+
+ try:
+ return SkillFrontmatter(
+ name=name, # type: ignore[arg-type] # Constructor validates non none vals
+ description=description, # type: ignore[arg-type]
+ license=parsed.get("license"),
+ compatibility=compatibility,
+ allowed_tools=allowed_tools or None,
+ metadata=metadata,
+ )
+ except ValueError as e:
+ logger.error("Skill at '%s' violates specification: %s", skill_file_path, e)
+ return None
@staticmethod
def _read_and_parse_skill_file(
skill_dir_path: str,
- ) -> tuple[str, str, str] | None:
+ ) -> tuple[SkillFrontmatter, str] | None:
"""Read and parse the SKILL.md file in *skill_dir_path*.
Args:
skill_dir_path: Absolute path to the directory containing ``SKILL.md``.
Returns:
- A ``(name, description, content)`` tuple where *content* is the
+ A ``(frontmatter, content)`` tuple where *content* is the
full raw file text, or ``None`` if the file cannot be read or
its frontmatter is invalid.
"""
@@ -2540,23 +2575,21 @@ def _read_and_parse_skill_file(
logger.error("Failed to read SKILL.md at '%s'", skill_file)
return None
- result = FileSkillsSource._extract_frontmatter(content, str(skill_file))
- if result is None:
+ frontmatter = FileSkillsSource._extract_frontmatter(content, str(skill_file))
+ if frontmatter is None:
return None
- name, description = result
-
dir_name = Path(skill_dir_path).name
- if name != dir_name:
+ if frontmatter.name != dir_name:
logger.error(
"SKILL.md at '%s' has frontmatter name '%s' that does not match the directory name '%s'; skipping.",
skill_file,
- name,
+ frontmatter.name,
dir_name,
)
return None
- return name, description, content
+ return frontmatter, content
@staticmethod
def _discover_skill_directories(skill_paths: Sequence[str]) -> list[str]:
diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py
index b268b31551..9b8cd98e79 100644
--- a/python/packages/core/tests/core/test_skills.py
+++ b/python/packages/core/tests/core/test_skills.py
@@ -34,6 +34,7 @@
DEFAULT_SCRIPT_EXTENSIONS,
InlineSkillResource,
InlineSkillScript,
+ SkillFrontmatter,
_create_resource_element,
_create_script_element,
_FileSkillResource,
@@ -129,10 +130,9 @@ def _read_and_parse_skill_file_for_test(skill_dir: Path) -> FileSkill:
"""Parse a SKILL.md file from the given directory, raising if invalid."""
result = FileSkillsSource._read_and_parse_skill_file(str(skill_dir))
assert result is not None, f"Failed to parse skill at {skill_dir}"
- name, description, content = result
+ frontmatter, content = result
return FileSkill(
- name=name,
- description=description,
+ frontmatter=frontmatter,
content=content,
path=str(skill_dir),
)
@@ -268,22 +268,21 @@ def test_valid_skill(self) -> None:
content = "---\nname: test-skill\ndescription: A test skill.\n---\n# Body\nInstructions here."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- name, description = result
- assert name == "test-skill"
- assert description == "A test skill."
+ assert result.name == "test-skill"
+ assert result.description == "A test skill."
def test_quoted_values(self) -> None:
content = "---\nname: \"test-skill\"\ndescription: 'A test skill.'\n---\nBody."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- assert result[0] == "test-skill"
- assert result[1] == "A test skill."
+ assert result.name == "test-skill"
+ assert result.description == "A test skill."
def test_utf8_bom(self) -> None:
content = "\ufeff---\nname: test-skill\ndescription: A test skill.\n---\nBody."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- assert result[0] == "test-skill"
+ assert result.name == "test-skill"
def test_missing_frontmatter(self) -> None:
content = "# Just a markdown file\nNo frontmatter here."
@@ -331,7 +330,110 @@ def test_extra_metadata_ignored(self) -> None:
content = "---\nname: test-skill\ndescription: A test skill.\nauthor: someone\nversion: 1.0\n---\nBody."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- assert result[0] == "test-skill"
+ assert result.name == "test-skill"
+
+
+# ---------------------------------------------------------------------------
+# Tests: SkillFrontmatter construction and validation
+# ---------------------------------------------------------------------------
+
+
+class TestSkillFrontmatter:
+ """Tests for SkillFrontmatter construction, defaults, and validation."""
+
+ # ── Basic construction ───────────────────────────────────────────────────
+
+ def test_minimal_construction(self) -> None:
+ fm = SkillFrontmatter(name="my-skill", description="Does stuff.")
+ assert fm.name == "my-skill"
+ assert fm.description == "Does stuff."
+ assert fm.license is None
+ assert fm.compatibility is None
+ assert fm.allowed_tools == []
+ assert fm.metadata is None
+
+ def test_all_fields(self) -> None:
+ fm = SkillFrontmatter(
+ name="full-skill",
+ description="Full skill.",
+ license="MIT",
+ compatibility="agent-framework>=1.0",
+ allowed_tools=["read_file", "write_file"],
+ metadata={"author": "alice", "version": "1.2.3"},
+ )
+ assert fm.name == "full-skill"
+ assert fm.description == "Full skill."
+ assert fm.license == "MIT"
+ assert fm.compatibility == "agent-framework>=1.0"
+ assert fm.allowed_tools == ["read_file", "write_file"]
+ assert fm.metadata == {"author": "alice", "version": "1.2.3"}
+
+ # ── compatibility ─────────────────────────────────────────────────────────
+
+ def test_compatibility_max_length_accepted(self) -> None:
+ long_compat = "x" * 500
+ fm = SkillFrontmatter(name="s", description="d", compatibility=long_compat)
+ assert fm.compatibility == long_compat
+
+ def test_compatibility_too_long_raises(self) -> None:
+ with pytest.raises(ValueError, match="compatibility"):
+ SkillFrontmatter(name="s", description="d", compatibility="x" * 501)
+
+ # ── name validation ───────────────────────────────────────────────────────
+
+ def test_name_empty_raises(self) -> None:
+ with pytest.raises(ValueError, match="cannot be empty"):
+ SkillFrontmatter(name="", description="d")
+
+ def test_name_whitespace_only_raises(self) -> None:
+ with pytest.raises(ValueError, match="cannot be empty"):
+ SkillFrontmatter(name=" ", description="d")
+
+ def test_name_uppercase_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid skill name"):
+ SkillFrontmatter(name="My-Skill", description="d")
+
+ def test_name_starts_with_hyphen_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid skill name"):
+ SkillFrontmatter(name="-bad", description="d")
+
+ def test_name_ends_with_hyphen_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid skill name"):
+ SkillFrontmatter(name="bad-", description="d")
+
+ def test_name_consecutive_hyphens_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid skill name"):
+ SkillFrontmatter(name="bad--name", description="d")
+
+ def test_name_too_long_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid skill name"):
+ SkillFrontmatter(name="a" * 65, description="d")
+
+ def test_name_max_length_accepted(self) -> None:
+ fm = SkillFrontmatter(name="a" * 64, description="d")
+ assert len(fm.name) == 64
+
+ def test_name_with_numbers_accepted(self) -> None:
+ fm = SkillFrontmatter(name="skill-v2", description="d")
+ assert fm.name == "skill-v2"
+
+ # ── description validation ────────────────────────────────────────────────
+
+ def test_description_empty_raises(self) -> None:
+ with pytest.raises(ValueError, match="cannot be empty"):
+ SkillFrontmatter(name="s", description="")
+
+ def test_description_whitespace_only_raises(self) -> None:
+ with pytest.raises(ValueError, match="cannot be empty"):
+ SkillFrontmatter(name="s", description=" ")
+
+ def test_description_too_long_raises(self) -> None:
+ with pytest.raises(ValueError, match="invalid description"):
+ SkillFrontmatter(name="s", description="a" * 1025)
+
+ def test_description_max_length_accepted(self) -> None:
+ fm = SkillFrontmatter(name="s", description="a" * 1024)
+ assert len(fm.description) == 1024
# ---------------------------------------------------------------------------
@@ -504,7 +606,7 @@ def test_returns_none_for_empty_skills(self) -> None:
def test_default_prompt_contains_skills(self) -> None:
skills = [
- InlineSkill(name="my-skill", description="Does stuff.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Does stuff."), instructions="Body"),
]
prompt = SkillsProvider._create_instructions(None, skills)
assert prompt is not None
@@ -514,8 +616,8 @@ def test_default_prompt_contains_skills(self) -> None:
def test_skills_sorted_alphabetically(self) -> None:
skills = [
- InlineSkill(name="zebra", description="Z skill.", instructions="Body"),
- InlineSkill(name="alpha", description="A skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="zebra", description="Z skill."), instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="alpha", description="A skill."), instructions="Body"),
]
prompt = SkillsProvider._create_instructions(None, skills)
assert prompt is not None
@@ -525,7 +627,9 @@ def test_skills_sorted_alphabetically(self) -> None:
def test_xml_escapes_metadata(self) -> None:
skills = [
- InlineSkill(name="my-skill", description='Uses & "quotes"', instructions="Body"),
+ InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description='Uses & "quotes"'), instructions="Body"
+ ),
]
prompt = SkillsProvider._create_instructions(None, skills)
assert prompt is not None
@@ -534,7 +638,7 @@ def test_xml_escapes_metadata(self) -> None:
def test_custom_prompt_template(self) -> None:
skills = [
- InlineSkill(name="my-skill", description="Does stuff.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Does stuff."), instructions="Body"),
]
custom = "Custom header:\n{skills}\nCustom footer."
prompt = SkillsProvider._create_instructions(custom, skills)
@@ -544,14 +648,14 @@ def test_custom_prompt_template(self) -> None:
def test_invalid_prompt_template_raises(self) -> None:
skills = [
- InlineSkill(name="my-skill", description="Does stuff.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Does stuff."), instructions="Body"),
]
with pytest.raises(ValueError, match="valid format string"):
SkillsProvider._create_instructions("{invalid}", skills)
def test_positional_placeholder_raises(self) -> None:
skills = [
- InlineSkill(name="my-skill", description="Does stuff.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Does stuff."), instructions="Body"),
]
with pytest.raises(ValueError, match="valid format string"):
SkillsProvider._create_instructions("Header {0} footer", skills)
@@ -942,16 +1046,20 @@ def test_skill_is_abstract(self) -> None:
def test_inline_skill_is_skill(self) -> None:
"""InlineSkill is a subclass of Skill."""
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
assert isinstance(skill, Skill)
def test_file_skill_is_skill(self) -> None:
"""FileSkill is a subclass of Skill."""
- skill = FileSkill(name="my-skill", description="A skill.", content="Body", path="/tmp/skill")
+ skill = FileSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), content="Body", path="/tmp/skill"
+ )
assert isinstance(skill, Skill)
def test_basic_construction(self) -> None:
- skill = InlineSkill(name="my-skill", description="A test skill.", instructions="Instructions.")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="A test skill."), instructions="Instructions."
+ )
assert skill.name == "my-skill"
assert skill.description == "A test skill."
assert skill.instructions == "Instructions."
@@ -959,8 +1067,7 @@ def test_basic_construction(self) -> None:
def test_construction_with_static_resources(self) -> None:
skill = InlineSkill(
- name="my-skill",
- description="A test skill.",
+ frontmatter=SkillFrontmatter(name="my-skill", description="A test skill."),
instructions="Instructions.",
resources=[
InlineSkillResource(name="ref", content="Reference content"),
@@ -971,34 +1078,36 @@ def test_construction_with_static_resources(self) -> None:
def test_empty_name_raises(self) -> None:
with pytest.raises(ValueError, match="cannot be empty"):
- InlineSkill(name="", description="A skill.", instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="", description="A skill."), instructions="Body")
def test_invalid_name_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
- InlineSkill(name="Invalid-Name", description="A skill.", instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="Invalid-Name", description="A skill."), instructions="Body")
def test_name_starts_with_hyphen_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
- InlineSkill(name="-bad-name", description="A skill.", instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="-bad-name", description="A skill."), instructions="Body")
def test_name_with_consecutive_hyphens_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
- InlineSkill(name="consecutive--hyphens", description="A skill.", instructions="Body")
+ InlineSkill(
+ frontmatter=SkillFrontmatter(name="consecutive--hyphens", description="A skill."), instructions="Body"
+ )
def test_name_too_long_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid skill name"):
- InlineSkill(name="a" * 65, description="A skill.", instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="a" * 65, description="A skill."), instructions="Body")
def test_empty_description_raises(self) -> None:
with pytest.raises(ValueError, match="cannot be empty"):
- InlineSkill(name="my-skill", description="", instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description=""), instructions="Body")
def test_description_too_long_raises(self) -> None:
with pytest.raises(ValueError, match="invalid description"):
- InlineSkill(name="my-skill", description="a" * 1025, instructions="Body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="a" * 1025), instructions="Body")
def test_resource_decorator_bare(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def get_schema() -> Any:
@@ -1012,7 +1121,7 @@ def get_schema() -> Any:
assert skill.resources[0].function is get_schema
def test_resource_decorator_with_args(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource(name="custom-name", description="Custom description")
def my_resource() -> Any:
@@ -1024,7 +1133,7 @@ def my_resource() -> Any:
def test_resource_decorator_returns_function(self) -> None:
"""Decorator should return the original function unchanged."""
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def get_data() -> Any:
@@ -1034,7 +1143,7 @@ def get_data() -> Any:
assert get_data() == "data"
def test_multiple_resources(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def resource_a() -> Any:
@@ -1050,7 +1159,7 @@ def resource_b() -> Any:
assert "resource_b" in names
def test_resource_decorator_async(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
async def get_async_data() -> Any:
@@ -1070,13 +1179,19 @@ class TestSkillsProviderCodeSkill:
"""Tests for SkillsProvider with code-defined skills."""
async def test_code_skill_only(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A code-defined skill.", instructions="Do the thing.")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A code-defined skill."),
+ instructions="Do the thing.",
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
assert "prog-skill" in _ctx(provider)[0]
async def test_load_skill_returns_content(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Code-defined instructions.")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."),
+ instructions="Code-defined instructions.",
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
result = provider._load_skill(_raw_skills(provider), "prog-skill")
@@ -1087,8 +1202,7 @@ async def test_load_skill_returns_content(self) -> None:
async def test_load_skill_appends_resource_listing(self) -> None:
skill = InlineSkill(
- name="prog-skill",
- description="A skill.",
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."),
instructions="Do things.",
resources=[
InlineSkillResource(name="ref-a", content="a", description="First resource"),
@@ -1106,7 +1220,9 @@ async def test_load_skill_appends_resource_listing(self) -> None:
assert '' in result
async def test_load_skill_no_resources_no_listing(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body only.")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body only."
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
result = provider._load_skill(_raw_skills(provider), "prog-skill")
@@ -1115,8 +1231,7 @@ async def test_load_skill_no_resources_no_listing(self) -> None:
async def test_read_static_resource(self) -> None:
skill = InlineSkill(
- name="prog-skill",
- description="A skill.",
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."),
instructions="Body",
resources=[InlineSkillResource(name="ref", content="static content")],
)
@@ -1126,7 +1241,9 @@ async def test_read_static_resource(self) -> None:
assert result == "static content"
async def test_read_callable_resource_sync(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def get_schema() -> Any:
@@ -1138,7 +1255,9 @@ def get_schema() -> Any:
assert result == "CREATE TABLE users"
async def test_read_callable_resource_async(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
async def get_data() -> Any:
@@ -1151,8 +1270,7 @@ async def get_data() -> Any:
async def test_read_resource_case_insensitive(self) -> None:
skill = InlineSkill(
- name="prog-skill",
- description="A skill.",
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."),
instructions="Body",
resources=[InlineSkillResource(name="MyRef", content="content")],
)
@@ -1162,14 +1280,18 @@ async def test_read_resource_case_insensitive(self) -> None:
assert result == "content"
async def test_read_unknown_resource_returns_error(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
result = await provider._read_skill_resource(_raw_skills(provider), "prog-skill", "nonexistent")
assert result.startswith("Error:")
async def test_read_callable_resource_sync_with_kwargs(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def get_user_config(**kwargs: Any) -> Any:
@@ -1184,7 +1306,9 @@ def get_user_config(**kwargs: Any) -> Any:
assert result == "config for user_123"
async def test_read_callable_resource_async_with_kwargs(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
async def get_user_data(**kwargs: Any) -> Any:
@@ -1200,7 +1324,9 @@ async def get_user_data(**kwargs: Any) -> Any:
async def test_read_callable_resource_without_kwargs_ignores_extra_args(self) -> None:
"""Resource functions without **kwargs should still work when kwargs are passed."""
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def static_resource() -> Any:
@@ -1215,7 +1341,9 @@ def static_resource() -> Any:
async def test_read_callable_resource_returns_dict(self) -> None:
"""Resource functions may return non-string types, passed through as-is."""
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def get_config() -> Any:
@@ -1228,7 +1356,9 @@ def get_config() -> Any:
async def test_read_callable_resource_returns_list(self) -> None:
"""Resource functions may return lists, passed through as-is."""
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def get_items() -> Any:
@@ -1241,7 +1371,9 @@ def get_items() -> Any:
async def test_read_callable_resource_returns_none(self) -> None:
"""Resource functions may return None."""
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Body"
+ )
@skill.resource
def get_nothing() -> Any:
@@ -1253,7 +1385,9 @@ def get_nothing() -> Any:
assert result is None
async def test_before_run_injects_code_skills(self) -> None:
- skill = InlineSkill(name="prog-skill", description="A code-defined skill.", instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A code-defined skill."), instructions="Body"
+ )
provider = SkillsProvider([skill])
context = SessionContext(input_messages=[])
@@ -1274,7 +1408,9 @@ async def test_before_run_empty_provider(self) -> None:
async def test_combined_file_and_code_skill(self, tmp_path: Path) -> None:
_write_skill(tmp_path, "file-skill")
- prog_skill = InlineSkill(name="prog-skill", description="Code-defined.", instructions="Body")
+ prog_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="Code-defined."), instructions="Body"
+ )
provider = SkillsProvider(
DeduplicatingSkillsSource(
AggregatingSkillsSource([
@@ -1289,7 +1425,9 @@ async def test_combined_file_and_code_skill(self, tmp_path: Path) -> None:
async def test_duplicate_name_file_wins(self, tmp_path: Path) -> None:
_write_skill(tmp_path, "my-skill", body="File version")
- prog_skill = InlineSkill(name="my-skill", description="Code-defined.", instructions="Prog version")
+ prog_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="Code-defined."), instructions="Prog version"
+ )
provider = SkillsProvider(
DeduplicatingSkillsSource(
AggregatingSkillsSource([
@@ -1304,7 +1442,9 @@ async def test_duplicate_name_file_wins(self, tmp_path: Path) -> None:
async def test_combined_prompt_includes_both(self, tmp_path: Path) -> None:
_write_skill(tmp_path, "file-skill")
- prog_skill = InlineSkill(name="prog-skill", description="A code-defined skill.", instructions="Body")
+ prog_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A code-defined skill."), instructions="Body"
+ )
provider = SkillsProvider(
DeduplicatingSkillsSource(
AggregatingSkillsSource([
@@ -1397,7 +1537,9 @@ async def test_file_skill_returns_raw_content(self, tmp_path: Path) -> None:
async def test_code_skill_wraps_in_xml(self) -> None:
"""Code-defined skills are wrapped with name, description, and instructions tags."""
- skill = InlineSkill(name="prog-skill", description="A skill.", instructions="Do stuff.")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."), instructions="Do stuff."
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
result = provider._load_skill(_raw_skills(provider), "prog-skill")
@@ -1408,8 +1550,7 @@ async def test_code_skill_wraps_in_xml(self) -> None:
async def test_code_skill_single_resource_no_description(self) -> None:
"""Resource without description omits the description attribute."""
skill = InlineSkill(
- name="prog-skill",
- description="A skill.",
+ frontmatter=SkillFrontmatter(name="prog-skill", description="A skill."),
instructions="Body.",
resources=[InlineSkillResource(name="data", content="val")],
)
@@ -1499,91 +1640,6 @@ def test_returns_false_for_empty_relative(self, tmp_path: Path) -> None:
assert FileSkillsSource._has_symlink_in_path(str(tmp_path), str(tmp_path)) is False
-# ---------------------------------------------------------------------------
-# Tests: _validate_skill_metadata
-# ---------------------------------------------------------------------------
-
-
-class TestValidateSkillMetadata:
- """Tests for _validate_skill_metadata."""
-
- def test_valid_metadata(self) -> None:
- assert FileSkillsSource._validate_skill_metadata("my-skill", "A description.", "source") is None
-
- def test_none_name(self) -> None:
- result = FileSkillsSource._validate_skill_metadata(None, "desc", "source")
- assert result is not None
- assert "missing a name" in result
-
- def test_empty_name(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("", "desc", "source")
- assert result is not None
- assert "missing a name" in result
-
- def test_whitespace_only_name(self) -> None:
- result = FileSkillsSource._validate_skill_metadata(" ", "desc", "source")
- assert result is not None
- assert "missing a name" in result
-
- def test_name_at_max_length(self) -> None:
- name = "a" * 64
- assert FileSkillsSource._validate_skill_metadata(name, "desc", "source") is None
-
- def test_name_exceeds_max_length(self) -> None:
- name = "a" * 65
- result = FileSkillsSource._validate_skill_metadata(name, "desc", "source")
- assert result is not None
- assert "invalid name" in result
-
- def test_name_with_uppercase(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("BadName", "desc", "source")
- assert result is not None
- assert "invalid name" in result
-
- def test_name_starts_with_hyphen(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("-bad", "desc", "source")
- assert result is not None
- assert "invalid name" in result
-
- def test_name_ends_with_hyphen(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("bad-", "desc", "source")
- assert result is not None
- assert "invalid name" in result
-
- def test_name_with_consecutive_hyphens(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("consecutive--hyphens", "desc", "source")
- assert result is not None
- assert "invalid name" in result
-
- def test_single_char_name(self) -> None:
- assert FileSkillsSource._validate_skill_metadata("a", "desc", "source") is None
-
- def test_none_description(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("my-skill", None, "source")
- assert result is not None
- assert "missing a description" in result
-
- def test_empty_description(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("my-skill", "", "source")
- assert result is not None
- assert "missing a description" in result
-
- def test_whitespace_only_description(self) -> None:
- result = FileSkillsSource._validate_skill_metadata("my-skill", " ", "source")
- assert result is not None
- assert "missing a description" in result
-
- def test_description_at_max_length(self) -> None:
- desc = "a" * 1024
- assert FileSkillsSource._validate_skill_metadata("my-skill", desc, "source") is None
-
- def test_description_exceeds_max_length(self) -> None:
- desc = "a" * 1025
- result = FileSkillsSource._validate_skill_metadata("my-skill", desc, "source")
- assert result is not None
- assert "invalid description" in result
-
-
# ---------------------------------------------------------------------------
# Tests: _discover_skill_directories
# ---------------------------------------------------------------------------
@@ -1642,9 +1698,9 @@ def test_valid_file(self, tmp_path: Path) -> None:
(skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: A skill.\n---\nBody.", encoding="utf-8")
result = FileSkillsSource._read_and_parse_skill_file(str(skill_dir))
assert result is not None
- name, desc, content = result
- assert name == "my-skill"
- assert desc == "A skill."
+ frontmatter, content = result
+ assert frontmatter.name == "my-skill"
+ assert frontmatter.description == "A skill."
assert "Body." in content
def test_missing_skill_md_returns_none(self, tmp_path: Path) -> None:
@@ -1838,14 +1894,14 @@ def test_name_exactly_max_length(self) -> None:
content = f"---\nname: {name}\ndescription: A skill.\n---\nBody."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- assert result[0] == name
+ assert result.name == name
def test_description_exactly_max_length(self) -> None:
desc = "a" * 1024
content = f"---\nname: test-skill\ndescription: {desc}\n---\nBody."
result = FileSkillsSource._extract_frontmatter(content, "test.md")
assert result is not None
- assert result[1] == desc
+ assert result.description == desc
# ---------------------------------------------------------------------------
@@ -1862,7 +1918,7 @@ def test_custom_template_with_empty_skills_returns_none(self) -> None:
def test_custom_template_with_literal_braces(self) -> None:
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
template = "Header {{literal}} {skills} footer."
result = SkillsProvider._create_instructions(template, skills)
@@ -1872,9 +1928,9 @@ def test_custom_template_with_literal_braces(self) -> None:
def test_multiple_skills_generates_sorted_xml(self) -> None:
skills = [
- InlineSkill(name="charlie", description="C.", instructions="Body"),
- InlineSkill(name="alpha", description="A.", instructions="Body"),
- InlineSkill(name="bravo", description="B.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="charlie", description="C."), instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="alpha", description="A."), instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="bravo", description="B."), instructions="Body"),
]
result = SkillsProvider._create_instructions(None, skills)
assert result is not None
@@ -1886,7 +1942,7 @@ def test_multiple_skills_generates_sorted_xml(self) -> None:
def test_custom_template_missing_runner_instructions_raises(self) -> None:
"""Custom template without {runner_instructions} raises when scripts are enabled."""
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
template = "Skills: {skills}"
with pytest.raises(ValueError, match="runner_instructions"):
@@ -1895,7 +1951,7 @@ def test_custom_template_missing_runner_instructions_raises(self) -> None:
def test_custom_template_missing_resource_instructions_raises(self) -> None:
"""Custom template without {resource_instructions} raises when resources exist."""
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
template = "Skills: {skills}"
with pytest.raises(ValueError, match="resource_instructions"):
@@ -1904,7 +1960,7 @@ def test_custom_template_missing_resource_instructions_raises(self) -> None:
def test_include_resource_instructions_true_adds_resource_text(self) -> None:
"""When include_resource_instructions is True, resource instructions appear in the prompt."""
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
result = SkillsProvider._create_instructions(None, skills, include_resource_instructions=True)
assert result is not None
@@ -1913,7 +1969,7 @@ def test_include_resource_instructions_true_adds_resource_text(self) -> None:
def test_include_resource_instructions_false_omits_resource_text(self) -> None:
"""When include_resource_instructions is False, resource instructions do not appear."""
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
result = SkillsProvider._create_instructions(None, skills, include_resource_instructions=False)
assert result is not None
@@ -1922,7 +1978,7 @@ def test_include_resource_instructions_false_omits_resource_text(self) -> None:
def test_custom_template_with_unknown_placeholder_raises(self) -> None:
"""Template with an unknown placeholder raises ValueError."""
skills = [
- InlineSkill(name="my-skill", description="Skill.", instructions="Body"),
+ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"),
]
template = "Skills: {skills} {unknown_key}"
with pytest.raises(ValueError, match="valid format string"):
@@ -1952,7 +2008,7 @@ async def test_load_skill_whitespace_name_returns_error(self, tmp_path: Path) ->
assert "empty" in result
async def test_read_skill_resource_whitespace_skill_name_returns_error(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
provider = SkillsProvider([skill])
await _init_provider(provider)
result = await provider._read_skill_resource(_raw_skills(provider), " ", "ref")
@@ -1960,7 +2016,7 @@ async def test_read_skill_resource_whitespace_skill_name_returns_error(self) ->
assert "empty" in result
async def test_read_skill_resource_whitespace_resource_name_returns_error(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
provider = SkillsProvider([skill])
await _init_provider(provider)
result = await provider._read_skill_resource(_raw_skills(provider), "my-skill", " ")
@@ -1968,7 +2024,7 @@ async def test_read_skill_resource_whitespace_resource_name_returns_error(self)
assert "empty" in result
async def test_read_callable_resource_exception_returns_error(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def exploding_resource() -> Any:
@@ -1981,7 +2037,7 @@ def exploding_resource() -> Any:
assert "Failed to read resource" in result
async def test_read_async_callable_resource_exception_returns_error(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
async def async_exploding() -> Any:
@@ -1993,7 +2049,9 @@ async def async_exploding() -> Any:
assert result.startswith("Error:")
async def test_load_code_skill_xml_escapes_metadata(self) -> None:
- skill = InlineSkill(name="my-skill", description='Uses & "quotes"', instructions="Body")
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description='Uses & "quotes"'), instructions="Body"
+ )
provider = SkillsProvider([skill])
await _init_provider(provider)
result = provider._load_skill(_raw_skills(provider), "my-skill")
@@ -2001,8 +2059,10 @@ async def test_load_code_skill_xml_escapes_metadata(self) -> None:
assert "&" in result
async def test_code_skill_deduplication(self) -> None:
- skill1 = InlineSkill(name="my-skill", description="First.", instructions="Body 1")
- skill2 = InlineSkill(name="my-skill", description="Second.", instructions="Body 2")
+ skill1 = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="First."), instructions="Body 1")
+ skill2 = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="Second."), instructions="Body 2"
+ )
provider = SkillsProvider([skill1, skill2])
await _init_provider(provider)
assert len(_ctx(provider)[0]) == 1
@@ -2010,7 +2070,7 @@ async def test_code_skill_deduplication(self) -> None:
async def test_before_run_extends_tools_even_without_instructions(self) -> None:
"""If instructions are somehow None but skills exist, tools should still be added."""
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
provider = SkillsProvider([skill])
context = SessionContext(input_messages=[])
@@ -2122,7 +2182,7 @@ class TestSkillResourceDecoratorEdgeCases:
"""Additional edge-case tests for the @skill.resource decorator."""
def test_decorator_no_docstring_description_is_none(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def no_docs() -> Any:
@@ -2131,7 +2191,7 @@ def no_docs() -> Any:
assert skill.resources[0].description is None
def test_decorator_with_name_only(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource(name="custom-name")
def get_data() -> Any:
@@ -2143,7 +2203,7 @@ def get_data() -> Any:
assert skill.resources[0].description is None
def test_decorator_with_description_only(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource(description="Custom desc")
def get_data() -> Any:
@@ -2153,7 +2213,7 @@ def get_data() -> Any:
assert skill.resources[0].description == "Custom desc"
def test_decorator_preserves_original_function_identity(self) -> None:
- skill = InlineSkill(name="my-skill", description="A skill.", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body")
@skill.resource
def original() -> Any:
@@ -2230,7 +2290,7 @@ def greet(name: str = "world") -> str:
return f"hello {name}"
script = InlineSkillScript(name="greet", function=greet)
- skill = InlineSkill(name="s", description="d", instructions="c")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
result = await script.run(skill, args={"name": "Alice"})
assert result == "hello Alice"
@@ -2239,7 +2299,7 @@ async def greet(name: str = "world") -> str:
return f"async {name}"
script = InlineSkillScript(name="greet", function=greet)
- skill = InlineSkill(name="s", description="d", instructions="c")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
result = await script.run(skill, args={"name": "Bob"})
assert result == "async Bob"
@@ -2248,13 +2308,13 @@ def func(x: int = 0, **kwargs: Any) -> dict[str, Any]:
return {"x": x, **kwargs}
script = InlineSkillScript(name="f", function=func)
- skill = InlineSkill(name="s", description="d", instructions="c")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
result = await script.run(skill, args={"x": 1}, extra="val")
assert result == {"x": 1, "extra": "val"}
async def test_run_code_defined_no_args(self) -> None:
script = InlineSkillScript(name="f", function=lambda: 42)
- skill = InlineSkill(name="s", description="d", instructions="c")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
result = await script.run(skill)
assert result == 42
@@ -2268,7 +2328,9 @@ def runner(skill: Skill, script: SkillScript, args: dict[str, Any] | None = None
return "runner_result"
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
- skill = FileSkill(name="my-skill", description="d", content="c", path=f"{_ABS}/test")
+ skill = FileSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="d"), content="c", path=f"{_ABS}/test"
+ )
result = await script.run(skill, args={"key": "val"})
assert result == "runner_result"
assert captured["skill"] == "my-skill"
@@ -2280,19 +2342,19 @@ async def runner(skill: Skill, script: SkillScript, args: dict[str, Any] | None
return "async_runner"
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=runner)
- skill = FileSkill(name="s", description="d", content="c", path=f"{_ABS}/test")
+ skill = FileSkill(frontmatter=SkillFrontmatter(name="s", description="d"), content="c", path=f"{_ABS}/test")
result = await script.run(skill, args=None)
assert result == "async_runner"
async def test_run_file_based_without_runner_raises(self) -> None:
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py")
- skill = FileSkill(name="s", description="d", content="c", path=f"{_ABS}/test")
+ skill = FileSkill(frontmatter=SkillFrontmatter(name="s", description="d"), content="c", path=f"{_ABS}/test")
with pytest.raises(ValueError, match="requires a runner"):
await script.run(skill)
async def test_run_file_based_with_non_file_skill_raises_type_error(self) -> None:
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py", runner=_noop_script_runner)
- skill = InlineSkill(name="s", description="d", instructions="c")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="s", description="d"), instructions="c")
with pytest.raises(TypeError, match="requires a FileSkill"):
await script.run(skill)
@@ -2314,7 +2376,7 @@ class TestSkillScriptDecorator:
"""Tests for the @skill.script decorator."""
def test_bare_decorator(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def analyze(query: str) -> str:
@@ -2328,7 +2390,7 @@ def analyze(query: str) -> str:
assert skill.scripts[0].function is analyze
def test_parameterized_decorator(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script(name="custom-name", description="Custom desc")
def my_func() -> str:
@@ -2341,7 +2403,7 @@ def my_func() -> str:
assert skill.scripts[0].function is my_func
def test_multiple_scripts(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def script_a() -> str:
@@ -2356,7 +2418,7 @@ def script_b() -> str:
assert skill.scripts[1].name == "script_b"
def test_async_script(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
async def fetch_data() -> str:
@@ -2369,7 +2431,7 @@ async def fetch_data() -> str:
assert skill.scripts[0].function is fetch_data
def test_decorator_returns_original_function(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def original() -> str:
@@ -2392,12 +2454,14 @@ class TestSkillWithScripts:
"""Tests for the Skill class with scripts attribute."""
def test_default_empty_scripts(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
assert skill.scripts == []
def test_scripts_at_construction(self) -> None:
scripts = [InlineSkillScript(name="s1", function=lambda: None)]
- skill = InlineSkill(name="my-skill", description="test", instructions="body", scripts=scripts)
+ skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body", scripts=scripts
+ )
assert len(skill.scripts) == 1
assert skill.scripts[0].name == "s1"
@@ -2419,7 +2483,7 @@ async def my_runner(skill, script, args=None):
assert isinstance(my_runner, SkillScriptRunner)
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = FileSkillScript(name="my-script", full_path=f"{_ABS}/test/scripts/run.py")
skill.scripts.append(script)
@@ -2437,7 +2501,7 @@ async def __call__(self, skill, script, args=None):
runner = _CustomRunner()
assert isinstance(runner, SkillScriptRunner)
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = InlineSkillScript(name="my-script", function=lambda: None)
skill.scripts.append(script)
@@ -2448,7 +2512,7 @@ async def test_runner_returns_none(self) -> None:
async def noop_runner(skill, script, args=None):
return None
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = InlineSkillScript(name="s1", function=lambda: None)
result = await noop_runner(skill, script)
@@ -2458,7 +2522,7 @@ async def test_runner_returns_object(self) -> None:
async def dict_runner(skill, script, args=None):
return {"exit_code": 0, "output": "ok"}
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = FileSkillScript(name="s1", full_path=f"{_ABS}/test/scripts/run.py")
result = await dict_runner(skill, script)
@@ -2473,7 +2537,7 @@ def my_runner(skill, script, args=None):
assert isinstance(my_runner, SkillScriptRunner)
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = FileSkillScript(name="my-script", full_path=f"{_ABS}/test/scripts/run.py")
skill.scripts.append(script)
@@ -2491,7 +2555,7 @@ def __call__(self, skill, script, args=None):
runner = _SyncRunner()
assert isinstance(runner, SkillScriptRunner)
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = InlineSkillScript(name="my-script", function=lambda: None)
skill.scripts.append(script)
@@ -2502,7 +2566,7 @@ def test_sync_runner_returns_none(self) -> None:
def noop_runner(skill, script, args=None):
return None
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = InlineSkillScript(name="s1", function=lambda: None)
result = noop_runner(skill, script)
@@ -2512,7 +2576,7 @@ def test_sync_runner_returns_object(self) -> None:
def dict_runner(skill, script, args=None):
return {"exit_code": 0, "output": "ok"}
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
script = FileSkillScript(name="s1", full_path=f"{_ABS}/test/scripts/run.py")
result = dict_runner(skill, script)
@@ -2528,7 +2592,7 @@ class TestSkillsProviderFactories:
"""Tests for the SkillsProvider constructor auto-wiring behavior."""
async def test_code_skills_with_scripts_creates_provider(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2538,7 +2602,7 @@ async def test_code_skills_with_scripts_creates_provider(self) -> None:
assert any(hasattr(t, "name") and t.name == "run_skill_script" for t in _ctx(provider)[2])
async def test_code_skills_no_scripts(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
# No scripts with functions, no runner, no resources — only load_skill
@@ -2549,7 +2613,7 @@ async def test_code_script_runs_directly(self) -> None:
def my_function(key: str = "") -> str:
return f"executed: {key}"
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=my_function))
provider = SkillsProvider([skill])
@@ -2560,7 +2624,7 @@ def my_function(key: str = "") -> str:
assert result == "executed: hello"
async def test_no_scripts_no_tool(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
# No scripts at all — no run_skill_script tool
provider = SkillsProvider([skill])
await _init_provider(provider)
@@ -2568,14 +2632,14 @@ async def test_no_scripts_no_tool(self) -> None:
async def test_no_resources_no_read_skill_resource_tool(self) -> None:
"""When no skill has resources, read_skill_resource tool is not advertised."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
assert not any(hasattr(t, "name") and t.name == "read_skill_resource" for t in _ctx(provider)[2])
async def test_resources_present_includes_read_skill_resource_tool(self) -> None:
"""When a skill has resources, read_skill_resource tool is advertised."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.resources.append(InlineSkillResource(name="ref", content="reference data"))
provider = SkillsProvider([skill])
await _init_provider(provider)
@@ -2583,7 +2647,7 @@ async def test_resources_present_includes_read_skill_resource_tool(self) -> None
async def test_resources_present_includes_resource_instructions(self) -> None:
"""When a skill has resources, instructions mention read_skill_resource."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.resources.append(InlineSkillResource(name="ref", content="reference data"))
provider = SkillsProvider([skill])
await _init_provider(provider)
@@ -2591,14 +2655,14 @@ async def test_resources_present_includes_resource_instructions(self) -> None:
async def test_no_resources_excludes_resource_instructions(self) -> None:
"""When no skill has resources, instructions do not mention read_skill_resource."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
assert "read_skill_resource" not in (_ctx(provider)[1] or "")
async def test_read_skill_resource_tool_returns_content(self) -> None:
"""The read_skill_resource tool returns resource content when invoked."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.resources.append(InlineSkillResource(name="ref", content="reference data"))
provider = SkillsProvider([skill])
await _init_provider(provider)
@@ -2695,7 +2759,9 @@ async def test_combined_skills(self, tmp_path: Path) -> None:
encoding="utf-8",
)
- code_skill = InlineSkill(name="code-skill", description="test", instructions="body")
+ code_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="code-skill", description="test"), instructions="body"
+ )
code_skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider(
@@ -2725,7 +2791,7 @@ async def test_file_scripts_without_runner_no_error_at_init(self, tmp_path: Path
async def test_file_script_error_without_runner(self) -> None:
# A skill with both a code script and a file-based script
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok"))
skill.scripts.append(FileSkillScript(name="file-s", full_path=f"{_ABS}/test/scripts/s1.py"))
@@ -2746,7 +2812,7 @@ async def test_async_code_script_runs_directly(self) -> None:
async def async_func(x: int = 0) -> str:
return f"async: {x}"
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=async_func))
provider = SkillsProvider([skill])
@@ -2761,7 +2827,7 @@ async def test_code_script_returns_object(self) -> None:
def returns_dict() -> dict:
return {"status": "ok", "value": 42}
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=returns_dict))
provider = SkillsProvider([skill])
@@ -2772,7 +2838,7 @@ def returns_dict() -> dict:
async def test_code_script_returns_none(self) -> None:
"""Code-defined scripts returning None pass through as None."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2783,7 +2849,7 @@ async def test_code_script_returns_none(self) -> None:
async def test_script_with_path_errors_without_runner(self) -> None:
"""A file-based script without a runner should return an error."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok"))
skill.scripts.append(FileSkillScript(name="path-s", full_path=f"{_ABS}/test/scripts/s1.py"))
@@ -2801,7 +2867,7 @@ async def test_script_with_path_errors_without_runner(self) -> None:
assert "script_runner" in result or "Failed to run" in result
async def test_run_skill_script_error_on_missing_skill(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2812,7 +2878,7 @@ async def test_run_skill_script_error_on_missing_skill(self) -> None:
assert "nonexistent" in result
async def test_run_skill_script_sync_with_kwargs(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def greet(name: str, **kwargs: Any) -> str:
@@ -2827,7 +2893,7 @@ def greet(name: str, **kwargs: Any) -> str:
assert result == "Hello Alice (user=u42)"
async def test_run_skill_script_async_with_kwargs(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
async def fetch(url: str, **kwargs: Any) -> str:
@@ -2843,7 +2909,7 @@ async def fetch(url: str, **kwargs: Any) -> str:
async def test_run_skill_script_without_kwargs_ignores_extra_args(self) -> None:
"""Script functions without **kwargs should still work when runtime kwargs are passed."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def simple(query: str) -> str:
@@ -2858,7 +2924,7 @@ def simple(query: str) -> str:
async def test_run_skill_script_conflicting_args_and_kwargs_raises(self) -> None:
"""Conflicting keys in args and kwargs should raise TypeError."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
@skill.script
def process(**kwargs: Any) -> str:
@@ -2872,7 +2938,7 @@ def process(**kwargs: Any) -> str:
assert "Error" in result
async def test_run_skill_script_error_on_missing_script(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2883,7 +2949,7 @@ async def test_run_skill_script_error_on_missing_script(self) -> None:
assert "nonexistent" in result
async def test_run_skill_script_error_on_empty_names(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2897,7 +2963,7 @@ async def test_run_skill_script_error_on_empty_names(self) -> None:
assert "Error" in result
async def test_instructions_include_script_runner_hints(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2906,14 +2972,14 @@ async def test_instructions_include_script_runner_hints(self) -> None:
assert "not as top-level tool parameters" in _ctx(provider)[1]
async def test_no_scripts_no_runner_no_script_instructions(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
# No scripts and no runner — instructions should not mention run_skill_script
assert "run_skill_script" not in (_ctx(provider)[1] or "")
async def test_tool_schema_args_description_mentions_key_format(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2925,7 +2991,7 @@ async def test_tool_schema_args_description_mentions_key_format(self) -> None:
async def test_require_script_approval_sets_approval_mode(self) -> None:
"""When require_script_approval=True, the run_skill_script tool has approval_mode='always_require'."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill], require_script_approval=True)
@@ -2935,7 +3001,7 @@ async def test_require_script_approval_sets_approval_mode(self) -> None:
async def test_require_script_approval_false_by_default(self) -> None:
"""By default, the run_skill_script tool has approval_mode='never_require'."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill])
@@ -2945,7 +3011,7 @@ async def test_require_script_approval_false_by_default(self) -> None:
async def test_require_script_approval_does_not_affect_other_tools(self) -> None:
"""The load_skill tool should never require approval."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider([skill], require_script_approval=True)
@@ -2961,7 +3027,7 @@ async def test_code_script_exception_returns_error(self) -> None:
def failing_script() -> str:
raise RuntimeError("Something went wrong")
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="boom", function=failing_script))
provider = SkillsProvider([skill])
@@ -2974,7 +3040,7 @@ def failing_script() -> str:
async def test_custom_template_without_runner_placeholder_raises(self) -> None:
"""Provider with code scripts and custom template missing {runner_instructions} raises."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider(
@@ -3138,7 +3204,7 @@ class TestCreateInstructionsWithScripts:
"""Tests for script metadata in skill advertisement."""
def test_excludes_script_count(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
result = SkillsProvider._create_instructions(None, [skill])
@@ -3146,7 +3212,7 @@ def test_excludes_script_count(self) -> None:
assert "" not in result
def test_no_scripts_element_when_empty(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
result = SkillsProvider._create_instructions(None, [skill])
assert result is not None
@@ -3162,7 +3228,7 @@ class TestLoadSkillWithScripts:
"""Tests for script metadata in load_skill output."""
async def test_code_skill_includes_scripts_element(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=lambda: None))
provider = SkillsProvider([skill])
@@ -3174,7 +3240,7 @@ async def test_code_skill_includes_scripts_element(self) -> None:
assert 'description="Run analysis"' in result
async def test_code_skill_no_scripts_element(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
result = provider._load_skill(_raw_skills(provider), "my-skill")
@@ -3190,7 +3256,7 @@ class _MinimalClassSkill(ClassSkill):
"""A minimal class-based skill with no resources or scripts."""
def __init__(self) -> None:
- super().__init__(name="minimal-skill", description="A minimal skill.")
+ super().__init__(frontmatter=SkillFrontmatter(name="minimal-skill", description="A minimal skill."))
@property
def instructions(self) -> str:
@@ -3201,7 +3267,7 @@ class _FullClassSkill(ClassSkill):
"""A class-based skill with resources and scripts."""
def __init__(self) -> None:
- super().__init__(name="full-skill", description="A full skill.")
+ super().__init__(frontmatter=SkillFrontmatter(name="full-skill", description="A full skill."))
self._resources: list[SkillResource] | None = None
self._scripts: list[SkillScript] | None = None
@@ -3338,7 +3404,9 @@ async def test_in_memory_source_with_class_skill(self) -> None:
assert skills[0].name == "minimal-skill"
async def test_mixed_inline_and_class_skills(self) -> None:
- inline = InlineSkill(name="inline-skill", description="Inline", instructions="inline body")
+ inline = InlineSkill(
+ frontmatter=SkillFrontmatter(name="inline-skill", description="Inline"), instructions="inline body"
+ )
class_skill = _MinimalClassSkill()
provider = SkillsProvider([inline, class_skill])
await _init_provider(provider)
@@ -3372,7 +3440,9 @@ class _DecoratorClassSkill(ClassSkill):
"""A class-based skill using @ClassSkill.resource and @ClassSkill.script decorators."""
def __init__(self) -> None:
- super().__init__(name="decorator-skill", description="A decorator-discovered skill.")
+ super().__init__(
+ frontmatter=SkillFrontmatter(name="decorator-skill", description="A decorator-discovered skill.")
+ )
@property
def instructions(self) -> str:
@@ -3395,7 +3465,7 @@ class _BareDecoratorSkill(ClassSkill):
"""Skill using bare decorators (no arguments) — name/description from method."""
def __init__(self) -> None:
- super().__init__(name="bare-skill", description="Bare decorator skill.")
+ super().__init__(frontmatter=SkillFrontmatter(name="bare-skill", description="Bare decorator skill."))
@property
def instructions(self) -> str:
@@ -3416,7 +3486,7 @@ class _DuplicateResourceSkill(ClassSkill):
"""Skill with duplicate resource names — should raise."""
def __init__(self) -> None:
- super().__init__(name="dup-skill", description="Dup.")
+ super().__init__(frontmatter=SkillFrontmatter(name="dup-skill", description="Dup."))
@property
def instructions(self) -> str:
@@ -3435,7 +3505,7 @@ class _DuplicateScriptSkill(ClassSkill):
"""Skill with duplicate script names — should raise."""
def __init__(self) -> None:
- super().__init__(name="dup-script-skill", description="Dup.")
+ super().__init__(frontmatter=SkillFrontmatter(name="dup-script-skill", description="Dup."))
@property
def instructions(self) -> str:
@@ -3454,7 +3524,7 @@ class _SelfAccessSkill(ClassSkill):
"""Skill where resource/script access instance state via self."""
def __init__(self, multiplier: int = 10) -> None:
- super().__init__(name="self-access", description="Self access skill.")
+ super().__init__(frontmatter=SkillFrontmatter(name="self-access", description="Self access skill."))
self.multiplier = multiplier
@property
@@ -3688,7 +3758,7 @@ def test_wrong_decorator_order_resource_raises(self) -> None:
class _BadOrder(ClassSkill):
def __init__(self) -> None:
- super().__init__(name="bad", description="bad")
+ super().__init__(frontmatter=SkillFrontmatter(name="bad", description="bad"))
@property
def instructions(self) -> str:
@@ -3705,7 +3775,7 @@ def test_wrong_decorator_order_script_raises(self) -> None:
class _BadOrder(ClassSkill):
def __init__(self) -> None:
- super().__init__(name="bad", description="bad")
+ super().__init__(frontmatter=SkillFrontmatter(name="bad", description="bad"))
@property
def instructions(self) -> str:
@@ -3722,7 +3792,7 @@ def test_invalid_explicit_resource_name_raises(self) -> None:
class _BadName(ClassSkill):
def __init__(self) -> None:
- super().__init__(name="bad", description="bad")
+ super().__init__(frontmatter=SkillFrontmatter(name="bad", description="bad"))
@property
def instructions(self) -> str:
@@ -3738,7 +3808,7 @@ def test_invalid_explicit_script_name_raises(self) -> None:
class _BadName(ClassSkill):
def __init__(self) -> None:
- super().__init__(name="bad", description="bad")
+ super().__init__(frontmatter=SkillFrontmatter(name="bad", description="bad"))
@property
def instructions(self) -> str:
@@ -3754,7 +3824,7 @@ def test_empty_explicit_name_raises(self) -> None:
class _EmptyName(ClassSkill):
def __init__(self) -> None:
- super().__init__(name="bad", description="bad")
+ super().__init__(frontmatter=SkillFrontmatter(name="bad", description="bad"))
@property
def instructions(self) -> str:
@@ -3798,7 +3868,7 @@ class _ExplicitDescriptionSkill(ClassSkill):
"""Skill with explicit descriptions on decorator."""
def __init__(self) -> None:
- super().__init__(name="desc-skill", description="Explicit desc.")
+ super().__init__(frontmatter=SkillFrontmatter(name="desc-skill", description="Explicit desc."))
@property
def instructions(self) -> str:
@@ -3817,7 +3887,7 @@ class _PropertyCallCountSkill(ClassSkill):
"""Tracks how many times the property getter is called."""
def __init__(self) -> None:
- super().__init__(name="callcount-skill", description="Tracks calls.")
+ super().__init__(frontmatter=SkillFrontmatter(name="callcount-skill", description="Tracks calls."))
self.getter_call_count = 0
@property
@@ -3847,7 +3917,7 @@ class _ChildSkill(_ParentSkill):
"""Child inheriting parent resources and adding its own."""
def __init__(self) -> None:
- super().__init__(name="child-skill", description="Child.")
+ super().__init__(frontmatter=SkillFrontmatter(name="child-skill", description="Child."))
@property
def instructions(self) -> str:
@@ -3862,7 +3932,7 @@ class _KwargsSkill(ClassSkill):
"""Skill that uses **kwargs from runtime."""
def __init__(self) -> None:
- super().__init__(name="kwargs-skill", description="Kwargs.")
+ super().__init__(frontmatter=SkillFrontmatter(name="kwargs-skill", description="Kwargs."))
@property
def instructions(self) -> str:
@@ -3886,7 +3956,7 @@ class _ChildWithInheritedPropertySkill(_ParentWithPropertyResource):
"""Child that should discover inherited property resource."""
def __init__(self) -> None:
- super().__init__(name="child-prop-skill", description="Child prop.")
+ super().__init__(frontmatter=SkillFrontmatter(name="child-prop-skill", description="Child prop."))
@property
def instructions(self) -> str:
@@ -3897,7 +3967,7 @@ class _PropertyResourceSkill(ClassSkill):
"""Skill with a property-based resource."""
def __init__(self) -> None:
- super().__init__(name="prop-skill", description="Property skill.")
+ super().__init__(frontmatter=SkillFrontmatter(name="prop-skill", description="Property skill."))
@property
def instructions(self) -> str:
@@ -3914,7 +3984,7 @@ class _MixedPropertyMethodSkill(ClassSkill):
"""Skill with both property and method resources."""
def __init__(self) -> None:
- super().__init__(name="mixed-prop", description="Mixed.")
+ super().__init__(frontmatter=SkillFrontmatter(name="mixed-prop", description="Mixed."))
@property
def instructions(self) -> str:
@@ -3937,7 +4007,7 @@ async def test_code_skill_scripts_element_contains_parameters(self) -> None:
def analyze(query: str, limit: int = 10) -> str:
return "result"
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=analyze))
provider = SkillsProvider([skill])
@@ -3954,7 +4024,7 @@ class TestReadSkillResourceWithScripts:
"""Tests for _read_skill_resource falling back to scripts."""
async def test_reads_script_with_static_content(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="generate.py", function=lambda: "print('hello')"))
provider = SkillsProvider([skill])
@@ -3964,7 +4034,7 @@ async def test_reads_script_with_static_content(self) -> None:
assert "not found" in result
async def test_script_not_accessible_via_read_resource(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="run.py", function=lambda: "script output"))
provider = SkillsProvider([skill])
@@ -3977,7 +4047,7 @@ async def test_async_script_not_accessible_via_read_resource(self) -> None:
async def async_script() -> str:
return "async output"
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="run.py", function=async_script))
provider = SkillsProvider([skill])
@@ -3986,7 +4056,7 @@ async def async_script() -> str:
assert "not found" in result
async def test_script_case_insensitive_not_in_resources(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="Generate.py", function=lambda: "code"))
provider = SkillsProvider([skill])
@@ -3995,7 +4065,7 @@ async def test_script_case_insensitive_not_in_resources(self) -> None:
assert "not found" in result
async def test_resource_takes_priority_over_script(self) -> None:
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.resources.append(InlineSkillResource(name="data.py", content="resource content"))
skill.scripts.append(InlineSkillScript(name="data.py", function=lambda: "script content"))
@@ -4008,7 +4078,7 @@ async def test_script_function_error_not_exposed_via_resources(self) -> None:
def failing_script() -> str:
raise RuntimeError("boom")
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="bad.py", function=failing_script))
provider = SkillsProvider([skill])
@@ -4217,7 +4287,7 @@ class TestLoadSkillsMerging:
def test_code_skill_with_invalid_name_raises(self) -> None:
"""Code skills with invalid metadata (e.g. uppercase name) raise at construction."""
with pytest.raises(ValueError, match="Invalid skill name"):
- InlineSkill(name="INVALID_NAME", description="valid", instructions="body")
+ InlineSkill(frontmatter=SkillFrontmatter(name="INVALID_NAME", description="valid"), instructions="body")
async def test_file_skill_takes_precedence_over_code_skill(self, tmp_path: Path) -> None:
"""When file-based and code-defined skills share a name, file-based wins."""
@@ -4235,7 +4305,9 @@ async def test_file_skill_takes_precedence_over_code_skill(self, tmp_path: Path)
encoding="utf-8",
)
- code_skill = InlineSkill(name="my-skill", description="Code skill.", instructions="Code body.")
+ code_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="my-skill", description="Code skill."), instructions="Code body."
+ )
source = DeduplicatingSkillsSource(
AggregatingSkillsSource([
@@ -4295,8 +4367,8 @@ async def test_in_memory_skills_source_returns_all_skills(self) -> None:
"""InMemorySkillsSource returns all provided skills."""
from agent_framework import InMemorySkillsSource
- s1 = InlineSkill(name="skill-a", description="A", instructions="body")
- s2 = InlineSkill(name="skill-b", description="B", instructions="body")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-a", description="A"), instructions="body")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-b", description="B"), instructions="body")
source = InMemorySkillsSource([s1, s2])
skills = await source.get_skills()
@@ -4308,8 +4380,8 @@ async def test_aggregating_source_combines_sources(self) -> None:
"""Aggregating source concatenates results from multiple sources."""
from agent_framework import AggregatingSkillsSource, InMemorySkillsSource
- s1 = InlineSkill(name="skill-a", description="A", instructions="body")
- s2 = InlineSkill(name="skill-b", description="B", instructions="body")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-a", description="A"), instructions="body")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-b", description="B"), instructions="body")
source = AggregatingSkillsSource([
InMemorySkillsSource([s1]),
@@ -4323,8 +4395,8 @@ async def test_filtering_source_filters_by_predicate(self) -> None:
"""FilteringSkillsSource only returns skills matching the predicate."""
from agent_framework import FilteringSkillsSource, InMemorySkillsSource
- s1 = InlineSkill(name="keep-me", description="keep", instructions="body")
- s2 = InlineSkill(name="drop-me", description="drop", instructions="body")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="keep-me", description="keep"), instructions="body")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="drop-me", description="drop"), instructions="body")
source = FilteringSkillsSource(
InMemorySkillsSource([s1, s2]),
@@ -4338,9 +4410,9 @@ async def test_deduplicating_source_removes_duplicates(self) -> None:
"""DeduplicatingSkillsSource keeps first skill with each name."""
from agent_framework import DeduplicatingSkillsSource, InMemorySkillsSource
- s1 = InlineSkill(name="my-skill", description="first", instructions="body1")
- s2 = InlineSkill(name="my-skill", description="second", instructions="body2")
- s3 = InlineSkill(name="other", description="other", instructions="body3")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="first"), instructions="body1")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="second"), instructions="body2")
+ s3 = InlineSkill(frontmatter=SkillFrontmatter(name="other", description="other"), instructions="body3")
source = DeduplicatingSkillsSource(InMemorySkillsSource([s1, s2, s3]))
skills = await source.get_skills()
@@ -4355,7 +4427,7 @@ async def test_delegating_source_delegates(self) -> None:
"""DelegatingSkillsSource delegates to inner source by default."""
from agent_framework import DelegatingSkillsSource, InMemorySkillsSource
- skill = InlineSkill(name="test-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body")
inner = InMemorySkillsSource([skill])
class PassthroughSource(DelegatingSkillsSource):
@@ -4385,7 +4457,9 @@ async def test_provider_source_overrides_legacy_params(self, tmp_path: Path) ->
"""When source= is provided, skill_paths and skills are ignored."""
from agent_framework import InMemorySkillsSource
- code_skill = InlineSkill(name="code-skill", description="test", instructions="body")
+ code_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="code-skill", description="test"), instructions="body"
+ )
source = InMemorySkillsSource([code_skill])
# Pass skill_paths that would normally discover file skills — should be ignored
@@ -4411,8 +4485,12 @@ async def test_composed_source_pipeline(self, tmp_path: Path) -> None:
encoding="utf-8",
)
- code_skill = InlineSkill(name="code-skill", description="Code.", instructions="Body.")
- internal = InlineSkill(name="internal", description="Internal.", instructions="Body.")
+ code_skill = InlineSkill(
+ frontmatter=SkillFrontmatter(name="code-skill", description="Code."), instructions="Body."
+ )
+ internal = InlineSkill(
+ frontmatter=SkillFrontmatter(name="internal", description="Internal."), instructions="Body."
+ )
source = FilteringSkillsSource(
DeduplicatingSkillsSource(
@@ -4453,15 +4531,15 @@ async def test_file_skills_source_with_provider(self, tmp_path: Path) -> None:
async def test_code_skills_with_provider(self) -> None:
"""InMemorySkillsSource with code skills creates a working provider."""
- skill = InlineSkill(name="code-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="code-skill", description="test"), instructions="body")
provider = SkillsProvider(DeduplicatingSkillsSource(InMemorySkillsSource([skill])))
await _init_provider(provider)
assert "code-skill" in _ctx(provider)[0]
async def test_multiple_code_skills(self) -> None:
"""InMemorySkillsSource with multiple skills registers them all."""
- s1 = InlineSkill(name="skill-a", description="A", instructions="body")
- s2 = InlineSkill(name="skill-b", description="B", instructions="body")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-a", description="A"), instructions="body")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="skill-b", description="B"), instructions="body")
provider = SkillsProvider(DeduplicatingSkillsSource(InMemorySkillsSource([s1, s2])))
await _init_provider(provider)
assert "skill-a" in _ctx(provider)[0]
@@ -4469,7 +4547,7 @@ async def test_multiple_code_skills(self) -> None:
async def test_custom_source_with_provider(self) -> None:
"""Custom source passed to SkillsProvider works."""
- skill = InlineSkill(name="custom", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="custom", description="test"), instructions="body")
source = InMemorySkillsSource([skill])
provider = SkillsProvider(DeduplicatingSkillsSource(source))
await _init_provider(provider)
@@ -4479,8 +4557,8 @@ async def test_filtering_source_excludes_skills(self) -> None:
"""FilteringSkillsSource excludes matching skills."""
from agent_framework import FilteringSkillsSource
- s1 = InlineSkill(name="keep-me", description="keep", instructions="body")
- s2 = InlineSkill(name="drop-me", description="drop", instructions="body")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="keep-me", description="keep"), instructions="body")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="drop-me", description="drop"), instructions="body")
source = DeduplicatingSkillsSource(
FilteringSkillsSource(
@@ -4495,8 +4573,8 @@ async def test_filtering_source_excludes_skills(self) -> None:
async def test_dedup_across_sources(self) -> None:
"""DeduplicatingSkillsSource deduplicates across aggregated sources."""
- s1 = InlineSkill(name="dup", description="first", instructions="body1")
- s2 = InlineSkill(name="dup", description="second", instructions="body2")
+ s1 = InlineSkill(frontmatter=SkillFrontmatter(name="dup", description="first"), instructions="body1")
+ s2 = InlineSkill(frontmatter=SkillFrontmatter(name="dup", description="second"), instructions="body2")
source = DeduplicatingSkillsSource(
AggregatingSkillsSource([
@@ -4527,7 +4605,7 @@ async def test_file_source_with_script_runner(self, tmp_path: Path) -> None:
async def test_script_approval_on_provider(self) -> None:
"""SkillsProvider with require_script_approval sets the approval mode."""
- skill = InlineSkill(name="my-skill", description="test", instructions="body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None))
provider = SkillsProvider(
@@ -4616,13 +4694,13 @@ async def test_from_paths_with_resource_extensions(self, tmp_path: Path) -> None
def test_init_with_skills_creates_provider(self) -> None:
"""Constructor with skill list returns a SkillsProvider instance."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill])
assert isinstance(provider, SkillsProvider)
async def test_init_with_skills_registers_skills(self) -> None:
"""Constructor with skill list registers code-defined skills."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill])
await _init_provider(provider)
assert "test-skill" in _ctx(provider)[0]
@@ -4635,7 +4713,7 @@ async def test_init_with_empty_list(self) -> None:
async def test_init_with_skills_and_options(self) -> None:
"""Constructor with skills passes through keyword options."""
- skill = InlineSkill(name="my-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Test"), instructions="Body")
provider = SkillsProvider(
[skill],
require_script_approval=True,
@@ -4648,7 +4726,7 @@ def test_init_with_source_creates_provider(self) -> None:
"""Constructor with SkillsSource returns a SkillsProvider instance."""
from agent_framework import InMemorySkillsSource
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
source = InMemorySkillsSource([skill])
provider = SkillsProvider(source)
assert isinstance(provider, SkillsProvider)
@@ -4657,7 +4735,7 @@ async def test_init_with_source_uses_provided_source(self) -> None:
"""Constructor with SkillsSource uses the exact source given."""
from agent_framework import InMemorySkillsSource
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
source = InMemorySkillsSource([skill])
provider = SkillsProvider(source)
await _init_provider(provider)
@@ -4674,7 +4752,7 @@ class TestDisableCaching:
async def test_default_caching_enabled(self) -> None:
"""By default, _get_or_create_context only builds once."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill])
await _init_provider(provider)
first_ctx = provider._cached_context # pyright: ignore[reportPrivateUsage]
@@ -4686,7 +4764,7 @@ async def test_default_caching_enabled(self) -> None:
async def test_disable_caching_rebuilds_on_every_call(self) -> None:
"""With disable_caching=True, _create_context rebuilds every time."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill], disable_caching=True)
await _init_provider(provider)
first_ctx = provider._cached_context # pyright: ignore[reportPrivateUsage]
@@ -4700,20 +4778,20 @@ async def test_disable_caching_via_constructor(self) -> None:
"""disable_caching works via the primary constructor."""
from agent_framework import InMemorySkillsSource
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
source = InMemorySkillsSource([skill])
provider = SkillsProvider(source, disable_caching=True)
assert provider._disable_caching is True
async def test_caching_enabled_by_default(self) -> None:
"""SkillsProvider defaults to caching enabled."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill])
assert provider._disable_caching is False
async def test_disable_caching_before_run_rebuilds(self) -> None:
"""before_run with disable_caching=True calls _create_context each time."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill], disable_caching=True)
context = SessionContext(input_messages=[])
await provider.before_run(agent=AsyncMock(), session=AsyncMock(), context=context, state={})
@@ -4730,7 +4808,7 @@ class TestSkillsProviderConstructorEdgeCases:
async def test_single_skill_accepted(self) -> None:
"""A single Skill (not a list) is accepted and wrapped."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider(skill)
await _init_provider(provider)
skills = _ctx(provider)[0]
@@ -4739,7 +4817,7 @@ async def test_single_skill_accepted(self) -> None:
async def test_template_missing_skills_placeholder_raises(self) -> None:
"""Instruction template without {skills} raises ValueError."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
provider = SkillsProvider([skill], instruction_template="No placeholder here.")
with pytest.raises(ValueError, match="skills"):
await _init_provider(provider)
@@ -4765,7 +4843,7 @@ class TestInlineSkillContentCaching:
def test_content_cached_after_first_access(self) -> None:
"""InlineSkill.content returns the same object on subsequent accesses."""
- skill = InlineSkill(name="test-skill", description="Test", instructions="Body")
+ skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body")
first = skill.content
second = skill.content
assert first is second # Same object (cached)