diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index e468a0828..6e71a966c 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -66,7 +66,7 @@ from crewai.mcp.tool_resolver import MCPToolResolver from crewai.rag.embeddings.types import EmbedderConfig from crewai.security.fingerprint import Fingerprint from crewai.skills.loader import activate_skill, discover_skills -from crewai.skills.models import DisclosureLevel, Skill as SkillModel +from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel from crewai.tools.agent_tools.agent_tools import AgentTools from crewai.utilities.agent_utils import ( get_tool_names, @@ -314,10 +314,7 @@ class Agent(BaseAgent): needs_work = self.skills and any( isinstance(s, Path) - or ( - isinstance(s, SkillModel) - and s.disclosure_level < DisclosureLevel.INSTRUCTIONS - ) + or (isinstance(s, SkillModel) and s.disclosure_level < INSTRUCTIONS) for s in self.skills ) if not needs_work and not crew_skills: diff --git a/lib/crewai/src/crewai/skills/__init__.py b/lib/crewai/src/crewai/skills/__init__.py index 5fa337fa3..b2b5f059f 100644 --- a/lib/crewai/src/crewai/skills/__init__.py +++ b/lib/crewai/src/crewai/skills/__init__.py @@ -10,6 +10,9 @@ from crewai.skills.loader import ( load_resources, ) from crewai.skills.models import ( + INSTRUCTIONS, + METADATA, + RESOURCES, DisclosureLevel, ResourceDirName, Skill, @@ -20,6 +23,9 @@ from crewai.skills.validation import coerce_skill_paths __all__ = [ + "INSTRUCTIONS", + "METADATA", + "RESOURCES", "DisclosureLevel", "ResourceDirName", "Skill", diff --git a/lib/crewai/src/crewai/skills/loader.py b/lib/crewai/src/crewai/skills/loader.py index 79c6ee3f9..78e244f90 100644 --- a/lib/crewai/src/crewai/skills/loader.py +++ b/lib/crewai/src/crewai/skills/loader.py @@ -10,10 +10,6 @@ import logging from pathlib import Path from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from crewai.agents.agent_builder.base_agent import BaseAgent - from crewai.events.event_bus import crewai_event_bus from crewai.events.types.skill_events import ( SkillActivatedEvent, @@ -22,7 +18,7 @@ from crewai.events.types.skill_events import ( SkillLoadFailedEvent, SkillLoadedEvent, ) -from crewai.skills.models import DisclosureLevel, Skill +from crewai.skills.models import INSTRUCTIONS, RESOURCES, Skill from crewai.skills.parser import ( SKILL_FILENAME, load_skill_instructions, @@ -31,6 +27,9 @@ from crewai.skills.parser import ( ) +if TYPE_CHECKING: + from crewai.agents.agent_builder.base_agent import BaseAgent + _logger = logging.getLogger(__name__) @@ -80,7 +79,7 @@ def discover_skills( from_agent=source, skill_name=skill.name, skill_path=skill.path, - disclosure_level=skill.disclosure_level.value, + disclosure_level=skill.disclosure_level, ), ) except Exception as e: @@ -125,7 +124,7 @@ def activate_skill( Returns: Skill at INSTRUCTIONS level or higher. """ - if skill.disclosure_level >= DisclosureLevel.INSTRUCTIONS: + if skill.disclosure_level >= INSTRUCTIONS: return skill activated = load_skill_instructions(skill) @@ -137,7 +136,7 @@ def activate_skill( from_agent=source, skill_name=activated.name, skill_path=activated.path, - disclosure_level=activated.disclosure_level.value, + disclosure_level=activated.disclosure_level, ), ) @@ -168,14 +167,14 @@ def format_skill_context(skill: Skill) -> str: Returns: Formatted skill context string. """ - if skill.disclosure_level >= DisclosureLevel.INSTRUCTIONS and skill.instructions: + if skill.disclosure_level >= INSTRUCTIONS and skill.instructions: parts = [ f"## Skill: {skill.name}", skill.description, "", skill.instructions, ] - if skill.disclosure_level >= DisclosureLevel.RESOURCES and skill.resource_files: + if skill.disclosure_level >= RESOURCES and skill.resource_files: parts.append("") parts.append("### Available Resources") for dir_name, files in sorted(skill.resource_files.items()): diff --git a/lib/crewai/src/crewai/skills/models.py b/lib/crewai/src/crewai/skills/models.py index 987f1cdfc..cde2b4f3b 100644 --- a/lib/crewai/src/crewai/skills/models.py +++ b/lib/crewai/src/crewai/skills/models.py @@ -6,9 +6,8 @@ progressive disclosure of skill information. from __future__ import annotations -from enum import IntEnum from pathlib import Path -from typing import Any, Final, Literal +from typing import Annotated, Any, Final, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -23,18 +22,22 @@ MAX_DESCRIPTION_LENGTH: Final[int] = 1024 ResourceDirName = Literal["scripts", "references", "assets"] -class DisclosureLevel(IntEnum): - """Progressive disclosure levels for skill loading. +DisclosureLevel = Annotated[ + Literal[1, 2, 3], "Progressive disclosure levels for skill loading." +] - Attributes: - METADATA: Only frontmatter metadata is loaded (name, description). - INSTRUCTIONS: Full SKILL.md body is loaded. - RESOURCES: Resource directories (scripts, references, assets) are cataloged. - """ - - METADATA = 1 - INSTRUCTIONS = 2 - RESOURCES = 3 +METADATA: Final[ + Annotated[ + DisclosureLevel, "Only frontmatter metadata is loaded (name, description)." + ] +] = 1 +INSTRUCTIONS: Final[Annotated[DisclosureLevel, "Full SKILL.md body is loaded."]] = 2 +RESOURCES: Final[ + Annotated[ + DisclosureLevel, + "Resource directories (scripts, references, assets) are cataloged.", + ] +] = 3 class SkillFrontmatter(BaseModel): @@ -110,7 +113,7 @@ class Skill(BaseModel): description="Filesystem path to the skill directory.", ) disclosure_level: DisclosureLevel = Field( - default=DisclosureLevel.METADATA, + default=METADATA, description="Current progressive disclosure level of the skill.", ) resource_files: dict[ResourceDirName, list[str]] | None = Field( diff --git a/lib/crewai/src/crewai/skills/parser.py b/lib/crewai/src/crewai/skills/parser.py index 3c6eb5d7a..d935e6ad1 100644 --- a/lib/crewai/src/crewai/skills/parser.py +++ b/lib/crewai/src/crewai/skills/parser.py @@ -9,12 +9,14 @@ from __future__ import annotations import logging from pathlib import Path import re -from typing import Any +from typing import Any, Final import yaml from crewai.skills.models import ( - DisclosureLevel, + INSTRUCTIONS, + METADATA, + RESOURCES, ResourceDirName, Skill, SkillFrontmatter, @@ -25,9 +27,9 @@ from crewai.skills.validation import validate_directory_name _logger = logging.getLogger(__name__) -SKILL_FILENAME: str = "SKILL.md" -_CLOSING_DELIMITER: re.Pattern[str] = re.compile(r"\n---[ \t]*(?:\n|$)") -_MAX_BODY_CHARS: int = 50_000 +SKILL_FILENAME: Final[str] = "SKILL.md" +_CLOSING_DELIMITER: Final[re.Pattern[str]] = re.compile(r"\n---[ \t]*(?:\n|$)") +_MAX_BODY_CHARS: Final[int] = 50_000 class SkillParseError(ValueError): @@ -120,7 +122,7 @@ def load_skill_metadata(skill_dir: Path) -> Skill: return Skill( frontmatter=frontmatter, path=skill_dir, - disclosure_level=DisclosureLevel.METADATA, + disclosure_level=METADATA, ) @@ -135,7 +137,7 @@ def load_skill_instructions(skill: Skill) -> Skill: Returns: New Skill instance at INSTRUCTIONS level. """ - if skill.disclosure_level >= DisclosureLevel.INSTRUCTIONS: + if skill.disclosure_level >= INSTRUCTIONS: return skill skill_md_path = skill.path / SKILL_FILENAME @@ -149,7 +151,7 @@ def load_skill_instructions(skill: Skill) -> Skill: _MAX_BODY_CHARS, ) return skill.with_disclosure_level( - level=DisclosureLevel.INSTRUCTIONS, + level=INSTRUCTIONS, instructions=body, ) @@ -165,10 +167,10 @@ def load_skill_resources(skill: Skill) -> Skill: Returns: New Skill instance at RESOURCES level. """ - if skill.disclosure_level >= DisclosureLevel.RESOURCES: + if skill.disclosure_level >= RESOURCES: return skill - if skill.disclosure_level < DisclosureLevel.INSTRUCTIONS: + if skill.disclosure_level < INSTRUCTIONS: skill = load_skill_instructions(skill) resource_dirs: list[tuple[ResourceDirName, Path]] = [ @@ -186,7 +188,7 @@ def load_skill_resources(skill: Skill) -> Skill: ) return skill.with_disclosure_level( - level=DisclosureLevel.RESOURCES, + level=RESOURCES, instructions=skill.instructions, resource_files=resource_files, ) diff --git a/lib/crewai/src/crewai/skills/validation.py b/lib/crewai/src/crewai/skills/validation.py index af1472a75..9b5b7d6a0 100644 --- a/lib/crewai/src/crewai/skills/validation.py +++ b/lib/crewai/src/crewai/skills/validation.py @@ -6,12 +6,13 @@ Validates skill names and directory structures per the Agent Skills standard. from __future__ import annotations from pathlib import Path -from typing import Any +import re +from typing import Any, Final -MAX_SKILL_NAME_LENGTH: int = 64 -MIN_SKILL_NAME_LENGTH: int = 1 -SKILL_NAME_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$" +MAX_SKILL_NAME_LENGTH: Final[int] = 64 +MIN_SKILL_NAME_LENGTH: Final[int] = 1 +SKILL_NAME_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") def coerce_skill_paths(v: list[Any] | None) -> list[Any] | None: diff --git a/lib/crewai/tests/skills/test_integration.py b/lib/crewai/tests/skills/test_integration.py index 6159a56dc..23004d79e 100644 --- a/lib/crewai/tests/skills/test_integration.py +++ b/lib/crewai/tests/skills/test_integration.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest from crewai.skills.loader import activate_skill, discover_skills, format_skill_context -from crewai.skills.models import DisclosureLevel +from crewai.skills.models import INSTRUCTIONS, METADATA def _create_skill_dir(parent: Path, name: str, body: str = "Body.") -> Path: @@ -25,10 +25,10 @@ class TestSkillDiscoveryAndActivation: _create_skill_dir(tmp_path, "my-skill", body="Use this skill.") skills = discover_skills(tmp_path) assert len(skills) == 1 - assert skills[0].disclosure_level == DisclosureLevel.METADATA + assert skills[0].disclosure_level == METADATA activated = activate_skill(skills[0]) - assert activated.disclosure_level == DisclosureLevel.INSTRUCTIONS + assert activated.disclosure_level == INSTRUCTIONS assert activated.instructions == "Use this skill." context = format_skill_context(activated) diff --git a/lib/crewai/tests/skills/test_loader.py b/lib/crewai/tests/skills/test_loader.py index c3b297e40..8303e19df 100644 --- a/lib/crewai/tests/skills/test_loader.py +++ b/lib/crewai/tests/skills/test_loader.py @@ -10,7 +10,7 @@ from crewai.skills.loader import ( format_skill_context, load_resources, ) -from crewai.skills.models import DisclosureLevel, Skill, SkillFrontmatter +from crewai.skills.models import INSTRUCTIONS, METADATA, RESOURCES, Skill, SkillFrontmatter from crewai.skills.parser import load_skill_metadata @@ -73,7 +73,7 @@ class TestActivateSkill: _create_skill_dir(tmp_path, "my-skill", body="Instructions.") skill = load_skill_metadata(tmp_path / "my-skill") activated = activate_skill(skill) - assert activated.disclosure_level == DisclosureLevel.INSTRUCTIONS + assert activated.disclosure_level == INSTRUCTIONS assert activated.instructions == "Instructions." def test_idempotent(self, tmp_path: Path) -> None: @@ -93,7 +93,7 @@ class TestLoadResources: (skill_dir / "scripts" / "run.sh").write_text("#!/bin/bash") skill = load_skill_metadata(skill_dir) full = load_resources(skill) - assert full.disclosure_level == DisclosureLevel.RESOURCES + assert full.disclosure_level == RESOURCES class TestFormatSkillContext: @@ -102,7 +102,7 @@ class TestFormatSkillContext: def test_metadata_level(self, tmp_path: Path) -> None: fm = SkillFrontmatter(name="test-skill", description="A skill") skill = Skill( - frontmatter=fm, path=tmp_path, disclosure_level=DisclosureLevel.METADATA + frontmatter=fm, path=tmp_path, disclosure_level=METADATA ) ctx = format_skill_context(skill) assert "## Skill: test-skill" in ctx @@ -113,7 +113,7 @@ class TestFormatSkillContext: skill = Skill( frontmatter=fm, path=tmp_path, - disclosure_level=DisclosureLevel.INSTRUCTIONS, + disclosure_level=INSTRUCTIONS, instructions="Do these things.", ) ctx = format_skill_context(skill) @@ -125,7 +125,7 @@ class TestFormatSkillContext: skill = Skill( frontmatter=fm, path=tmp_path, - disclosure_level=DisclosureLevel.INSTRUCTIONS, + disclosure_level=INSTRUCTIONS, instructions=None, ) ctx = format_skill_context(skill) @@ -136,7 +136,7 @@ class TestFormatSkillContext: skill = Skill( frontmatter=fm, path=tmp_path, - disclosure_level=DisclosureLevel.RESOURCES, + disclosure_level=RESOURCES, instructions="Do things.", resource_files={ "scripts": ["run.sh"], @@ -153,7 +153,7 @@ class TestFormatSkillContext: skill = Skill( frontmatter=fm, path=tmp_path, - disclosure_level=DisclosureLevel.RESOURCES, + disclosure_level=RESOURCES, instructions="Do things.", resource_files={}, ) diff --git a/lib/crewai/tests/skills/test_models.py b/lib/crewai/tests/skills/test_models.py index 8de07b7b5..57f15d763 100644 --- a/lib/crewai/tests/skills/test_models.py +++ b/lib/crewai/tests/skills/test_models.py @@ -4,20 +4,26 @@ from pathlib import Path import pytest -from crewai.skills.models import DisclosureLevel, Skill, SkillFrontmatter +from crewai.skills.models import ( + INSTRUCTIONS, + METADATA, + RESOURCES, + Skill, + SkillFrontmatter, +) class TestDisclosureLevel: - """Tests for DisclosureLevel enum.""" + """Tests for DisclosureLevel constants.""" def test_ordering(self) -> None: - assert DisclosureLevel.METADATA < DisclosureLevel.INSTRUCTIONS - assert DisclosureLevel.INSTRUCTIONS < DisclosureLevel.RESOURCES + assert METADATA < INSTRUCTIONS + assert INSTRUCTIONS < RESOURCES def test_values(self) -> None: - assert DisclosureLevel.METADATA == 1 - assert DisclosureLevel.INSTRUCTIONS == 2 - assert DisclosureLevel.RESOURCES == 3 + assert METADATA == 1 + assert INSTRUCTIONS == 2 + assert RESOURCES == 3 class TestSkillFrontmatter: @@ -62,7 +68,7 @@ class TestSkill: skill = Skill(frontmatter=fm, path=tmp_path / "test-skill") assert skill.name == "test-skill" assert skill.description == "desc" - assert skill.disclosure_level == DisclosureLevel.METADATA + assert skill.disclosure_level == METADATA def test_resource_dirs(self, tmp_path: Path) -> None: skill_dir = tmp_path / "test-skill" @@ -77,9 +83,9 @@ class TestSkill: fm = SkillFrontmatter(name="test-skill", description="desc") skill = Skill(frontmatter=fm, path=tmp_path) promoted = skill.with_disclosure_level( - DisclosureLevel.INSTRUCTIONS, + INSTRUCTIONS, instructions="Do this.", ) - assert promoted.disclosure_level == DisclosureLevel.INSTRUCTIONS + assert promoted.disclosure_level == INSTRUCTIONS assert promoted.instructions == "Do this." - assert skill.disclosure_level == DisclosureLevel.METADATA + assert skill.disclosure_level == METADATA diff --git a/lib/crewai/tests/skills/test_parser.py b/lib/crewai/tests/skills/test_parser.py index 72d20cd11..dab15d175 100644 --- a/lib/crewai/tests/skills/test_parser.py +++ b/lib/crewai/tests/skills/test_parser.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from crewai.skills.models import DisclosureLevel +from crewai.skills.models import INSTRUCTIONS, METADATA, RESOURCES from crewai.skills.parser import ( SkillParseError, load_skill_instructions, @@ -93,7 +93,7 @@ class TestLoadSkillMetadata: ) skill = load_skill_metadata(skill_dir) assert skill.name == "my-skill" - assert skill.disclosure_level == DisclosureLevel.METADATA + assert skill.disclosure_level == METADATA assert skill.instructions is None def test_directory_name_mismatch(self, tmp_path: Path) -> None: @@ -117,7 +117,7 @@ class TestLoadSkillInstructions: ) skill = load_skill_metadata(skill_dir) promoted = load_skill_instructions(skill) - assert promoted.disclosure_level == DisclosureLevel.INSTRUCTIONS + assert promoted.disclosure_level == INSTRUCTIONS assert promoted.instructions == "Full body." def test_idempotent(self, tmp_path: Path) -> None: @@ -148,7 +148,7 @@ class TestLoadSkillResources: skill = load_skill_metadata(skill_dir) full = load_skill_resources(skill) - assert full.disclosure_level == DisclosureLevel.RESOURCES + assert full.disclosure_level == RESOURCES assert full.instructions == "Body." assert full.resource_files is not None assert "scripts" in full.resource_files