Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions libs/deepagents-cli/deepagents_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,121 @@ deepagents = "deepagents.cli:cli_main"
```

This means when users install the package, they can run `deepagents` directly.

## Skills

The CLI implements **Agent Skills** - Anthropic's pattern for equipping agents with specialized, discoverable capabilities. Skills are markdown files with YAML frontmatter that provide structured guidance for specific tasks.

### Creating Skills

```bash
# Create a new skill from template
deepagents skills create web-scraping

# This creates ~/.deepagents/skills/web-scraping/SKILL.md with:
# ---
# name: web-scraping
# description: [Brief description]
# ---
# [Detailed instructions, examples, and best practices]
```

### Managing Skills

```bash
# List all available skills
deepagents skills list

# View detailed skill information
deepagents skills info web-research
```

### Skill Directory Structure

Skills are stored globally in `~/.deepagents/skills/` and shared across all agents:

```
~/.deepagents/
├── skills/ # Global skills library
│ ├── web-research/
│ │ ├── SKILL.md # Main instructions (YAML + Markdown)
│ │ └── helper.py # Optional: supporting files
│ └── code-review/
│ ├── SKILL.md
│ └── checklist.md
└── agent/ # Per-agent memory
└── agent.md
```

### How Skills Work (Progressive Disclosure)

Skills follow Anthropic's **progressive disclosure** pattern:

1. **Discovery**: Agent sees skill names + descriptions in system prompt at startup
2. **Loading**: Agent reads full `SKILL.md` when task matches skill domain
3. **Execution**: Agent follows skill's step-by-step instructions
4. **Resources**: Agent accesses supporting files as needed

### Built-in Skills

DeepAgents CLI includes two example skills:

**`web-research`**: Comprehensive methodology for conducting structured web research
- Search strategy and source evaluation
- Information organization patterns
- Synthesis techniques with examples

**`code-review`**: Systematic code review across 8 dimensions
- Functionality, readability, security, performance
- Review templates and examples
- Integration with memory system

### Using Skills in the CLI

When you run the CLI, agents automatically:

```python
# 1. Skills are loaded at session start
Skills Middleware → Loads metadata from ~/.deepagents/skills/

# 2. Skills list injected into system prompt
System Prompt includes:
"""
Available Skills:
- web-research: Structured approach to web research
→ Read /skills/web-research/SKILL.md for full instructions
- code-review: Systematic code review methodology
→ Read /skills/code-review/SKILL.md for full instructions
"""

# 3. Agent recognizes when skills apply
User: "Can you research the latest Python frameworks?"
Agent: Recognizes web-research skill is relevant

# 4. Agent loads full skill instructions
Agent: read_file('/skills/web-research/SKILL.md')

# 5. Agent follows skill workflow
Agent: Executes research following skill's structured approach
```

### Example Session with Skills

```bash
$ deepagents --agent researcher

> Research the latest developments in quantum computing

[Agent recognizes web-research skill applies]

✓ Reading /skills/web-research/SKILL.md
✓ Following research methodology from skill
✓ Step 1: Defining research questions
✓ Step 2: Executing targeted searches
- web_search("quantum computing 2025 breakthroughs")
- web_search("quantum error correction advances")
✓ Step 3: Organizing findings in /memories/research-quantum.md
✓ Step 4: Synthesizing report

[Agent delivers comprehensive research report following skill's structure]
```
12 changes: 10 additions & 2 deletions libs/deepagents-cli/deepagents_cli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .agent_memory import AgentMemoryMiddleware
from .config import COLORS, config, console, get_default_coding_instructions
from .skills_middleware import SkillsMiddleware


def list_agents():
Expand Down Expand Up @@ -156,14 +157,21 @@ def create_agent_with_config(model, assistant_id: str, tools: list):
# This handles both /memories/ files and /agent.md
long_term_backend = FilesystemBackend(root_dir=agent_dir, virtual_mode=True)

# Composite backend: current working directory for default, agent directory for /memories/
# Skills backend - global skills directory shared across agents
skills_dir = Path.home() / ".deepagents" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
skills_backend = FilesystemBackend(root_dir=skills_dir, virtual_mode=True)

# Composite backend: current working directory for default, agent directory for /memories/, skills for /skills/
backend = CompositeBackend(
default=FilesystemBackend(), routes={"/memories/": long_term_backend}
default=FilesystemBackend(),
routes={"/memories/": long_term_backend, "/skills/": skills_backend},
)

# Use the same backend for agent memory middleware
agent_middleware = [
AgentMemoryMiddleware(backend=long_term_backend, memory_path="/memories/"),
SkillsMiddleware(skills_dir=skills_dir, skills_path="/skills/"),
shell_middleware,
]

Expand Down
31 changes: 31 additions & 0 deletions libs/deepagents-cli/deepagents_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .config import COLORS, DEEP_AGENTS_ASCII, SessionState, console, create_model
from .execution import execute_task
from .input import create_prompt_session
from .skills_commands import create_skill, list_skills, show_skill_info
from .tools import fetch_url, http_request, tavily_client, web_search
from .ui import TokenTracker, show_help

Expand Down Expand Up @@ -78,6 +79,21 @@ def parse_args():
"--target", dest="source_agent", help="Copy prompt from another agent"
)

# Skills command
skills_parser = subparsers.add_parser("skills", help="Manage agent skills")
skills_subparsers = skills_parser.add_subparsers(dest="skills_command", help="Skills command")

# Skills list
skills_subparsers.add_parser("list", help="List all available skills")

# Skills create
create_parser = skills_subparsers.add_parser("create", help="Create a new skill")
create_parser.add_argument("name", help="Name of the skill to create (e.g., web-research)")

# Skills info
info_parser = skills_subparsers.add_parser("info", help="Show detailed information about a skill")
info_parser.add_argument("name", help="Name of the skill to show info for")

# Default interactive mode
parser.add_argument(
"--agent",
Expand Down Expand Up @@ -210,6 +226,21 @@ def cli_main():
list_agents()
elif args.command == "reset":
reset_agent(args.agent, args.source_agent)
elif args.command == "skills":
# Handle skills subcommands
if args.skills_command == "list":
list_skills()
elif args.skills_command == "create":
create_skill(args.name)
elif args.skills_command == "info":
show_skill_info(args.name)
else:
# No subcommand provided, show help
console.print("[yellow]Please specify a skills subcommand: list, create, or info[/yellow]")
console.print("\nExamples:")
console.print(" deepagents skills list")
console.print(" deepagents skills create web-research")
console.print(" deepagents skills info web-research")
else:
# Create session state from args
session_state = SessionState(auto_approve=args.auto_approve)
Expand Down
186 changes: 186 additions & 0 deletions libs/deepagents-cli/deepagents_cli/skill_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Skill loader for parsing and loading agent skills from SKILL.md files.

This module implements Anthropic's agent skills pattern with YAML frontmatter parsing.
Each skill is a directory containing a SKILL.md file with:
- YAML frontmatter (name, description required)
- Markdown instructions for the agent
- Optional supporting files (scripts, configs, etc.)

Example SKILL.md structure:
```markdown
---
name: web-research
description: Structured approach to conducting thorough web research
---

# Web Research Skill

## When to Use
- User asks you to research a topic
...
```
"""

import re
from pathlib import Path
from typing import TypedDict


class SkillMetadata(TypedDict):
"""Metadata for a skill."""

name: str
"""Name of the skill."""

description: str
"""Description of what the skill does."""

path: str
"""Path to the SKILL.md file."""


class SkillLoader:
"""Loader for agent skills with YAML frontmatter parsing.

Skills are organized as:
skills/
├── skill-name/
│ ├── SKILL.md # Required: instructions with YAML frontmatter
│ ├── script.py # Optional: supporting files
│ └── config.json # Optional: supporting files

Example:
```python
loader = SkillLoader(skills_dir="~/.deepagents/skills")
skills = loader.load_skills()
for skill in skills:
print(f"{skill['name']}: {skill['description']}")
```
"""

def __init__(self, skills_dir: str | Path = "~/.deepagents/skills") -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid hard-coding a defaul there, and if possible restrict input to just Path. This isn't user facing, so wide inputs are bad

"""Initialize the skill loader.

Args:
skills_dir: Path to the skills directory. Defaults to ~/.deepagents/skills
"""
self.skills_dir = Path(skills_dir).expanduser()
self.skills: list[SkillMetadata] = []

def _parse_skill_metadata(self, skill_md_path: Path) -> SkillMetadata | None:
"""Parse YAML frontmatter from a SKILL.md file.

Args:
skill_md_path: Path to the SKILL.md file.

Returns:
SkillMetadata with name, description, and path, or None if parsing fails.
"""
try:
content = skill_md_path.read_text(encoding="utf-8")

# Match YAML frontmatter between --- delimiters
frontmatter_pattern = r"^---\s*\n(.*?)\n---\s*\n"
match = re.match(frontmatter_pattern, content, re.DOTALL)

if not match:
return None

frontmatter = match.group(1)

# Parse key-value pairs from YAML (simple parsing, no nested structures)
metadata: dict[str, str] = {}
for line in frontmatter.split("\n"):
# Match "key: value" pattern
kv_match = re.match(r"^(\w+):\s*(.+)$", line.strip())
if kv_match:
key, value = kv_match.groups()
metadata[key] = value.strip()

# Validate required fields
if "name" not in metadata or "description" not in metadata:
return None

return SkillMetadata(
name=metadata["name"],
description=metadata["description"],
path=str(skill_md_path),
)

except Exception:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we refine the exception to be specific

# Silently skip malformed files
return None

def load_skills(self) -> list[SkillMetadata]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename as list perhaps?

"""Load all skills from the skills directory.

Scans the skills directory for subdirectories containing SKILL.md files,
parses YAML frontmatter, and returns skill metadata.

Returns:
List of skill metadata dictionaries with name, description, and path.
"""
self.skills = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does mutation in place, which we don't want for the loader -- it causes a bunch of extra boiler plate to appear (e.g., the wrapper function)


# Check if skills directory exists
if not self.skills_dir.exists():
return self.skills

# Iterate through subdirectories
for skill_dir in self.skills_dir.iterdir():
if not skill_dir.is_dir():
continue

# Look for SKILL.md file
skill_md_path = skill_dir / "SKILL.md"
if not skill_md_path.exists():
continue

# Parse metadata
metadata = self._parse_skill_metadata(skill_md_path)
if metadata:
self.skills.append(metadata)

return self.skills

def get_skill_names(self) -> list[str]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would suggest to remove the helper to get_skill_names

get_skills / or list_skills should be sufficient, no extra helpers

"""Get list of loaded skill names.

Returns:
List of skill names.
"""
return [skill["name"] for skill in self.skills]

def format_skills_for_system_message(self) -> str:
"""Format skills metadata for injection into system prompt.

Creates a formatted list of skills with their descriptions and paths,
following Anthropic's progressive disclosure pattern.

Returns:
Formatted string for system prompt.
"""
if not self.skills:
return "No skills available."

lines = ["Available skills:"]
for skill in self.skills:
lines.append(f"- **{skill['name']}**: {skill['description']}")
lines.append(f" Read `/skills/{Path(skill['path']).parent.name}/SKILL.md` for details")

return "\n".join(lines)


def load_skills(skills_dir: str | Path = "~/.deepagents/skills") -> tuple[list[SkillMetadata], str]:
"""Convenience function to load skills and format for system prompt.

Args:
skills_dir: Path to the skills directory.

Returns:
Tuple of (skills metadata list, formatted string for system prompt).
"""
loader = SkillLoader(skills_dir)
skills = loader.load_skills()
formatted = loader.format_skills_for_system_message()
return skills, formatted
Loading
Loading