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
23 changes: 14 additions & 9 deletions src/fast_agent/agents/agent_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"""
Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/fast_agent/agents/mcp_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/fast_agent/cli/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 5 additions & 20 deletions src/fast_agent/core/direct_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]]]:
"""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 12 additions & 20 deletions src/fast_agent/core/fastagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions src/fast_agent/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/fast_agent/agents/test_mcp_agent_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand Down
Loading