From f797bc70d9e7b3c7d92be29853eaebe32bc582cf Mon Sep 17 00:00:00 2001 From: Containerized Agent Date: Wed, 4 Feb 2026 23:19:37 +0000 Subject: [PATCH 1/9] Add skills support design document This design proposes adding a skills parameter to the Agent class for loading and managing AgentSkills.io compatible skills. Key features: - Simple API: Agent(skills='./skills') - Dynamic skill management between invocations - Active skill tracking - allowed_tools enforcement via hooks - Session manager integration Addresses: https://github.com/strands-agents/sdk-python/issues/1181 --- designs/0001-skills-support.md | 383 +++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 designs/0001-skills-support.md diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md new file mode 100644 index 00000000..464f3982 --- /dev/null +++ b/designs/0001-skills-support.md @@ -0,0 +1,383 @@ +# Skills Support + +**Status**: Proposed + +**Date**: 2025-02-04 + +**Issue**: https://github.com/strands-agents/sdk-python/issues/1181 + +## Context + +### What is the issue? + +Users want to load specialized instruction packages (skills) for their agents without managing complex prompt engineering. Skills are reusable packages of instructions that teach agents how to perform specialized tasks, following the [AgentSkills.io](https://agentskills.io) specification developed by Anthropic. + +### What task are you trying to accomplish? + +- Load relevant knowledge/instruction files depending on specific tasks +- Reuse agents for different tasks by adding/removing skills +- Organize complex agent instructions into shareable packages +- Integrate with the existing skills ecosystem (Anthropic skills, AgentSkills.io) + +### What makes it difficult today? + +Currently, users must: +- Manually read SKILL.md files and inject into system prompts +- Build their own skill management logic +- Use the `skills` tool from strands-tools (runtime only, no static configuration) + +### Who experiences this problem? + +- Developers building agents that need specialized behaviors +- Teams sharing agent capabilities across projects +- Users of Anthropic's skills who want SDK integration + +### What Are Skills? + +Skills are self-contained packages consisting of: +- **SKILL.md file**: YAML frontmatter (metadata) + markdown body (instructions) +- **Optional resources**: Scripts, reference docs, templates in subdirectories + +**SKILL.md Format:** + +```markdown +--- +name: code-review +description: Reviews code for bugs, security vulnerabilities, and best practices +license: Apache-2.0 +allowed-tools: file_read, shell +--- + +# Code Review Instructions + +When reviewing code, follow these steps: +1. Security Analysis: Check for common vulnerabilities +2. Code Quality: Look for bugs and edge cases +3. Best Practices: Verify coding standards +... +``` + +**Directory Structure:** + +``` +skills/ +├── code-review/ +│ ├── SKILL.md +│ ├── scripts/ +│ └── references/ +└── documentation/ + └── SKILL.md +``` + +## Decision + +Add a `skills` parameter to the Agent class that accepts skill paths or Skill objects, with support for dynamic skill management and active skill tracking. + +### API Changes + +#### Agent Parameter + +```python +class Agent: + def __init__( + self, + # ... existing parameters ... + skills: str | Path | Sequence[str | Path | Skill] | None = None, + ): + """ + Args: + skills: Skills to make available to the agent. Can be: + - String/Path to a directory containing skill subdirectories + - Sequence of paths to individual skill directories + - Sequence of Skill objects + - Mix of the above + + Skills are evaluated at each invocation, so changes to this + property take effect on the next agent call. + """ + + @property + def skills(self) -> list[Skill] | None: + """Currently configured skills. Mutable - changes apply to next invocation.""" + + @skills.setter + def skills(self, value: str | Path | Sequence[str | Path | Skill] | None) -> None: + """Set skills. Accepts same types as __init__ parameter.""" + + @property + def active_skill(self) -> Skill | None: + """The skill currently being used, if any.""" +``` + +#### Skill Class + +```python +@dataclass +class Skill: + """A skill that provides specialized instructions to an agent.""" + name: str + description: str + instructions: str = "" + path: Path | None = None + allowed_tools: list[str] | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_path(cls, skill_path: str | Path) -> "Skill": + """Load a skill from a directory or SKILL.md file.""" +``` + +#### Loader Functions + +```python +def load_skills(skills_dir: str | Path) -> list[Skill]: + """Load all skills from a directory.""" + +def load_skill(skill_path: str | Path) -> Skill: + """Load a single skill from a directory or SKILL.md file.""" +``` + +#### Module Exports + +```python +# strands/__init__.py +from strands.skills import Skill + +# strands/skills/__init__.py +from strands.skills.skill import Skill +from strands.skills.loader import load_skills, load_skill +from strands.skills.errors import SkillLoadError + +__all__ = ["Skill", "load_skills", "load_skill", "SkillLoadError"] +``` + +### How It Integrates + +**System Prompt Composition:** + +When `skills` is set, skill metadata is appended to the system prompt: + +``` +[User's system_prompt] + +## Available Skills + +You have access to specialized skills that provide detailed instructions. +When a task matches a skill's description, read its full instructions. + +- **code-review**: Reviews code for bugs, security vulnerabilities... + Location: /path/to/skills/code-review/SKILL.md + +- **documentation**: Generates clear, comprehensive documentation + Location: /path/to/skills/documentation/SKILL.md +``` + +**Dynamic Evaluation:** + +Skills are processed at each invocation, not at init. This allows: +- Changing skills between calls +- Adding/removing skills programmatically +- Session-based skill management + +**allowed_tools Enforcement:** + +When a skill with `allowed_tools` is active, tool calls are filtered via `BeforeToolCallEvent`: + +```python +# Internal hook enforces tool restrictions +class SkillToolEnforcer(HookProvider): + def check_tool_allowed(self, event: BeforeToolCallEvent): + if active_skill and active_skill.allowed_tools: + if tool_name not in active_skill.allowed_tools: + event.cancel_tool = f"Tool '{tool_name}' not allowed by skill" +``` + +**Session Manager Integration:** + +Skills state persists with SessionManager: + +```python +# Session stores: +{ + "skills": { + "configured": ["./skills/code-review", "./skills/docs"], + "active_skill_name": "code-review" + } +} +``` + +### Relationship to `skills` Tool + +| SDK `skills` param | `skills` tool | +|-------------------|---------------| +| Static config at init/runtime | Dynamic discovery by agent | +| Metadata in system prompt | Full progressive disclosure | +| Developer controls which skills | Agent decides when to activate | + +They complement each other - use SDK param for "always available" skills, use tool for "discover as needed". + +## Developer Experience + +### Basic Usage + +```python +from strands import Agent +from strands_tools import file_read + +# Point to skills directory - that's it +agent = Agent( + skills="./skills", + tools=[file_read] +) + +result = agent("Review my code for security issues") +print(f"Used skill: {agent.active_skill.name if agent.active_skill else 'none'}") +``` + +### Dynamic Skill Management + +```python +agent = Agent(tools=[file_read]) + +# No skills initially +agent("Hello") + +# Add skills dynamically +agent.skills = "./skills" +agent("Review my code") + +# Switch skills +agent.skills = ["./skills/documentation"] +agent("Write docs for this API") + +# Clear skills +agent.skills = None +``` + +### With Custom System Prompt + +```python +agent = Agent( + system_prompt="You are a senior engineer at Acme Corp.", + skills="./company-skills", + tools=[file_read, shell] +) +``` + +### Tool Restrictions + +```python +# SKILL.md with: allowed-tools: file_read + +agent = Agent( + skills=["./skills/safe-analyzer"], + tools=[file_read, shell, http_request] # shell, http blocked when skill active +) +``` + +### Programmatic Skills + +```python +from strands import Agent, Skill + +review_skill = Skill( + name="quick-review", + description="Quick code review focusing on obvious issues", + instructions="# Guidelines\n\n1. Focus on bugs\n2. Check security...", + allowed_tools=["file_read"] +) + +agent = Agent(skills=[review_skill]) +``` + +### Session Persistence + +```python +from strands.session import FileSessionManager + +agent = Agent( + skills="./skills", + session_manager=FileSessionManager("./sessions"), + session_id="project-alpha" +) + +# Skills config persists across sessions +``` + +### Active Skill in Hooks + +```python +class SkillAnalytics(HookProvider): + def register_hooks(self, registry): + registry.add_callback(AfterInvocationEvent, self.track) + + def track(self, event): + if event.agent.active_skill: + print(f"Used skill: {event.agent.active_skill.name}") +``` + +## Alternatives Considered + +### 1. Skills as a Separate Package + +**Approach**: Keep skills entirely in `strands-tools` or a separate `strands-skills` package. + +**Why not chosen**: +- Less discoverable +- Requires additional dependency +- Doesn't integrate with Agent lifecycle (session, hooks) + +### 2. Deep Integration with Skill Modes + +**Approach**: Add `skill_mode` parameter with options like "inject", "tool", "agent" for different skill activation patterns. + +**Why not chosen**: +- Adds complexity without clear benefit +- Single mode (system prompt injection) covers most cases +- Users can build custom modes using hooks if needed + +### 3. SkillProvider Interface + +**Approach**: Create a `SkillProvider` protocol similar to `ToolProvider`. + +**Why not chosen**: +- Over-engineering for the use case +- Skills are simpler than tools (just data, no execution) +- List of Skill objects is sufficient + +### 4. Skill-Specific Hooks + +**Approach**: Add `SkillActivatedEvent`, `SkillDeactivatedEvent`, etc. + +**Why not chosen**: +- Existing hooks + `active_skill` property provide same capability +- Follows decision record: "Hooks as Low-Level Primitives" + +## Consequences + +### What becomes easier + +- Loading and using skills from directories +- Sharing skills across agents and projects +- Tracking which skill is being used +- Restricting tools when skills are active +- Persisting skill state across sessions + +### What becomes more difficult + +- Nothing significant - the feature is additive + +### Future Extensions + +The design allows for future additions: +- Remote skill registries +- Skill versioning +- Multiple active skills +- Custom prompt templates + +## Willingness to Implement + +Yes, with guidance on: +- Exact placement of skills processing in the event loop +- Session manager schema changes +- Test coverage expectations From 59e474636f3f31eeb296db7277bb2442dd75f4dd Mon Sep 17 00:00:00 2001 From: Containerized Agent Date: Wed, 4 Feb 2026 23:26:34 +0000 Subject: [PATCH 2/9] Rewrite skills design doc with proper voice and tone - Follow documentation style guide (collaborative 'we', active voice) - Teach the concept before diving into API details - Show the problem first, then the solution - Use concrete examples throughout - Remove unnecessary complexity - Clearer structure following the design template --- designs/0001-skills-support.md | 429 +++++++++++++++++---------------- 1 file changed, 216 insertions(+), 213 deletions(-) diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md index 464f3982..50d87938 100644 --- a/designs/0001-skills-support.md +++ b/designs/0001-skills-support.md @@ -8,213 +8,240 @@ ## Context -### What is the issue? +### The Problem -Users want to load specialized instruction packages (skills) for their agents without managing complex prompt engineering. Skills are reusable packages of instructions that teach agents how to perform specialized tasks, following the [AgentSkills.io](https://agentskills.io) specification developed by Anthropic. +Imagine you're building an agent that reviews code. You've carefully crafted instructions covering security analysis, best practices, and common pitfalls. Now you want to reuse those instructions across multiple agents, share them with your team, or swap them out depending on the task. -### What task are you trying to accomplish? +Today, you'd need to: -- Load relevant knowledge/instruction files depending on specific tasks -- Reuse agents for different tasks by adding/removing skills -- Organize complex agent instructions into shareable packages -- Integrate with the existing skills ecosystem (Anthropic skills, AgentSkills.io) +1. Manually read instruction files and concatenate them into your system prompt +2. Build your own logic to manage which instructions are active +3. Handle the plumbing of loading, parsing, and injecting skill content -### What makes it difficult today? - -Currently, users must: -- Manually read SKILL.md files and inject into system prompts -- Build their own skill management logic -- Use the `skills` tool from strands-tools (runtime only, no static configuration) - -### Who experiences this problem? - -- Developers building agents that need specialized behaviors -- Teams sharing agent capabilities across projects -- Users of Anthropic's skills who want SDK integration +This is exactly the kind of repetitive work that should be handled by the SDK. ### What Are Skills? -Skills are self-contained packages consisting of: -- **SKILL.md file**: YAML frontmatter (metadata) + markdown body (instructions) -- **Optional resources**: Scripts, reference docs, templates in subdirectories - -**SKILL.md Format:** +Skills are reusable instruction packages that follow the [AgentSkills.io](https://agentskills.io) specification—an open standard developed by Anthropic. A skill is simply a folder containing a `SKILL.md` file with metadata and instructions: ```markdown --- name: code-review description: Reviews code for bugs, security vulnerabilities, and best practices -license: Apache-2.0 allowed-tools: file_read, shell --- # Code Review Instructions When reviewing code, follow these steps: -1. Security Analysis: Check for common vulnerabilities -2. Code Quality: Look for bugs and edge cases -3. Best Practices: Verify coding standards + +1. **Security Analysis**: Check for SQL injection, XSS, and auth issues +2. **Code Quality**: Look for bugs, edge cases, and logic errors +3. **Best Practices**: Verify coding standards and patterns + +## Examples ... ``` -**Directory Structure:** +Skills can also include supporting resources like scripts and reference docs: ``` skills/ ├── code-review/ │ ├── SKILL.md │ ├── scripts/ +│ │ └── analyze.py │ └── references/ +│ └── security-checklist.md └── documentation/ └── SKILL.md ``` -## Decision +### Who Needs This? -Add a `skills` parameter to the Agent class that accepts skill paths or Skill objects, with support for dynamic skill management and active skill tracking. +- **Developers** building agents that need specialized behaviors for different tasks +- **Teams** sharing agent capabilities across projects +- **Anyone** using Anthropic's skills or the AgentSkills.io ecosystem -### API Changes +## Decision -#### Agent Parameter +We're adding a `skills` parameter to the Agent class. Point it at a directory, and your agent gains access to those skills. ```python -class Agent: - def __init__( - self, - # ... existing parameters ... - skills: str | Path | Sequence[str | Path | Skill] | None = None, - ): - """ - Args: - skills: Skills to make available to the agent. Can be: - - String/Path to a directory containing skill subdirectories - - Sequence of paths to individual skill directories - - Sequence of Skill objects - - Mix of the above - - Skills are evaluated at each invocation, so changes to this - property take effect on the next agent call. - """ - - @property - def skills(self) -> list[Skill] | None: - """Currently configured skills. Mutable - changes apply to next invocation.""" - - @skills.setter - def skills(self, value: str | Path | Sequence[str | Path | Skill] | None) -> None: - """Set skills. Accepts same types as __init__ parameter.""" - - @property - def active_skill(self) -> Skill | None: - """The skill currently being used, if any.""" +from strands import Agent + +agent = Agent( + skills="./skills", + tools=[file_read] +) ``` -#### Skill Class +That's the happy path. For more control, you can pass specific skill paths, `Skill` objects, or mix them: ```python -@dataclass -class Skill: - """A skill that provides specialized instructions to an agent.""" - name: str - description: str - instructions: str = "" - path: Path | None = None - allowed_tools: list[str] | None = None - metadata: dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_path(cls, skill_path: str | Path) -> "Skill": - """Load a skill from a directory or SKILL.md file.""" +agent = Agent( + skills=[ + "./skills/code-review", + "./skills/documentation", + my_custom_skill, + ] +) ``` -#### Loader Functions +### How It Works -```python -def load_skills(skills_dir: str | Path) -> list[Skill]: - """Load all skills from a directory.""" +When you set `skills`, the SDK: + +1. Loads skill metadata (name, description, location) from each `SKILL.md` +2. Appends this metadata to your system prompt +3. Re-evaluates skills on each invocation, so you can change them between calls + +The agent sees something like this in its system prompt: + +``` +## Available Skills + +You have access to specialized skills. When a task matches a skill's +description, read its full instructions from the location shown. + +- **code-review**: Reviews code for bugs, security vulnerabilities... + Location: /path/to/skills/code-review/SKILL.md -def load_skill(skill_path: str | Path) -> Skill: - """Load a single skill from a directory or SKILL.md file.""" +- **documentation**: Generates clear, comprehensive documentation + Location: /path/to/skills/documentation/SKILL.md ``` -#### Module Exports +The agent then uses `file_read` (or similar) to load full instructions when it decides a skill applies. This is progressive disclosure—metadata upfront, full content on demand. + +### Dynamic Skill Management + +Skills aren't baked in at init time. You can change them between invocations: ```python -# strands/__init__.py -from strands.skills import Skill +agent = Agent(tools=[file_read]) -# strands/skills/__init__.py -from strands.skills.skill import Skill -from strands.skills.loader import load_skills, load_skill -from strands.skills.errors import SkillLoadError +# Start with no skills +agent("Hello!") + +# Add skills for a code task +agent.skills = "./skills" +agent("Review this function for security issues") -__all__ = ["Skill", "load_skills", "load_skill", "SkillLoadError"] +# Switch to different skills +agent.skills = ["./skills/documentation"] +agent("Write API docs for this module") + +# Clear skills entirely +agent.skills = None +agent("What's the weather?") ``` -### How It Integrates +### Tracking the Active Skill -**System Prompt Composition:** +The SDK tracks which skill the agent is currently using via `agent.active_skill`: -When `skills` is set, skill metadata is appended to the system prompt: +```python +result = agent("Review my authentication code") +if agent.active_skill: + print(f"Agent used: {agent.active_skill.name}") ``` -[User's system_prompt] -## Available Skills +This is useful for analytics, conditional logic, and session persistence. The active skill is detected when the agent reads a `SKILL.md` file during invocation. -You have access to specialized skills that provide detailed instructions. -When a task matches a skill's description, read its full instructions. +### Tool Restrictions with `allowed_tools` -- **code-review**: Reviews code for bugs, security vulnerabilities... - Location: /path/to/skills/code-review/SKILL.md +Skills can specify which tools they're allowed to use: -- **documentation**: Generates clear, comprehensive documentation - Location: /path/to/skills/documentation/SKILL.md +```yaml +--- +name: safe-analyzer +description: Analyzes files without executing code +allowed-tools: file_read +--- ``` -**Dynamic Evaluation:** +When this skill is active, the SDK blocks calls to tools not in the list. If the agent tries to use `shell`, it receives an error message explaining the restriction. This enforcement happens via the existing `BeforeToolCallEvent` hook. -Skills are processed at each invocation, not at init. This allows: -- Changing skills between calls -- Adding/removing skills programmatically -- Session-based skill management +### Session Persistence -**allowed_tools Enforcement:** +When you use a SessionManager, skill configuration persists across sessions: -When a skill with `allowed_tools` is active, tool calls are filtered via `BeforeToolCallEvent`: +```python +agent = Agent( + skills="./skills", + session_manager=FileSessionManager("./sessions"), + session_id="project-alpha" +) +``` + +The session stores which skills are configured and which skill was last active. When you restore the session, skills are reloaded from their paths. + +### API Surface + +**Agent changes:** ```python -# Internal hook enforces tool restrictions -class SkillToolEnforcer(HookProvider): - def check_tool_allowed(self, event: BeforeToolCallEvent): - if active_skill and active_skill.allowed_tools: - if tool_name not in active_skill.allowed_tools: - event.cancel_tool = f"Tool '{tool_name}' not allowed by skill" +class Agent: + def __init__( + self, + # ... existing parameters ... + skills: str | Path | Sequence[str | Path | Skill] | None = None, + ): ... + + @property + def skills(self) -> list[Skill] | None: ... + + @skills.setter + def skills(self, value: str | Path | Sequence[str | Path | Skill] | None): ... + + @property + def active_skill(self) -> Skill | None: ... ``` -**Session Manager Integration:** +**New Skill class:** -Skills state persists with SessionManager: +```python +@dataclass +class Skill: + name: str + description: str + instructions: str = "" + path: Path | None = None + allowed_tools: list[str] | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_path(cls, skill_path: str | Path) -> "Skill": ... +``` + +**Helper functions:** + +```python +def load_skills(skills_dir: str | Path) -> list[Skill]: ... +def load_skill(skill_path: str | Path) -> Skill: ... +``` + +**Module exports:** ```python -# Session stores: -{ - "skills": { - "configured": ["./skills/code-review", "./skills/docs"], - "active_skill_name": "code-review" - } -} +# strands/__init__.py exports Skill +# strands/skills/__init__.py exports Skill, load_skills, load_skill, SkillLoadError ``` -### Relationship to `skills` Tool +### Relationship to the `skills` Tool + +The `skills` tool in strands-tools handles runtime skill discovery—the agent decides when to look for and activate skills. The SDK `skills` parameter handles static configuration—you decide which skills are available. -| SDK `skills` param | `skills` tool | -|-------------------|---------------| -| Static config at init/runtime | Dynamic discovery by agent | +They complement each other: + +| SDK `skills` parameter | `skills` tool | +|------------------------|---------------| +| You control which skills | Agent discovers skills | +| Configured at init or between calls | Activated during invocation | | Metadata in system prompt | Full progressive disclosure | -| Developer controls which skills | Agent decides when to activate | -They complement each other - use SDK param for "always available" skills, use tool for "discover as needed". +Use the SDK parameter for "always available" skills. Use the tool for "discover as needed" scenarios. ## Developer Experience @@ -224,58 +251,29 @@ They complement each other - use SDK param for "always available" skills, use to from strands import Agent from strands_tools import file_read -# Point to skills directory - that's it agent = Agent( skills="./skills", tools=[file_read] ) result = agent("Review my code for security issues") -print(f"Used skill: {agent.active_skill.name if agent.active_skill else 'none'}") -``` - -### Dynamic Skill Management - -```python -agent = Agent(tools=[file_read]) - -# No skills initially -agent("Hello") - -# Add skills dynamically -agent.skills = "./skills" -agent("Review my code") - -# Switch skills -agent.skills = ["./skills/documentation"] -agent("Write docs for this API") - -# Clear skills -agent.skills = None ``` -### With Custom System Prompt +### Combining with a Custom System Prompt ```python agent = Agent( - system_prompt="You are a senior engineer at Acme Corp.", + system_prompt="You are a senior engineer at Acme Corp. Be thorough.", skills="./company-skills", tools=[file_read, shell] ) ``` -### Tool Restrictions +Your system prompt comes first, then the skills metadata is appended. -```python -# SKILL.md with: allowed-tools: file_read +### Creating Skills Programmatically -agent = Agent( - skills=["./skills/safe-analyzer"], - tools=[file_read, shell, http_request] # shell, http blocked when skill active -) -``` - -### Programmatic Skills +You don't need `SKILL.md` files. Define skills in code: ```python from strands import Agent, Skill @@ -283,101 +281,106 @@ from strands import Agent, Skill review_skill = Skill( name="quick-review", description="Quick code review focusing on obvious issues", - instructions="# Guidelines\n\n1. Focus on bugs\n2. Check security...", + instructions=""" +# Quick Review Guidelines + +Focus on: +1. Obvious bugs and typos +2. Missing error handling +3. Security red flags + +Skip style nitpicks and optimization suggestions. +""", allowed_tools=["file_read"] ) agent = Agent(skills=[review_skill]) ``` -### Session Persistence +### Filtering Skills ```python -from strands.session import FileSessionManager +from strands.skills import load_skills -agent = Agent( - skills="./skills", - session_manager=FileSessionManager("./sessions"), - session_id="project-alpha" -) +all_skills = load_skills("./skills") + +# Only allow specific skills +safe_skills = [s for s in all_skills if s.name in ["docs", "summarizer"]] -# Skills config persists across sessions +# Or filter out skills that can execute code +safe_skills = [s for s in all_skills + if not s.allowed_tools or "shell" not in s.allowed_tools] + +agent = Agent(skills=safe_skills) ``` -### Active Skill in Hooks +### Using Active Skill in Hooks ```python +from strands.hooks import HookProvider, AfterInvocationEvent + class SkillAnalytics(HookProvider): + def __init__(self): + self.usage = {} + def register_hooks(self, registry): registry.add_callback(AfterInvocationEvent, self.track) def track(self, event): - if event.agent.active_skill: - print(f"Used skill: {event.agent.active_skill.name}") + skill = event.agent.active_skill + if skill: + self.usage[skill.name] = self.usage.get(skill.name, 0) + 1 + +analytics = SkillAnalytics() +agent = Agent(skills="./skills", hooks=[analytics]) ``` ## Alternatives Considered -### 1. Skills as a Separate Package - -**Approach**: Keep skills entirely in `strands-tools` or a separate `strands-skills` package. - -**Why not chosen**: -- Less discoverable -- Requires additional dependency -- Doesn't integrate with Agent lifecycle (session, hooks) +### Skills as a Separate Package -### 2. Deep Integration with Skill Modes +We considered keeping skills entirely in strands-tools or a separate package. This would be less discoverable and wouldn't integrate with the agent lifecycle (sessions, hooks). Skills are simple enough that they belong in the core SDK. -**Approach**: Add `skill_mode` parameter with options like "inject", "tool", "agent" for different skill activation patterns. +### Skill Modes (inject/tool/agent) -**Why not chosen**: -- Adds complexity without clear benefit -- Single mode (system prompt injection) covers most cases -- Users can build custom modes using hooks if needed +We considered adding a `skill_mode` parameter to control how skills are activated—injected into prompts, loaded via tool calls, or run in sub-agents. This adds complexity without clear benefit. The single approach (metadata in prompt, full content on demand) covers most cases. Users who need different patterns can build them using hooks. -### 3. SkillProvider Interface +### SkillProvider Interface -**Approach**: Create a `SkillProvider` protocol similar to `ToolProvider`. +We considered a `SkillProvider` protocol similar to `ToolProvider`. Skills are simpler than tools—they're just data, not executable code. A list of `Skill` objects is sufficient. -**Why not chosen**: -- Over-engineering for the use case -- Skills are simpler than tools (just data, no execution) -- List of Skill objects is sufficient +### Skill-Specific Hooks -### 4. Skill-Specific Hooks - -**Approach**: Add `SkillActivatedEvent`, `SkillDeactivatedEvent`, etc. - -**Why not chosen**: -- Existing hooks + `active_skill` property provide same capability -- Follows decision record: "Hooks as Low-Level Primitives" +We considered adding `SkillActivatedEvent` and similar hooks. The existing hooks combined with `agent.active_skill` provide the same capability without new abstractions. ## Consequences -### What becomes easier +### What Becomes Easier -- Loading and using skills from directories +- Loading skills from directories with a single parameter - Sharing skills across agents and projects -- Tracking which skill is being used -- Restricting tools when skills are active +- Changing skills dynamically between invocations +- Tracking which skill the agent is using +- Restricting tools when specific skills are active - Persisting skill state across sessions -### What becomes more difficult +### What Becomes More Difficult -- Nothing significant - the feature is additive +Nothing significant. The feature is additive. ### Future Extensions -The design allows for future additions: +This design allows for: + - Remote skill registries - Skill versioning -- Multiple active skills -- Custom prompt templates +- Multiple simultaneous active skills +- Custom prompt templates for skill metadata ## Willingness to Implement Yes, with guidance on: -- Exact placement of skills processing in the event loop -- Session manager schema changes + +- Exact placement of skills processing in the invocation flow +- Session manager schema for skill state - Test coverage expectations From 4e746ccc3499b666912e8c8459fa6feecf97ecdb Mon Sep 17 00:00:00 2001 From: Containerized Agent Date: Thu, 5 Feb 2026 04:58:27 +0000 Subject: [PATCH 3/9] Replace hook-based tool enforcement with pre-filtering approach Key changes to the Skills SDK design: - Remove BeforeToolCallEvent hook approach for tool restrictions - Add pre-filtering mechanism: filter tools BEFORE sending to model - Model only sees tools it's allowed to use (cleaner, no wasted tokens) - Add detailed implementation section showing the filtering flow - Add 'Hook-Based Tool Enforcement' to Alternatives Considered section explaining why pre-filtering is the better approach The insight: why show the model tools it can't use? Pre-filtering is more efficient and requires no error recovery logic. --- designs/0001-skills-support.md | 75 +++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md index 50d87938..13a663f0 100644 --- a/designs/0001-skills-support.md +++ b/designs/0001-skills-support.md @@ -161,7 +161,70 @@ allowed-tools: file_read --- ``` -When this skill is active, the SDK blocks calls to tools not in the list. If the agent tries to use `shell`, it receives an error message explaining the restriction. This enforcement happens via the existing `BeforeToolCallEvent` hook. +When this skill is active, the SDK **filters tools before sending them to the model**. The model only sees tools it's allowed to use—it never even knows about tools that are restricted. + +This is implemented via **pre-filtering at model invocation time**: + +1. When `agent.tool_registry.get_all_tool_specs()` is called (in `event_loop.py`) +2. If an active skill has `allowed_tools`, the returned specs are filtered to only include those tools +3. The model receives a reduced tool list and can only call what it sees + +This approach is cleaner than post-hoc validation because: +- **No wasted tokens**: The model doesn't waste context or reasoning on tools it can't use +- **No confusing errors**: The model never tries to call a tool and gets rejected +- **Simpler mental model**: Tools you can see are tools you can use + +#### Implementation: Tool Pre-Filtering + +The filtering happens in the event loop, where tools are prepared for model invocation. Here's the flow: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Agent Invocation │ +├─────────────────────────────────────────────────────────────────────────┤ +│ 1. User calls agent("Review this code") │ +│ │ +│ 2. Event loop prepares for model call │ +│ └─ get_tool_specs_for_invocation() checks: │ +│ ├─ Is there an active skill? │ +│ ├─ Does it have allowed_tools? │ +│ └─ If yes, filter tool_specs to only allowed tools │ +│ │ +│ 3. Model receives filtered tool list │ +│ └─ Only sees: file_read (not shell, not python_repl, etc.) │ +│ │ +│ 4. Model can only call tools it sees │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +The key modification is in the event loop's model invocation flow. Instead of: + +```python +# Current: all tools sent to model +tool_specs = agent.tool_registry.get_all_tool_specs() +``` + +It becomes: + +```python +# With skills: filtered tools sent to model +tool_specs = agent.get_tool_specs_for_invocation() + +# Inside get_tool_specs_for_invocation(): +def get_tool_specs_for_invocation(self) -> list[ToolSpec]: + all_specs = self.tool_registry.get_all_tool_specs() + + if self.active_skill and self.active_skill.allowed_tools: + allowed = set(self.active_skill.allowed_tools) + return [spec for spec in all_specs if spec["name"] in allowed] + + return all_specs +``` + +This approach: +- **Keeps the model honest**: It physically can't call restricted tools +- **Reduces context usage**: Fewer tool definitions = more room for conversation +- **Requires no error handling**: No need to handle "tool not allowed" errors ### Session Persistence @@ -353,6 +416,16 @@ We considered a `SkillProvider` protocol similar to `ToolProvider`. Skills are s We considered adding `SkillActivatedEvent` and similar hooks. The existing hooks combined with `agent.active_skill` provide the same capability without new abstractions. +### Hook-Based Tool Enforcement + +We considered using a `BeforeToolCallEvent` hook to validate tool calls after the model makes them—rejecting calls to tools not in the skill's `allowed_tools` list. This was rejected for several reasons: + +1. **Wasteful**: The model spends tokens reasoning about tools it can't use, then gets an error +2. **Confusing UX**: The model sees tools, tries to use them, and fails—this creates a poor experience +3. **Complex error handling**: We'd need to handle rejected tool calls gracefully and help the model recover + +**Pre-filtering is the better approach**: By filtering tools *before* sending them to the model, the model only sees what it can use. This is cleaner, more efficient, and requires no error recovery logic. + ## Consequences ### What Becomes Easier From d396e1039d0eec538cd0d7e5c49aa2e59f781e0a Mon Sep 17 00:00:00 2001 From: Containerized Agent Date: Thu, 5 Feb 2026 05:21:41 +0000 Subject: [PATCH 4/9] Trim tool filtering explanation to match document style Less is more - the original doc is concise and matter-of-fact. Removed verbose explanations, ASCII diagrams, and repetition. --- designs/0001-skills-support.md | 73 +--------------------------------- 1 file changed, 2 insertions(+), 71 deletions(-) diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md index 13a663f0..d59af893 100644 --- a/designs/0001-skills-support.md +++ b/designs/0001-skills-support.md @@ -161,70 +161,7 @@ allowed-tools: file_read --- ``` -When this skill is active, the SDK **filters tools before sending them to the model**. The model only sees tools it's allowed to use—it never even knows about tools that are restricted. - -This is implemented via **pre-filtering at model invocation time**: - -1. When `agent.tool_registry.get_all_tool_specs()` is called (in `event_loop.py`) -2. If an active skill has `allowed_tools`, the returned specs are filtered to only include those tools -3. The model receives a reduced tool list and can only call what it sees - -This approach is cleaner than post-hoc validation because: -- **No wasted tokens**: The model doesn't waste context or reasoning on tools it can't use -- **No confusing errors**: The model never tries to call a tool and gets rejected -- **Simpler mental model**: Tools you can see are tools you can use - -#### Implementation: Tool Pre-Filtering - -The filtering happens in the event loop, where tools are prepared for model invocation. Here's the flow: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Agent Invocation │ -├─────────────────────────────────────────────────────────────────────────┤ -│ 1. User calls agent("Review this code") │ -│ │ -│ 2. Event loop prepares for model call │ -│ └─ get_tool_specs_for_invocation() checks: │ -│ ├─ Is there an active skill? │ -│ ├─ Does it have allowed_tools? │ -│ └─ If yes, filter tool_specs to only allowed tools │ -│ │ -│ 3. Model receives filtered tool list │ -│ └─ Only sees: file_read (not shell, not python_repl, etc.) │ -│ │ -│ 4. Model can only call tools it sees │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -The key modification is in the event loop's model invocation flow. Instead of: - -```python -# Current: all tools sent to model -tool_specs = agent.tool_registry.get_all_tool_specs() -``` - -It becomes: - -```python -# With skills: filtered tools sent to model -tool_specs = agent.get_tool_specs_for_invocation() - -# Inside get_tool_specs_for_invocation(): -def get_tool_specs_for_invocation(self) -> list[ToolSpec]: - all_specs = self.tool_registry.get_all_tool_specs() - - if self.active_skill and self.active_skill.allowed_tools: - allowed = set(self.active_skill.allowed_tools) - return [spec for spec in all_specs if spec["name"] in allowed] - - return all_specs -``` - -This approach: -- **Keeps the model honest**: It physically can't call restricted tools -- **Reduces context usage**: Fewer tool definitions = more room for conversation -- **Requires no error handling**: No need to handle "tool not allowed" errors +When this skill is active, the SDK filters tool specs before sending them to the model. The model only sees tools in the `allowed_tools` list—it can't call what it can't see. ### Session Persistence @@ -418,13 +355,7 @@ We considered adding `SkillActivatedEvent` and similar hooks. The existing hooks ### Hook-Based Tool Enforcement -We considered using a `BeforeToolCallEvent` hook to validate tool calls after the model makes them—rejecting calls to tools not in the skill's `allowed_tools` list. This was rejected for several reasons: - -1. **Wasteful**: The model spends tokens reasoning about tools it can't use, then gets an error -2. **Confusing UX**: The model sees tools, tries to use them, and fails—this creates a poor experience -3. **Complex error handling**: We'd need to handle rejected tool calls gracefully and help the model recover - -**Pre-filtering is the better approach**: By filtering tools *before* sending them to the model, the model only sees what it can use. This is cleaner, more efficient, and requires no error recovery logic. +We considered using `BeforeToolCallEvent` to reject calls to restricted tools. Pre-filtering is simpler—don't show the model tools it can't use. ## Consequences From f2eba936b6dc887148b2fe2086f5da3d6dc2fc3a Mon Sep 17 00:00:00 2001 From: Containerized Agent Date: Thu, 5 Feb 2026 05:49:59 +0000 Subject: [PATCH 5/9] Address API Bar Raising requirements and SDK tenets - Add docstrings to API signatures (properties, class methods) - Document error handling: SkillLoadError, when it's raised - Clarify active_skill detection mechanism - Add comment on allowed_tools=None meaning - Show proper import patterns in module exports - Reference DECISIONS.md re: why Skill doesn't extend HookProvider - Document behavior for non-existent tools in allowed_tools (warn, don't fail) --- designs/0001-skills-support.md | 58 +++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md index d59af893..4638f89f 100644 --- a/designs/0001-skills-support.md +++ b/designs/0001-skills-support.md @@ -147,7 +147,9 @@ if agent.active_skill: print(f"Agent used: {agent.active_skill.name}") ``` -This is useful for analytics, conditional logic, and session persistence. The active skill is detected when the agent reads a `SKILL.md` file during invocation. +**How detection works:** When the agent calls `file_read` on a path matching a configured skill's `SKILL.md`, that skill becomes active. The active skill resets to `None` at the start of each invocation. + +This is useful for analytics, conditional logic, and session persistence. ### Tool Restrictions with `allowed_tools` @@ -190,13 +192,19 @@ class Agent: ): ... @property - def skills(self) -> list[Skill] | None: ... + def skills(self) -> list[Skill] | None: + """Configured skills, or None if no skills are set.""" + ... @skills.setter - def skills(self, value: str | Path | Sequence[str | Path | Skill] | None): ... + def skills(self, value: str | Path | Sequence[str | Path | Skill] | None) -> None: + """Set skills from a directory path, list of paths, Skill objects, or None to clear.""" + ... @property - def active_skill(self) -> Skill | None: ... + def active_skill(self) -> Skill | None: + """The skill currently in use, or None. Set when the agent reads a SKILL.md file.""" + ... ``` **New Skill class:** @@ -208,27 +216,55 @@ class Skill: description: str instructions: str = "" path: Path | None = None - allowed_tools: list[str] | None = None + allowed_tools: list[str] | None = None # None means all tools allowed metadata: dict[str, Any] = field(default_factory=dict) @classmethod - def from_path(cls, skill_path: str | Path) -> "Skill": ... + def from_path(cls, skill_path: str | Path) -> "Skill": + """Load a skill from a directory containing SKILL.md. + + Raises: + SkillLoadError: If SKILL.md is missing or malformed. + """ + ... ``` **Helper functions:** ```python -def load_skills(skills_dir: str | Path) -> list[Skill]: ... -def load_skill(skill_path: str | Path) -> Skill: ... +def load_skills(skills_dir: str | Path) -> list[Skill]: + """Load all skills from subdirectories of skills_dir. + + Each subdirectory must contain a SKILL.md file. Subdirectories + without SKILL.md are silently skipped. + """ + ... + +def load_skill(skill_path: str | Path) -> Skill: + """Load a single skill from a directory. + + Raises: + SkillLoadError: If SKILL.md is missing or malformed. + """ + ... ``` **Module exports:** ```python -# strands/__init__.py exports Skill -# strands/skills/__init__.py exports Skill, load_skills, load_skill, SkillLoadError +# Top-level export for common usage +from strands import Agent, Skill + +# Submodule for helpers and errors +from strands.skills import load_skills, load_skill, SkillLoadError ``` +**Error handling:** + +- `SkillLoadError` raised when `SKILL.md` is missing or has invalid YAML frontmatter +- Invalid paths in `Agent(skills=...)` raise `SkillLoadError` at init time +- Skills with `allowed_tools` referencing non-existent tools log a warning but don't fail + ### Relationship to the `skills` Tool The `skills` tool in strands-tools handles runtime skill discovery—the agent decides when to look for and activate skills. The SDK `skills` parameter handles static configuration—you decide which skills are available. @@ -347,7 +383,7 @@ We considered adding a `skill_mode` parameter to control how skills are activate ### SkillProvider Interface -We considered a `SkillProvider` protocol similar to `ToolProvider`. Skills are simpler than tools—they're just data, not executable code. A list of `Skill` objects is sufficient. +We considered a `SkillProvider` protocol similar to `ToolProvider`, or having `Skill` extend `HookProvider`. Skills are simpler than tools or lifecycle integrations—they're just data (name, description, instructions). A plain dataclass is sufficient. Per [When Internal Interfaces Should Extend HookProvider](https://github.com/strands-agents/docs/blob/main/team/DECISIONS.md), skills don't need to respond to multiple lifecycle events, so a simple interface is appropriate. ### Skill-Specific Hooks From dca3c7a5036aa74e528483ed010c75d0e8a8a768 Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 13 Feb 2026 13:56:01 -0500 Subject: [PATCH 6/9] feat: update skills based on plugin design --- designs/0001-skills-support.md | 426 -------------------------------- designs/skills-support.md | 433 +++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+), 426 deletions(-) delete mode 100644 designs/0001-skills-support.md create mode 100644 designs/skills-support.md diff --git a/designs/0001-skills-support.md b/designs/0001-skills-support.md deleted file mode 100644 index 4638f89f..00000000 --- a/designs/0001-skills-support.md +++ /dev/null @@ -1,426 +0,0 @@ -# Skills Support - -**Status**: Proposed - -**Date**: 2025-02-04 - -**Issue**: https://github.com/strands-agents/sdk-python/issues/1181 - -## Context - -### The Problem - -Imagine you're building an agent that reviews code. You've carefully crafted instructions covering security analysis, best practices, and common pitfalls. Now you want to reuse those instructions across multiple agents, share them with your team, or swap them out depending on the task. - -Today, you'd need to: - -1. Manually read instruction files and concatenate them into your system prompt -2. Build your own logic to manage which instructions are active -3. Handle the plumbing of loading, parsing, and injecting skill content - -This is exactly the kind of repetitive work that should be handled by the SDK. - -### What Are Skills? - -Skills are reusable instruction packages that follow the [AgentSkills.io](https://agentskills.io) specification—an open standard developed by Anthropic. A skill is simply a folder containing a `SKILL.md` file with metadata and instructions: - -```markdown ---- -name: code-review -description: Reviews code for bugs, security vulnerabilities, and best practices -allowed-tools: file_read, shell ---- - -# Code Review Instructions - -When reviewing code, follow these steps: - -1. **Security Analysis**: Check for SQL injection, XSS, and auth issues -2. **Code Quality**: Look for bugs, edge cases, and logic errors -3. **Best Practices**: Verify coding standards and patterns - -## Examples -... -``` - -Skills can also include supporting resources like scripts and reference docs: - -``` -skills/ -├── code-review/ -│ ├── SKILL.md -│ ├── scripts/ -│ │ └── analyze.py -│ └── references/ -│ └── security-checklist.md -└── documentation/ - └── SKILL.md -``` - -### Who Needs This? - -- **Developers** building agents that need specialized behaviors for different tasks -- **Teams** sharing agent capabilities across projects -- **Anyone** using Anthropic's skills or the AgentSkills.io ecosystem - -## Decision - -We're adding a `skills` parameter to the Agent class. Point it at a directory, and your agent gains access to those skills. - -```python -from strands import Agent - -agent = Agent( - skills="./skills", - tools=[file_read] -) -``` - -That's the happy path. For more control, you can pass specific skill paths, `Skill` objects, or mix them: - -```python -agent = Agent( - skills=[ - "./skills/code-review", - "./skills/documentation", - my_custom_skill, - ] -) -``` - -### How It Works - -When you set `skills`, the SDK: - -1. Loads skill metadata (name, description, location) from each `SKILL.md` -2. Appends this metadata to your system prompt -3. Re-evaluates skills on each invocation, so you can change them between calls - -The agent sees something like this in its system prompt: - -``` -## Available Skills - -You have access to specialized skills. When a task matches a skill's -description, read its full instructions from the location shown. - -- **code-review**: Reviews code for bugs, security vulnerabilities... - Location: /path/to/skills/code-review/SKILL.md - -- **documentation**: Generates clear, comprehensive documentation - Location: /path/to/skills/documentation/SKILL.md -``` - -The agent then uses `file_read` (or similar) to load full instructions when it decides a skill applies. This is progressive disclosure—metadata upfront, full content on demand. - -### Dynamic Skill Management - -Skills aren't baked in at init time. You can change them between invocations: - -```python -agent = Agent(tools=[file_read]) - -# Start with no skills -agent("Hello!") - -# Add skills for a code task -agent.skills = "./skills" -agent("Review this function for security issues") - -# Switch to different skills -agent.skills = ["./skills/documentation"] -agent("Write API docs for this module") - -# Clear skills entirely -agent.skills = None -agent("What's the weather?") -``` - -### Tracking the Active Skill - -The SDK tracks which skill the agent is currently using via `agent.active_skill`: - -```python -result = agent("Review my authentication code") - -if agent.active_skill: - print(f"Agent used: {agent.active_skill.name}") -``` - -**How detection works:** When the agent calls `file_read` on a path matching a configured skill's `SKILL.md`, that skill becomes active. The active skill resets to `None` at the start of each invocation. - -This is useful for analytics, conditional logic, and session persistence. - -### Tool Restrictions with `allowed_tools` - -Skills can specify which tools they're allowed to use: - -```yaml ---- -name: safe-analyzer -description: Analyzes files without executing code -allowed-tools: file_read ---- -``` - -When this skill is active, the SDK filters tool specs before sending them to the model. The model only sees tools in the `allowed_tools` list—it can't call what it can't see. - -### Session Persistence - -When you use a SessionManager, skill configuration persists across sessions: - -```python -agent = Agent( - skills="./skills", - session_manager=FileSessionManager("./sessions"), - session_id="project-alpha" -) -``` - -The session stores which skills are configured and which skill was last active. When you restore the session, skills are reloaded from their paths. - -### API Surface - -**Agent changes:** - -```python -class Agent: - def __init__( - self, - # ... existing parameters ... - skills: str | Path | Sequence[str | Path | Skill] | None = None, - ): ... - - @property - def skills(self) -> list[Skill] | None: - """Configured skills, or None if no skills are set.""" - ... - - @skills.setter - def skills(self, value: str | Path | Sequence[str | Path | Skill] | None) -> None: - """Set skills from a directory path, list of paths, Skill objects, or None to clear.""" - ... - - @property - def active_skill(self) -> Skill | None: - """The skill currently in use, or None. Set when the agent reads a SKILL.md file.""" - ... -``` - -**New Skill class:** - -```python -@dataclass -class Skill: - name: str - description: str - instructions: str = "" - path: Path | None = None - allowed_tools: list[str] | None = None # None means all tools allowed - metadata: dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_path(cls, skill_path: str | Path) -> "Skill": - """Load a skill from a directory containing SKILL.md. - - Raises: - SkillLoadError: If SKILL.md is missing or malformed. - """ - ... -``` - -**Helper functions:** - -```python -def load_skills(skills_dir: str | Path) -> list[Skill]: - """Load all skills from subdirectories of skills_dir. - - Each subdirectory must contain a SKILL.md file. Subdirectories - without SKILL.md are silently skipped. - """ - ... - -def load_skill(skill_path: str | Path) -> Skill: - """Load a single skill from a directory. - - Raises: - SkillLoadError: If SKILL.md is missing or malformed. - """ - ... -``` - -**Module exports:** - -```python -# Top-level export for common usage -from strands import Agent, Skill - -# Submodule for helpers and errors -from strands.skills import load_skills, load_skill, SkillLoadError -``` - -**Error handling:** - -- `SkillLoadError` raised when `SKILL.md` is missing or has invalid YAML frontmatter -- Invalid paths in `Agent(skills=...)` raise `SkillLoadError` at init time -- Skills with `allowed_tools` referencing non-existent tools log a warning but don't fail - -### Relationship to the `skills` Tool - -The `skills` tool in strands-tools handles runtime skill discovery—the agent decides when to look for and activate skills. The SDK `skills` parameter handles static configuration—you decide which skills are available. - -They complement each other: - -| SDK `skills` parameter | `skills` tool | -|------------------------|---------------| -| You control which skills | Agent discovers skills | -| Configured at init or between calls | Activated during invocation | -| Metadata in system prompt | Full progressive disclosure | - -Use the SDK parameter for "always available" skills. Use the tool for "discover as needed" scenarios. - -## Developer Experience - -### Basic Usage - -```python -from strands import Agent -from strands_tools import file_read - -agent = Agent( - skills="./skills", - tools=[file_read] -) - -result = agent("Review my code for security issues") -``` - -### Combining with a Custom System Prompt - -```python -agent = Agent( - system_prompt="You are a senior engineer at Acme Corp. Be thorough.", - skills="./company-skills", - tools=[file_read, shell] -) -``` - -Your system prompt comes first, then the skills metadata is appended. - -### Creating Skills Programmatically - -You don't need `SKILL.md` files. Define skills in code: - -```python -from strands import Agent, Skill - -review_skill = Skill( - name="quick-review", - description="Quick code review focusing on obvious issues", - instructions=""" -# Quick Review Guidelines - -Focus on: -1. Obvious bugs and typos -2. Missing error handling -3. Security red flags - -Skip style nitpicks and optimization suggestions. -""", - allowed_tools=["file_read"] -) - -agent = Agent(skills=[review_skill]) -``` - -### Filtering Skills - -```python -from strands.skills import load_skills - -all_skills = load_skills("./skills") - -# Only allow specific skills -safe_skills = [s for s in all_skills if s.name in ["docs", "summarizer"]] - -# Or filter out skills that can execute code -safe_skills = [s for s in all_skills - if not s.allowed_tools or "shell" not in s.allowed_tools] - -agent = Agent(skills=safe_skills) -``` - -### Using Active Skill in Hooks - -```python -from strands.hooks import HookProvider, AfterInvocationEvent - -class SkillAnalytics(HookProvider): - def __init__(self): - self.usage = {} - - def register_hooks(self, registry): - registry.add_callback(AfterInvocationEvent, self.track) - - def track(self, event): - skill = event.agent.active_skill - if skill: - self.usage[skill.name] = self.usage.get(skill.name, 0) + 1 - -analytics = SkillAnalytics() -agent = Agent(skills="./skills", hooks=[analytics]) -``` - -## Alternatives Considered - -### Skills as a Separate Package - -We considered keeping skills entirely in strands-tools or a separate package. This would be less discoverable and wouldn't integrate with the agent lifecycle (sessions, hooks). Skills are simple enough that they belong in the core SDK. - -### Skill Modes (inject/tool/agent) - -We considered adding a `skill_mode` parameter to control how skills are activated—injected into prompts, loaded via tool calls, or run in sub-agents. This adds complexity without clear benefit. The single approach (metadata in prompt, full content on demand) covers most cases. Users who need different patterns can build them using hooks. - -### SkillProvider Interface - -We considered a `SkillProvider` protocol similar to `ToolProvider`, or having `Skill` extend `HookProvider`. Skills are simpler than tools or lifecycle integrations—they're just data (name, description, instructions). A plain dataclass is sufficient. Per [When Internal Interfaces Should Extend HookProvider](https://github.com/strands-agents/docs/blob/main/team/DECISIONS.md), skills don't need to respond to multiple lifecycle events, so a simple interface is appropriate. - -### Skill-Specific Hooks - -We considered adding `SkillActivatedEvent` and similar hooks. The existing hooks combined with `agent.active_skill` provide the same capability without new abstractions. - -### Hook-Based Tool Enforcement - -We considered using `BeforeToolCallEvent` to reject calls to restricted tools. Pre-filtering is simpler—don't show the model tools it can't use. - -## Consequences - -### What Becomes Easier - -- Loading skills from directories with a single parameter -- Sharing skills across agents and projects -- Changing skills dynamically between invocations -- Tracking which skill the agent is using -- Restricting tools when specific skills are active -- Persisting skill state across sessions - -### What Becomes More Difficult - -Nothing significant. The feature is additive. - -### Future Extensions - -This design allows for: - -- Remote skill registries -- Skill versioning -- Multiple simultaneous active skills -- Custom prompt templates for skill metadata - -## Willingness to Implement - -Yes, with guidance on: - -- Exact placement of skills processing in the invocation flow -- Session manager schema for skill state -- Test coverage expectations diff --git a/designs/skills-support.md b/designs/skills-support.md new file mode 100644 index 00000000..4344df48 --- /dev/null +++ b/designs/skills-support.md @@ -0,0 +1,433 @@ +# Skills Support + +**Status**: Proposed (Updated) + +**Date**: 2026-02-13 + +**Issue**: https://github.com/strands-agents/sdk-python/issues/1181 + +**Depends on**: [Plugins](https://github.com/strands-agents/docs/pull/530) + +## Context + +### The problem + +Imagine you're building an agent that reviews code. You've carefully crafted instructions covering security analysis, best practices, and common pitfalls. Now you want to reuse those instructions across multiple agents, share them with your team, or swap them out depending on the task. + +Today, you'd need to: + +1. Manually read instruction files and concatenate them into your system prompt +2. Build your own logic to manage which instructions are active +3. Handle the plumbing of loading, parsing, and injecting skill content + +This is exactly the kind of repetitive work that the SDK handles. + +### What are skills? + +Skills are reusable instruction packages that follow the [AgentSkills.io](https://agentskills.io) specification—an open standard developed by Anthropic. A skill is a folder containing a `SKILL.md` file with metadata and instructions: + +```markdown +--- +name: code-review +description: Reviews code for bugs, security vulnerabilities, and best practices +allowed-tools: file_read shell +--- + +# Code Review Instructions + +When reviewing code, follow these steps: + +1. **Security Analysis**: Check for SQL injection, XSS, and auth issues +2. **Code Quality**: Look for bugs, edge cases, and logic errors +3. **Best Practices**: Verify coding standards and patterns + +## Examples +... +``` + +Skills can also include supporting resources like scripts and reference docs: + +``` +skills/ +├── code-review/ +│ ├── SKILL.md +│ ├── scripts/ +│ │ └── analyze.py +│ └── references/ +│ └── security-checklist.md +└── documentation/ + └── SKILL.md +``` + +### Who needs this? + +- Developers building agents that need specialized behaviors for different tasks +- Teams sharing agent capabilities across projects +- Anyone using the AgentSkills.io ecosystem + +### Skills as a plugin + +The [Plugins proposal](https://github.com/strands-agents/docs/pull/530) introduces a `Plugin` protocol for high-level features that modify agent behavior across multiple primitives (system prompt, tools, hooks). Skills is a textbook plugin: it needs to modify the system prompt (inject skill metadata), manage tools (filter via `allowed_tools`, register a `skills` tool), and respond to lifecycle events (track active skill, update prompt before invocations). + +Rather than adding a `skills` parameter to the Agent constructor, skills is implemented as a `SkillsPlugin` that ships with the SDK. + +## Decision + +### Developer experience + +```python +from strands import Agent +from strands.plugins import SkillsPlugin + +agent = Agent( + plugins=[SkillsPlugin(skills=["./skills/code-review", "./skills/documentation"])] +) + +result = agent("Review my code for security issues") +``` + +The plugin auto-registers everything it needs during `init_plugin`: a `skills` tool, hooks for system prompt management, and hooks for tool filtering. You don't wire anything up manually. + +### How it works + +The `SkillsPlugin` implements the `Plugin` protocol. When `init_plugin` is called: + +1. Loads skill metadata (name, description, location) from each `SKILL.md` +2. Registers a `skills` tool on the agent with `activate` and `deactivate` actions +3. Registers a `BeforeInvocationEvent` hook that appends skill metadata to the system prompt +4. When a skill with `allowed_tools` is activated, optionally removes non-allowed tools from the agent (keeping them in memory for restoration on deactivate) + +The agent sees something like this in its system prompt: + +``` +## Available Skills + +You have access to specialized skills. When a task matches a skill's +description, use the skills tool to activate it and read its full instructions. + +- **code-review**: Reviews code for bugs, security vulnerabilities... +- **documentation**: Generates clear, comprehensive documentation... +``` + +The agent then uses the `skills` tool to activate a skill when it decides one applies. A skill stays active until the agent explicitly deactivates it. This is progressive disclosure—metadata upfront, full content on demand. + +### Plugin internals + +```python +class SkillsPlugin(Plugin): + name = "skills" + + def __init__(self, skills: list[str | Path | Skill]): + self._skills_config = skills + self._loaded_skills: list[Skill] = [] + self._active_skill: Skill | None = None + self._filtered_tools: list[Tool] | None = None + + async def init_plugin(self, agent: Agent): + self._loaded_skills = self._resolve_skills(self._skills_config) + # Tools and hooks are auto-registered via decorators + + @tool + def skills(self, action: str, skill_name: str) -> str: + """Activate or deactivate a skill. + + Args: + action: "activate" or "deactivate" + skill_name: Name of the skill + """ + if action == "activate": + skill = self._find_skill(skill_name) + self._active_skill = skill + if skill.allowed_tools: + self._apply_tool_filter(skill.allowed_tools) + return skill.instructions + elif action == "deactivate": + if self._filtered_tools is not None: + self._restore_filtered_tools() + self._active_skill = None + return f"Deactivated skill: {skill_name}" + + @hook + def _inject_skill_metadata(self, event: BeforeInvocationEvent): + """Append skill metadata to system prompt before each invocation.""" + ... +``` + +The `@tool` and `@hook` decorators inside the plugin class auto-register with the agent during `init_plugin`. This is the DX we want: declare what you need, the plugin protocol handles the wiring. + +### Skill sources + +The `skills` parameter accepts a list. Each entry can be: + +- A local filesystem path to a skill directory (containing `SKILL.md`) +- A local filesystem path to a parent directory (containing multiple skill subdirectories) +- A `Skill` object created programmatically + +When a path contains a `SKILL.md`, it's treated as a single skill. When it doesn't, it's treated as a parent directory and all subdirectories containing `SKILL.md` are loaded. This means `SkillsPlugin(skills=["./skills"])` works whether `./skills` is a single skill or a directory of skills. + +We start with filesystem support, but the design accommodates future sources: + +- URLs (remote skill repositories) +- S3 locations (`s3://bucket/skills/code-review/`) +- MCP servers (the MCP community is [exploring skills over MCP](https://github.com/modelcontextprotocol/experimental-ext-skills/blob/main/README.md), where MCP servers could expose skills as resources) + +The `Skill` dataclass abstracts over the source, so adding new loaders doesn't change the plugin interface. + +```python +# Filesystem (P0) +SkillsPlugin(skills=["./skills/code-review"]) + +# Programmatic +SkillsPlugin(skills=[ + Skill(name="quick-review", description="...", instructions="..."), + "./skills/documentation", +]) + +# Future: URLs, S3, MCP (not in initial implementation) +# SkillsPlugin(skills=[ +# "https://example.com/skills/code-review", +# "s3://my-bucket/skills/documentation", +# ]) +``` + +### Dynamic skill management + +Skills aren't baked in at init time. You can change them between invocations: + +```python +skills_plugin = SkillsPlugin(skills=["./skills/code-review"]) +agent = Agent(plugins=[skills_plugin]) + +agent("Review this function for security issues") + +# Switch to different skills +skills_plugin.skills = ["./skills/documentation"] +agent("Write API docs for this module") + +# Add more skills +skills_plugin.skills = ["./skills/documentation", "./skills/code-review"] +agent("Document and review this module") +``` + +### Tracking the active skill + +The plugin tracks which skill the agent is currently using: + +```python +skills_plugin = SkillsPlugin(skills=["./skills"]) +agent = Agent(plugins=[skills_plugin]) + +result = agent("Review my authentication code") + +if skills_plugin.active_skill: + print(f"Agent used: {skills_plugin.active_skill.name}") +``` + +Detection works through the `skills` tool: when the agent activates a skill, it becomes the active skill. It stays active until the agent explicitly deactivates it. + +### Multiple active skills + +Only one skill can be active at a time. Activating a new skill while one is already active implicitly deactivates the previous one (restoring any filtered tools before applying the new skill's `allowed_tools`). This keeps the mental model simple and avoids ambiguity around conflicting `allowed_tools` sets. + +If a future use case requires multiple simultaneous active skills, we can extend the design then. Starting with single-active is the safer default. + +### Tool restrictions with `allowed_tools` + +Skills can optionally specify which tools they're allowed to use: + +```yaml +--- +name: safe-analyzer +description: Analyzes files without executing code +allowed-tools: file_read +--- +``` + +Tool filtering is opt-in. When a skill with `allowed_tools` is activated, the plugin removes non-allowed tools from the agent, keeping them in memory. When the skill is deactivated, the full tool set is restored. If a skill doesn't specify `allowed_tools`, all tools remain available. + +Not every skill author will know the exact tool names in every agent, so `allowed_tools` is best suited for controlled environments where the skill author knows the agent's tool set. For portable skills shared across different agents, omitting `allowed_tools` is the safer default. + +### Session persistence + +The `SkillsPlugin` stores its state in `agent.state["skills_plugin"]` so it gets persisted automatically when a `SessionManager` saves the agent's state. On restore, the plugin reads from `agent.state` and re-applies its configuration. + +```python +# The plugin writes to agent.state during lifecycle events +agent.state["skills_plugin"] = { + "skills": [str(s.path) for s in self._loaded_skills if s.path], + "active_skill": self._active_skill.name if self._active_skill else None, + "filtered_tools": [t.name for t in self._filtered_tools] if self._filtered_tools else None, +} +``` + +Since `SessionManager` already persists `agent.state`, no special plugin-aware logic is needed. When the session is restored, the plugin reads `agent.state["skills_plugin"]` during initialization and re-applies skill configuration, active skill, and tool filtering. + +### API surface + +**Skill dataclass:** + +```python +@dataclass +class Skill: + name: str + description: str + instructions: str = "" + path: Path | None = None + allowed_tools: list[str] | None = None # None means all tools allowed + metadata: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_path(cls, skill_path: str | Path) -> "Skill": + """Load a skill from a directory containing SKILL.md. + + Raises: + ValueError: If SKILL.md is missing or malformed. + """ + ... +``` + +**Helper functions:** + +```python +def load_skills(skills_dir: str | Path) -> list[Skill]: + """Load all skills from subdirectories of skills_dir. + + Each subdirectory must contain a SKILL.md file. Subdirectories + without SKILL.md are silently skipped. + """ + ... + +def load_skill(skill_path: str | Path) -> Skill: + """Load a single skill from a directory. + + Raises: + ValueError: If SKILL.md is missing or malformed. + """ + ... +``` + +**Module exports:** + +```python +# Skill dataclass from top-level +from strands import Skill + +# Plugin from plugins submodule +from strands.plugins import SkillsPlugin + +# Helpers from skills submodule +from strands.skills import load_skills, load_skill +``` + +**Error handling:** + +- Invalid paths in `SkillsPlugin(skills=...)` raise `ValueError` at init time +- Skills with `allowed_tools` referencing non-existent tools log a warning but don't fail + +## Developer experience + +### Basic usage + +```python +from strands import Agent +from strands.plugins import SkillsPlugin + +agent = Agent( + plugins=[SkillsPlugin(skills=["./skills"])] +) + +result = agent("Review my code for security issues") +``` + +### Combining with a custom system prompt + +```python +agent = Agent( + system_prompt="You are a senior engineer at Acme Corp. Be thorough.", + plugins=[SkillsPlugin(skills=["./company-skills/code-review", "./company-skills/docs"])], +) +``` + +Your system prompt comes first, then the skills metadata is appended. + +### Creating skills programmatically + +You don't need `SKILL.md` files. Define skills in code: + +```python +from strands import Agent, Skill +from strands.plugins import SkillsPlugin + +review_skill = Skill( + name="quick-review", + description="Quick code review focusing on obvious issues", + instructions=""" +# Quick Review Guidelines + +Focus on: +1. Obvious bugs and typos +2. Missing error handling +3. Security red flags + +Skip style nitpicks and optimization suggestions. +""", + allowed_tools=["file_read"] +) + +agent = Agent(plugins=[SkillsPlugin(skills=[review_skill])]) +``` + +### Filtering skills before loading + +```python +from strands.skills import load_skills +from strands.plugins import SkillsPlugin + +all_skills = load_skills("./skills") + +# Only allow specific skills +safe_skills = [s for s in all_skills if s.name in ["docs", "summarizer"]] + +agent = Agent(plugins=[SkillsPlugin(skills=safe_skills)]) +``` + +## Alternatives considered + +### Skills as a top-level Agent parameter + +The original design proposed `Agent(skills="./skills")`. With the plugins system, this adds unnecessary surface area to the Agent constructor. Skills is a cross-component feature (system prompt + tools + hooks) which is exactly what plugins are for. Keeping it as a plugin also means the pattern is consistent: if you want skills, you add a plugin. If you want steering, you add a plugin. + +### Skill modes (inject/tool/agent) + +We considered adding a `skill_mode` parameter to control how skills are activated—injected into prompts, loaded via tool calls, or run in sub-agents. This adds complexity without clear benefit. The single approach (metadata in prompt, full content via tool on demand) covers most cases. Users who need different patterns can build their own plugin. + +### SkillProvider interface + +We considered a `SkillProvider` protocol similar to `ToolProvider`. Skills are simpler than tools—they're just data (name, description, instructions). A plain dataclass is sufficient for the data model, and the plugin handles the integration. + +## Consequences + +### What becomes easier + +- Loading skills from directories with a single plugin +- Sharing skills across agents and projects +- Changing skills dynamically between invocations +- Tracking which skill the agent is using +- Restricting tools when specific skills are active +- Persisting skill state across sessions +- Future extensibility to remote skill sources (URLs, S3, MCP) + +### What becomes more difficult + +Nothing significant. The feature is additive. + +### Future extensions + +This design allows for: + +- Remote skill sources (URLs, S3 buckets) +- MCP-based skill discovery (as the [MCP Skills Interest Group](https://github.com/modelcontextprotocol/experimental-ext-skills) explores standardization) +- Skill versioning +- Multiple simultaneous active skills +- Custom prompt templates for skill metadata +- Skill registries and marketplaces From f993b823632c32060fa3fcb413fd190967eded96 Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 13 Feb 2026 13:59:32 -0500 Subject: [PATCH 7/9] feat: add resources and scripts info --- designs/skills-support.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/designs/skills-support.md b/designs/skills-support.md index 4344df48..957dab81 100644 --- a/designs/skills-support.md +++ b/designs/skills-support.md @@ -155,6 +155,28 @@ class SkillsPlugin(Plugin): The `@tool` and `@hook` decorators inside the plugin class auto-register with the agent during `init_plugin`. This is the DX we want: declare what you need, the plugin protocol handles the wiring. +### Resources and script execution + +The `SkillsPlugin` deliberately does not register tools for reading skill resources or executing skill scripts. The AgentSkills.io spec defines `scripts/`, `references/`, and `assets/` directories, but how those are accessed and executed depends entirely on the agent's environment. + +Code execution within skills is dependent on agent configuration. Skills as a concept does not prescribe how to execute code or access resources — that design choice is left to the developer. You can add a `shell` tool to run scripts in a terminal, use AgentCore Code Interpreter, a Python REPL, or any other execution environment. The same applies to resources: the most basic implementation reads from the filesystem, but resources could just as well be S3 URLs accessed via `http_request`. + +In practice, you select where to host your skills and how to execute their scripts by configuring the agent's tools: + +```python +from strands import Agent +from strands.plugins import SkillsPlugin +from strands_tools import file_read, shell + +# Filesystem skills with shell execution +agent = Agent( + plugins=[SkillsPlugin(skills=["./skills"])], + tools=[file_read, shell] +) +``` + +This keeps the plugin focused on skill discovery and activation, while the execution surface stays under the developer's control. + ### Skill sources The `skills` parameter accepts a list. Each entry can be: From a4042e56c5699b9265104061e14ff45370c86b4b Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 13 Feb 2026 14:00:54 -0500 Subject: [PATCH 8/9] fix: dedupe init --- designs/skills-support.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/designs/skills-support.md b/designs/skills-support.md index 957dab81..503861ed 100644 --- a/designs/skills-support.md +++ b/designs/skills-support.md @@ -118,15 +118,10 @@ class SkillsPlugin(Plugin): name = "skills" def __init__(self, skills: list[str | Path | Skill]): - self._skills_config = skills - self._loaded_skills: list[Skill] = [] + self._loaded_skills = self._resolve_skills(skills) self._active_skill: Skill | None = None self._filtered_tools: list[Tool] | None = None - async def init_plugin(self, agent: Agent): - self._loaded_skills = self._resolve_skills(self._skills_config) - # Tools and hooks are auto-registered via decorators - @tool def skills(self, action: str, skill_name: str) -> str: """Activate or deactivate a skill. From acb79181cc0a9fa6976c33cdfd64e10d52e84bc7 Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 13 Feb 2026 14:01:43 -0500 Subject: [PATCH 9/9] fix: small fixes --- designs/skills-support.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/designs/skills-support.md b/designs/skills-support.md index 503861ed..7182c704 100644 --- a/designs/skills-support.md +++ b/designs/skills-support.md @@ -86,11 +86,11 @@ agent = Agent( result = agent("Review my code for security issues") ``` -The plugin auto-registers everything it needs during `init_plugin`: a `skills` tool, hooks for system prompt management, and hooks for tool filtering. You don't wire anything up manually. +The plugin loads skills in `__init__` and auto-registers everything it needs when passed to the Agent: a `skills` tool, hooks for system prompt management, and hooks for tool filtering. You don't wire anything up manually. ### How it works -The `SkillsPlugin` implements the `Plugin` protocol. When `init_plugin` is called: +The `SkillsPlugin` implements the `Plugin` protocol. During `__init__`, it loads all skills. When the plugin is passed to the Agent: 1. Loads skill metadata (name, description, location) from each `SKILL.md` 2. Registers a `skills` tool on the agent with `activate` and `deactivate` actions @@ -148,7 +148,7 @@ class SkillsPlugin(Plugin): ... ``` -The `@tool` and `@hook` decorators inside the plugin class auto-register with the agent during `init_plugin`. This is the DX we want: declare what you need, the plugin protocol handles the wiring. +The `@tool` and `@hook` decorators inside the plugin class auto-register with the agent when the plugin is passed to the Agent constructor. This is the DX we want: declare what you need, the plugin protocol handles the wiring. ### Resources and script execution