mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-28 10:09:21 +00:00
skills progressive disclosure
This commit is contained in:
@@ -9,7 +9,10 @@ mode: "wide"
|
|||||||
|
|
||||||
Skills are self-contained directories that provide agents with **domain-specific instructions, guidelines, and reference material**. Each skill is defined by a `SKILL.md` file with YAML frontmatter and a markdown body.
|
Skills are self-contained directories that provide agents with **domain-specific instructions, guidelines, and reference material**. Each skill is defined by a `SKILL.md` file with YAML frontmatter and a markdown body.
|
||||||
|
|
||||||
When activated, a skill's instructions are injected directly into the agent's task prompt — giving the agent expertise without requiring any code changes.
|
Agents first receive each configured skill's name and description. When a
|
||||||
|
description applies to the current request, the agent loads that skill's full
|
||||||
|
instructions for that execution. This keeps unrelated instructions out of the
|
||||||
|
context while giving the agent the relevant expertise without code changes.
|
||||||
|
|
||||||
<Note type="info" title="Skills vs Tools — The Key Distinction">
|
<Note type="info" title="Skills vs Tools — The Key Distinction">
|
||||||
**Skills are NOT tools.** This is the most common point of confusion.
|
**Skills are NOT tools.** This is the most common point of confusion.
|
||||||
@@ -79,12 +82,13 @@ reviewer = Agent(
|
|||||||
role="Senior Code Reviewer",
|
role="Senior Code Reviewer",
|
||||||
goal="Review pull requests for quality and security issues",
|
goal="Review pull requests for quality and security issues",
|
||||||
backstory="Staff engineer with expertise in secure coding practices.",
|
backstory="Staff engineer with expertise in secure coding practices.",
|
||||||
skills=["./skills"], # Injects review guidelines
|
skills=["./skills"], # Discovers review skills
|
||||||
tools=[GithubSearchTool(), FileReadTool()], # Lets agent read code
|
tools=[GithubSearchTool(), FileReadTool()], # Lets agent read code
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The agent now has both **expertise** (from the skill) and **capabilities** (from the tools).
|
The agent now has both **expertise** (loaded from the relevant skill when
|
||||||
|
needed) and **capabilities** (from the tools).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -324,7 +328,8 @@ The directory name must match the `name` field in `SKILL.md`. The `scripts/`, `r
|
|||||||
|
|
||||||
## Pre-loading Skills
|
## Pre-loading Skills
|
||||||
|
|
||||||
For more control, you can discover and activate skills programmatically:
|
For more control, you can discover and activate skills programmatically.
|
||||||
|
Passing an activated skill makes its instructions always-on:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -351,12 +356,21 @@ agent = Agent(
|
|||||||
|
|
||||||
Skills use **progressive disclosure** — only loading what's needed at each stage:
|
Skills use **progressive disclosure** — only loading what's needed at each stage:
|
||||||
|
|
||||||
| Stage | What's loaded | When |
|
| Stage | What's loaded | When |
|
||||||
| :--------- | :------------------------------------ | :------------------ |
|
| :--------- | :------------------------------------ | :---------------------------------------- |
|
||||||
| Discovery | Name, description, frontmatter fields | `discover_skills()` |
|
| Discovery | Name, description, frontmatter fields | Agent setup or `discover_skills()` |
|
||||||
| Activation | Full SKILL.md body text | `activate_skill()` |
|
| Activation | Full SKILL.md body text | Relevant runtime request or `activate_skill()` |
|
||||||
|
| Resources | Resource directory catalog | Explicit `load_resources()` call |
|
||||||
|
|
||||||
During normal agent execution (passing directory paths via `skills=["./skills"]`), skills are automatically discovered and activated. The progressive loading only matters when using the programmatic API.
|
With `skills=["./skills"]`, the directory is discovered at setup but the full
|
||||||
|
instructions are not placed in every prompt. The agent reviews the metadata on
|
||||||
|
each execution and loads only a skill that applies. The loaded instructions are
|
||||||
|
scoped to that execution, so skills selected for earlier calls do not accumulate
|
||||||
|
on the agent.
|
||||||
|
|
||||||
|
Inline skill strings and `Skill` objects already activated with
|
||||||
|
`activate_skill()` remain always-on. This provides an explicit opt-in when the
|
||||||
|
instructions should apply to every request.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ from crewai.mcp.config import MCPServerConfig
|
|||||||
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 load_skills
|
from crewai.skills.loader import load_skills
|
||||||
from crewai.skills.models import Skill as SkillModel
|
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
|
||||||
from crewai.state.checkpoint_config import CheckpointConfig, apply_checkpoint
|
from crewai.state.checkpoint_config import CheckpointConfig, apply_checkpoint
|
||||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||||
from crewai.types.callback import SerializableCallable
|
from crewai.types.callback import SerializableCallable
|
||||||
@@ -480,9 +480,27 @@ class Agent(BaseAgent):
|
|||||||
|
|
||||||
self.skills = cast(
|
self.skills = cast(
|
||||||
list[Path | SkillModel | str] | None,
|
list[Path | SkillModel | str] | None,
|
||||||
load_skills(items, source=self) or None,
|
load_skills(items, source=self, activate=False) or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _add_skill_loader_tool(
|
||||||
|
self,
|
||||||
|
tools: list[BaseTool],
|
||||||
|
task: Task | None = None,
|
||||||
|
) -> list[BaseTool]:
|
||||||
|
"""Add the internal loader used for request-scoped skill disclosure."""
|
||||||
|
from crewai.skills.tool import LoadSkillTool, create_skill_loader_tool
|
||||||
|
|
||||||
|
tools = [tool for tool in tools if not isinstance(tool, LoadSkillTool)]
|
||||||
|
|
||||||
|
skill_models = [
|
||||||
|
skill for skill in self.skills or [] if isinstance(skill, SkillModel)
|
||||||
|
]
|
||||||
|
loader = create_skill_loader_tool(skill_models, source=self, task=task)
|
||||||
|
if loader is None:
|
||||||
|
return tools
|
||||||
|
return [*tools, loader]
|
||||||
|
|
||||||
def _is_any_available_memory(self) -> bool:
|
def _is_any_available_memory(self) -> bool:
|
||||||
"""Check if unified memory is available (agent or crew)."""
|
"""Check if unified memory is available (agent or crew)."""
|
||||||
if getattr(self, "memory", None):
|
if getattr(self, "memory", None):
|
||||||
@@ -557,11 +575,11 @@ class Agent(BaseAgent):
|
|||||||
return apply_training_data(self, task_prompt)
|
return apply_training_data(self, task_prompt)
|
||||||
|
|
||||||
def _emit_skill_usage(self, task: Task) -> None:
|
def _emit_skill_usage(self, task: Task) -> None:
|
||||||
"""Emit one SkillUsedEvent per skill injected into this task's prompt.
|
"""Emit usage for always-on skills injected into this task's prompt.
|
||||||
|
|
||||||
Skills are agent-scoped and rendered into the prompt on every execution,
|
Metadata-only skills emit from ``LoadSkillTool`` if the model selects
|
||||||
so this is the runtime usage signal traces need — attributing each skill
|
them. This method covers explicitly activated and inline skills, whose
|
||||||
to the agent and task that used it.
|
instructions are rendered on every execution.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task: The task whose prompt the skills are being applied to.
|
task: The task whose prompt the skills are being applied to.
|
||||||
@@ -570,7 +588,10 @@ class Agent(BaseAgent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for skill in self.skills:
|
for skill in self.skills:
|
||||||
if not isinstance(skill, SkillModel):
|
if (
|
||||||
|
not isinstance(skill, SkillModel)
|
||||||
|
or skill.disclosure_level < INSTRUCTIONS
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
self,
|
self,
|
||||||
@@ -1082,7 +1103,8 @@ class Agent(BaseAgent):
|
|||||||
Returns:
|
Returns:
|
||||||
An instance of the CrewAgentExecutor class.
|
An instance of the CrewAgentExecutor class.
|
||||||
"""
|
"""
|
||||||
raw_tools: list[BaseTool] = tools or self.tools or []
|
configured_tools = tools if tools is not None else self.tools or []
|
||||||
|
raw_tools = self._add_skill_loader_tool(list(configured_tools), task=task)
|
||||||
parsed_tools = parse_tools(raw_tools)
|
parsed_tools = parse_tools(raw_tools)
|
||||||
|
|
||||||
prompt, stop_words, rpm_limit_fn = self._build_execution_prompt(raw_tools)
|
prompt, stop_words, rpm_limit_fn = self._build_execution_prompt(raw_tools)
|
||||||
@@ -1421,6 +1443,9 @@ class Agent(BaseAgent):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (executor, inputs, agent_info, parsed_tools) ready for execution.
|
Tuple of (executor, inputs, agent_info, parsed_tools) ready for execution.
|
||||||
"""
|
"""
|
||||||
|
if self.tools_handler:
|
||||||
|
self.tools_handler.last_used_tool = None
|
||||||
|
|
||||||
if self.apps:
|
if self.apps:
|
||||||
platform_tools = self.get_platform_tools(self.apps)
|
platform_tools = self.get_platform_tools(self.apps)
|
||||||
if platform_tools:
|
if platform_tools:
|
||||||
@@ -1434,7 +1459,7 @@ class Agent(BaseAgent):
|
|||||||
self.tools = []
|
self.tools = []
|
||||||
self.tools.extend(mcps)
|
self.tools.extend(mcps)
|
||||||
|
|
||||||
raw_tools: list[BaseTool] = self.tools or []
|
raw_tools = list(self.tools or [])
|
||||||
|
|
||||||
agent_memory = getattr(self, "memory", None)
|
agent_memory = getattr(self, "memory", None)
|
||||||
if agent_memory is not None:
|
if agent_memory is not None:
|
||||||
@@ -1447,6 +1472,7 @@ class Agent(BaseAgent):
|
|||||||
if sanitize_tool_name(mt.name) not in existing_names
|
if sanitize_tool_name(mt.name) not in existing_names
|
||||||
)
|
)
|
||||||
|
|
||||||
|
raw_tools = self._add_skill_loader_tool(raw_tools)
|
||||||
parsed_tools = parse_tools(raw_tools)
|
parsed_tools = parse_tools(raw_tools)
|
||||||
|
|
||||||
agent_info = {
|
agent_info = {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from crewai.agents.agent_builder.base_agent import BaseAgent
|
|||||||
from crewai.crews.crew_output import CrewOutput
|
from crewai.crews.crew_output import CrewOutput
|
||||||
from crewai.llms.base_llm import BaseLLM
|
from crewai.llms.base_llm import BaseLLM
|
||||||
from crewai.rag.embeddings.types import EmbedderConfig
|
from crewai.rag.embeddings.types import EmbedderConfig
|
||||||
from crewai.skills.loader import activate_skill, load_skills
|
from crewai.skills.loader import load_skills
|
||||||
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
|
from crewai.skills.models import Skill as SkillModel
|
||||||
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
|
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
|
||||||
from crewai.utilities.file_store import store_files
|
from crewai.utilities.file_store import store_files
|
||||||
from crewai.utilities.streaming import (
|
from crewai.utilities.streaming import (
|
||||||
@@ -59,13 +59,7 @@ def _resolve_crew_skills(crew: Crew) -> list[SkillModel] | None:
|
|||||||
if not isinstance(crew.skills, list) or not crew.skills:
|
if not isinstance(crew.skills, list) or not crew.skills:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
resolved = load_skills(crew.skills)
|
return load_skills(crew.skills, activate=False) or None
|
||||||
if not resolved:
|
|
||||||
return None
|
|
||||||
return [
|
|
||||||
activate_skill(skill) if skill.disclosure_level < INSTRUCTIONS else skill
|
|
||||||
for skill in resolved
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def setup_agents(
|
def setup_agents(
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ class SkillUsedEvent(SkillEvent):
|
|||||||
"""Event emitted when an agent uses a skill during task execution.
|
"""Event emitted when an agent uses a skill during task execution.
|
||||||
|
|
||||||
Discovery/load/activation events describe setup. This one is the runtime
|
Discovery/load/activation events describe setup. This one is the runtime
|
||||||
signal: it fires each time a skill's context is injected into an agent's
|
signal: it fires when a metadata skill is selected or an always-on skill's
|
||||||
prompt for a task, so traces can attribute skill usage to an agent and task.
|
context is injected, so traces can attribute usage to an agent and task.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: Literal["skill_used"] = "skill_used"
|
type: Literal["skill_used"] = "skill_used"
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ from crewai.events.types.skill_events import (
|
|||||||
SkillLoadFailedEvent,
|
SkillLoadFailedEvent,
|
||||||
SkillLoadedEvent,
|
SkillLoadedEvent,
|
||||||
)
|
)
|
||||||
from crewai.skills.models import INSTRUCTIONS, RESOURCES, Skill, SkillFrontmatter
|
from crewai.skills.models import (
|
||||||
|
INSTRUCTIONS,
|
||||||
|
RESOURCES,
|
||||||
|
Skill,
|
||||||
|
SkillFrontmatter,
|
||||||
|
)
|
||||||
from crewai.skills.parser import (
|
from crewai.skills.parser import (
|
||||||
SKILL_FILENAME,
|
SKILL_FILENAME,
|
||||||
load_skill_instructions,
|
load_skill_instructions,
|
||||||
@@ -148,25 +153,39 @@ def activate_skill(
|
|||||||
def load_skill(
|
def load_skill(
|
||||||
skill: Path | Skill | str,
|
skill: Path | Skill | str,
|
||||||
source: BaseAgent | None = None,
|
source: BaseAgent | None = None,
|
||||||
|
*,
|
||||||
|
activate: bool = True,
|
||||||
) -> list[Skill]:
|
) -> list[Skill]:
|
||||||
"""Load one skill input into Skill objects.
|
"""Load one skill input into Skill objects.
|
||||||
|
|
||||||
Accepts a pre-loaded Skill object, skill search path, inline SKILL.md
|
Accepts a pre-loaded Skill object, skill search path, inline SKILL.md
|
||||||
string, or '@org/name' registry reference. Path inputs can expand to many
|
string, or '@org/name' registry reference. Path inputs can expand to many
|
||||||
skills. Path and inline inputs are activated immediately; pre-loaded Skill
|
skills. Pre-loaded Skill objects keep their disclosure level. Path and
|
||||||
objects keep their disclosure level.
|
registry inputs are activated by default; callers performing runtime
|
||||||
|
progressive disclosure can leave them at metadata level.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skill: Skill input to resolve.
|
||||||
|
source: Optional event source for event emission.
|
||||||
|
activate: Whether discovered path and registry skills should load their
|
||||||
|
full instructions immediately.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resolved Skill objects.
|
||||||
"""
|
"""
|
||||||
if isinstance(skill, Skill):
|
if isinstance(skill, Skill):
|
||||||
return [skill]
|
return [skill]
|
||||||
if isinstance(skill, Path):
|
if isinstance(skill, Path):
|
||||||
return [
|
discovered = discover_skills(skill, source=source)
|
||||||
activate_skill(s, source=source)
|
if not activate:
|
||||||
for s in discover_skills(skill, source=source)
|
return discovered
|
||||||
]
|
return [activate_skill(s, source=source) for s in discovered]
|
||||||
if isinstance(skill, str) and skill.startswith("@"):
|
if isinstance(skill, str) and skill.startswith("@"):
|
||||||
from crewai.skills.registry import resolve_registry_ref
|
from crewai.skills.registry import resolve_registry_ref
|
||||||
|
|
||||||
return [resolve_registry_ref(skill, source=source)]
|
if activate:
|
||||||
|
return [resolve_registry_ref(skill, source=source)]
|
||||||
|
return [resolve_registry_ref(skill, source=source, activate=False)]
|
||||||
if isinstance(skill, str) and skill.lstrip().startswith("---\n"):
|
if isinstance(skill, str) and skill.lstrip().startswith("---\n"):
|
||||||
frontmatter_dict, body = parse_frontmatter(skill.strip())
|
frontmatter_dict, body = parse_frontmatter(skill.strip())
|
||||||
return [
|
return [
|
||||||
@@ -178,10 +197,10 @@ def load_skill(
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
if isinstance(skill, str):
|
if isinstance(skill, str):
|
||||||
return [
|
discovered = discover_skills(Path(skill), source=source)
|
||||||
activate_skill(s, source=source)
|
if not activate:
|
||||||
for s in discover_skills(Path(skill), source=source)
|
return discovered
|
||||||
]
|
return [activate_skill(s, source=source) for s in discovered]
|
||||||
|
|
||||||
msg = f"Unsupported skill input: {skill!r}"
|
msg = f"Unsupported skill input: {skill!r}"
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
@@ -190,16 +209,27 @@ def load_skill(
|
|||||||
def load_skills(
|
def load_skills(
|
||||||
skills: Iterable[Path | Skill | str],
|
skills: Iterable[Path | Skill | str],
|
||||||
source: BaseAgent | None = None,
|
source: BaseAgent | None = None,
|
||||||
|
*,
|
||||||
|
activate: bool = True,
|
||||||
) -> list[Skill]:
|
) -> list[Skill]:
|
||||||
"""Load skill inputs into de-duplicated Skill objects.
|
"""Load skill inputs into de-duplicated Skill objects.
|
||||||
|
|
||||||
Preserves first-seen order when multiple inputs resolve to the same skill
|
Preserves first-seen order when multiple inputs resolve to the same skill
|
||||||
name. Registry refs are scoped by org so different orgs can publish skills
|
name. Registry refs are scoped by org so different orgs can publish skills
|
||||||
that share a frontmatter name.
|
that share a frontmatter name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skills: Skill inputs to resolve.
|
||||||
|
source: Optional event source for event emission.
|
||||||
|
activate: Whether discovered path and registry skills should load their
|
||||||
|
full instructions immediately.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
De-duplicated Skill objects.
|
||||||
"""
|
"""
|
||||||
loaded: dict[str, Skill] = {}
|
loaded: dict[str, Skill] = {}
|
||||||
for skill_input in skills:
|
for skill_input in skills:
|
||||||
for skill in load_skill(skill_input, source=source):
|
for skill in load_skill(skill_input, source=source, activate=activate):
|
||||||
dedup_key = skill.name
|
dedup_key = skill.name
|
||||||
if isinstance(skill_input, str) and skill_input.startswith("@"):
|
if isinstance(skill_input, str) and skill_input.startswith("@"):
|
||||||
from crewai.skills.registry import parse_registry_ref
|
from crewai.skills.registry import parse_registry_ref
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ def parse_registry_ref(ref: str) -> tuple[str, str]:
|
|||||||
def resolve_registry_ref(
|
def resolve_registry_ref(
|
||||||
ref: str,
|
ref: str,
|
||||||
source: Any = None,
|
source: Any = None,
|
||||||
|
*,
|
||||||
|
activate: bool = True,
|
||||||
) -> Skill: # type: ignore[name-defined] # noqa: F821
|
) -> Skill: # type: ignore[name-defined] # noqa: F821
|
||||||
"""Resolve a registry reference to a Skill object.
|
"""Resolve a registry reference to a Skill object.
|
||||||
|
|
||||||
@@ -129,9 +131,11 @@ def resolve_registry_ref(
|
|||||||
Args:
|
Args:
|
||||||
ref: A registry reference, e.g. '@acme/my-skill' or '@acme/my-skill@1.2.0'.
|
ref: A registry reference, e.g. '@acme/my-skill' or '@acme/my-skill@1.2.0'.
|
||||||
source: Optional source object passed through to skill loaders (for events).
|
source: Optional source object passed through to skill loaders (for events).
|
||||||
|
activate: Whether to load the full SKILL.md instructions.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Skill loaded at INSTRUCTIONS disclosure level.
|
A Skill loaded at INSTRUCTIONS level by default, or METADATA level
|
||||||
|
when ``activate`` is false.
|
||||||
"""
|
"""
|
||||||
from crewai.skills.loader import activate_skill
|
from crewai.skills.loader import activate_skill
|
||||||
|
|
||||||
@@ -143,7 +147,7 @@ def resolve_registry_ref(
|
|||||||
# is the only thing a pin can be checked against.
|
# is the only thing a pin can be checked against.
|
||||||
skill = _load_matching_skill(local_path, version)
|
skill = _load_matching_skill(local_path, version)
|
||||||
if skill is not None:
|
if skill is not None:
|
||||||
return activate_skill(skill, source=source)
|
return activate_skill(skill, source=source) if activate else skill
|
||||||
|
|
||||||
cache = SkillCacheManager()
|
cache = SkillCacheManager()
|
||||||
cached_path = cache.get_cached_path(org, name, version=version)
|
cached_path = cache.get_cached_path(org, name, version=version)
|
||||||
@@ -154,9 +158,15 @@ def resolve_registry_ref(
|
|||||||
# metadata.version and re-download it on each resolution.
|
# metadata.version and re-download it on each resolution.
|
||||||
skill = _load_skill(cached_path)
|
skill = _load_skill(cached_path)
|
||||||
if skill is not None:
|
if skill is not None:
|
||||||
return activate_skill(skill, source=source)
|
return activate_skill(skill, source=source) if activate else skill
|
||||||
|
|
||||||
return download_skill(org, name, source=source, version=version)
|
return download_skill(
|
||||||
|
org,
|
||||||
|
name,
|
||||||
|
source=source,
|
||||||
|
version=version,
|
||||||
|
activate=activate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_skill(path: Path) -> Skill | None: # type: ignore[name-defined] # noqa: F821
|
def _load_skill(path: Path) -> Skill | None: # type: ignore[name-defined] # noqa: F821
|
||||||
@@ -268,6 +278,7 @@ def download_skill(
|
|||||||
source: Any = None,
|
source: Any = None,
|
||||||
*,
|
*,
|
||||||
version: str | None = None,
|
version: str | None = None,
|
||||||
|
activate: bool = True,
|
||||||
) -> Skill: # type: ignore[name-defined] # noqa: F821
|
) -> Skill: # type: ignore[name-defined] # noqa: F821
|
||||||
"""Download a skill from the registry and store it in the cache.
|
"""Download a skill from the registry and store it in the cache.
|
||||||
|
|
||||||
@@ -276,9 +287,11 @@ def download_skill(
|
|||||||
name: Skill name.
|
name: Skill name.
|
||||||
source: Optional source for event emission.
|
source: Optional source for event emission.
|
||||||
version: Optional pinned version; the latest is fetched when omitted.
|
version: Optional pinned version; the latest is fetched when omitted.
|
||||||
|
activate: Whether to load the full SKILL.md instructions.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The downloaded Skill at INSTRUCTIONS level.
|
The downloaded Skill at INSTRUCTIONS level by default, or METADATA
|
||||||
|
level when ``activate`` is false.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If *version* is given but blank.
|
ValueError: If *version* is given but blank.
|
||||||
@@ -376,4 +389,4 @@ def download_skill(
|
|||||||
f"Skill archive for {ref!r} downloaded but no SKILL.md found in {skill_dir}"
|
f"Skill archive for {ref!r} downloaded but no SKILL.md found in {skill_dir}"
|
||||||
)
|
)
|
||||||
skill = load_skill_metadata(skill_dir)
|
skill = load_skill_metadata(skill_dir)
|
||||||
return activate_skill(skill, source=source)
|
return activate_skill(skill, source=source) if activate else skill
|
||||||
|
|||||||
74
lib/crewai/src/crewai/skills/tool.py
Normal file
74
lib/crewai/src/crewai/skills/tool.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""Runtime tool for progressively disclosing Agent Skill instructions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from crewai.events.event_bus import crewai_event_bus
|
||||||
|
from crewai.events.types.skill_events import SkillUsedEvent
|
||||||
|
from crewai.skills.loader import activate_skill, format_skill_context
|
||||||
|
from crewai.skills.models import INSTRUCTIONS, Skill
|
||||||
|
from crewai.tools.base_tool import BaseTool
|
||||||
|
|
||||||
|
|
||||||
|
class LoadSkillSchema(BaseModel):
|
||||||
|
"""Arguments accepted by the runtime skill loader."""
|
||||||
|
|
||||||
|
skill_name: str = Field(
|
||||||
|
...,
|
||||||
|
description="Exact name of the available skill whose instructions are needed.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LoadSkillTool(BaseTool):
|
||||||
|
"""Load one relevant skill's instructions for the current execution."""
|
||||||
|
|
||||||
|
name: str = "load_skill"
|
||||||
|
description: str = (
|
||||||
|
"Load the full instructions for one available skill. Use this before "
|
||||||
|
"working on a request when an available skill's description applies."
|
||||||
|
)
|
||||||
|
args_schema: type[BaseModel] = LoadSkillSchema
|
||||||
|
skills: list[Skill] = Field(default_factory=list, exclude=True)
|
||||||
|
source: Any = Field(default=None, exclude=True)
|
||||||
|
task: Any = Field(default=None, exclude=True)
|
||||||
|
|
||||||
|
def _run(self, skill_name: str, **kwargs: Any) -> str:
|
||||||
|
"""Load and return one skill's instruction block."""
|
||||||
|
skill = next((item for item in self.skills if item.name == skill_name), None)
|
||||||
|
if skill is None:
|
||||||
|
available = ", ".join(item.name for item in self.skills)
|
||||||
|
return (
|
||||||
|
f"Skill {skill_name!r} is not available. "
|
||||||
|
f"Available skills: {available or 'none'}."
|
||||||
|
)
|
||||||
|
|
||||||
|
activated = activate_skill(skill, source=self.source)
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self.source,
|
||||||
|
event=SkillUsedEvent(
|
||||||
|
from_agent=self.source,
|
||||||
|
from_task=self.task,
|
||||||
|
skill_name=activated.name,
|
||||||
|
skill_path=activated.path,
|
||||||
|
disclosure_level=activated.disclosure_level,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return format_skill_context(activated)
|
||||||
|
|
||||||
|
|
||||||
|
def create_skill_loader_tool(
|
||||||
|
skills: list[Skill] | None,
|
||||||
|
*,
|
||||||
|
source: Any = None,
|
||||||
|
task: Any = None,
|
||||||
|
) -> LoadSkillTool | None:
|
||||||
|
"""Create a loader tool when metadata-only skills are available."""
|
||||||
|
available = [
|
||||||
|
skill for skill in skills or [] if skill.disclosure_level < INSTRUCTIONS
|
||||||
|
]
|
||||||
|
if not available:
|
||||||
|
return None
|
||||||
|
return LoadSkillTool(skills=available, source=source, task=task)
|
||||||
@@ -115,22 +115,49 @@ class Prompts(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _build_skill_block(self) -> str:
|
def _build_skill_block(self) -> str:
|
||||||
"""Render the agent's activated skills as a stable XML block.
|
"""Render always-on instructions and the available skill catalog.
|
||||||
|
|
||||||
Skills are agent-scoped (do not change per task), so they live in the
|
Metadata-only skills remain a stable prompt-cache anchor. Their full
|
||||||
system prompt where prompt-cache prefixes can survive across calls.
|
instructions are disclosed through the runtime ``load_skill`` tool only
|
||||||
|
when the model determines that a skill applies to the current request.
|
||||||
"""
|
"""
|
||||||
skills = getattr(self.agent, "skills", None)
|
skills = getattr(self.agent, "skills", None)
|
||||||
if not skills:
|
if not skills:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
from crewai.skills.loader import format_skill_context
|
from crewai.skills.loader import format_skill_context
|
||||||
from crewai.skills.models import Skill
|
from crewai.skills.models import INSTRUCTIONS, Skill
|
||||||
|
|
||||||
sections = [format_skill_context(s) for s in skills if isinstance(s, Skill)]
|
active = [
|
||||||
if not sections:
|
format_skill_context(skill)
|
||||||
|
for skill in skills
|
||||||
|
if isinstance(skill, Skill)
|
||||||
|
and skill.disclosure_level >= INSTRUCTIONS
|
||||||
|
]
|
||||||
|
available = [
|
||||||
|
format_skill_context(skill)
|
||||||
|
for skill in skills
|
||||||
|
if isinstance(skill, Skill)
|
||||||
|
and skill.disclosure_level < INSTRUCTIONS
|
||||||
|
]
|
||||||
|
|
||||||
|
blocks: list[str] = []
|
||||||
|
if active:
|
||||||
|
blocks.append("<skills>\n" + "\n\n".join(active) + "\n</skills>")
|
||||||
|
if available:
|
||||||
|
catalog = "\n\n".join(available)
|
||||||
|
blocks.append(
|
||||||
|
"<available_skills>\n"
|
||||||
|
"Review these skill descriptions before answering. When one "
|
||||||
|
"clearly applies to the current request, call `load_skill` with "
|
||||||
|
"its exact name to load its full instructions. Do not use a "
|
||||||
|
"skill's instructions without loading it first.\n\n"
|
||||||
|
f"{catalog}\n"
|
||||||
|
"</available_skills>"
|
||||||
|
)
|
||||||
|
if not blocks:
|
||||||
return ""
|
return ""
|
||||||
return "\n\n<skills>\n" + "\n\n".join(sections) + "\n</skills>"
|
return "\n\n" + "\n\n".join(blocks)
|
||||||
|
|
||||||
def _build_prompt(
|
def _build_prompt(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import pytest
|
|||||||
from crewai import Agent, Task
|
from crewai import Agent, Task
|
||||||
from crewai.events import crewai_event_bus
|
from crewai.events import crewai_event_bus
|
||||||
from crewai.events.types.skill_events import SkillUsedEvent
|
from crewai.events.types.skill_events import SkillUsedEvent
|
||||||
|
from crewai.skills.loader import activate_skill, discover_skills
|
||||||
|
from crewai.skills.tool import LoadSkillTool
|
||||||
|
|
||||||
|
|
||||||
# Handlers run on the event bus thread pool, so tests must flush before
|
# Handlers run on the event bus thread pool, so tests must flush before
|
||||||
@@ -29,12 +31,13 @@ def _create_skill_dir(parent: Path, name: str, body: str = "Body.") -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _agent_with_skills(search_path: Path) -> Agent:
|
def _agent_with_skills(search_path: Path) -> Agent:
|
||||||
# discover_skills scans the SUBdirectories of the given path.
|
"""Create an agent with explicitly activated, always-on skills."""
|
||||||
|
skills = [activate_skill(skill) for skill in discover_skills(search_path)]
|
||||||
return Agent(
|
return Agent(
|
||||||
role="Analyst",
|
role="Analyst",
|
||||||
goal="Analyze things",
|
goal="Analyze things",
|
||||||
backstory="Experienced",
|
backstory="Experienced",
|
||||||
skills=[str(search_path)],
|
skills=skills,
|
||||||
llm="gpt-4o-mini",
|
llm="gpt-4o-mini",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +47,43 @@ def _task_for(agent: Agent) -> Task:
|
|||||||
|
|
||||||
|
|
||||||
class TestSkillUsedEvent:
|
class TestSkillUsedEvent:
|
||||||
|
def test_metadata_skill_emits_only_after_runtime_selection(
|
||||||
|
self, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
_create_skill_dir(tmp_path, "alpha")
|
||||||
|
agent = Agent(
|
||||||
|
role="Analyst",
|
||||||
|
goal="Analyze things",
|
||||||
|
backstory="Experienced",
|
||||||
|
skills=[tmp_path],
|
||||||
|
llm="gpt-4o-mini",
|
||||||
|
)
|
||||||
|
task = _task_for(agent)
|
||||||
|
agent.create_agent_executor(task=task)
|
||||||
|
assert agent.agent_executor is not None
|
||||||
|
loader = next(
|
||||||
|
tool
|
||||||
|
for tool in agent.agent_executor.original_tools
|
||||||
|
if isinstance(tool, LoadSkillTool)
|
||||||
|
)
|
||||||
|
|
||||||
|
received: list[SkillUsedEvent] = []
|
||||||
|
with crewai_event_bus.scoped_handlers():
|
||||||
|
|
||||||
|
@crewai_event_bus.on(SkillUsedEvent)
|
||||||
|
def _handler(source, event: SkillUsedEvent) -> None: # noqa: ARG001
|
||||||
|
received.append(event)
|
||||||
|
|
||||||
|
agent._emit_skill_usage(task)
|
||||||
|
assert crewai_event_bus.flush(timeout=10)
|
||||||
|
assert received == []
|
||||||
|
|
||||||
|
loader.run(skill_name="alpha")
|
||||||
|
assert crewai_event_bus.flush(timeout=10)
|
||||||
|
|
||||||
|
assert [event.skill_name for event in received] == ["alpha"]
|
||||||
|
assert received[0].task_id == str(task.id)
|
||||||
|
|
||||||
def test_emits_one_event_per_skill_with_agent_and_task_attribution(
|
def test_emits_one_event_per_skill_with_agent_and_task_attribution(
|
||||||
self, tmp_path: Path
|
self, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -109,7 +149,7 @@ class TestSkillUsedEvent:
|
|||||||
assert len(received) == 2
|
assert len(received) == 2
|
||||||
|
|
||||||
def test_reports_disclosure_level(self, tmp_path: Path) -> None:
|
def test_reports_disclosure_level(self, tmp_path: Path) -> None:
|
||||||
"""Path-loaded skills are activated, so they report INSTRUCTIONS level."""
|
"""Explicitly activated skills report INSTRUCTIONS level."""
|
||||||
_create_skill_dir(tmp_path, "alpha")
|
_create_skill_dir(tmp_path, "alpha")
|
||||||
agent = _agent_with_skills(tmp_path)
|
agent = _agent_with_skills(tmp_path)
|
||||||
task = _task_for(agent)
|
task = _task_for(agent)
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ class TestSkillDiscoveryAndActivation:
|
|||||||
|
|
||||||
assert agent.skills is not None
|
assert agent.skills is not None
|
||||||
assert [skill.name for skill in agent.skills] == ["path-skill"]
|
assert [skill.name for skill in agent.skills] == ["path-skill"]
|
||||||
assert [skill.instructions for skill in agent.skills] == ["Use the path skill."]
|
assert [skill.disclosure_level for skill in agent.skills] == [METADATA]
|
||||||
|
assert [skill.instructions for skill in agent.skills] == [None]
|
||||||
|
|
||||||
def test_crew_resolves_inline_skill_string(self) -> None:
|
def test_crew_resolves_inline_skill_string(self) -> None:
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
@@ -175,7 +176,7 @@ class TestSkillDiscoveryAndActivation:
|
|||||||
assert [skill.name for skill in skills] == ["crew-inline-review"]
|
assert [skill.name for skill in skills] == ["crew-inline-review"]
|
||||||
assert [skill.instructions for skill in skills] == ["Apply this to every agent."]
|
assert [skill.instructions for skill in skills] == ["Apply this to every agent."]
|
||||||
|
|
||||||
def test_crew_activates_preloaded_metadata_skill(self, tmp_path: Path) -> None:
|
def test_crew_preserves_preloaded_metadata_skill(self, tmp_path: Path) -> None:
|
||||||
_create_skill_dir(
|
_create_skill_dir(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
"crew-preloaded",
|
"crew-preloaded",
|
||||||
@@ -202,7 +203,5 @@ class TestSkillDiscoveryAndActivation:
|
|||||||
|
|
||||||
assert skills is not None
|
assert skills is not None
|
||||||
assert [skill.name for skill in skills] == ["crew-preloaded"]
|
assert [skill.name for skill in skills] == ["crew-preloaded"]
|
||||||
assert [skill.disclosure_level for skill in skills] == [INSTRUCTIONS]
|
assert [skill.disclosure_level for skill in skills] == [METADATA]
|
||||||
assert [skill.instructions for skill in skills] == [
|
assert [skill.instructions for skill in skills] == [None]
|
||||||
"Apply this crew-level guidance to every agent."
|
|
||||||
]
|
|
||||||
|
|||||||
208
lib/crewai/tests/skills/test_progressive_disclosure.py
Normal file
208
lib/crewai/tests/skills/test_progressive_disclosure.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""Regression tests for runtime skill progressive disclosure.
|
||||||
|
|
||||||
|
Run this focused test file with:
|
||||||
|
|
||||||
|
uv run pytest lib/crewai/tests/skills/test_progressive_disclosure.py -q
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from crewai import Agent, Task
|
||||||
|
from crewai.llms.base_llm import BaseLLM
|
||||||
|
from crewai.skills.models import METADATA
|
||||||
|
from crewai.skills.tool import LoadSkillTool
|
||||||
|
from crewai.utilities.prompts import Prompts
|
||||||
|
|
||||||
|
|
||||||
|
def _create_skill(
|
||||||
|
parent: Path,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
instructions: str,
|
||||||
|
) -> None:
|
||||||
|
skill_dir = parent / name
|
||||||
|
skill_dir.mkdir()
|
||||||
|
(skill_dir / "SKILL.md").write_text(
|
||||||
|
f"---\nname: {name}\ndescription: {description}\n---\n{instructions}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _SkillChoosingLLM(BaseLLM):
|
||||||
|
"""Small deterministic LLM that exercises the real agent tool loop."""
|
||||||
|
|
||||||
|
def call(self, messages: Any, **kwargs: Any) -> str:
|
||||||
|
rendered = str(messages)
|
||||||
|
if "PYTHON_REVIEW_PRIVATE_INSTRUCTIONS" in rendered:
|
||||||
|
return "Thought: I loaded the review skill.\nFinal Answer: python loaded"
|
||||||
|
if "TRAVEL_PRIVATE_INSTRUCTIONS" in rendered:
|
||||||
|
return "Thought: I loaded the travel skill.\nFinal Answer: travel loaded"
|
||||||
|
if "Review this Python change" in rendered:
|
||||||
|
return (
|
||||||
|
"Thought: The Python review skill applies.\n"
|
||||||
|
"Action: load_skill\n"
|
||||||
|
'Action Input: {"skill_name": "python-review"}'
|
||||||
|
)
|
||||||
|
if "Plan a weekend trip" in rendered:
|
||||||
|
return (
|
||||||
|
"Thought: The travel planning skill applies.\n"
|
||||||
|
"Action: load_skill\n"
|
||||||
|
'Action Input: {"skill_name": "travel-planning"}'
|
||||||
|
)
|
||||||
|
return "Thought: No skill applies.\nFinal Answer: no skill"
|
||||||
|
|
||||||
|
def supports_function_calling(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def supports_stop_words(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_context_window_size(self) -> int:
|
||||||
|
return 8_192
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_directory_skills_do_not_eagerly_disclose_instructions(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""An agent should initially receive only the skill catalog metadata."""
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"python-review",
|
||||||
|
"Review Python code when the user asks for a code review.",
|
||||||
|
"PYTHON_REVIEW_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"travel-planning",
|
||||||
|
"Plan travel when the user asks for a trip itinerary.",
|
||||||
|
"TRAVEL_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Assistant",
|
||||||
|
goal="Help with the current request.",
|
||||||
|
backstory="A general-purpose assistant.",
|
||||||
|
skills=[tmp_path],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert agent.skills is not None
|
||||||
|
assert [skill.disclosure_level for skill in agent.skills] == [
|
||||||
|
METADATA,
|
||||||
|
METADATA,
|
||||||
|
]
|
||||||
|
assert [skill.instructions for skill in agent.skills] == [None, None]
|
||||||
|
|
||||||
|
|
||||||
|
def test_consecutive_kickoffs_select_skills_without_cross_call_leakage(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Each execution should independently choose its relevant skill."""
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"python-review",
|
||||||
|
"Review Python code when the user asks for a code review.",
|
||||||
|
"PYTHON_REVIEW_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"travel-planning",
|
||||||
|
"Plan travel when the user asks for a trip itinerary.",
|
||||||
|
"TRAVEL_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
agent = Agent(
|
||||||
|
role="Assistant",
|
||||||
|
goal="Help with the current request.",
|
||||||
|
backstory="A general-purpose assistant.",
|
||||||
|
skills=[tmp_path],
|
||||||
|
llm=_SkillChoosingLLM(model="skill-test"),
|
||||||
|
max_iter=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
review_output = agent.kickoff("Review this Python change")
|
||||||
|
travel_output = agent.kickoff("Plan a weekend trip")
|
||||||
|
repeated_review_output = agent.kickoff("Review this Python change")
|
||||||
|
|
||||||
|
assert review_output.raw == "python loaded"
|
||||||
|
assert travel_output.raw == "travel loaded"
|
||||||
|
assert repeated_review_output.raw == "python loaded"
|
||||||
|
assert agent.skills is not None
|
||||||
|
assert [skill.disclosure_level for skill in agent.skills] == [
|
||||||
|
METADATA,
|
||||||
|
METADATA,
|
||||||
|
]
|
||||||
|
assert [skill.instructions for skill in agent.skills] == [None, None]
|
||||||
|
|
||||||
|
prompt = Prompts(
|
||||||
|
agent=agent,
|
||||||
|
has_tools=False,
|
||||||
|
use_system_prompt=True,
|
||||||
|
).task_execution()
|
||||||
|
rendered = getattr(prompt, "system", "") or prompt.prompt
|
||||||
|
|
||||||
|
assert "python-review" in rendered
|
||||||
|
assert "travel-planning" in rendered
|
||||||
|
assert "PYTHON_REVIEW_PRIVATE_INSTRUCTIONS" not in rendered
|
||||||
|
assert "TRAVEL_PRIVATE_INSTRUCTIONS" not in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_each_execution_can_disclose_only_its_relevant_skill(tmp_path: Path) -> None:
|
||||||
|
"""Loading one skill must not leak or permanently activate another."""
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"python-review",
|
||||||
|
"Review Python code when the user asks for a code review.",
|
||||||
|
"PYTHON_REVIEW_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
_create_skill(
|
||||||
|
tmp_path,
|
||||||
|
"travel-planning",
|
||||||
|
"Plan travel when the user asks for a trip itinerary.",
|
||||||
|
"TRAVEL_PRIVATE_INSTRUCTIONS",
|
||||||
|
)
|
||||||
|
agent = Agent(
|
||||||
|
role="Assistant",
|
||||||
|
goal="Help with the current request.",
|
||||||
|
backstory="A general-purpose assistant.",
|
||||||
|
skills=[tmp_path],
|
||||||
|
)
|
||||||
|
|
||||||
|
review_task = Task(
|
||||||
|
description="Review this Python change.",
|
||||||
|
expected_output="Review findings.",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
agent.create_agent_executor(task=review_task)
|
||||||
|
assert agent.agent_executor is not None
|
||||||
|
review_loader = next(
|
||||||
|
tool
|
||||||
|
for tool in agent.agent_executor.original_tools
|
||||||
|
if isinstance(tool, LoadSkillTool)
|
||||||
|
)
|
||||||
|
review_context = review_loader.run(skill_name="python-review")
|
||||||
|
|
||||||
|
assert "PYTHON_REVIEW_PRIVATE_INSTRUCTIONS" in review_context
|
||||||
|
assert "TRAVEL_PRIVATE_INSTRUCTIONS" not in review_context
|
||||||
|
|
||||||
|
travel_task = Task(
|
||||||
|
description="Plan a weekend trip.",
|
||||||
|
expected_output="A travel itinerary.",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
agent.create_agent_executor(task=travel_task)
|
||||||
|
assert agent.agent_executor is not None
|
||||||
|
travel_loader = next(
|
||||||
|
tool
|
||||||
|
for tool in agent.agent_executor.original_tools
|
||||||
|
if isinstance(tool, LoadSkillTool)
|
||||||
|
)
|
||||||
|
travel_context = travel_loader.run(skill_name="travel-planning")
|
||||||
|
|
||||||
|
assert "TRAVEL_PRIVATE_INSTRUCTIONS" in travel_context
|
||||||
|
assert "PYTHON_REVIEW_PRIVATE_INSTRUCTIONS" not in travel_context
|
||||||
|
assert agent.skills is not None
|
||||||
|
assert [skill.disclosure_level for skill in agent.skills] == [
|
||||||
|
METADATA,
|
||||||
|
METADATA,
|
||||||
|
]
|
||||||
|
assert [skill.instructions for skill in agent.skills] == [None, None]
|
||||||
@@ -10,6 +10,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from crewai.context import platform_context
|
from crewai.context import platform_context
|
||||||
from crewai.skills.cache import SkillCacheManager
|
from crewai.skills.cache import SkillCacheManager
|
||||||
|
from crewai.skills.models import METADATA
|
||||||
from crewai.skills.registry import (
|
from crewai.skills.registry import (
|
||||||
SkillRef,
|
SkillRef,
|
||||||
download_skill,
|
download_skill,
|
||||||
@@ -181,6 +182,17 @@ class TestParseSkillRef:
|
|||||||
|
|
||||||
|
|
||||||
class TestResolveRegistryRef:
|
class TestResolveRegistryRef:
|
||||||
|
def test_can_resolve_metadata_without_loading_instructions(
|
||||||
|
self, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
_write_local_skill(tmp_path, "my-skill")
|
||||||
|
|
||||||
|
with patch.object(Path, "cwd", return_value=tmp_path):
|
||||||
|
skill = resolve_registry_ref("@acme/my-skill", activate=False)
|
||||||
|
|
||||||
|
assert skill.disclosure_level == METADATA
|
||||||
|
assert skill.instructions is None
|
||||||
|
|
||||||
def test_prefers_project_local_skill_over_cached_skill(
|
def test_prefers_project_local_skill_over_cached_skill(
|
||||||
self, tmp_path: Path
|
self, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user