Agent Profiles and Skills System
Two major systems added in v2.0: Agent Profiles for configurable behavior presets and Skills for injectable domain knowledge.
Part 1: Agent Profiles
Overview
Directory: vibe/core/agents/
vibe/core/agents/
├── models.py # AgentProfile, AgentSafety, AgentType, BuiltinAgentName
└── manager.py # AgentManager discovery & switching
AgentSafety Enum
File: vibe/core/agents/models.py:23
class AgentSafety(StrEnum):
SAFE = auto() # Read-only, no modifications
NEUTRAL = auto() # Default, requires approval
DESTRUCTIVE = auto() # Auto-approves some write operations
YOLO = auto() # Auto-approves everythingAgentType Enum
File: vibe/core/agents/models.py:30
class AgentType(StrEnum):
AGENT = auto() # Primary agent (user-switchable)
SUBAGENT = auto() # Subagent (used by Task tool only)BuiltinAgentName Enum
File: vibe/core/agents/models.py:35
class BuiltinAgentName(StrEnum):
DEFAULT = "default"
PLAN = "plan"
ACCEPT_EDITS = "accept-edits"
AUTO_APPROVE = "auto-approve"
EXPLORE = "explore"AgentProfile Dataclass
File: vibe/core/agents/models.py:43
@dataclass(frozen=True)
class AgentProfile:
name: str
display_name: str
description: str
safety: AgentSafety
agent_type: AgentType = AgentType.AGENT
overrides: dict[str, Any] = field(default_factory=dict)Key methods:
| Method | Line | Purpose |
|---|---|---|
apply_to_config() |
52 | Deep-merge overrides into base VibeConfig |
from_toml() |
58 | Load custom agent from TOML file |
Built-in Agent Profiles
Defined at: models.py:74-122
| Agent | Safety | Type | Overrides |
|---|---|---|---|
| default | NEUTRAL | AGENT | None (requires approval) |
| plan | SAFE | AGENT | auto_approve=True, enabled_tools=["grep", "read_file", "todo", "ask_user_question", "task"] |
| accept-edits | DESTRUCTIVE | AGENT | tools.write_file.permission="always", tools.search_replace.permission="always" |
| auto-approve | YOLO | AGENT | auto_approve=True |
| explore | SAFE | SUBAGENT | enabled_tools=["grep", "read_file"] |
The explore agent is a SUBAGENT, meaning it can only be used via the Task tool (not directly switchable by the user).
AgentManager
File: vibe/core/agents/manager.py:24
class AgentManager:
def __init__(
self,
config_getter: Callable[[], VibeConfig],
initial_agent: str = BuiltinAgentName.DEFAULT,
) -> None:Initialization
AgentManager.__init__() [manager.py:25]
│
├─1─► Compute search paths [manager.py:31]
│ _compute_search_paths(config)
│
├─2─► Discover agents [manager.py:32]
│ _discover_agents() → dict[str, AgentProfile]
│
├─3─► Log custom agent count [manager.py:34-43]
│
└─4─► Set active profile [manager.py:45-47]
Falls back to DEFAULT if initial_agent not found
Search Path Resolution
Method: _compute_search_paths() at manager.py:83
@staticmethod
def _compute_search_paths(config: VibeConfig) -> list[Path]:
paths: list[Path] = []
# 1. Config-specified paths
for path in config.agent_paths:
if path.is_dir():
paths.append(path)
# 2. Project-local .vibe/agents/ (trust-aware)
if (agents_dir := resolve_local_agents_dir(Path.cwd())) is not None:
paths.append(agents_dir)
# 3. Global ~/.vibe/agents/
if GLOBAL_AGENTS_DIR.path.is_dir():
paths.append(GLOBAL_AGENTS_DIR.path)
return unique_pathsAgent Discovery
Method: _discover_agents() at manager.py:100
_discover_agents() [manager.py:100-123]
│
├─1─► Start with all BUILTIN_AGENTS [manager.py:101]
│
└─2─► For each search path:
│
├─► Find all .toml files [manager.py:106]
│
├─► Load via AgentProfile.from_toml() [manager.py:109]
│
├─► If name matches builtin: override with warning [manager.py:110-113]
│
└─► If duplicate: skip [manager.py:114-120]
Key Properties and Methods
| Property/Method | Line | Purpose |
|---|---|---|
active_profile |
45 | Currently active AgentProfile |
available_agents |
55 | Filtered dict respecting enabled/disabled config |
config |
71 | VibeConfig with active profile overrides applied |
switch_profile() |
76 | Switch to different agent, invalidate cached config |
invalidate_config() |
80 | Force config recalculation |
get_agent() |
134 | Look up agent by name |
get_subagents() |
139 | List agents with type SUBAGENT |
get_agent_order() |
146 | Ordered list of primary agents for cycling |
next_agent() |
162 | Get next agent in cycle order |
Agent Cycling (Shift+Tab)
The get_agent_order() method returns agents in this order:
- Built-in agents: default → plan → accept-edits → auto-approve
- Custom agents (sorted alphabetically)
Subagents are excluded from the cycle.
Creating Custom Agents
Create a .toml file in any agent search path:
# ~/.vibe/agents/my-agent.toml
display_name = "My Custom Agent"
description = "A custom agent with specific settings"
safety = "neutral"
agent_type = "agent"
# Config overrides (any valid VibeConfig fields)
auto_approve = false
active_model = "devstral-small"
[tools.bash]
permission = "always"
timeout = 120The file stem becomes the agent name (e.g., my-agent).
Part 2: Skills System
Overview
Directory: vibe/core/skills/
vibe/core/skills/
├── models.py # SkillMetadata, SkillInfo
├── manager.py # SkillManager discovery
└── parser.py # YAML frontmatter parser
SKILL.md Format
Skills are directories containing a SKILL.md file with YAML frontmatter:
---
name: my-skill
description: "What this skill does and when to use it"
license: MIT
compatibility: "Requires Node.js 18+"
user-invocable: true
allowed-tools: "bash read_file write_file"
metadata:
author: "Your Name"
version: "1.0"
---
# Skill Instructions
This content is injected into the system prompt when the skill is active.
The LLM will see these instructions and follow them.SkillMetadata Model
File: vibe/core/skills/models.py:9
class SkillMetadata(BaseModel):
name: str # Lowercase, hyphens only (pattern: ^[a-z0-9]+(-[a-z0-9]+)*$)
description: str # What this skill does (max 1024 chars)
license: str | None # License name
compatibility: str | None # Environment requirements
metadata: dict[str, str] # Arbitrary key-value pairs
allowed_tools: list[str] # Pre-approved tools (space-delimited string or list)
user_invocable: bool = True # Whether it appears in slash command menuSkillInfo Model
File: vibe/core/skills/models.py:65
class SkillInfo(BaseModel):
name: str
description: str
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str]
allowed_tools: list[str]
user_invocable: bool = True
skill_path: Path # Absolute path to SKILL.md
@property
def skill_dir(self) -> Path:
return self.skill_path.parent.resolve()SkillManager
File: vibe/core/skills/manager.py:20
class SkillManager:
def __init__(self, config_getter: Callable[[], VibeConfig]) -> None:Search Path Resolution
Method: _compute_search_paths() at manager.py:53
@staticmethod
def _compute_search_paths(config: VibeConfig) -> list[Path]:
paths: list[Path] = []
# 1. Config-specified paths
for path in config.skill_paths:
if path.is_dir():
paths.append(path)
# 2. Project-local .vibe/skills/ (trust-aware)
if (skills_dir := resolve_local_skills_dir(Path.cwd())) is not None:
paths.append(skills_dir)
# 3. Global ~/.vibe/skills/
if GLOBAL_SKILLS_DIR.path.is_dir():
paths.append(GLOBAL_SKILLS_DIR.path)
return unique_pathsSkill Discovery
Method: _discover_skills() at manager.py:75
_discover_skills() [manager.py:75-90]
│
for each search path:
│
├─► List subdirectories [manager.py:94]
│
├─► Look for SKILL.md in each subdir [manager.py:97-98]
│
├─► Parse frontmatter + validate [manager.py:112-119]
│ parse_frontmatter(content) → (yaml_dict, markdown_body)
│ SkillMetadata.model_validate(yaml_dict)
│
├─► Warn if skill name != directory name [manager.py:122-128]
│
└─► First occurrence wins (skip duplicates) [manager.py:81-89]
Filtering
The available_skills property respects enabled_skills / disabled_skills config:
@property
def available_skills(self) -> dict[str, SkillInfo]:
if self._config.enabled_skills:
return {name: info for name, info in self._available.items()
if name_matches(name, self._config.enabled_skills)}
if self._config.disabled_skills:
return {name: info for name, info in self._available.items()
if not name_matches(name, self._config.disabled_skills)}
return dict(self._available)Frontmatter Parser
File: vibe/core/skills/parser.py
def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
"""Parse YAML frontmatter delimited by --- markers."""
# Splits on --- boundaries
# Returns (yaml_dict, markdown_body)Raises SkillParseError if frontmatter is missing, invalid YAML, or not a dictionary.
Skill Placement
Skills can be placed in:
~/.vibe/skills/ # Global skills
my-skill/
SKILL.md
./.vibe/skills/ # Project-local (requires trusted folder)
project-skill/
SKILL.md
/custom/path/skills/ # Config-specified via skill_paths
custom-skill/
SKILL.md
Integration with System Prompt
Skills are injected into the system prompt via get_universal_system_prompt():
get_universal_system_prompt(tool_manager, config, skill_manager, agent_manager)
│
├── Include tool descriptions
├── Include agent info
└── Include skill content from all available skills
Markdown body of each SKILL.md is appended to the prompt
Source File References
| File | Key Lines | Description |
|---|---|---|
agents/models.py:13-20 |
_deep_merge() |
Deep merge utility for config overrides |
agents/models.py:23-27 |
AgentSafety |
Safety level enum |
agents/models.py:30-32 |
AgentType |
Agent vs subagent enum |
agents/models.py:35-40 |
BuiltinAgentName |
Built-in agent name constants |
agents/models.py:43-69 |
AgentProfile |
Profile dataclass with apply_to_config, from_toml |
agents/models.py:72 |
PLAN_AGENT_TOOLS |
Allowed tools for plan agent |
agents/models.py:74-122 |
Built-in agents | DEFAULT, PLAN, ACCEPT_EDITS, AUTO_APPROVE, EXPLORE |
agents/manager.py:24-166 |
AgentManager |
Discovery, switching, config merging |
agents/manager.py:83-98 |
_compute_search_paths() |
Search path resolution |
agents/manager.py:100-123 |
_discover_agents() |
Agent file discovery |
agents/manager.py:146-160 |
get_agent_order() |
Cycling order for Shift+Tab |
skills/models.py:9-63 |
SkillMetadata |
YAML frontmatter model |
skills/models.py:65-92 |
SkillInfo |
Runtime skill representation |
skills/manager.py:20-134 |
SkillManager |
Discovery, filtering |
skills/manager.py:53-73 |
_compute_search_paths() |
Search path resolution |
skills/parser.py:9-12 |
SkillParseError |
Parse error exception |
skills/parser.py:18-39 |
parse_frontmatter() |
YAML frontmatter parser |