Skip to content

Commit f15844b

Browse files
Lorenze/imp/skills progressive disclosure (#6675)
* skills progressive disclosure * skills progressive disclosure * improving progressive disclosure * addressed comment * fix test --------- Co-authored-by: João Moura <joaomdmoura@gmail.com>
1 parent e9caf1e commit f15844b

12 files changed

Lines changed: 735 additions & 71 deletions

File tree

docs/edge/en/concepts/skills.mdx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ mode: "wide"
99

1010
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.
1111

12-
When activated, a skill's instructions are injected directly into the agent's task prompt — giving the agent expertise without requiring any code changes.
12+
Agents first receive each configured skill's name and description. When a
13+
description applies to the current request, the agent loads that skill's full
14+
instructions for that execution. This keeps unrelated instructions out of the
15+
context while giving the agent the relevant expertise without code changes.
1316

1417
<Note type="info" title="Skills vs Tools — The Key Distinction">
1518
**Skills are NOT tools.** This is the most common point of confusion.
@@ -79,12 +82,13 @@ reviewer = Agent(
7982
role="Senior Code Reviewer",
8083
goal="Review pull requests for quality and security issues",
8184
backstory="Staff engineer with expertise in secure coding practices.",
82-
skills=["./skills"], # Injects review guidelines
85+
skills=["./skills"], # Discovers review skills
8386
tools=[GithubSearchTool(), FileReadTool()], # Lets agent read code
8487
)
8588
```
8689

87-
The agent now has both **expertise** (from the skill) and **capabilities** (from the tools).
90+
The agent now has both **expertise** (loaded from the relevant skill when
91+
needed) and **capabilities** (from the tools).
8892

8993
---
9094

@@ -324,7 +328,8 @@ The directory name must match the `name` field in `SKILL.md`. The `scripts/`, `r
324328

325329
## Pre-loading Skills
326330

327-
For more control, you can discover and activate skills programmatically:
331+
For more control, you can discover and activate skills programmatically.
332+
Passing an activated skill makes its instructions always-on:
328333

329334
```python
330335
from pathlib import Path
@@ -351,12 +356,21 @@ agent = Agent(
351356

352357
Skills use **progressive disclosure** — only loading what's needed at each stage:
353358

354-
| Stage | What's loaded | When |
355-
| :--------- | :------------------------------------ | :------------------ |
356-
| Discovery | Name, description, frontmatter fields | `discover_skills()` |
357-
| Activation | Full SKILL.md body text | `activate_skill()` |
358-
359-
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.
359+
| Stage | What's loaded | When |
360+
| :--------- | :------------------------------------ | :---------------------------------------- |
361+
| Discovery | Name, description, frontmatter fields | Agent setup or `discover_skills()` |
362+
| Activation | Full SKILL.md body text | Relevant runtime request or `activate_skill()` |
363+
| Resources | Resource directory catalog | Explicit `load_resources()` call |
364+
365+
With `skills=["./skills"]`, the directory is discovered at setup but the full
366+
instructions are not placed in every prompt. The agent reviews the metadata on
367+
each execution and loads only a skill that applies. The loaded instructions are
368+
scoped to that execution, so skills selected for earlier calls do not accumulate
369+
on the agent.
370+
371+
Inline skill strings and `Skill` objects already activated with
372+
`activate_skill()` remain always-on. This provides an explicit opt-in when the
373+
instructions should apply to every request.
360374

361375
---
362376

lib/crewai/src/crewai/agent/core.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
from crewai.rag.embeddings.types import EmbedderConfig
8484
from crewai.security.fingerprint import Fingerprint
8585
from crewai.skills.loader import load_skills
86-
from crewai.skills.models import Skill as SkillModel
86+
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
8787
from crewai.state.checkpoint_config import CheckpointConfig, apply_checkpoint
8888
from crewai.tools.agent_tools.agent_tools import AgentTools
8989
from crewai.types.callback import SerializableCallable
@@ -480,9 +480,32 @@ def set_skills(
480480

481481
self.skills = cast(
482482
list[Path | SkillModel | str] | None,
483-
load_skills(items, source=self) or None,
483+
load_skills(items, source=self, activate=False) or None,
484484
)
485485

486+
def _add_skill_loader_tool(
487+
self,
488+
tools: list[BaseTool],
489+
task: Task | None = None,
490+
) -> list[BaseTool]:
491+
"""Add the internal loader used for request-scoped skill disclosure."""
492+
from crewai.skills.tool import LoadSkillTool, create_skill_loader_tool
493+
494+
tools = [tool for tool in tools if not isinstance(tool, LoadSkillTool)]
495+
496+
skill_models = [
497+
skill for skill in self.skills or [] if isinstance(skill, SkillModel)
498+
]
499+
loader = create_skill_loader_tool(
500+
skill_models,
501+
source=self,
502+
task=task,
503+
reserved_names=[tool.name for tool in tools],
504+
)
505+
if loader is None:
506+
return tools
507+
return [*tools, loader]
508+
486509
def _is_any_available_memory(self) -> bool:
487510
"""Check if unified memory is available (agent or crew)."""
488511
if getattr(self, "memory", None):
@@ -557,11 +580,11 @@ def _finalize_task_prompt(
557580
return apply_training_data(self, task_prompt)
558581

559582
def _emit_skill_usage(self, task: Task) -> None:
560-
"""Emit one SkillUsedEvent per skill injected into this task's prompt.
583+
"""Emit usage for always-on skills injected into this task's prompt.
561584
562-
Skills are agent-scoped and rendered into the prompt on every execution,
563-
so this is the runtime usage signal traces need — attributing each skill
564-
to the agent and task that used it.
585+
Metadata-only skills emit from ``LoadSkillTool`` if the model selects
586+
them. This method covers explicitly activated and inline skills, whose
587+
instructions are rendered on every execution.
565588
566589
Args:
567590
task: The task whose prompt the skills are being applied to.
@@ -570,7 +593,10 @@ def _emit_skill_usage(self, task: Task) -> None:
570593
return
571594

572595
for skill in self.skills:
573-
if not isinstance(skill, SkillModel):
596+
if (
597+
not isinstance(skill, SkillModel)
598+
or skill.disclosure_level < INSTRUCTIONS
599+
):
574600
continue
575601
crewai_event_bus.emit(
576602
self,
@@ -1050,6 +1076,8 @@ def _build_execution_prompt(
10501076
Returns:
10511077
A tuple of (prompt, stop_words, rpm_limit_fn).
10521078
"""
1079+
from crewai.skills.tool import LoadSkillTool
1080+
10531081
use_native_tool_calling = self._supports_native_tool_calling(raw_tools)
10541082

10551083
prompt = Prompts(
@@ -1060,6 +1088,10 @@ def _build_execution_prompt(
10601088
system_template=self.system_template,
10611089
prompt_template=self.prompt_template,
10621090
response_template=self.response_template,
1091+
skill_loader_tool_name=next(
1092+
(tool.name for tool in raw_tools if isinstance(tool, LoadSkillTool)),
1093+
None,
1094+
),
10631095
).task_execution()
10641096

10651097
stop_words = [I18N_DEFAULT.slice("observation")]
@@ -1082,7 +1114,8 @@ def create_agent_executor(
10821114
Returns:
10831115
An instance of the CrewAgentExecutor class.
10841116
"""
1085-
raw_tools: list[BaseTool] = tools or self.tools or []
1117+
configured_tools = tools if tools is not None else self.tools or []
1118+
raw_tools = self._add_skill_loader_tool(list(configured_tools), task=task)
10861119
parsed_tools = parse_tools(raw_tools)
10871120

10881121
prompt, stop_words, rpm_limit_fn = self._build_execution_prompt(raw_tools)
@@ -1421,6 +1454,9 @@ def _prepare_kickoff(
14211454
Returns:
14221455
Tuple of (executor, inputs, agent_info, parsed_tools) ready for execution.
14231456
"""
1457+
if self.tools_handler:
1458+
self.tools_handler.last_used_tool = None
1459+
14241460
if self.apps:
14251461
platform_tools = self.get_platform_tools(self.apps)
14261462
if platform_tools:
@@ -1434,7 +1470,7 @@ def _prepare_kickoff(
14341470
self.tools = []
14351471
self.tools.extend(mcps)
14361472

1437-
raw_tools: list[BaseTool] = self.tools or []
1473+
raw_tools = list(self.tools or [])
14381474

14391475
agent_memory = getattr(self, "memory", None)
14401476
if agent_memory is not None:
@@ -1447,6 +1483,7 @@ def _prepare_kickoff(
14471483
if sanitize_tool_name(mt.name) not in existing_names
14481484
)
14491485

1486+
raw_tools = self._add_skill_loader_tool(raw_tools)
14501487
parsed_tools = parse_tools(raw_tools)
14511488

14521489
agent_info = {

lib/crewai/src/crewai/crews/utils.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from crewai.crews.crew_output import CrewOutput
1313
from crewai.llms.base_llm import BaseLLM
1414
from crewai.rag.embeddings.types import EmbedderConfig
15-
from crewai.skills.loader import activate_skill, load_skills
16-
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
15+
from crewai.skills.loader import load_skills
16+
from crewai.skills.models import Skill as SkillModel
1717
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
1818
from crewai.utilities.file_store import store_files
1919
from crewai.utilities.streaming import (
@@ -59,13 +59,7 @@ def _resolve_crew_skills(crew: Crew) -> list[SkillModel] | None:
5959
if not isinstance(crew.skills, list) or not crew.skills:
6060
return None
6161

62-
resolved = load_skills(crew.skills)
63-
if not resolved:
64-
return None
65-
return [
66-
activate_skill(skill) if skill.disclosure_level < INSTRUCTIONS else skill
67-
for skill in resolved
68-
]
62+
return load_skills(crew.skills, activate=False) or None
6963

7064

7165
def setup_agents(

lib/crewai/src/crewai/events/types/skill_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ class SkillUsedEvent(SkillEvent):
6666
"""Event emitted when an agent uses a skill during task execution.
6767
6868
Discovery/load/activation events describe setup. This one is the runtime
69-
signal: it fires each time a skill's context is injected into an agent's
70-
prompt for a task, so traces can attribute skill usage to an agent and task.
69+
signal: it fires when a metadata skill is selected or an always-on skill's
70+
context is injected, so traces can attribute usage to an agent and task.
7171
"""
7272

7373
type: Literal["skill_used"] = "skill_used"

0 commit comments

Comments
 (0)