emdash-core 0.1.33__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.
@@ -37,12 +37,17 @@ def build_system_prompt(toolkit) -> str:
37
37
  Complete system prompt string
38
38
  """
39
39
  tools_section = build_tools_section(toolkit)
40
+ agents_section = build_agents_section(toolkit)
40
41
  skills_section = build_skills_section()
41
42
  rules_section = build_rules_section()
42
43
 
43
44
  # Main agent always uses the same prompt - it orchestrates and delegates
44
45
  prompt = BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
45
46
 
47
+ # Add agents section so main agent knows what agents are available
48
+ if agents_section:
49
+ prompt += "\n" + agents_section
50
+
46
51
  # Add rules section if there are rules defined
47
52
  if rules_section:
48
53
  prompt += "\n" + rules_section
@@ -80,6 +85,36 @@ def build_skills_section() -> str:
80
85
  return registry.get_skills_for_prompt()
81
86
 
82
87
 
88
+ def build_agents_section(toolkit) -> str:
89
+ """Build the agents section describing available sub-agents.
90
+
91
+ Args:
92
+ toolkit: The agent toolkit (to access repo_root)
93
+
94
+ Returns:
95
+ Formatted string with agent descriptions, or empty string if none
96
+ """
97
+ from ..toolkits import get_agents_with_descriptions
98
+
99
+ repo_root = getattr(toolkit, '_repo_root', None)
100
+ agents = get_agents_with_descriptions(repo_root)
101
+
102
+ if not agents:
103
+ return ""
104
+
105
+ lines = [
106
+ "## Available Agents",
107
+ "",
108
+ "Use the `task` tool to delegate work to these specialized agents:",
109
+ "",
110
+ ]
111
+
112
+ for agent in agents:
113
+ lines.append(f"- **{agent['name']}**: {agent['description']}")
114
+
115
+ return "\n".join(lines)
116
+
117
+
83
118
  def build_tools_section(toolkit) -> str:
84
119
  """Build the tools section of the system prompt from registered tools.
85
120
 
@@ -178,12 +178,19 @@ SUBAGENT_PROMPTS = {
178
178
  "Research": RESEARCH_PROMPT,
179
179
  }
180
180
 
181
+ # Built-in agent descriptions (for main agent's system prompt)
182
+ BUILTIN_AGENTS = {
183
+ "Explore": "Fast codebase exploration - searches files, reads code, finds patterns",
184
+ "Plan": "Designs implementation plans - analyzes architecture, writes to .emdash/plans/",
185
+ }
186
+
181
187
 
182
- def get_subagent_prompt(subagent_type: str) -> str:
188
+ def get_subagent_prompt(subagent_type: str, repo_root=None) -> str:
183
189
  """Get the system prompt for a sub-agent type.
184
190
 
185
191
  Args:
186
- subagent_type: Type of agent (e.g., "Explore", "Plan")
192
+ subagent_type: Type of agent (e.g., "Explore", "Plan", or custom agent name)
193
+ repo_root: Repository root for finding custom agents
187
194
 
188
195
  Returns:
189
196
  System prompt string
@@ -191,9 +198,18 @@ def get_subagent_prompt(subagent_type: str) -> str:
191
198
  Raises:
192
199
  ValueError: If agent type is not known
193
200
  """
194
- if subagent_type not in SUBAGENT_PROMPTS:
195
- available = list(SUBAGENT_PROMPTS.keys())
196
- raise ValueError(
197
- f"Unknown agent type: {subagent_type}. Available: {available}"
198
- )
199
- return SUBAGENT_PROMPTS[subagent_type]
201
+ # Check built-in agents first
202
+ if subagent_type in SUBAGENT_PROMPTS:
203
+ return SUBAGENT_PROMPTS[subagent_type]
204
+
205
+ # Check custom agents
206
+ from ..toolkits import get_custom_agent
207
+ custom_agent = get_custom_agent(subagent_type, repo_root)
208
+ if custom_agent:
209
+ return custom_agent.system_prompt
210
+
211
+ # Not found
212
+ available = list(SUBAGENT_PROMPTS.keys())
213
+ raise ValueError(
214
+ f"Unknown agent type: {subagent_type}. Available: {available}"
215
+ )
@@ -104,6 +104,12 @@ glob("**/pages/**/*.astro")
104
104
  **Plan agent**: Implementation tasks that modify code
105
105
  - New features, refactoring, architectural changes
106
106
  - NOT for research/reading tasks
107
+
108
+ **Custom agents** (from `.emdash/agents/*.md`):
109
+ - User-defined specialized agents with custom system prompts
110
+ - Spawned via `task(subagent_type="<agent-name>", prompt="...")`
111
+ - Use the same tools as Explore agent (read-only by default)
112
+ - Examples: security-audit, api-review, test-generator
107
113
  """
108
114
 
109
115
  # Exploration strategy for code navigation
@@ -311,40 +317,48 @@ SIZING_GUIDELINES = """
311
317
 
312
318
  # Todo list usage guidance
313
319
  TODO_LIST_GUIDANCE = """
314
- ## Todo List Usage
320
+ ## Todo List Usage - USE PROACTIVELY
315
321
 
316
- You have access to `write_todo` and `update_todo_list` tools. Use them strategically - not for every task.
322
+ You have access to `write_todo` and `update_todo_list` tools. **Use them frequently** to track progress and give the user visibility into what you're doing.
317
323
 
318
- ### When to USE the todo list:
319
- - **3+ distinct steps** needed to complete the task
324
+ ### ALWAYS use the todo list when:
325
+ - **2+ distinct steps** needed to complete the task
320
326
  - **Multiple files** need to be changed
321
327
  - **User gives a list** of tasks (numbered or comma-separated)
322
- - **Complex feature** implementation with multiple pieces
323
- - **Need to track progress** across iterations or when task spans multiple tool calls
328
+ - **Any implementation task** that isn't trivial
329
+ - **Multi-step debugging** or investigation
330
+ - **Before starting work** - plan out what you'll do
331
+
332
+ ### Benefits of using todos:
333
+ - User can see your progress in real-time
334
+ - Helps you stay organized on complex tasks
335
+ - Creates a clear record of what was done
336
+ - Prevents forgetting steps in multi-part tasks
324
337
 
325
- ### When to SKIP the todo list:
326
- - **Single focused change** (one edit, one file)
327
- - **Trivial fixes** (typo, add a log statement)
328
- - **Research/informational questions** (just answer them)
329
- - **Task completes in 1-2 steps** (just do it)
338
+ ### Only SKIP the todo list for:
339
+ - **Truly trivial fixes** (single typo, one-line change)
340
+ - **Simple questions** that need only a text answer
341
+ - **Reading a single file** when asked
330
342
 
331
343
  ### Examples:
332
344
 
333
345
  **Use todo list:**
334
- - "Implement user authentication with login, logout, and session management" → 3+ steps, multiple files
335
- - "Fix these 5 type errors" → list of tasks
336
- - "Add dark mode support across the app" complex, multiple files
346
+ - "Fix the login bug" → investigate, identify cause, fix, verify
347
+ - "Add a new API endpoint" → create route, handler, types, tests
348
+ - "Update the config" read current, plan changes, update, verify
349
+ - "Implement dark mode" → multiple files, multiple steps
337
350
 
338
351
  **Skip todo list:**
339
- - "Fix the typo in README" → single focused change
340
- - "Add tool_choice parameter to this function" → one edit
341
- - "What files handle routing?" → informational question
342
- - "Update the error message here" → trivial fix
352
+ - "What does this function do?" → just read and answer
353
+ - "Fix typo in line 5" → single trivial edit
343
354
 
344
355
  ### Usage pattern:
345
- 1. Use `write_todo(title="...", reset=true)` to start fresh with first task
346
- 2. Use `write_todo(title="...")` to add more tasks
347
- 3. Use `update_todo_list(task_id="1", status="in_progress")` when starting a task
348
- 4. Use `update_todo_list(task_id="1", status="completed")` when done
349
- 5. Mark tasks complete IMMEDIATELY after finishing - don't batch completions
356
+ 1. **Start immediately**: Use `write_todo(title="...", reset=true)` as soon as you understand the task
357
+ 2. **Add all steps**: Use `write_todo(title="...")` to add each step you'll take
358
+ 3. **Mark in_progress**: Use `update_todo_list(task_id="1", status="in_progress")` BEFORE starting each task
359
+ 4. **Mark completed**: Use `update_todo_list(task_id="1", status="completed")` IMMEDIATELY after finishing
360
+ 5. **Never batch**: Mark each task complete right away, don't wait
361
+
362
+ ### When in doubt, USE THE TODO LIST
363
+ It's better to over-track than under-track. The user appreciates seeing progress.
350
364
  """
@@ -107,6 +107,8 @@ class AgentRunner(PlanMixin):
107
107
  self._tools_used_this_run: set[str] = set()
108
108
  # Plan approval state (from PlanMixin)
109
109
  self._pending_plan: Optional[dict] = None
110
+ # Callback for autosave after each iteration (set by API layer)
111
+ self._on_iteration_callback: Optional[callable] = None
110
112
 
111
113
  def _get_default_plan_file_path(self) -> str:
112
114
  """Get the default plan file path based on repo root.
@@ -504,6 +506,9 @@ class AgentRunner(PlanMixin):
504
506
  "content": result_json,
505
507
  })
506
508
 
509
+ # Emit context frame after each iteration (for autosave and UI updates)
510
+ self._emit_context_frame(messages)
511
+
507
512
  # If a clarification question was asked, pause and wait for user input
508
513
  if needs_user_input:
509
514
  log.debug("Pausing agent loop - waiting for user input")
@@ -644,6 +649,13 @@ DO NOT output more text. Use a tool NOW.""",
644
649
  total_output_tokens=self._total_output_tokens,
645
650
  )
646
651
 
652
+ # Call iteration callback for autosave if set
653
+ if self._on_iteration_callback and messages:
654
+ try:
655
+ self._on_iteration_callback(messages)
656
+ except Exception as e:
657
+ log.debug(f"Iteration callback failed: {e}")
658
+
647
659
  def chat(self, message: str, images: Optional[list] = None) -> str:
648
660
  """Continue a conversation with a new message.
649
661
 
@@ -4,6 +4,7 @@ This module contains functions for estimating, compacting, and managing
4
4
  conversation context during agent runs.
5
5
  """
6
6
 
7
+ import os
7
8
  from typing import Optional, TYPE_CHECKING
8
9
 
9
10
  from ...utils.logger import log
@@ -299,7 +300,7 @@ def get_reranked_context(
299
300
  # Get exploration steps for context extraction
300
301
  steps = toolkit.get_exploration_steps()
301
302
  if not steps:
302
- return {"item_count": 0, "items": []}
303
+ return {"item_count": 0, "items": [], "query": current_query, "debug": "no exploration steps"}
303
304
 
304
305
  # Use context service to extract context items from exploration
305
306
  service = ContextService(connection=toolkit.connection)
@@ -314,34 +315,52 @@ def get_reranked_context(
314
315
  # Get context items
315
316
  items = service.get_context_items(terminal_id)
316
317
  if not items:
317
- return {"item_count": 0, "items": []}
318
+ return {"item_count": 0, "items": [], "query": current_query, "debug": f"no items from service ({len(steps)} steps)"}
319
+
320
+ # Get max tokens from env (default 2000)
321
+ max_tokens = int(os.getenv("CONTEXT_FRAME_MAX_TOKENS", "2000"))
318
322
 
319
323
  # Rerank by query relevance
320
324
  if current_query:
321
325
  items = rerank_context_items(
322
326
  items,
323
327
  current_query,
324
- top_k=20,
328
+ top_k=50, # Get more candidates, then filter by tokens
325
329
  )
326
330
 
327
- # Convert to serializable format
331
+ # Convert to serializable format, limiting by token count
328
332
  result_items = []
329
- for item in items[:20]: # Limit to 20 items
330
- result_items.append({
333
+ total_tokens = 0
334
+ for item in items:
335
+ item_dict = {
331
336
  "name": item.qualified_name,
332
337
  "type": item.entity_type,
333
338
  "file": item.file_path,
334
339
  "score": round(item.score, 3) if hasattr(item, 'score') else None,
335
- })
340
+ "description": item.description[:200] if item.description else None,
341
+ "touch_count": item.touch_count,
342
+ "neighbors": item.neighbors[:5] if item.neighbors else [],
343
+ }
344
+ # Estimate tokens for this item (~4 chars per token)
345
+ item_chars = len(str(item_dict))
346
+ item_tokens = item_chars // 4
347
+
348
+ if total_tokens + item_tokens > max_tokens:
349
+ break
350
+
351
+ result_items.append(item_dict)
352
+ total_tokens += item_tokens
336
353
 
337
354
  return {
338
355
  "item_count": len(result_items),
339
356
  "items": result_items,
357
+ "query": current_query,
358
+ "total_tokens": total_tokens,
340
359
  }
341
360
 
342
361
  except Exception as e:
343
- log.debug(f"Failed to get reranked context: {e}")
344
- return {"item_count": 0, "items": []}
362
+ log.warning(f"Failed to get reranked context: {e}")
363
+ return {"item_count": 0, "items": [], "query": current_query, "debug": str(e)}
345
364
 
346
365
 
347
366
  def emit_context_frame(
@@ -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