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.
- emdash_core/agent/agents.py +84 -23
- emdash_core/agent/hooks.py +419 -0
- emdash_core/agent/inprocess_subagent.py +44 -9
- emdash_core/agent/prompts/main_agent.py +35 -0
- emdash_core/agent/prompts/subagents.py +24 -8
- emdash_core/agent/prompts/workflow.py +37 -23
- emdash_core/agent/runner/agent_runner.py +12 -0
- emdash_core/agent/runner/context.py +28 -9
- 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 +18 -0
- emdash_core/agent/tools/task.py +11 -4
- emdash_core/api/agent.py +154 -3
- emdash_core/ingestion/repository.py +17 -198
- emdash_core/models/agent.py +4 -0
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/METADATA +3 -1
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/RECORD +20 -19
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
-
###
|
|
319
|
-
- **
|
|
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
|
-
- **
|
|
323
|
-
- **
|
|
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
|
-
###
|
|
326
|
-
- **
|
|
327
|
-
- **
|
|
328
|
-
- **
|
|
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
|
-
- "
|
|
335
|
-
- "
|
|
336
|
-
- "
|
|
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
|
-
- "
|
|
340
|
-
- "
|
|
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)`
|
|
346
|
-
2. Use `write_todo(title="...")` to add
|
|
347
|
-
3. Use `update_todo_list(task_id="1", status="in_progress")`
|
|
348
|
-
4. Use `update_todo_list(task_id="1", status="completed")`
|
|
349
|
-
5. Mark
|
|
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=
|
|
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
|
-
|
|
330
|
-
|
|
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.
|
|
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
|
-
|
|
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
|