Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 74 additions & 21 deletions python/packages/core/agent_framework/_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,13 @@ def _build_skill_content(
) -> str:
"""Build XML-structured content for code-defined and class-based skills.

Produces an XML document containing name, description, instructions,
resources, and scripts elements. Used by both :class:`InlineSkill`
and :class:`ClassSkill` to generate their ``content`` property.
Produces an XML document containing name, description, instructions, and
``<available_resources>`` / ``<available_scripts>`` blocks. The two blocks
are always emitted: when a category has no entries, a self-closing element
(e.g. ``<available_scripts />``) is emitted so the model knows none are
available and does not hallucinate their names. Used by both
:class:`InlineSkill` and :class:`ClassSkill` to generate their ``content``
property.

Args:
name: The skill name.
Expand All @@ -698,13 +702,8 @@ def _build_skill_content(
"</instructions>"
)

if resources:
resource_lines = "\n".join(_create_resource_element(r) for r in resources)
result += f"\n\n<resources>\n{resource_lines}\n</resources>"

if scripts:
script_lines = "\n".join(_create_script_element(s) for s in scripts)
result += f"\n\n<scripts>\n{script_lines}\n</scripts>"
result += f"\n\n{_build_available_resources_block(resources)}"
result += f"\n\n{_build_available_scripts_block(scripts)}"

return result

Expand All @@ -725,6 +724,50 @@ def _create_resource_element(resource: SkillResource) -> str:
return f" <resource {attrs}/>"


def _build_available_resources_block(resources: Sequence[SkillResource] | None) -> str:
"""Build an ``<available_resources>`` XML block for the given resources.

Each resource is emitted as a ``<resource name="…"/>`` element (with an
optional ``description`` attribute). When there are no resources, a
self-closing ``<available_resources />`` element is returned so the model
knows none are available and does not hallucinate resource names.

Args:
resources: The resources to include in the block, if any.

Returns:
The ``<available_resources>`` XML block, or ``<available_resources />``
when *resources* is empty or ``None``.
"""
if not resources:
return "<available_resources />"
resource_lines = "\n".join(_create_resource_element(r) for r in resources)
return f"<available_resources>\n{resource_lines}\n</available_resources>"


def _build_available_scripts_block(scripts: Sequence[SkillScript] | None) -> str:
"""Build an ``<available_scripts>`` XML block for the given scripts.

Each script is emitted as a ``<script name="…">`` element; when the script
has a parameter schema it is wrapped in a nested ``<parameters_schema>``
element, otherwise a self-closing ``<script …/>`` element is used. When
there are no scripts, a self-closing ``<available_scripts />`` element is
returned so the model knows none are available and does not hallucinate
script names.

Args:
scripts: The scripts to include in the block, if any.

Returns:
The ``<available_scripts>`` XML block, or ``<available_scripts />``
when *scripts* is empty or ``None``.
"""
if not scripts:
return "<available_scripts />"
script_lines = "\n".join(_create_script_element(s) for s in scripts)
return f"<available_scripts>\n{script_lines}\n</available_scripts>"


@experimental(feature_id=ExperimentalFeature.SKILLS)
class InlineSkill(Skill):
"""A skill defined entirely in code with resources and scripts.
Expand Down Expand Up @@ -782,6 +825,10 @@ def frontmatter(self) -> SkillFrontmatter:
async def get_content(self) -> str:
"""Synthesized XML content with name, description, instructions, resources, and scripts.

The ``<available_resources>`` and ``<available_scripts>`` blocks are
always emitted; an empty category is rendered as a self-closing element
(e.g. ``<available_scripts />``) so the model knows none are available.

The result is cached after the first access. Adding resources or
scripts after the first access will not be reflected.

Expand Down Expand Up @@ -1351,6 +1398,10 @@ def scripts(self) -> list[SkillScript]:
async def get_content(self) -> str:
"""Synthesized XML content containing name, description, instructions, resources, and scripts.

The ``<available_resources>`` and ``<available_scripts>`` blocks are
always emitted; an empty category is rendered as a self-closing element
(e.g. ``<available_scripts />``) so the model knows none are available.

The result is cached after the first access.

Returns:
Expand Down Expand Up @@ -1437,25 +1488,27 @@ def frontmatter(self) -> SkillFrontmatter:
return self._frontmatter

async def get_content(self) -> str:
"""The skill content with appended scripts block.
"""The skill content with appended resource and script blocks.

When scripts are present, a ``<scripts>`` XML block is appended
to the raw SKILL.md content so that the LLM can discover each
script's ``<parameters_schema>``.
The raw SKILL.md content is followed by ``<available_resources>`` and
``<available_scripts>`` blocks. Both are always emitted: a category
with no entries is appended as a self-closing element (e.g.
``<available_scripts />``) so the model knows none are available and
does not hallucinate their names. When entries are present, scripts
include their ``<parameters_schema>`` so the LLM can discover the
argument format.

The result is cached after the first access. Adding scripts
after the first access will not be reflected.
The result is cached after the first access. Adding resources or
scripts after the first access will not be reflected.

Returns:
The skill content string.
"""
if self._cached_content is not None:
return self._cached_content
if not self._scripts:
self._cached_content = self._content
else:
script_lines = "\n".join(_create_script_element(s) for s in self._scripts)
self._cached_content = f"{self._content}\n\n<scripts>\n{script_lines}\n</scripts>"
resources_block = _build_available_resources_block(self._resources)
scripts_block = _build_available_scripts_block(self._scripts)
self._cached_content = f"{self._content}\n\n{resources_block}\n\n{scripts_block}"
return self._cached_content

async def get_resource(self, name: str) -> SkillResource | None:
Expand Down
59 changes: 37 additions & 22 deletions python/packages/core/tests/core/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,8 @@ async def test_load_skill_returns_content(self) -> None:
assert "<name>prog-skill</name>" in result
assert "<description>A skill.</description>" in result
assert "<instructions>\nCode-defined instructions.\n</instructions>" in result
assert "<resources>" not in result
assert "<available_resources />" in result
assert "<available_scripts />" in result

async def test_load_skill_appends_resource_listing(self) -> None:
skill = InlineSkill(
Expand All @@ -1184,7 +1185,7 @@ async def test_load_skill_appends_resource_listing(self) -> None:
assert "<name>prog-skill</name>" in result
assert "<description>A skill.</description>" in result
assert "Do things." in result
assert "<resources>" in result
assert "<available_resources>" in result
assert '<resource name="ref-a" description="First resource"/>' in result
assert '<resource name="ref-b"/>' in result

Expand All @@ -1196,7 +1197,7 @@ async def test_load_skill_no_resources_no_listing(self) -> None:
await _init_provider(provider)
result = await provider._load_skill(_raw_skills(provider), "prog-skill")
assert "Body only." in result
assert "<resources>" not in result
assert "<available_resources />" in result

async def test_read_static_resource(self) -> None:
skill = InlineSkill(
Expand Down Expand Up @@ -3969,16 +3970,16 @@ async def test_code_skill_includes_scripts_element(self) -> None:
await _init_provider(provider)
result = await provider._load_skill(_raw_skills(provider), "my-skill")

assert "<scripts>" in result
assert "<available_scripts>" in result
assert 'name="analyze"' in result
assert 'description="Run analysis"' in result

async def test_code_skill_no_scripts_element(self) -> None:
async def test_code_skill_emits_empty_scripts_element(self) -> None:
skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body")
provider = SkillsProvider([skill])
await _init_provider(provider)
result = await provider._load_skill(_raw_skills(provider), "my-skill")
assert "<scripts>" not in result
assert "<available_scripts />" in result


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -4056,13 +4057,13 @@ async def test_minimal_skill_content_contains_instructions(self) -> None:
skill = _MinimalClassSkill()
assert "Do minimal things." in (await skill.get_content())

async def test_minimal_skill_content_no_resources_element(self) -> None:
async def test_minimal_skill_content_emits_empty_resources_element(self) -> None:
skill = _MinimalClassSkill()
assert "<resources>" not in (await skill.get_content())
assert "<available_resources />" in (await skill.get_content())

async def test_minimal_skill_content_no_scripts_element(self) -> None:
async def test_minimal_skill_content_emits_empty_scripts_element(self) -> None:
skill = _MinimalClassSkill()
assert "<scripts>" not in (await skill.get_content())
assert "<available_scripts />" in (await skill.get_content())

def test_full_skill_has_resources(self) -> None:
skill = _FullClassSkill()
Expand All @@ -4076,12 +4077,12 @@ def test_full_skill_has_scripts(self) -> None:

async def test_full_skill_content_contains_resources(self) -> None:
skill = _FullClassSkill()
assert "<resources>" in (await skill.get_content())
assert "<available_resources>" in (await skill.get_content())
assert 'name="test-resource"' in (await skill.get_content())

async def test_full_skill_content_contains_scripts(self) -> None:
skill = _FullClassSkill()
assert "<scripts>" in (await skill.get_content())
assert "<available_scripts>" in (await skill.get_content())
assert 'name="test-script"' in (await skill.get_content())

async def test_content_is_cached(self) -> None:
Expand Down Expand Up @@ -4127,8 +4128,8 @@ async def test_provider_loads_class_skill_content(self) -> None:

result = await provider._load_skill(_raw_skills(provider), "full-skill")
assert "Use this skill for full tasks." in result
assert "<resources>" in result
assert "<scripts>" in result
assert "<available_resources>" in result
assert "<available_scripts>" in result

async def test_in_memory_source_with_class_skill(self) -> None:
skill = _MinimalClassSkill()
Expand Down Expand Up @@ -4345,12 +4346,12 @@ def test_scripts_cached(self) -> None:

async def test_content_includes_discovered_resources(self) -> None:
skill = _DecoratorClassSkill()
assert "<resources>" in (await skill.get_content())
assert "<available_resources>" in (await skill.get_content())
assert 'name="lookup-table"' in (await skill.get_content())

async def test_content_includes_discovered_scripts(self) -> None:
skill = _DecoratorClassSkill()
assert "<scripts>" in (await skill.get_content())
assert "<available_scripts>" in (await skill.get_content())
assert 'name="convert"' in (await skill.get_content())

def test_duplicate_resource_name_raises(self) -> None:
Expand Down Expand Up @@ -4748,7 +4749,7 @@ def analyze(query: str, limit: int = 10) -> str:
await _init_provider(provider)
result = await provider._load_skill(_raw_skills(provider), "my-skill")

assert "<scripts>" in result
assert "<available_scripts>" in result
assert 'name="analyze"' in result
assert "<parameters_schema>" in result
assert '"query"' in result
Expand Down Expand Up @@ -5734,24 +5735,38 @@ async def test_run_skill_script_inline_with_list_args_returns_error(self) -> Non
assert "Failed to run" in result

async def test_file_skill_content_includes_scripts_block(self) -> None:
"""FileSkill.content appends a <scripts> block when scripts are present."""
"""FileSkill.content appends an <available_scripts> block when scripts are present."""
script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py")
skill = FileSkill(
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
content="---\nname: my-skill\n---\nBody",
path=f"{_ABS}/test",
scripts=[script],
)
assert "<scripts>" in (await skill.get_content())
assert "<available_scripts>" in (await skill.get_content())
assert 'name="run.py"' in (await skill.get_content())
assert "<parameters_schema>" in (await skill.get_content())
assert '"type": "array"' in (await skill.get_content())

async def test_file_skill_content_no_scripts_no_block(self) -> None:
"""FileSkill.content does not append a <scripts> block when no scripts."""
async def test_file_skill_content_no_scripts_emits_empty_block(self) -> None:
"""FileSkill.content always emits self-closing resource and script blocks when empty."""
skill = FileSkill(
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
content="---\nname: my-skill\n---\nBody",
path=f"{_ABS}/test",
)
content = await skill.get_content()
assert "<available_resources />" in content
assert "<available_scripts />" in content

async def test_file_skill_content_includes_resources_block(self) -> None:
"""FileSkill.content appends an <available_resources> block when resources are present."""
skill = FileSkill(
frontmatter=SkillFrontmatter(name="my-skill", description="test"),
content="---\nname: my-skill\n---\nBody",
path=f"{_ABS}/test",
resources=[InlineSkillResource(name="ref-data", content="data")],
)
assert "<scripts>" not in (await skill.get_content())
content = await skill.get_content()
assert "<available_resources>" in content
assert '<resource name="ref-data"/>' in content
Loading