fix: deduplicate coerce_skill_paths and activate pre-loaded METADATA skills

This commit is contained in:
Greyson Lalonde
2026-03-12 01:11:01 -04:00
parent 10c65272f2
commit 6a2e5b5906
5 changed files with 34 additions and 13 deletions

View File

@@ -66,7 +66,7 @@ from crewai.mcp.tool_resolver import MCPToolResolver
from crewai.rag.embeddings.types import EmbedderConfig from crewai.rag.embeddings.types import EmbedderConfig
from crewai.security.fingerprint import Fingerprint from crewai.security.fingerprint import Fingerprint
from crewai.skills.loader import activate_skill, discover_skills from crewai.skills.loader import activate_skill, discover_skills
from crewai.skills.models import Skill as SkillModel from crewai.skills.models import DisclosureLevel, Skill as SkillModel
from crewai.tools.agent_tools.agent_tools import AgentTools from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.utilities.agent_utils import ( from crewai.utilities.agent_utils import (
get_tool_names, get_tool_names,
@@ -296,11 +296,10 @@ class Agent(BaseAgent):
raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from e raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from e
def set_skills(self) -> None: def set_skills(self) -> None:
"""Resolve skill paths into loaded Skill objects. """Resolve skill paths and activate skills to INSTRUCTIONS level.
Path entries trigger discovery and activation. Skill entries pass through. Path entries trigger discovery and activation. Pre-loaded Skill objects
Crew-level skill paths are merged in. Skips work when all items are below INSTRUCTIONS level are activated. Crew-level skills are merged in.
already resolved and there are no crew-level paths to merge.
""" """
from crewai.crew import Crew from crewai.crew import Crew
@@ -313,8 +312,15 @@ class Agent(BaseAgent):
if not self.skills and not crew_skills: if not self.skills and not crew_skills:
return return
has_unresolved = self.skills and any(isinstance(s, Path) for s in self.skills) needs_work = self.skills and any(
if not has_unresolved and not crew_skills: isinstance(s, Path)
or (
isinstance(s, SkillModel)
and s.disclosure_level < DisclosureLevel.INSTRUCTIONS
)
for s in self.skills
)
if not needs_work and not crew_skills:
return return
seen: set[str] = set() seen: set[str] = set()

View File

@@ -31,6 +31,7 @@ from crewai.mcp.config import MCPServerConfig
from crewai.rag.embeddings.types import EmbedderConfig from crewai.rag.embeddings.types import EmbedderConfig
from crewai.security.security_config import SecurityConfig from crewai.security.security_config import SecurityConfig
from crewai.skills.models import Skill from crewai.skills.models import Skill
from crewai.skills.validation import coerce_skill_paths as _coerce_skill_paths
from crewai.tools.base_tool import BaseTool, Tool from crewai.tools.base_tool import BaseTool, Tool
from crewai.utilities.config import process_config from crewai.utilities.config import process_config
from crewai.utilities.i18n import I18N, get_i18n from crewai.utilities.i18n import I18N, get_i18n
@@ -225,9 +226,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
@classmethod @classmethod
def coerce_skill_paths(cls, v: list[Any] | None) -> list[Path | Skill] | None: def coerce_skill_paths(cls, v: list[Any] | None) -> list[Path | Skill] | None:
"""Coerce string entries to Path objects.""" """Coerce string entries to Path objects."""
if not v: return _coerce_skill_paths(v)
return v
return [Path(item) if isinstance(item, str) else item for item in v]
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod

View File

@@ -90,6 +90,7 @@ from crewai.rag.types import SearchResult
from crewai.security.fingerprint import Fingerprint from crewai.security.fingerprint import Fingerprint
from crewai.security.security_config import SecurityConfig from crewai.security.security_config import SecurityConfig
from crewai.skills.models import Skill from crewai.skills.models import Skill
from crewai.skills.validation import coerce_skill_paths as _coerce_skill_paths
from crewai.task import Task from crewai.task import Task
from crewai.tasks.conditional_task import ConditionalTask from crewai.tasks.conditional_task import ConditionalTask
from crewai.tasks.task_output import TaskOutput from crewai.tasks.task_output import TaskOutput
@@ -302,9 +303,7 @@ class Crew(FlowTrackable, BaseModel):
@classmethod @classmethod
def coerce_skill_paths(cls, v: list[Any] | None) -> list[Path | Skill] | None: def coerce_skill_paths(cls, v: list[Any] | None) -> list[Path | Skill] | None:
"""Coerce string entries to Path objects, pass through Skill instances.""" """Coerce string entries to Path objects, pass through Skill instances."""
if not v: return _coerce_skill_paths(v)
return v
return [Path(item) if isinstance(item, str) else item for item in v]
security_config: SecurityConfig = Field( security_config: SecurityConfig = Field(
default_factory=SecurityConfig, default_factory=SecurityConfig,

View File

@@ -16,6 +16,7 @@ from crewai.skills.models import (
SkillFrontmatter, SkillFrontmatter,
) )
from crewai.skills.parser import SkillParseError, parse_skill_md from crewai.skills.parser import SkillParseError, parse_skill_md
from crewai.skills.validation import coerce_skill_paths
__all__ = [ __all__ = [
@@ -25,6 +26,7 @@ __all__ = [
"SkillFrontmatter", "SkillFrontmatter",
"SkillParseError", "SkillParseError",
"activate_skill", "activate_skill",
"coerce_skill_paths",
"discover_skills", "discover_skills",
"format_skill_context", "format_skill_context",
"load_resources", "load_resources",

View File

@@ -6,6 +6,7 @@ Validates skill names and directory structures per the Agent Skills standard.
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
MAX_SKILL_NAME_LENGTH: int = 64 MAX_SKILL_NAME_LENGTH: int = 64
@@ -13,6 +14,20 @@ MIN_SKILL_NAME_LENGTH: int = 1
SKILL_NAME_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$" SKILL_NAME_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
def coerce_skill_paths(v: list[Any] | None) -> list[Any] | None:
"""Coerce string entries to Path objects, pass through other types.
Args:
v: List of skill paths or Skill objects, or None.
Returns:
The list with string entries converted to Path objects, or None.
"""
if not v:
return v
return [Path(item) if isinstance(item, str) else item for item in v]
def validate_directory_name(skill_dir: Path, skill_name: str) -> None: def validate_directory_name(skill_dir: Path, skill_name: str) -> None:
"""Validate that a directory name matches the skill name. """Validate that a directory name matches the skill name.