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.
Files changed (39) hide show
  1. emdash_core/agent/__init__.py +4 -0
  2. emdash_core/agent/agents.py +84 -23
  3. emdash_core/agent/events.py +42 -20
  4. emdash_core/agent/hooks.py +419 -0
  5. emdash_core/agent/inprocess_subagent.py +166 -18
  6. emdash_core/agent/prompts/__init__.py +4 -3
  7. emdash_core/agent/prompts/main_agent.py +67 -2
  8. emdash_core/agent/prompts/plan_mode.py +236 -107
  9. emdash_core/agent/prompts/subagents.py +103 -23
  10. emdash_core/agent/prompts/workflow.py +159 -26
  11. emdash_core/agent/providers/factory.py +2 -2
  12. emdash_core/agent/providers/openai_provider.py +67 -15
  13. emdash_core/agent/runner/__init__.py +49 -0
  14. emdash_core/agent/runner/agent_runner.py +765 -0
  15. emdash_core/agent/runner/context.py +470 -0
  16. emdash_core/agent/runner/factory.py +108 -0
  17. emdash_core/agent/runner/plan.py +217 -0
  18. emdash_core/agent/runner/sdk_runner.py +324 -0
  19. emdash_core/agent/runner/utils.py +67 -0
  20. emdash_core/agent/skills.py +47 -8
  21. emdash_core/agent/toolkit.py +46 -14
  22. emdash_core/agent/toolkits/__init__.py +117 -18
  23. emdash_core/agent/toolkits/base.py +87 -2
  24. emdash_core/agent/toolkits/explore.py +18 -0
  25. emdash_core/agent/toolkits/plan.py +27 -11
  26. emdash_core/agent/tools/__init__.py +2 -2
  27. emdash_core/agent/tools/coding.py +48 -4
  28. emdash_core/agent/tools/modes.py +151 -143
  29. emdash_core/agent/tools/task.py +52 -6
  30. emdash_core/api/agent.py +706 -1
  31. emdash_core/ingestion/repository.py +17 -198
  32. emdash_core/models/agent.py +4 -0
  33. emdash_core/skills/frontend-design/SKILL.md +56 -0
  34. emdash_core/sse/stream.py +4 -0
  35. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/METADATA +4 -1
  36. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/RECORD +38 -30
  37. emdash_core/agent/runner.py +0 -1123
  38. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
  39. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/entry_points.txt +0 -0
@@ -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 .emdash/skills/ directory.
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
- if not skills_dir.exists():
109
- return {}
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
- log.debug(f"Loaded skill: {skill.name}")
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]:
@@ -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 (only in non-plan mode)
114
- if not self.plan_mode:
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 (not in plan mode)
133
- if not self.plan_mode:
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
- - enter_mode: Available in code mode to enter other modes (e.g., plan)
166
- - exit_plan: Available in plan mode to submit plan and request approval
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 EnterModeTool, ExitPlanModeTool, GetModeTool
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
- if self.plan_mode:
175
- # In plan mode: can exit with plan submission
176
- self.register_tool(ExitPlanModeTool())
177
- else:
178
- # In code mode: can enter other modes
179
- self.register_tool(EnterModeTool())
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
- if subagent_type not in TOOLKIT_REGISTRY:
37
- available = list(TOOLKIT_REGISTRY.keys())
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
- # Import lazily to avoid circular imports
43
- import importlib
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
- module_path, class_name = TOOLKIT_REGISTRY[subagent_type].rsplit(":", 1)
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 list_agent_types() -> list[str]:
52
- """List all available agent types.
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 agent type names
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
- return list(TOOLKIT_REGISTRY.keys())
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__(self, repo_root: Path):
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 plus plan writing capability."""
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
- """Toolkit for planning with limited write access (plan files only).
20
+ """Read-only toolkit for Plan subagent.
14
21
 
15
- Provides all read-only exploration tools plus the ability to write
16
- implementation plans to .emdash/plans/*.md.
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
- - write_plan: Write implementation plans (restricted to .emdash/plans/)
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 and plan writing tools."""
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, EnterModeTool, ExitPlanModeTool, GetModeTool
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
- "EnterModeTool",
114
+ "EnterPlanModeTool",
115
115
  "ExitPlanModeTool",
116
116
  "GetModeTool",
117
117
  # Spec