Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ agent_framework/
- **`SkillScriptRunner`** - Protocol for file-based script execution. Any callable matching `(skill, script, args) -> Any` satisfies it. Code-defined scripts do not use a runner.
- **`SkillScriptArgumentParser`** - Public type alias for an optional callable `(raw args: dict | list[str] | str | None) -> dict | None` that converts the raw `args` value before an `InlineSkillScript` runs (applied before the inline list-args guard). It is an opt-in customization hook (port of .NET PR #6498) that lets callers support backends sending tool-call arguments in a non-conforming shape (e.g. vLLM JSON strings). The output is constrained to a `dict` (named keyword arguments) or `None`, because inline scripts bind arguments by keyword name. Supply it via the `argument_parser=` constructor arg on `InlineSkillScript`, `InlineSkill` (default for scripts added via `@skill.script`), or `ClassSkill` (default for scripts discovered via `@ClassSkill.script`). When `None` (the default), the raw value is used unchanged. File-based scripts are unaffected (their runner owns arg handling).
- **`SkillsProvider`** - Context provider (extends `ContextProvider`) that discovers file-based skills from `SKILL.md` files and/or accepts code-defined `Skill` instances. Follows progressive disclosure: advertise → load → read resources / run scripts. By default all three tools it exposes (`load_skill`, `read_skill_resource`, `run_skill_script`) are registered with `approval_mode="always_require"`, so every skill operation needs approval. To run unattended, pass one of the static auto-approval rules to `ToolApprovalMiddleware` (via `auto_approval_rules`): `SkillsProvider.read_only_tools_auto_approval_rule` approves only the read-only tools (`load_skill`, `read_skill_resource`) while still prompting for `run_skill_script`, and `SkillsProvider.all_tools_auto_approval_rule` approves every skill tool including script execution. Both rules reject any call carrying a `server_label` so they stay scoped to this provider's local tools and never auto-approve a same-named hosted tool. Alternatively, for trusted skills, the constructor / `from_paths` kwargs `disable_load_skill_approval`, `disable_read_skill_resource_approval`, and `disable_run_skill_script_approval` (all default `False`) opt individual tools out of approval entirely by registering them with `approval_mode="never_require"` (the auto-approval rules only apply to tools that still require approval). The tool names are also exposed as class constants (`LOAD_SKILL_TOOL_NAME`, `READ_SKILL_RESOURCE_TOOL_NAME`, `RUN_SKILL_SCRIPT_TOOL_NAME`).
- **`SkillsSource` decorators** - Skill sources are composable: `SkillsSource` is the abstract base, with concrete sources (`InMemorySkillsSource`, `FileSkillsSource`, `MCPSkillsSource`) and decorators that wrap an inner source — `AggregatingSkillsSource` (concatenate several sources), `FilteringSkillsSource` (predicate filter), `DeduplicatingSkillsSource` (first-wins by name), and `CachingSkillsSource` (cache the inner source's skills list). `DelegatingSkillsSource` is the abstract base for decorators. **`get_skills` takes a `SkillsSourceContext`**: every source/decorator implements `async def get_skills(self, context: SkillsSourceContext) -> list[Skill]` and forwards `context` to inner sources. `SkillsSourceContext` (frozen, experimental) carries the invoking `agent` (`SupportsAgentRun`) and optional `session` (`AgentSession | None`); `SkillsProvider` builds it from `before_run`'s `agent`/`session` and passes it into the pipeline. `FilteringSkillsSource`'s predicate is context-aware: `Callable[[Skill, SkillsSourceContext], bool]` (port of .NET #6797). **Default caching is applied only to the built-in, context-independent leaf sources**: for the `Skill` / sequence-of-skills / `from_paths` constructors, `SkillsProvider` builds `DeduplicatingSkillsSource(CachingSkillsSource(<file|in-memory leaf>))` so expensive filesystem/network discovery runs once. A **caller-supplied `SkillsSource` is used as-is — never auto-wrapped in caching or deduplication** — because auto-caching a context-aware caller source in a single shared bucket would replay the first invocation's skills for later `SkillsSourceContext`s and leak skills across agents/tenants (matches .NET, whose custom-source constructor also adds no caching/dedup). Callers who want caching on a custom pipeline compose `CachingSkillsSource(inner, cache_isolation_key_selector=...)` themselves. `disable_caching=True` only affects the built-in leaf caching (it has no effect on a caller-supplied source, which is never cached). `CachingSkillsSource` shares a single in-flight fetch across concurrent callers (per cache key) and resets its cache on failure so the next call retries. By default all callers share one cache bucket; pass `cache_isolation_key_selector=Callable[[SkillsSourceContext], str | None]` to cache separately per key (e.g. per agent name) for context-aware inner sources — the key should be low-cardinality and stable, and returning `None` (or leaving the selector `None`) uses the shared bucket.
- **`SkillsSource` decorators** - Skill sources are composable: `SkillsSource` is the abstract base, with concrete sources (`InMemorySkillsSource`, `FileSkillsSource`, `MCPSkillsSource`) and decorators that wrap an inner source — `AggregatingSkillsSource` (concatenate several sources), `FilteringSkillsSource` (predicate filter), `DeduplicatingSkillsSource` (first-wins by name), and `CachingSkillsSource` (cache the inner source's skills list). `DelegatingSkillsSource` is the abstract base for decorators. **`get_skills` takes a `SkillsSourceContext`**: every source/decorator implements `async def get_skills(self, context: SkillsSourceContext) -> list[Skill]` and forwards `context` to inner sources. `SkillsSourceContext` (frozen) carries the invoking `agent` (`SupportsAgentRun`) and optional `session` (`AgentSession | None`); `SkillsProvider` builds it from `before_run`'s `agent`/`session` and passes it into the pipeline. `FilteringSkillsSource`'s predicate is context-aware: `Callable[[Skill, SkillsSourceContext], bool]` (port of .NET #6797). **Default caching is applied only to the built-in, context-independent leaf sources**: for the `Skill` / sequence-of-skills / `from_paths` constructors, `SkillsProvider` builds `DeduplicatingSkillsSource(CachingSkillsSource(<file|in-memory leaf>))` so expensive filesystem/network discovery runs once. A **caller-supplied `SkillsSource` is used as-is — never auto-wrapped in caching or deduplication** — because auto-caching a context-aware caller source in a single shared bucket would replay the first invocation's skills for later `SkillsSourceContext`s and leak skills across agents/tenants (matches .NET, whose custom-source constructor also adds no caching/dedup). Callers who want caching on a custom pipeline compose `CachingSkillsSource(inner, cache_isolation_key_selector=...)` themselves. `disable_caching=True` only affects the built-in leaf caching (it has no effect on a caller-supplied source, which is never cached). `CachingSkillsSource` shares a single in-flight fetch across concurrent callers (per cache key) and resets its cache on failure so the next call retries. By default all callers share one cache bucket; pass `cache_isolation_key_selector=Callable[[SkillsSourceContext], str | None]` to cache separately per key (e.g. per agent name) for context-aware inner sources — the key should be low-cardinality and stable, and returning `None` (or leaving the selector `None`) uses the shared bucket.

### Model Context Protocol (`_mcp.py`)

Expand Down
1 change: 0 additions & 1 deletion python/packages/core/agent_framework/_feature_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ class ExperimentalFeature(str, Enum):
MCP_LONG_RUNNING_TASKS = "MCP_LONG_RUNNING_TASKS"
MCP_SKILLS = "MCP_SKILLS"
PROGRESSIVE_TOOLS = "PROGRESSIVE_TOOLS"
SKILLS = "SKILLS"
TO_PROMPT_AGENT = "TO_PROMPT_AGENT"


Expand Down
15 changes: 0 additions & 15 deletions python/packages/core/agent_framework/_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"""


@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillResource(ABC):
"""Abstract base class for supplementary content attached to a skill.

Expand Down Expand Up @@ -138,7 +137,6 @@ async def read(self, **kwargs: Any) -> Any:
"""


@experimental(feature_id=ExperimentalFeature.SKILLS)
class InlineSkillResource(SkillResource):
"""A code-defined skill resource backed by static content or a callable.

Expand Down Expand Up @@ -278,7 +276,6 @@ async def read(self, **kwargs: Any) -> Any:
return await asyncio.to_thread(Path(self.full_path).read_text, encoding="utf-8")


@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillScript(ABC):
"""Abstract base class for executable scripts attached to a skill.

Expand Down Expand Up @@ -332,7 +329,6 @@ async def run(self, skill: Skill, args: dict[str, Any] | list[str] | None = None
"""


@experimental(feature_id=ExperimentalFeature.SKILLS)
class InlineSkillScript(SkillScript):
"""A code-defined skill script backed by a callable.

Expand Down Expand Up @@ -449,7 +445,6 @@ async def run(self, skill: Skill, args: dict[str, Any] | list[str] | str | None
return result


@experimental(feature_id=ExperimentalFeature.SKILLS)
class FileSkillScript(SkillScript):
"""A file-path-backed skill script requiring an external runner.

Expand Down Expand Up @@ -535,7 +530,6 @@ async def run(self, skill: Skill, args: dict[str, Any] | list[str] | None = None
return result


@experimental(feature_id=ExperimentalFeature.SKILLS)
class Skill(ABC):
"""Abstract base class for all agent skills.

Expand Down Expand Up @@ -600,7 +594,6 @@ async def get_script(self, name: str) -> SkillScript | None:
return None


@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillFrontmatter:
"""L1 discovery metadata for a :class:`Skill`.

Expand Down Expand Up @@ -815,7 +808,6 @@ def _build_available_scripts_block(scripts: Sequence[SkillScript] | None) -> str
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 @@ -1129,7 +1121,6 @@ def _discover_marked_members(cls: type, marker_attr: str) -> list[tuple[str, dic
return results


@experimental(feature_id=ExperimentalFeature.SKILLS)
class ClassSkill(Skill, ABC):
"""Abstract base class for defining skills as reusable Python classes.

Expand Down Expand Up @@ -1507,7 +1498,6 @@ async def get_script(self, name: str) -> SkillScript | None:
return next((s for s in self.scripts if s.name.lower() == name_lower), None)


@experimental(feature_id=ExperimentalFeature.SKILLS)
class FileSkill(Skill):
"""A :class:`Skill` discovered from a filesystem directory backed by a SKILL.md file.

Expand Down Expand Up @@ -1605,7 +1595,6 @@ async def get_script(self, name: str) -> SkillScript | None:


@runtime_checkable
@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillScriptRunner(Protocol):
"""Protocol for skill script runners.

Expand Down Expand Up @@ -1828,7 +1817,6 @@ def _indent_width(s: str) -> int:
_TSkillsProvider = TypeVar("_TSkillsProvider", bound="SkillsProvider")


@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillsProvider(ContextProvider):
"""Context provider that advertises skills and exposes skill tools.

Expand Down Expand Up @@ -2680,7 +2668,6 @@ def _create_script_element(script: SkillScript) -> str:
# region Skill Sources


@experimental(feature_id=ExperimentalFeature.SKILLS)
@dataclass(frozen=True)
class SkillsSourceContext:
"""Contextual information passed to a :class:`SkillsSource` when retrieving skills.
Expand All @@ -2702,7 +2689,6 @@ class SkillsSourceContext:
session: AgentSession | None = None


@experimental(feature_id=ExperimentalFeature.SKILLS)
class SkillsSource(ABC):
"""Abstract base class for skill sources.

Expand Down Expand Up @@ -3709,7 +3695,6 @@ async def get_skills(self, context: SkillsSourceContext) -> list[Skill]:
return [s for s in skills if self._predicate(s, context)]


@experimental(feature_id=ExperimentalFeature.SKILLS)
class CachingSkillsSource(DelegatingSkillsSource):
"""Decorator that caches the skills list returned by an inner source.

Expand Down
43 changes: 17 additions & 26 deletions python/packages/core/tests/core/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@

from .conftest import MockAgent, MockAgentSession

pytestmark = pytest.mark.filterwarnings(r"ignore:\[SKILLS\].*:FutureWarning")

# Cross-platform absolute path prefix for tests
_ABS = "C:\\skills" if os.name == "nt" else "/skills"

Expand Down Expand Up @@ -1010,42 +1008,35 @@ def test_discover_skips_symlinked_script(self, tmp_path: Path) -> None:
# ---------------------------------------------------------------------------


class TestSkillsExperimentalStage:
"""Tests for the experimental stage annotations applied to skills APIs."""
class TestSkillsStableStage:
"""Tests confirming the skills APIs are stable (no experimental annotation)."""

def test_docstrings_include_experimental_warning(self) -> None:
def test_docstrings_omit_experimental_warning(self) -> None:
Comment thread
giles17 marked this conversation as resolved.
Outdated
assert SkillResource.__doc__ is not None
assert SkillScript.__doc__ is not None
assert Skill.__doc__ is not None
assert SkillScriptRunner.__doc__ is not None
assert SkillsProvider.__doc__ is not None
assert SkillScript.parameters_schema.__doc__ is not None

assert ".. warning:: Experimental" in SkillResource.__doc__
assert ".. warning:: Experimental" in SkillScript.__doc__
assert ".. warning:: Experimental" in Skill.__doc__
assert ".. warning:: Experimental" in SkillScriptRunner.__doc__
assert ".. warning:: Experimental" in SkillsProvider.__doc__
assert ".. warning:: Experimental" not in SkillResource.__doc__
assert ".. warning:: Experimental" not in SkillScript.__doc__
assert ".. warning:: Experimental" not in Skill.__doc__
assert ".. warning:: Experimental" not in SkillScriptRunner.__doc__
assert ".. warning:: Experimental" not in SkillsProvider.__doc__
assert ".. warning:: Experimental" not in SkillScript.parameters_schema.__doc__

def test_feature_metadata_is_set(self) -> None:
assert getattr(SkillResource, "__feature_stage__", None) == "experimental"
assert getattr(SkillScript, "__feature_stage__", None) == "experimental"
assert getattr(Skill, "__feature_stage__", None) == "experimental"
assert getattr(SkillsProvider, "__feature_stage__", None) == "experimental"
feature_ids: list[str | None] = [
getattr(SkillResource, "__feature_id__", None),
getattr(SkillScript, "__feature_id__", None),
getattr(Skill, "__feature_id__", None),
getattr(SkillsProvider, "__feature_id__", None),
]
assert all(isinstance(feature_id, str) and feature_id for feature_id in feature_ids)
assert len(set(feature_ids)) == 1
def test_feature_metadata_is_absent(self) -> None:
assert getattr(SkillResource, "__feature_stage__", None) is None
assert getattr(SkillScript, "__feature_stage__", None) is None
assert getattr(Skill, "__feature_stage__", None) is None
assert getattr(SkillsProvider, "__feature_stage__", None) is None
assert getattr(SkillScriptRunner, "__feature_stage__", None) is None
assert getattr(SkillResource, "__feature_id__", None) is None
assert getattr(SkillScript, "__feature_id__", None) is None
assert getattr(Skill, "__feature_id__", None) is None
assert getattr(SkillsProvider, "__feature_id__", None) is None
assert getattr(SkillScriptRunner, "__feature_id__", None) is None
Comment thread
giles17 marked this conversation as resolved.
Outdated
assert SkillScript.parameters_schema.fget is not None # type: ignore[attr-defined]
assert not hasattr(SkillScript.parameters_schema.fget, "__feature_stage__") # type: ignore[attr-defined]
assert not hasattr(SkillScript.parameters_schema.fget, "__feature_id__") # type: ignore[attr-defined]


class TestSkillResource:
Expand Down
9 changes: 5 additions & 4 deletions python/samples/02-agents/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ All samples require:
- Azure CLI authentication (`az login`)
- Environment variables set in a `.env` file (see `python/.env.example`)

## Suppressing the experimental warning
## Suppressing the experimental MCP Skills warning

The Agent Skills APIs in these samples are still experimental. Each sample includes
a short commented `warnings.filterwarnings(...)` snippet near the imports. Uncomment
it if you want to suppress the Skills warning before using the experimental APIs.
The core Agent Skills APIs are stable. MCP-based skill discovery
(`MCPSkillsSource`) is still experimental, so the [mcp_based_skill](mcp_based_skill/)
sample includes a short commented `warnings.filterwarnings(...)` snippet near the
imports. Uncomment it if you want to suppress the MCP Skills warning.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import asyncio
import json
import os

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent

from agent_framework import Agent, ClassSkill, SkillFrontmatter, SkillsProvider, ToolApprovalMiddleware
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import asyncio
import json
import os

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent
from typing import Any

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import asyncio
import os
import sys

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path

from agent_framework import Agent, SkillsProvider, ToolApprovalMiddleware
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import asyncio
import os

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# Uncomment this filter to suppress the experimental MCP Skills warning before
# using the sample's MCP Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
# warnings.filterwarnings("ignore", message=r"\[MCP_SKILLS\].*", category=FutureWarning)
from agent_framework import Agent, MCPSkillsSource, SkillsProvider, ToolApprovalMiddleware
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
Expand Down
5 changes: 0 additions & 5 deletions python/samples/02-agents/skills/mixed_skills/mixed_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
import json
import os
import sys

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path
from textwrap import dedent
from typing import Any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

import asyncio
import os

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent

from agent_framework import Agent, Content, InlineSkill, Message, SkillFrontmatter, SkillsProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import asyncio
import json
import os

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent
from typing import Any

Expand Down
5 changes: 0 additions & 5 deletions python/samples/02-agents/skills/subprocess_script_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@

import subprocess
import sys

# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path
from typing import Any

Expand Down
Loading