refactor: replace DisclosureLevel enum with Literal and Final constants

This commit is contained in:
Greyson Lalonde
2026-03-12 10:14:30 -04:00
parent 6a2e5b5906
commit 5b5aa309a0
10 changed files with 84 additions and 70 deletions

View File

@@ -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:

View File

@@ -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",

View File

@@ -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()):

View File

@@ -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(

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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={},
)

View File

@@ -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

View File

@@ -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