emdash-core 0.1.25__py3-none-any.whl → 0.1.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emdash_core/agent/__init__.py +4 -0
- emdash_core/agent/agents.py +84 -23
- emdash_core/agent/events.py +42 -20
- emdash_core/agent/hooks.py +419 -0
- emdash_core/agent/inprocess_subagent.py +166 -18
- emdash_core/agent/prompts/__init__.py +4 -3
- emdash_core/agent/prompts/main_agent.py +67 -2
- emdash_core/agent/prompts/plan_mode.py +236 -107
- emdash_core/agent/prompts/subagents.py +103 -23
- emdash_core/agent/prompts/workflow.py +159 -26
- emdash_core/agent/providers/factory.py +2 -2
- emdash_core/agent/providers/openai_provider.py +67 -15
- emdash_core/agent/runner/__init__.py +49 -0
- emdash_core/agent/runner/agent_runner.py +765 -0
- emdash_core/agent/runner/context.py +470 -0
- emdash_core/agent/runner/factory.py +108 -0
- emdash_core/agent/runner/plan.py +217 -0
- emdash_core/agent/runner/sdk_runner.py +324 -0
- emdash_core/agent/runner/utils.py +67 -0
- emdash_core/agent/skills.py +47 -8
- emdash_core/agent/toolkit.py +46 -14
- emdash_core/agent/toolkits/__init__.py +117 -18
- emdash_core/agent/toolkits/base.py +87 -2
- emdash_core/agent/toolkits/explore.py +18 -0
- emdash_core/agent/toolkits/plan.py +27 -11
- emdash_core/agent/tools/__init__.py +2 -2
- emdash_core/agent/tools/coding.py +48 -4
- emdash_core/agent/tools/modes.py +151 -143
- emdash_core/agent/tools/task.py +52 -6
- emdash_core/api/agent.py +706 -1
- emdash_core/ingestion/repository.py +17 -198
- emdash_core/models/agent.py +4 -0
- emdash_core/skills/frontend-design/SKILL.md +56 -0
- emdash_core/sse/stream.py +4 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/METADATA +4 -1
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/RECORD +38 -30
- emdash_core/agent/runner.py +0 -1123
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/entry_points.txt +0 -0
emdash_core/agent/skills.py
CHANGED
|
@@ -6,6 +6,10 @@ when relevant or explicitly invoked via /skill_name.
|
|
|
6
6
|
|
|
7
7
|
Similar to Claude Code's skills system:
|
|
8
8
|
https://docs.anthropic.com/en/docs/claude-code/skills
|
|
9
|
+
|
|
10
|
+
Skills are loaded from two locations:
|
|
11
|
+
1. Built-in skills bundled with emdash_core (always available)
|
|
12
|
+
2. User repo skills in .emdash/skills/ (can override built-in)
|
|
9
13
|
"""
|
|
10
14
|
|
|
11
15
|
from dataclasses import dataclass, field
|
|
@@ -15,6 +19,11 @@ from typing import Optional
|
|
|
15
19
|
from ..utils.logger import log
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
def _get_builtin_skills_dir() -> Path:
|
|
23
|
+
"""Get the directory containing built-in skills bundled with emdash_core."""
|
|
24
|
+
return Path(__file__).parent.parent / "skills"
|
|
25
|
+
|
|
26
|
+
|
|
18
27
|
@dataclass
|
|
19
28
|
class Skill:
|
|
20
29
|
"""A skill configuration loaded from SKILL.md.
|
|
@@ -26,6 +35,7 @@ class Skill:
|
|
|
26
35
|
tools: List of tools this skill needs access to
|
|
27
36
|
user_invocable: Whether skill can be invoked with /name
|
|
28
37
|
file_path: Source file path
|
|
38
|
+
_builtin: Whether this is a built-in skill bundled with emdash_core
|
|
29
39
|
"""
|
|
30
40
|
|
|
31
41
|
name: str
|
|
@@ -34,6 +44,7 @@ class Skill:
|
|
|
34
44
|
tools: list[str] = field(default_factory=list)
|
|
35
45
|
user_invocable: bool = False
|
|
36
46
|
file_path: Optional[Path] = None
|
|
47
|
+
_builtin: bool = False
|
|
37
48
|
|
|
38
49
|
|
|
39
50
|
class SkillRegistry:
|
|
@@ -67,7 +78,11 @@ class SkillRegistry:
|
|
|
67
78
|
cls._instance._skills_dir = None
|
|
68
79
|
|
|
69
80
|
def load_skills(self, skills_dir: Optional[Path] = None) -> dict[str, Skill]:
|
|
70
|
-
"""Load skills from
|
|
81
|
+
"""Load skills from built-in and user repo directories.
|
|
82
|
+
|
|
83
|
+
Skills are loaded from two locations (in order):
|
|
84
|
+
1. Built-in skills bundled with emdash_core (always available)
|
|
85
|
+
2. User repo skills in .emdash/skills/ (can override built-in)
|
|
71
86
|
|
|
72
87
|
Each skill is a directory containing a SKILL.md file:
|
|
73
88
|
|
|
@@ -94,7 +109,7 @@ class SkillRegistry:
|
|
|
94
109
|
```
|
|
95
110
|
|
|
96
111
|
Args:
|
|
97
|
-
skills_dir: Directory containing skill subdirectories.
|
|
112
|
+
skills_dir: Directory containing user skill subdirectories.
|
|
98
113
|
Defaults to .emdash/skills/ in cwd.
|
|
99
114
|
|
|
100
115
|
Returns:
|
|
@@ -105,9 +120,34 @@ class SkillRegistry:
|
|
|
105
120
|
|
|
106
121
|
self._skills_dir = skills_dir
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
skills = {}
|
|
124
|
+
|
|
125
|
+
# First, load built-in skills bundled with emdash_core
|
|
126
|
+
builtin_dir = _get_builtin_skills_dir()
|
|
127
|
+
if builtin_dir.exists():
|
|
128
|
+
builtin_skills = self._load_skills_from_dir(builtin_dir, is_builtin=True)
|
|
129
|
+
skills.update(builtin_skills)
|
|
130
|
+
|
|
131
|
+
# Then, load user repo skills (can override built-in)
|
|
132
|
+
if skills_dir.exists():
|
|
133
|
+
user_skills = self._load_skills_from_dir(skills_dir, is_builtin=False)
|
|
134
|
+
skills.update(user_skills)
|
|
110
135
|
|
|
136
|
+
if skills:
|
|
137
|
+
log.info(f"Loaded {len(skills)} skills ({len([s for s in self._skills.values() if getattr(s, '_builtin', False)])} built-in)")
|
|
138
|
+
|
|
139
|
+
return skills
|
|
140
|
+
|
|
141
|
+
def _load_skills_from_dir(self, skills_dir: Path, is_builtin: bool = False) -> dict[str, Skill]:
|
|
142
|
+
"""Load skills from a specific directory.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
skills_dir: Directory containing skill subdirectories
|
|
146
|
+
is_builtin: Whether these are built-in skills
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict mapping skill name to Skill
|
|
150
|
+
"""
|
|
111
151
|
skills = {}
|
|
112
152
|
|
|
113
153
|
# Look for SKILL.md in subdirectories
|
|
@@ -125,15 +165,14 @@ class SkillRegistry:
|
|
|
125
165
|
try:
|
|
126
166
|
skill = _parse_skill_file(skill_file, skill_dir.name)
|
|
127
167
|
if skill:
|
|
168
|
+
skill._builtin = is_builtin # Mark as built-in or user-defined
|
|
128
169
|
skills[skill.name] = skill
|
|
129
170
|
self._skills[skill.name] = skill
|
|
130
|
-
|
|
171
|
+
source = "built-in" if is_builtin else "user"
|
|
172
|
+
log.debug(f"Loaded {source} skill: {skill.name}")
|
|
131
173
|
except Exception as e:
|
|
132
174
|
log.warning(f"Failed to load skill from {skill_file}: {e}")
|
|
133
175
|
|
|
134
|
-
if skills:
|
|
135
|
-
log.info(f"Loaded {len(skills)} skills")
|
|
136
|
-
|
|
137
176
|
return skills
|
|
138
177
|
|
|
139
178
|
def get_skill(self, name: str) -> Optional[Skill]:
|
emdash_core/agent/toolkit.py
CHANGED
|
@@ -41,6 +41,7 @@ class AgentToolkit:
|
|
|
41
41
|
repo_root: Optional[Path] = None,
|
|
42
42
|
plan_mode: bool = False,
|
|
43
43
|
save_spec_path: Optional[Path] = None,
|
|
44
|
+
plan_file_path: Optional[str] = None,
|
|
44
45
|
):
|
|
45
46
|
"""Initialize the agent toolkit.
|
|
46
47
|
|
|
@@ -53,6 +54,7 @@ class AgentToolkit:
|
|
|
53
54
|
If None, uses repo_root from config or current working directory.
|
|
54
55
|
plan_mode: Whether to include spec planning tools and restrict to read-only.
|
|
55
56
|
save_spec_path: If provided, specs will be saved to this path.
|
|
57
|
+
plan_file_path: Path to the plan file (only writable file in plan mode).
|
|
56
58
|
"""
|
|
57
59
|
self.connection = connection or get_connection()
|
|
58
60
|
self.session = AgentSession() if enable_session else None
|
|
@@ -61,6 +63,7 @@ class AgentToolkit:
|
|
|
61
63
|
self._mcp_config_path = mcp_config_path
|
|
62
64
|
self.plan_mode = plan_mode
|
|
63
65
|
self.save_spec_path = save_spec_path
|
|
66
|
+
self.plan_file_path = plan_file_path
|
|
64
67
|
|
|
65
68
|
# Get repo_root from config if not explicitly provided
|
|
66
69
|
if repo_root is None:
|
|
@@ -70,8 +73,12 @@ class AgentToolkit:
|
|
|
70
73
|
repo_root = Path(config.repo_root)
|
|
71
74
|
self._repo_root = repo_root or Path.cwd()
|
|
72
75
|
|
|
73
|
-
# Configure spec state if plan mode
|
|
76
|
+
# Configure mode state and spec state if plan mode
|
|
74
77
|
if plan_mode:
|
|
78
|
+
from .tools.modes import ModeState, AgentMode
|
|
79
|
+
mode_state = ModeState.get_instance()
|
|
80
|
+
mode_state.current_mode = AgentMode.PLAN
|
|
81
|
+
|
|
75
82
|
from .tools.spec import SpecState
|
|
76
83
|
spec_state = SpecState.get_instance()
|
|
77
84
|
spec_state.configure(save_path=save_spec_path)
|
|
@@ -110,8 +117,18 @@ class AgentToolkit:
|
|
|
110
117
|
self.register_tool(ReadFileTool(self._repo_root, self.connection))
|
|
111
118
|
self.register_tool(ListFilesTool(self._repo_root, self.connection))
|
|
112
119
|
|
|
113
|
-
# Register write tools
|
|
114
|
-
if
|
|
120
|
+
# Register write tools
|
|
121
|
+
if self.plan_mode:
|
|
122
|
+
# In plan mode: only allow writing to the plan file
|
|
123
|
+
if self.plan_file_path:
|
|
124
|
+
from .tools.coding import WriteToFileTool
|
|
125
|
+
self.register_tool(WriteToFileTool(
|
|
126
|
+
self._repo_root,
|
|
127
|
+
self.connection,
|
|
128
|
+
allowed_paths=[self.plan_file_path],
|
|
129
|
+
))
|
|
130
|
+
else:
|
|
131
|
+
# In code mode: full write access
|
|
115
132
|
from .tools.coding import (
|
|
116
133
|
WriteToFileTool,
|
|
117
134
|
ApplyDiffTool,
|
|
@@ -129,8 +146,12 @@ class AgentToolkit:
|
|
|
129
146
|
# Register mode tools
|
|
130
147
|
self._register_mode_tools()
|
|
131
148
|
|
|
132
|
-
# Register task management tools
|
|
133
|
-
|
|
149
|
+
# Register task management tools
|
|
150
|
+
# In plan mode: only register ask_followup_question for clarifications
|
|
151
|
+
# In code mode: register all task tools
|
|
152
|
+
if self.plan_mode:
|
|
153
|
+
self._register_plan_mode_task_tools()
|
|
154
|
+
else:
|
|
134
155
|
self._register_task_tools()
|
|
135
156
|
|
|
136
157
|
# Register spec planning tools (only in plan mode)
|
|
@@ -162,21 +183,32 @@ class AgentToolkit:
|
|
|
162
183
|
def _register_mode_tools(self) -> None:
|
|
163
184
|
"""Register mode switching tools.
|
|
164
185
|
|
|
165
|
-
-
|
|
166
|
-
- exit_plan: Available in
|
|
186
|
+
- enter_plan_mode: Available in code mode to request entering plan mode
|
|
187
|
+
- exit_plan: Available in both modes to submit plan for approval
|
|
167
188
|
- get_mode: Always available to check current mode
|
|
168
189
|
"""
|
|
169
|
-
from .tools.modes import
|
|
190
|
+
from .tools.modes import EnterPlanModeTool, ExitPlanModeTool, GetModeTool
|
|
170
191
|
|
|
171
192
|
# get_mode is always available
|
|
172
193
|
self.register_tool(GetModeTool())
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
# exit_plan is available in both modes:
|
|
196
|
+
# - In plan mode: submit plan written to plan file
|
|
197
|
+
# - In code mode: submit plan received from Plan subagent
|
|
198
|
+
self.register_tool(ExitPlanModeTool())
|
|
199
|
+
|
|
200
|
+
if not self.plan_mode:
|
|
201
|
+
# In code mode: can also request to enter plan mode
|
|
202
|
+
self.register_tool(EnterPlanModeTool())
|
|
203
|
+
|
|
204
|
+
def _register_plan_mode_task_tools(self) -> None:
|
|
205
|
+
"""Register subset of task tools for plan mode.
|
|
206
|
+
|
|
207
|
+
In plan mode, the agent can ask clarifying questions but
|
|
208
|
+
doesn't need completion/todo tools since exit_plan handles that.
|
|
209
|
+
"""
|
|
210
|
+
from .tools.tasks import AskFollowupQuestionTool
|
|
211
|
+
self.register_tool(AskFollowupQuestionTool())
|
|
180
212
|
|
|
181
213
|
def _register_task_tools(self) -> None:
|
|
182
214
|
"""Register task management tools.
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Provides specialized toolkits for different agent types.
|
|
4
4
|
Each toolkit contains a curated set of tools appropriate for the agent's purpose.
|
|
5
|
+
|
|
6
|
+
Custom agents from .emdash/agents/*.md are also supported and use the Explore toolkit
|
|
7
|
+
by default (unless they specify different tools in their frontmatter).
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Dict, Type
|
|
11
|
+
from typing import TYPE_CHECKING, Dict, Type, Optional
|
|
9
12
|
|
|
10
13
|
if TYPE_CHECKING:
|
|
11
14
|
from .base import BaseToolkit
|
|
@@ -20,45 +23,141 @@ TOOLKIT_REGISTRY: Dict[str, str] = {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
def _get_custom_agents(repo_root: Optional[Path] = None) -> dict:
|
|
27
|
+
"""Load custom agents from .emdash/agents/ directory.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
repo_root: Repository root (defaults to cwd)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dict mapping agent name to CustomAgent
|
|
34
|
+
"""
|
|
35
|
+
from ..agents import load_agents
|
|
36
|
+
from ...utils.logger import log
|
|
37
|
+
|
|
38
|
+
agents_dir = (repo_root or Path.cwd()) / ".emdash" / "agents"
|
|
39
|
+
log.debug(f"Loading custom agents from: {agents_dir} (exists={agents_dir.exists()})")
|
|
40
|
+
agents = load_agents(agents_dir)
|
|
41
|
+
log.debug(f"Loaded custom agents: {list(agents.keys())}")
|
|
42
|
+
return agents
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
def get_toolkit(subagent_type: str, repo_root: Path) -> "BaseToolkit":
|
|
24
46
|
"""Get toolkit for agent type.
|
|
25
47
|
|
|
26
48
|
Args:
|
|
27
|
-
subagent_type: Type of agent (e.g., "Explore", "Plan")
|
|
49
|
+
subagent_type: Type of agent (e.g., "Explore", "Plan", or custom agent name)
|
|
28
50
|
repo_root: Root directory of the repository
|
|
29
51
|
|
|
30
52
|
Returns:
|
|
31
53
|
Toolkit instance
|
|
32
54
|
|
|
33
55
|
Raises:
|
|
34
|
-
ValueError: If agent type is not registered
|
|
56
|
+
ValueError: If agent type is not registered or found
|
|
57
|
+
"""
|
|
58
|
+
# Check built-in agents first
|
|
59
|
+
if subagent_type in TOOLKIT_REGISTRY:
|
|
60
|
+
import importlib
|
|
61
|
+
module_path, class_name = TOOLKIT_REGISTRY[subagent_type].rsplit(":", 1)
|
|
62
|
+
module = importlib.import_module(module_path)
|
|
63
|
+
toolkit_class = getattr(module, class_name)
|
|
64
|
+
return toolkit_class(repo_root)
|
|
65
|
+
|
|
66
|
+
# Check custom agents
|
|
67
|
+
custom_agents = _get_custom_agents(repo_root)
|
|
68
|
+
if subagent_type in custom_agents:
|
|
69
|
+
# Custom agents use Explore toolkit by default (read-only, safe)
|
|
70
|
+
# This gives them: glob, grep, read_file, list_files, semantic_search
|
|
71
|
+
# Plus any MCP servers defined in the agent's frontmatter
|
|
72
|
+
import importlib
|
|
73
|
+
from ...utils.logger import log
|
|
74
|
+
|
|
75
|
+
custom_agent = custom_agents[subagent_type]
|
|
76
|
+
module_path, class_name = TOOLKIT_REGISTRY["Explore"].rsplit(":", 1)
|
|
77
|
+
module = importlib.import_module(module_path)
|
|
78
|
+
toolkit_class = getattr(module, class_name)
|
|
79
|
+
|
|
80
|
+
# Pass MCP servers if defined
|
|
81
|
+
mcp_servers = custom_agent.mcp_servers if custom_agent.mcp_servers else None
|
|
82
|
+
if mcp_servers:
|
|
83
|
+
log.info(f"Custom agent '{subagent_type}' has {len(mcp_servers)} MCP servers")
|
|
84
|
+
|
|
85
|
+
return toolkit_class(repo_root, mcp_servers=mcp_servers)
|
|
86
|
+
|
|
87
|
+
# Not found
|
|
88
|
+
available = list_agent_types(repo_root)
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Unknown agent type: {subagent_type}. Available: {available}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def list_agent_types(repo_root: Optional[Path] = None) -> list[str]:
|
|
95
|
+
"""List all available agent types (built-in + custom).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
repo_root: Repository root for finding custom agents
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of agent type names
|
|
35
102
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
raise ValueError(
|
|
39
|
-
f"Unknown agent type: {subagent_type}. Available: {available}"
|
|
40
|
-
)
|
|
103
|
+
# Start with built-in agents
|
|
104
|
+
types = list(TOOLKIT_REGISTRY.keys())
|
|
41
105
|
|
|
42
|
-
#
|
|
43
|
-
|
|
106
|
+
# Add custom agents
|
|
107
|
+
custom_agents = _get_custom_agents(repo_root)
|
|
108
|
+
for name in custom_agents.keys():
|
|
109
|
+
if name not in types:
|
|
110
|
+
types.append(name)
|
|
44
111
|
|
|
45
|
-
|
|
46
|
-
module = importlib.import_module(module_path)
|
|
47
|
-
toolkit_class = getattr(module, class_name)
|
|
48
|
-
return toolkit_class(repo_root)
|
|
112
|
+
return types
|
|
49
113
|
|
|
50
114
|
|
|
51
|
-
def
|
|
52
|
-
"""
|
|
115
|
+
def get_agents_with_descriptions(repo_root: Optional[Path] = None) -> list[dict]:
|
|
116
|
+
"""Get all agents with their names and descriptions.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
repo_root: Repository root for finding custom agents
|
|
53
120
|
|
|
54
121
|
Returns:
|
|
55
|
-
List of
|
|
122
|
+
List of dicts with 'name' and 'description' keys
|
|
123
|
+
"""
|
|
124
|
+
from ..prompts.subagents import BUILTIN_AGENTS
|
|
125
|
+
|
|
126
|
+
agents = []
|
|
127
|
+
|
|
128
|
+
# Built-in agents
|
|
129
|
+
for name, description in BUILTIN_AGENTS.items():
|
|
130
|
+
agents.append({"name": name, "description": description})
|
|
131
|
+
|
|
132
|
+
# Custom agents
|
|
133
|
+
custom_agents = _get_custom_agents(repo_root)
|
|
134
|
+
for name, agent in custom_agents.items():
|
|
135
|
+
agents.append({
|
|
136
|
+
"name": name,
|
|
137
|
+
"description": agent.description or "Custom agent"
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return agents
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_custom_agent(name: str, repo_root: Optional[Path] = None):
|
|
144
|
+
"""Get a specific custom agent by name.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Agent name
|
|
148
|
+
repo_root: Repository root
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
CustomAgent or None
|
|
56
152
|
"""
|
|
57
|
-
|
|
153
|
+
custom_agents = _get_custom_agents(repo_root)
|
|
154
|
+
return custom_agents.get(name)
|
|
58
155
|
|
|
59
156
|
|
|
60
157
|
__all__ = [
|
|
61
158
|
"get_toolkit",
|
|
62
159
|
"list_agent_types",
|
|
160
|
+
"get_agents_with_descriptions",
|
|
161
|
+
"get_custom_agent",
|
|
63
162
|
"TOOLKIT_REGISTRY",
|
|
64
163
|
]
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
6
|
|
|
7
7
|
from ..tools.base import BaseTool, ToolResult
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..agents import AgentMCPServerConfig
|
|
11
|
+
from ..mcp.manager import MCPServerManager
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
class BaseToolkit(ABC):
|
|
11
15
|
"""Abstract base class for sub-agent toolkits.
|
|
@@ -15,20 +19,30 @@ class BaseToolkit(ABC):
|
|
|
15
19
|
- Registering appropriate tools
|
|
16
20
|
- Providing OpenAI function schemas
|
|
17
21
|
- Executing tools by name
|
|
22
|
+
- Managing per-agent MCP servers (optional)
|
|
18
23
|
"""
|
|
19
24
|
|
|
20
25
|
# List of tool names this toolkit provides (for documentation)
|
|
21
26
|
TOOLS: list[str] = []
|
|
22
27
|
|
|
23
|
-
def __init__(
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
repo_root: Path,
|
|
31
|
+
mcp_servers: Optional[list["AgentMCPServerConfig"]] = None,
|
|
32
|
+
):
|
|
24
33
|
"""Initialize the toolkit.
|
|
25
34
|
|
|
26
35
|
Args:
|
|
27
36
|
repo_root: Root directory of the repository
|
|
37
|
+
mcp_servers: Optional list of MCP server configurations for this agent
|
|
28
38
|
"""
|
|
29
39
|
self.repo_root = repo_root.resolve()
|
|
30
40
|
self._tools: dict[str, BaseTool] = {}
|
|
41
|
+
self._mcp_manager: Optional["MCPServerManager"] = None
|
|
42
|
+
self._mcp_servers_config = mcp_servers or []
|
|
43
|
+
|
|
31
44
|
self._register_tools()
|
|
45
|
+
self._init_mcp_servers()
|
|
32
46
|
|
|
33
47
|
@abstractmethod
|
|
34
48
|
def _register_tools(self) -> None:
|
|
@@ -38,6 +52,77 @@ class BaseToolkit(ABC):
|
|
|
38
52
|
"""
|
|
39
53
|
pass
|
|
40
54
|
|
|
55
|
+
def _init_mcp_servers(self) -> None:
|
|
56
|
+
"""Initialize MCP servers for this agent if configured.
|
|
57
|
+
|
|
58
|
+
Creates an MCPServerManager with the agent's MCP server configs
|
|
59
|
+
and registers the tools from those servers. Only enabled servers
|
|
60
|
+
are started.
|
|
61
|
+
"""
|
|
62
|
+
if not self._mcp_servers_config:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Filter to only enabled servers
|
|
66
|
+
enabled_servers = [s for s in self._mcp_servers_config if s.enabled]
|
|
67
|
+
if not enabled_servers:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
from ..mcp.config import MCPServerConfig, MCPConfigFile
|
|
71
|
+
from ..mcp.manager import MCPServerManager
|
|
72
|
+
from ..mcp.tool_factory import create_tools_from_mcp
|
|
73
|
+
from ...utils.logger import log
|
|
74
|
+
|
|
75
|
+
log.info(f"Initializing {len(enabled_servers)} MCP servers for agent")
|
|
76
|
+
|
|
77
|
+
# Create a temporary config file object with our servers
|
|
78
|
+
config = MCPConfigFile()
|
|
79
|
+
for server_cfg in enabled_servers:
|
|
80
|
+
mcp_config = MCPServerConfig(
|
|
81
|
+
name=server_cfg.name,
|
|
82
|
+
command=server_cfg.command,
|
|
83
|
+
args=server_cfg.args,
|
|
84
|
+
env=server_cfg.env,
|
|
85
|
+
enabled=True,
|
|
86
|
+
timeout=server_cfg.timeout,
|
|
87
|
+
)
|
|
88
|
+
config.add_server(mcp_config)
|
|
89
|
+
|
|
90
|
+
# Create manager with in-memory config (not from file)
|
|
91
|
+
self._mcp_manager = MCPServerManager(repo_root=self.repo_root)
|
|
92
|
+
self._mcp_manager._config = config # Inject our config directly
|
|
93
|
+
|
|
94
|
+
# Start all servers and register tools
|
|
95
|
+
try:
|
|
96
|
+
started = self._mcp_manager.start_all_enabled()
|
|
97
|
+
log.info(f"Started MCP servers for agent: {started}")
|
|
98
|
+
|
|
99
|
+
# Create tool wrappers and register them
|
|
100
|
+
mcp_tools = create_tools_from_mcp(self._mcp_manager)
|
|
101
|
+
for tool in mcp_tools:
|
|
102
|
+
self.register_tool(tool)
|
|
103
|
+
log.debug(f"Registered MCP tool: {tool.name}")
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
log.warning(f"Failed to initialize MCP servers for agent: {e}")
|
|
107
|
+
|
|
108
|
+
def shutdown(self) -> None:
|
|
109
|
+
"""Shutdown the toolkit and cleanup resources.
|
|
110
|
+
|
|
111
|
+
Stops any running MCP servers.
|
|
112
|
+
"""
|
|
113
|
+
if self._mcp_manager:
|
|
114
|
+
from ...utils.logger import log
|
|
115
|
+
log.info("Shutting down agent MCP servers")
|
|
116
|
+
self._mcp_manager.shutdown_all()
|
|
117
|
+
self._mcp_manager = None
|
|
118
|
+
|
|
119
|
+
def __del__(self):
|
|
120
|
+
"""Cleanup on garbage collection."""
|
|
121
|
+
try:
|
|
122
|
+
self.shutdown()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
41
126
|
def register_tool(self, tool: BaseTool) -> None:
|
|
42
127
|
"""Register a tool.
|
|
43
128
|
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"""Explorer toolkit - read-only tools for fast codebase exploration."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
5
|
|
|
5
6
|
from .base import BaseToolkit
|
|
6
7
|
from ..tools.coding import ReadFileTool, ListFilesTool
|
|
7
8
|
from ..tools.search import SemanticSearchTool, GrepTool, GlobTool
|
|
8
9
|
from ...utils.logger import log
|
|
9
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..agents import AgentMCPServerConfig
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class ExploreToolkit(BaseToolkit):
|
|
12
16
|
"""Read-only toolkit for fast codebase exploration.
|
|
@@ -16,6 +20,7 @@ class ExploreToolkit(BaseToolkit):
|
|
|
16
20
|
- Listing directory contents
|
|
17
21
|
- Searching with patterns (grep, glob)
|
|
18
22
|
- Semantic code search
|
|
23
|
+
- MCP server tools (if configured)
|
|
19
24
|
|
|
20
25
|
All tools are read-only - no file modifications allowed.
|
|
21
26
|
"""
|
|
@@ -28,6 +33,19 @@ class ExploreToolkit(BaseToolkit):
|
|
|
28
33
|
"semantic_search",
|
|
29
34
|
]
|
|
30
35
|
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
repo_root: Path,
|
|
39
|
+
mcp_servers: Optional[list["AgentMCPServerConfig"]] = None,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize the explore toolkit.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repo_root: Root directory of the repository
|
|
45
|
+
mcp_servers: Optional MCP server configurations for this agent
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(repo_root, mcp_servers=mcp_servers)
|
|
48
|
+
|
|
31
49
|
def _register_tools(self) -> None:
|
|
32
50
|
"""Register read-only exploration tools."""
|
|
33
51
|
# File reading
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
"""Plan toolkit - exploration tools
|
|
1
|
+
"""Plan toolkit - read-only exploration tools for planning.
|
|
2
|
+
|
|
3
|
+
The Plan subagent explores the codebase and returns a plan as text.
|
|
4
|
+
The main agent (in plan mode) writes the plan to .emdash/<feature>.md.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
9
|
|
|
5
10
|
from .base import BaseToolkit
|
|
6
11
|
from ..tools.coding import ReadFileTool, ListFilesTool
|
|
7
12
|
from ..tools.search import SemanticSearchTool, GrepTool, GlobTool
|
|
8
|
-
from ..tools.plan_write import WritePlanTool
|
|
9
13
|
from ...utils.logger import log
|
|
10
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..agents import AgentMCPServerConfig
|
|
17
|
+
|
|
11
18
|
|
|
12
19
|
class PlanToolkit(BaseToolkit):
|
|
13
|
-
"""
|
|
20
|
+
"""Read-only toolkit for Plan subagent.
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
The Plan subagent explores the codebase and returns a structured plan.
|
|
23
|
+
It does NOT write files - the main agent handles that.
|
|
17
24
|
|
|
18
25
|
Tools available:
|
|
19
26
|
- read_file: Read file contents
|
|
@@ -21,7 +28,7 @@ class PlanToolkit(BaseToolkit):
|
|
|
21
28
|
- glob: Find files by pattern
|
|
22
29
|
- grep: Search file contents
|
|
23
30
|
- semantic_search: AI-powered code search
|
|
24
|
-
-
|
|
31
|
+
- MCP server tools (if configured)
|
|
25
32
|
"""
|
|
26
33
|
|
|
27
34
|
TOOLS = [
|
|
@@ -30,11 +37,23 @@ class PlanToolkit(BaseToolkit):
|
|
|
30
37
|
"glob",
|
|
31
38
|
"grep",
|
|
32
39
|
"semantic_search",
|
|
33
|
-
"write_plan",
|
|
34
40
|
]
|
|
35
41
|
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
repo_root: Path,
|
|
45
|
+
mcp_servers: Optional[list["AgentMCPServerConfig"]] = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize the plan toolkit.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
repo_root: Root directory of the repository
|
|
51
|
+
mcp_servers: Optional MCP server configurations for this agent
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(repo_root, mcp_servers=mcp_servers)
|
|
54
|
+
|
|
36
55
|
def _register_tools(self) -> None:
|
|
37
|
-
"""Register exploration
|
|
56
|
+
"""Register read-only exploration tools."""
|
|
38
57
|
# All read-only exploration tools
|
|
39
58
|
self.register_tool(ReadFileTool(repo_root=self.repo_root))
|
|
40
59
|
self.register_tool(ListFilesTool(repo_root=self.repo_root))
|
|
@@ -49,7 +68,4 @@ class PlanToolkit(BaseToolkit):
|
|
|
49
68
|
except Exception as e:
|
|
50
69
|
log.debug(f"Semantic search not available: {e}")
|
|
51
70
|
|
|
52
|
-
# Special: can only write to .emdash/plans/*.md
|
|
53
|
-
self.register_tool(WritePlanTool(repo_root=self.repo_root))
|
|
54
|
-
|
|
55
71
|
log.debug(f"PlanToolkit registered {len(self._tools)} tools")
|
|
@@ -43,7 +43,7 @@ from .tasks import (
|
|
|
43
43
|
from .plan import PlanExplorationTool
|
|
44
44
|
|
|
45
45
|
# Mode tools
|
|
46
|
-
from .modes import AgentMode, ModeState,
|
|
46
|
+
from .modes import AgentMode, ModeState, EnterPlanModeTool, ExitPlanModeTool, GetModeTool
|
|
47
47
|
|
|
48
48
|
# Spec tools
|
|
49
49
|
from .spec import SubmitSpecTool, GetSpecTool, UpdateSpecTool
|
|
@@ -111,7 +111,7 @@ __all__ = [
|
|
|
111
111
|
# Mode
|
|
112
112
|
"AgentMode",
|
|
113
113
|
"ModeState",
|
|
114
|
-
"
|
|
114
|
+
"EnterPlanModeTool",
|
|
115
115
|
"ExitPlanModeTool",
|
|
116
116
|
"GetModeTool",
|
|
117
117
|
# Spec
|