Skip to content

Commit 086f15f

Browse files
authored
Respect skills=None as explicit disable (#588)
* Respect explicit skills disable per agent * Add tests for skills default vs disable * Document skills disable behavior
1 parent a911462 commit 086f15f

File tree

7 files changed

+100
-52
lines changed

7 files changed

+100
-52
lines changed

src/fast_agent/agents/agent_types.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from dataclasses import dataclass, field
66
from enum import StrEnum, auto
77
from pathlib import Path
8+
from typing import TypeAlias
89

910
from mcp.client.session import ElicitationFnT
1011

1112
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
12-
from fast_agent.skills import SkillManifest, SkillRegistry
13+
from fast_agent.skills import SKILLS_DEFAULT, SkillManifest, SkillRegistry, SkillsDefault
1314

1415
# Forward imports to avoid circular dependencies
1516
from fast_agent.types import RequestParams
@@ -30,6 +31,17 @@ class AgentType(StrEnum):
3031
MAKER = auto()
3132

3233

34+
SkillConfig: TypeAlias = (
35+
SkillManifest
36+
| SkillRegistry
37+
| Path
38+
| str
39+
| list[SkillManifest | SkillRegistry | Path | str | None]
40+
| None
41+
| SkillsDefault
42+
)
43+
44+
3345
@dataclass
3446
class AgentConfig:
3547
"""Configuration for an Agent instance"""
@@ -40,14 +52,7 @@ class AgentConfig:
4052
tools: dict[str, list[str]] = field(default_factory=dict) # filters for tools
4153
resources: dict[str, list[str]] = field(default_factory=dict) # filters for resources
4254
prompts: dict[str, list[str]] = field(default_factory=dict) # filters for prompts
43-
skills: (
44-
SkillManifest
45-
| SkillRegistry
46-
| Path
47-
| str
48-
| list[SkillManifest | SkillRegistry | Path | str | None]
49-
| None
50-
) = None
55+
skills: SkillConfig = SKILLS_DEFAULT
5156
skill_manifests: list[SkillManifest] = field(default_factory=list, repr=False)
5257
model: str | None = None
5358
use_history: bool = True

src/fast_agent/agents/mcp_agent.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
is_namespaced_name,
4848
)
4949
from fast_agent.mcp.mcp_aggregator import MCPAggregator, NamespacedTool, ServerStatus
50-
from fast_agent.skills import SkillManifest
50+
from fast_agent.skills import SKILLS_DEFAULT, SkillManifest
5151
from fast_agent.skills.registry import SkillRegistry
5252
from fast_agent.tools.elicitation import (
5353
get_elicitation_tool,
@@ -114,7 +114,12 @@ def __init__(
114114
self.executor = context.executor if context else None
115115
self.logger = get_logger(f"{__name__}.{self._name}")
116116
manifests: list[SkillManifest] = list(getattr(self.config, "skill_manifests", []) or [])
117-
if not manifests and context and context.skill_registry:
117+
if (
118+
self.config.skills is SKILLS_DEFAULT
119+
and not manifests
120+
and context
121+
and context.skill_registry
122+
):
118123
try:
119124
manifests = list(context.skill_registry.load_manifests()) # type: ignore[assignment]
120125
except Exception:
@@ -127,7 +132,7 @@ def __init__(
127132
self.skill_registry: SkillRegistry | None = None
128133
if isinstance(self.config.skills, SkillRegistry):
129134
self.skill_registry = self.config.skills
130-
elif self.config.skills is None and context and context.skill_registry:
135+
elif self.config.skills is SKILLS_DEFAULT and context and context.skill_registry:
131136
self.skill_registry = context.skill_registry
132137
self._warnings: list[str] = []
133138
self._warning_messages_seen: set[str] = set()

src/fast_agent/cli/commands/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ fast-agent serve [OPTIONS]
9797
- `--description`, `-d TEXT`: Description used for each send tool (supports `{agent}` placeholder)
9898
- `--instance-scope [shared|connection|request]`: Control how MCP clients receive isolated agent instances (default: shared)
9999

100+
### Skills behavior
101+
102+
When configuring agents in code, `skills=None` explicitly disables skills for that agent. If `skills` is omitted, the default skills registry is used.
103+
100104
### Examples
101105

102106
```bash

src/fast_agent/core/direct_decorators.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
from mcp.client.session import ElicitationFnT
2020
from pydantic import AnyUrl
2121

22-
from fast_agent.agents.agent_types import AgentConfig, AgentType
22+
from fast_agent.agents.agent_types import AgentConfig, AgentType, SkillConfig
2323
from fast_agent.agents.workflow.iterative_planner import ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE
2424
from fast_agent.agents.workflow.router_agent import (
2525
ROUTING_SYSTEM_INSTRUCTION,
2626
)
2727
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
28-
from fast_agent.skills import SkillManifest, SkillRegistry
28+
from fast_agent.skills import SKILLS_DEFAULT
2929
from fast_agent.types import RequestParams
3030

3131
# Type variables for the decorated function
@@ -193,12 +193,7 @@ def _decorator_impl(
193193
tools: dict[str, list[str]] | None = None,
194194
resources: dict[str, list[str]] | None = None,
195195
prompts: dict[str, list[str]] | None = None,
196-
skills: SkillManifest
197-
| SkillRegistry
198-
| Path
199-
| str
200-
| list[SkillManifest | SkillRegistry | Path | str | None]
201-
| None = None,
196+
skills: SkillConfig = SKILLS_DEFAULT,
202197
**extra_kwargs,
203198
) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]:
204199
"""
@@ -275,12 +270,7 @@ def agent(
275270
tools: dict[str, list[str]] | None = None,
276271
resources: dict[str, list[str]] | None = None,
277272
prompts: dict[str, list[str]] | None = None,
278-
skills: SkillManifest
279-
| SkillRegistry
280-
| Path
281-
| str
282-
| list[SkillManifest | SkillRegistry | Path | str | None]
283-
| None = None,
273+
skills: SkillConfig = SKILLS_DEFAULT,
284274
model: str | None = None,
285275
use_history: bool = True,
286276
request_params: RequestParams | None = None,
@@ -358,12 +348,7 @@ def custom(
358348
tools: dict[str, list[str]] | None = None,
359349
resources: dict[str, list[str]] | None = None,
360350
prompts: dict[str, list[str]] | None = None,
361-
skills: SkillManifest
362-
| SkillRegistry
363-
| Path
364-
| str
365-
| list[SkillManifest | SkillRegistry | Path | str | None]
366-
| None = None,
351+
skills: SkillConfig = SKILLS_DEFAULT,
367352
model: str | None = None,
368353
use_history: bool = True,
369354
request_params: RequestParams | None = None,

src/fast_agent/core/fastagent.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
validate_workflow_references,
8888
)
8989
from fast_agent.mcp.prompts.prompt_load import load_prompt
90-
from fast_agent.skills import SkillManifest, SkillRegistry
90+
from fast_agent.skills import SKILLS_DEFAULT, SkillManifest, SkillRegistry, SkillsDefault
9191
from fast_agent.ui.console import configure_console_stream
9292
from fast_agent.ui.usage_display import display_usage_report
9393

@@ -103,6 +103,7 @@
103103
F = TypeVar("F", bound=Callable[..., Any]) # For decorated functions
104104
logger = get_logger(__name__)
105105
SkillEntry: TypeAlias = SkillManifest | SkillRegistry | Path | str
106+
SkillConfig: TypeAlias = SkillEntry | list[SkillEntry | None] | None | SkillsDefault
106107

107108

108109
class FastAgent:
@@ -370,12 +371,7 @@ def agent(
370371
tools: dict[str, list[str]] | None = None,
371372
resources: dict[str, list[str]] | None = None,
372373
prompts: dict[str, list[str]] | None = None,
373-
skills: SkillManifest
374-
| SkillRegistry
375-
| Path
376-
| str
377-
| list[SkillManifest | SkillRegistry | Path | str | None]
378-
| None = None,
374+
skills: SkillConfig = SKILLS_DEFAULT,
379375
model: str | None = None,
380376
use_history: bool = True,
381377
request_params: RequestParams | None = None,
@@ -402,12 +398,7 @@ def custom(
402398
tools: dict[str, list[str]] | None = None,
403399
resources: dict[str, list[str]] | None = None,
404400
prompts: dict[str, list[str]] | None = None,
405-
skills: SkillManifest
406-
| SkillRegistry
407-
| Path
408-
| str
409-
| list[SkillManifest | SkillRegistry | Path | str | None]
410-
| None = None,
401+
skills: SkillConfig = SKILLS_DEFAULT,
411402
model: str | None = None,
412403
use_history: bool = True,
413404
request_params: RequestParams | None = None,
@@ -944,21 +935,22 @@ def _apply_skills_to_agent_configs(self, default_skills: list[SkillManifest]) ->
944935
if not config_obj:
945936
continue
946937

947-
resolved = self._resolve_skills(config_obj.skills)
948-
if config_obj.skills is None:
949-
if not resolved:
950-
resolved = list(default_skills)
951-
else:
952-
resolved = self._deduplicate_skills(resolved)
938+
if config_obj.skills is SKILLS_DEFAULT:
939+
resolved = list(default_skills)
940+
elif config_obj.skills is None:
941+
resolved = []
953942
else:
943+
resolved = self._resolve_skills(config_obj.skills)
954944
resolved = self._deduplicate_skills(resolved)
955945

956946
config_obj.skill_manifests = resolved
957947

958948
def _resolve_skills(
959949
self,
960-
entry: SkillEntry | list[SkillEntry | None] | None,
950+
entry: SkillConfig,
961951
) -> list[SkillManifest]:
952+
if entry is SKILLS_DEFAULT:
953+
return []
962954
if entry is None:
963955
return []
964956
if isinstance(entry, list):

src/fast_agent/skills/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@
22

33
from .registry import SkillManifest, SkillRegistry, format_skills_for_prompt
44

5+
6+
class SkillsDefault:
7+
"""Sentinel for default skills resolution (use context/global skills)."""
8+
9+
def __repr__(self) -> str:
10+
return "SKILLS_DEFAULT"
11+
12+
13+
SKILLS_DEFAULT = SkillsDefault()
14+
515
__all__ = [
16+
"SKILLS_DEFAULT",
17+
"SkillsDefault",
618
"SkillManifest",
719
"SkillRegistry",
820
"format_skills_for_prompt",

tests/unit/fast_agent/agents/test_mcp_agent_skills.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from fast_agent.agents.agent_types import AgentConfig
99
from fast_agent.agents.mcp_agent import McpAgent
1010
from fast_agent.context import Context
11+
from fast_agent.skills import SKILLS_DEFAULT
1112
from fast_agent.skills.registry import SkillRegistry, format_skills_for_prompt
1213
from fast_agent.tools.skill_reader import SkillReader
1314

@@ -62,6 +63,50 @@ async def test_mcp_agent_exposes_skill_tools(tmp_path: Path) -> None:
6263
assert manifests[0].path.is_absolute()
6364

6465

66+
@pytest.mark.asyncio
67+
async def test_mcp_agent_skills_default_uses_context_registry(tmp_path: Path) -> None:
68+
skills_root = tmp_path / "skills"
69+
create_skill(skills_root, "alpha", body="Alpha body")
70+
71+
context = Context()
72+
context.skill_registry = SkillRegistry(base_dir=tmp_path, directories=[skills_root])
73+
74+
config = AgentConfig(
75+
name="test",
76+
instruction="Instruction",
77+
servers=[],
78+
skills=SKILLS_DEFAULT,
79+
)
80+
81+
agent = McpAgent(config=config, context=context)
82+
83+
tools_result = await agent.list_tools()
84+
tool_names = {tool.name for tool in tools_result.tools}
85+
assert "read_skill" in tool_names
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_mcp_agent_skills_none_disables_context_registry(tmp_path: Path) -> None:
90+
skills_root = tmp_path / "skills"
91+
create_skill(skills_root, "alpha", body="Alpha body")
92+
93+
context = Context()
94+
context.skill_registry = SkillRegistry(base_dir=tmp_path, directories=[skills_root])
95+
96+
config = AgentConfig(
97+
name="test",
98+
instruction="Instruction",
99+
servers=[],
100+
skills=None,
101+
)
102+
103+
agent = McpAgent(config=config, context=context)
104+
105+
tools_result = await agent.list_tools()
106+
tool_names = {tool.name for tool in tools_result.tools}
107+
assert "read_skill" not in tool_names
108+
109+
65110
@pytest.mark.asyncio
66111
async def test_skill_reader_rejects_relative_path(tmp_path: Path) -> None:
67112
"""SkillReader must reject non-absolute paths to prevent traversal tricks."""

0 commit comments

Comments
 (0)