diff --git a/src/fast_agent/agents/agent_types.py b/src/fast_agent/agents/agent_types.py index e1fbad65a..f4a0d214a 100644 --- a/src/fast_agent/agents/agent_types.py +++ b/src/fast_agent/agents/agent_types.py @@ -5,11 +5,12 @@ from dataclasses import dataclass, field from enum import StrEnum, auto from pathlib import Path +from typing import TypeAlias from mcp.client.session import ElicitationFnT from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION -from fast_agent.skills import SkillManifest, SkillRegistry +from fast_agent.skills import SKILLS_DEFAULT, SkillManifest, SkillRegistry, SkillsDefault # Forward imports to avoid circular dependencies from fast_agent.types import RequestParams @@ -30,6 +31,17 @@ class AgentType(StrEnum): MAKER = auto() +SkillConfig: TypeAlias = ( + SkillManifest + | SkillRegistry + | Path + | str + | list[SkillManifest | SkillRegistry | Path | str | None] + | None + | SkillsDefault +) + + @dataclass class AgentConfig: """Configuration for an Agent instance""" @@ -40,14 +52,7 @@ class AgentConfig: tools: dict[str, list[str]] = field(default_factory=dict) # filters for tools resources: dict[str, list[str]] = field(default_factory=dict) # filters for resources prompts: dict[str, list[str]] = field(default_factory=dict) # filters for prompts - skills: ( - SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None - ) = None + skills: SkillConfig = SKILLS_DEFAULT skill_manifests: list[SkillManifest] = field(default_factory=list, repr=False) model: str | None = None use_history: bool = True diff --git a/src/fast_agent/agents/mcp_agent.py b/src/fast_agent/agents/mcp_agent.py index 39a68d912..f3459c57d 100644 --- a/src/fast_agent/agents/mcp_agent.py +++ b/src/fast_agent/agents/mcp_agent.py @@ -47,7 +47,7 @@ is_namespaced_name, ) from fast_agent.mcp.mcp_aggregator import MCPAggregator, NamespacedTool, ServerStatus -from fast_agent.skills import SkillManifest +from fast_agent.skills import SKILLS_DEFAULT, SkillManifest from fast_agent.skills.registry import SkillRegistry from fast_agent.tools.elicitation import ( get_elicitation_tool, @@ -114,7 +114,12 @@ def __init__( self.executor = context.executor if context else None self.logger = get_logger(f"{__name__}.{self._name}") manifests: list[SkillManifest] = list(getattr(self.config, "skill_manifests", []) or []) - if not manifests and context and context.skill_registry: + if ( + self.config.skills is SKILLS_DEFAULT + and not manifests + and context + and context.skill_registry + ): try: manifests = list(context.skill_registry.load_manifests()) # type: ignore[assignment] except Exception: @@ -127,7 +132,7 @@ def __init__( self.skill_registry: SkillRegistry | None = None if isinstance(self.config.skills, SkillRegistry): self.skill_registry = self.config.skills - elif self.config.skills is None and context and context.skill_registry: + elif self.config.skills is SKILLS_DEFAULT and context and context.skill_registry: self.skill_registry = context.skill_registry self._warnings: list[str] = [] self._warning_messages_seen: set[str] = set() diff --git a/src/fast_agent/cli/commands/README.md b/src/fast_agent/cli/commands/README.md index 112d34691..8f2060123 100644 --- a/src/fast_agent/cli/commands/README.md +++ b/src/fast_agent/cli/commands/README.md @@ -97,6 +97,10 @@ fast-agent serve [OPTIONS] - `--description`, `-d TEXT`: Description used for each send tool (supports `{agent}` placeholder) - `--instance-scope [shared|connection|request]`: Control how MCP clients receive isolated agent instances (default: shared) +### Skills behavior + +When configuring agents in code, `skills=None` explicitly disables skills for that agent. If `skills` is omitted, the default skills registry is used. + ### Examples ```bash diff --git a/src/fast_agent/core/direct_decorators.py b/src/fast_agent/core/direct_decorators.py index 8c4bf6d10..ee113a43b 100644 --- a/src/fast_agent/core/direct_decorators.py +++ b/src/fast_agent/core/direct_decorators.py @@ -19,13 +19,13 @@ from mcp.client.session import ElicitationFnT from pydantic import AnyUrl -from fast_agent.agents.agent_types import AgentConfig, AgentType +from fast_agent.agents.agent_types import AgentConfig, AgentType, SkillConfig from fast_agent.agents.workflow.iterative_planner import ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE from fast_agent.agents.workflow.router_agent import ( ROUTING_SYSTEM_INSTRUCTION, ) from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION -from fast_agent.skills import SkillManifest, SkillRegistry +from fast_agent.skills import SKILLS_DEFAULT from fast_agent.types import RequestParams # Type variables for the decorated function @@ -193,12 +193,7 @@ def _decorator_impl( tools: dict[str, list[str]] | None = None, resources: dict[str, list[str]] | None = None, prompts: dict[str, list[str]] | None = None, - skills: SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None = None, + skills: SkillConfig = SKILLS_DEFAULT, **extra_kwargs, ) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]: """ @@ -275,12 +270,7 @@ def agent( tools: dict[str, list[str]] | None = None, resources: dict[str, list[str]] | None = None, prompts: dict[str, list[str]] | None = None, - skills: SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None = None, + skills: SkillConfig = SKILLS_DEFAULT, model: str | None = None, use_history: bool = True, request_params: RequestParams | None = None, @@ -358,12 +348,7 @@ def custom( tools: dict[str, list[str]] | None = None, resources: dict[str, list[str]] | None = None, prompts: dict[str, list[str]] | None = None, - skills: SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None = None, + skills: SkillConfig = SKILLS_DEFAULT, model: str | None = None, use_history: bool = True, request_params: RequestParams | None = None, diff --git a/src/fast_agent/core/fastagent.py b/src/fast_agent/core/fastagent.py index eb077c9b7..f7a59cbac 100644 --- a/src/fast_agent/core/fastagent.py +++ b/src/fast_agent/core/fastagent.py @@ -87,7 +87,7 @@ validate_workflow_references, ) from fast_agent.mcp.prompts.prompt_load import load_prompt -from fast_agent.skills import SkillManifest, SkillRegistry +from fast_agent.skills import SKILLS_DEFAULT, SkillManifest, SkillRegistry, SkillsDefault from fast_agent.ui.console import configure_console_stream from fast_agent.ui.usage_display import display_usage_report @@ -103,6 +103,7 @@ F = TypeVar("F", bound=Callable[..., Any]) # For decorated functions logger = get_logger(__name__) SkillEntry: TypeAlias = SkillManifest | SkillRegistry | Path | str +SkillConfig: TypeAlias = SkillEntry | list[SkillEntry | None] | None | SkillsDefault class FastAgent: @@ -370,12 +371,7 @@ def agent( tools: dict[str, list[str]] | None = None, resources: dict[str, list[str]] | None = None, prompts: dict[str, list[str]] | None = None, - skills: SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None = None, + skills: SkillConfig = SKILLS_DEFAULT, model: str | None = None, use_history: bool = True, request_params: RequestParams | None = None, @@ -402,12 +398,7 @@ def custom( tools: dict[str, list[str]] | None = None, resources: dict[str, list[str]] | None = None, prompts: dict[str, list[str]] | None = None, - skills: SkillManifest - | SkillRegistry - | Path - | str - | list[SkillManifest | SkillRegistry | Path | str | None] - | None = None, + skills: SkillConfig = SKILLS_DEFAULT, model: str | None = None, use_history: bool = True, request_params: RequestParams | None = None, @@ -944,21 +935,22 @@ def _apply_skills_to_agent_configs(self, default_skills: list[SkillManifest]) -> if not config_obj: continue - resolved = self._resolve_skills(config_obj.skills) - if config_obj.skills is None: - if not resolved: - resolved = list(default_skills) - else: - resolved = self._deduplicate_skills(resolved) + if config_obj.skills is SKILLS_DEFAULT: + resolved = list(default_skills) + elif config_obj.skills is None: + resolved = [] else: + resolved = self._resolve_skills(config_obj.skills) resolved = self._deduplicate_skills(resolved) config_obj.skill_manifests = resolved def _resolve_skills( self, - entry: SkillEntry | list[SkillEntry | None] | None, + entry: SkillConfig, ) -> list[SkillManifest]: + if entry is SKILLS_DEFAULT: + return [] if entry is None: return [] if isinstance(entry, list): diff --git a/src/fast_agent/skills/__init__.py b/src/fast_agent/skills/__init__.py index 4647b784c..e5b6e512b 100644 --- a/src/fast_agent/skills/__init__.py +++ b/src/fast_agent/skills/__init__.py @@ -2,7 +2,19 @@ from .registry import SkillManifest, SkillRegistry, format_skills_for_prompt + +class SkillsDefault: + """Sentinel for default skills resolution (use context/global skills).""" + + def __repr__(self) -> str: + return "SKILLS_DEFAULT" + + +SKILLS_DEFAULT = SkillsDefault() + __all__ = [ + "SKILLS_DEFAULT", + "SkillsDefault", "SkillManifest", "SkillRegistry", "format_skills_for_prompt", diff --git a/tests/unit/fast_agent/agents/test_mcp_agent_skills.py b/tests/unit/fast_agent/agents/test_mcp_agent_skills.py index fff33fb3a..027c7a2ae 100644 --- a/tests/unit/fast_agent/agents/test_mcp_agent_skills.py +++ b/tests/unit/fast_agent/agents/test_mcp_agent_skills.py @@ -8,6 +8,7 @@ from fast_agent.agents.agent_types import AgentConfig from fast_agent.agents.mcp_agent import McpAgent from fast_agent.context import Context +from fast_agent.skills import SKILLS_DEFAULT from fast_agent.skills.registry import SkillRegistry, format_skills_for_prompt from fast_agent.tools.skill_reader import SkillReader @@ -62,6 +63,50 @@ async def test_mcp_agent_exposes_skill_tools(tmp_path: Path) -> None: assert manifests[0].path.is_absolute() +@pytest.mark.asyncio +async def test_mcp_agent_skills_default_uses_context_registry(tmp_path: Path) -> None: + skills_root = tmp_path / "skills" + create_skill(skills_root, "alpha", body="Alpha body") + + context = Context() + context.skill_registry = SkillRegistry(base_dir=tmp_path, directories=[skills_root]) + + config = AgentConfig( + name="test", + instruction="Instruction", + servers=[], + skills=SKILLS_DEFAULT, + ) + + agent = McpAgent(config=config, context=context) + + tools_result = await agent.list_tools() + tool_names = {tool.name for tool in tools_result.tools} + assert "read_skill" in tool_names + + +@pytest.mark.asyncio +async def test_mcp_agent_skills_none_disables_context_registry(tmp_path: Path) -> None: + skills_root = tmp_path / "skills" + create_skill(skills_root, "alpha", body="Alpha body") + + context = Context() + context.skill_registry = SkillRegistry(base_dir=tmp_path, directories=[skills_root]) + + config = AgentConfig( + name="test", + instruction="Instruction", + servers=[], + skills=None, + ) + + agent = McpAgent(config=config, context=context) + + tools_result = await agent.list_tools() + tool_names = {tool.name for tool in tools_result.tools} + assert "read_skill" not in tool_names + + @pytest.mark.asyncio async def test_skill_reader_rejects_relative_path(tmp_path: Path) -> None: """SkillReader must reject non-absolute paths to prevent traversal tricks."""