emdash-core 0.1.7__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/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Main AgentToolkit class for LLM agent graph exploration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ..graph.connection import KuzuConnection, get_connection
|
|
7
|
+
from .tools.base import BaseTool, ToolResult, ToolCategory
|
|
8
|
+
from .session import AgentSession
|
|
9
|
+
from ..utils.logger import log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentToolkit:
|
|
13
|
+
"""Main entry point for LLM agent graph exploration.
|
|
14
|
+
|
|
15
|
+
Provides a unified interface for executing graph exploration tools
|
|
16
|
+
and managing exploration session state.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
toolkit = AgentToolkit()
|
|
20
|
+
|
|
21
|
+
# Search for relevant code
|
|
22
|
+
result = toolkit.search("user authentication")
|
|
23
|
+
|
|
24
|
+
# Expand the top result
|
|
25
|
+
if result.success:
|
|
26
|
+
top = result.data["results"][0]
|
|
27
|
+
expanded = toolkit.expand(top["type"], top["qualified_name"])
|
|
28
|
+
|
|
29
|
+
# Get OpenAI schemas for function calling
|
|
30
|
+
schemas = toolkit.get_all_schemas()
|
|
31
|
+
|
|
32
|
+
# With custom MCP servers
|
|
33
|
+
toolkit = AgentToolkit(mcp_config_path=Path(".emdash/mcp.json"))
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
connection: Optional[KuzuConnection] = None,
|
|
39
|
+
enable_session: bool = True,
|
|
40
|
+
mcp_config_path: Optional[Path] = None,
|
|
41
|
+
repo_root: Optional[Path] = None,
|
|
42
|
+
plan_mode: bool = False,
|
|
43
|
+
save_spec_path: Optional[Path] = None,
|
|
44
|
+
):
|
|
45
|
+
"""Initialize the agent toolkit.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
connection: Kuzu connection. If None, uses global connection.
|
|
49
|
+
enable_session: Whether to track exploration state across calls.
|
|
50
|
+
mcp_config_path: Path to MCP config file for dynamic tool registration.
|
|
51
|
+
If None, checks for .emdash/mcp.json in cwd.
|
|
52
|
+
repo_root: Root directory of the repository for file operations.
|
|
53
|
+
If None, uses repo_root from config or current working directory.
|
|
54
|
+
plan_mode: Whether to include spec planning tools and restrict to read-only.
|
|
55
|
+
save_spec_path: If provided, specs will be saved to this path.
|
|
56
|
+
"""
|
|
57
|
+
self.connection = connection or get_connection()
|
|
58
|
+
self.session = AgentSession() if enable_session else None
|
|
59
|
+
self._tools: dict[str, BaseTool] = {}
|
|
60
|
+
self._mcp_manager = None
|
|
61
|
+
self._mcp_config_path = mcp_config_path
|
|
62
|
+
self.plan_mode = plan_mode
|
|
63
|
+
self.save_spec_path = save_spec_path
|
|
64
|
+
|
|
65
|
+
# Get repo_root from config if not explicitly provided
|
|
66
|
+
if repo_root is None:
|
|
67
|
+
from ..config import get_config
|
|
68
|
+
config = get_config()
|
|
69
|
+
if config.repo_root:
|
|
70
|
+
repo_root = Path(config.repo_root)
|
|
71
|
+
self._repo_root = repo_root or Path.cwd()
|
|
72
|
+
|
|
73
|
+
# Configure spec state if plan mode
|
|
74
|
+
if plan_mode:
|
|
75
|
+
from .tools.spec import SpecState
|
|
76
|
+
spec_state = SpecState.get_instance()
|
|
77
|
+
spec_state.configure(save_path=save_spec_path)
|
|
78
|
+
|
|
79
|
+
self._register_default_tools()
|
|
80
|
+
|
|
81
|
+
# Register dynamic MCP tools from config
|
|
82
|
+
self._init_mcp_manager()
|
|
83
|
+
|
|
84
|
+
def _register_default_tools(self) -> None:
|
|
85
|
+
"""Register all built-in tools."""
|
|
86
|
+
# Import tools here to avoid circular imports
|
|
87
|
+
from .tools.search import (
|
|
88
|
+
SemanticSearchTool,
|
|
89
|
+
# TextSearchTool, # Disabled due to DB locking issues
|
|
90
|
+
GrepTool,
|
|
91
|
+
GlobTool,
|
|
92
|
+
)
|
|
93
|
+
from .tools.web import WebTool
|
|
94
|
+
from .tools.coding import (
|
|
95
|
+
ReadFileTool,
|
|
96
|
+
ListFilesTool,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Register search tools
|
|
100
|
+
self.register_tool(SemanticSearchTool(self.connection))
|
|
101
|
+
# self.register_tool(TextSearchTool(self.connection)) # Disabled due to DB locking issues
|
|
102
|
+
self.register_tool(GrepTool(self.connection))
|
|
103
|
+
self.register_tool(GlobTool(self.connection))
|
|
104
|
+
self.register_tool(WebTool(self.connection))
|
|
105
|
+
|
|
106
|
+
# Register read-only file tools (always available)
|
|
107
|
+
self.register_tool(ReadFileTool(self._repo_root, self.connection))
|
|
108
|
+
self.register_tool(ListFilesTool(self._repo_root, self.connection))
|
|
109
|
+
|
|
110
|
+
# Register write tools (only in non-plan mode)
|
|
111
|
+
if not self.plan_mode:
|
|
112
|
+
from .tools.coding import (
|
|
113
|
+
WriteToFileTool,
|
|
114
|
+
ApplyDiffTool,
|
|
115
|
+
DeleteFileTool,
|
|
116
|
+
ExecuteCommandTool,
|
|
117
|
+
)
|
|
118
|
+
self.register_tool(WriteToFileTool(self._repo_root, self.connection))
|
|
119
|
+
self.register_tool(ApplyDiffTool(self._repo_root, self.connection))
|
|
120
|
+
self.register_tool(DeleteFileTool(self._repo_root, self.connection))
|
|
121
|
+
self.register_tool(ExecuteCommandTool(self._repo_root, self.connection))
|
|
122
|
+
|
|
123
|
+
# Register sub-agent tools for spawning lightweight agents
|
|
124
|
+
self._register_subagent_tools()
|
|
125
|
+
|
|
126
|
+
# Register task management tools (not in plan mode)
|
|
127
|
+
if not self.plan_mode:
|
|
128
|
+
self._register_task_tools()
|
|
129
|
+
|
|
130
|
+
# Register spec planning tools (only in plan mode)
|
|
131
|
+
if self.plan_mode:
|
|
132
|
+
self._register_spec_tools()
|
|
133
|
+
|
|
134
|
+
# Traversal tools (expand_node, get_callers, etc.) and analytics tools
|
|
135
|
+
# (get_area_importance, get_top_pagerank, etc.) are now provided
|
|
136
|
+
# by the emdash-graph MCP server - registered via _init_mcp_manager()
|
|
137
|
+
|
|
138
|
+
# NOTE: GitHub MCP tools are registered via _init_mcp_manager()
|
|
139
|
+
# from the MCP config file (e.g., .emdash/mcp.json)
|
|
140
|
+
# This allows using the official github-mcp-server directly
|
|
141
|
+
|
|
142
|
+
log.debug(f"Registered {len(self._tools)} agent tools")
|
|
143
|
+
|
|
144
|
+
def _register_subagent_tools(self) -> None:
|
|
145
|
+
"""Register sub-agent tools for spawning lightweight agents.
|
|
146
|
+
|
|
147
|
+
These tools allow spawning specialized sub-agents as subprocesses
|
|
148
|
+
for focused tasks like exploration and planning.
|
|
149
|
+
"""
|
|
150
|
+
from .tools.task import TaskTool
|
|
151
|
+
from .tools.task_output import TaskOutputTool
|
|
152
|
+
|
|
153
|
+
self.register_tool(TaskTool(repo_root=self._repo_root, connection=self.connection))
|
|
154
|
+
self.register_tool(TaskOutputTool(repo_root=self._repo_root, connection=self.connection))
|
|
155
|
+
|
|
156
|
+
def _register_task_tools(self) -> None:
|
|
157
|
+
"""Register task management tools.
|
|
158
|
+
|
|
159
|
+
These tools enable structured task tracking with todos,
|
|
160
|
+
user interaction via follow-up questions, and completion signaling.
|
|
161
|
+
"""
|
|
162
|
+
from .tools.tasks import (
|
|
163
|
+
WriteTodoTool,
|
|
164
|
+
UpdateTodoListTool,
|
|
165
|
+
AskFollowupQuestionTool,
|
|
166
|
+
AttemptCompletionTool,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.register_tool(WriteTodoTool())
|
|
170
|
+
self.register_tool(UpdateTodoListTool())
|
|
171
|
+
self.register_tool(AskFollowupQuestionTool())
|
|
172
|
+
self.register_tool(AttemptCompletionTool())
|
|
173
|
+
|
|
174
|
+
def _register_spec_tools(self) -> None:
|
|
175
|
+
"""Register spec planning tools.
|
|
176
|
+
|
|
177
|
+
These tools are only available in plan_mode and enable
|
|
178
|
+
structured specification output.
|
|
179
|
+
"""
|
|
180
|
+
from .tools.spec import (
|
|
181
|
+
SubmitSpecTool,
|
|
182
|
+
GetSpecTool,
|
|
183
|
+
UpdateSpecTool,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self.register_tool(SubmitSpecTool())
|
|
187
|
+
self.register_tool(GetSpecTool())
|
|
188
|
+
self.register_tool(UpdateSpecTool())
|
|
189
|
+
|
|
190
|
+
def _register_mcp_tools(self) -> None:
|
|
191
|
+
"""Register GitHub MCP tools if available.
|
|
192
|
+
|
|
193
|
+
MCP tools provide enhanced GitHub research capabilities including
|
|
194
|
+
code search, file content retrieval, and rich PR analysis.
|
|
195
|
+
These tools require:
|
|
196
|
+
- GITHUB_TOKEN or GITHUB_PERSONAL_ACCESS_TOKEN environment variable
|
|
197
|
+
- github-mcp-server binary installed
|
|
198
|
+
"""
|
|
199
|
+
from .tools.github_mcp import (
|
|
200
|
+
GitHubSearchCodeTool,
|
|
201
|
+
GitHubGetFileContentTool,
|
|
202
|
+
GitHubPRDetailsTool,
|
|
203
|
+
GitHubListPRsTool,
|
|
204
|
+
GitHubSearchReposTool,
|
|
205
|
+
GitHubSearchPRsTool,
|
|
206
|
+
GitHubGetIssueTool,
|
|
207
|
+
GitHubViewRepoStructureTool,
|
|
208
|
+
GitHubCreateReviewTool,
|
|
209
|
+
)
|
|
210
|
+
from ..core.config import get_config
|
|
211
|
+
|
|
212
|
+
config = get_config()
|
|
213
|
+
|
|
214
|
+
# Only register MCP tools if token is available
|
|
215
|
+
if not config.mcp.is_available:
|
|
216
|
+
log.debug("GitHub MCP tools not registered (no token configured)")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# Register GitHub MCP tools
|
|
220
|
+
self.register_tool(GitHubSearchCodeTool(self.connection))
|
|
221
|
+
self.register_tool(GitHubGetFileContentTool(self.connection))
|
|
222
|
+
self.register_tool(GitHubPRDetailsTool(self.connection))
|
|
223
|
+
self.register_tool(GitHubListPRsTool(self.connection))
|
|
224
|
+
self.register_tool(GitHubSearchReposTool(self.connection))
|
|
225
|
+
self.register_tool(GitHubSearchPRsTool(self.connection))
|
|
226
|
+
self.register_tool(GitHubGetIssueTool(self.connection))
|
|
227
|
+
self.register_tool(GitHubViewRepoStructureTool(self.connection))
|
|
228
|
+
self.register_tool(GitHubCreateReviewTool(self.connection))
|
|
229
|
+
|
|
230
|
+
log.debug("Registered 8 GitHub MCP tools")
|
|
231
|
+
|
|
232
|
+
def _init_mcp_manager(self) -> None:
|
|
233
|
+
"""Initialize MCP manager and register dynamic tools from config.
|
|
234
|
+
|
|
235
|
+
This method loads the MCP configuration file and registers all tools
|
|
236
|
+
from enabled MCP servers. It's called after default tool registration.
|
|
237
|
+
Creates default MCP config if it doesn't exist.
|
|
238
|
+
"""
|
|
239
|
+
from .mcp import (
|
|
240
|
+
MCPServerManager,
|
|
241
|
+
get_default_mcp_config_path,
|
|
242
|
+
create_tools_from_mcp,
|
|
243
|
+
)
|
|
244
|
+
from .mcp.config import ensure_mcp_config
|
|
245
|
+
|
|
246
|
+
# Determine config path
|
|
247
|
+
config_path = self._mcp_config_path
|
|
248
|
+
if config_path is None:
|
|
249
|
+
config_path = get_default_mcp_config_path()
|
|
250
|
+
|
|
251
|
+
# Ensure MCP config exists (creates default with github + emdash-graph)
|
|
252
|
+
ensure_mcp_config(config_path)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Create manager
|
|
256
|
+
self._mcp_manager = MCPServerManager(config_path=config_path)
|
|
257
|
+
|
|
258
|
+
# Create and register dynamic tools
|
|
259
|
+
tools = create_tools_from_mcp(self._mcp_manager, self.connection)
|
|
260
|
+
for tool in tools:
|
|
261
|
+
# Skip if tool name conflicts with existing tool
|
|
262
|
+
if tool.name in self._tools:
|
|
263
|
+
log.warning(f"Skipping MCP tool '{tool.name}': conflicts with existing tool")
|
|
264
|
+
continue
|
|
265
|
+
self.register_tool(tool)
|
|
266
|
+
|
|
267
|
+
if tools:
|
|
268
|
+
log.info(f"Registered {len(tools)} dynamic MCP tools from config")
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
log.warning(f"Failed to initialize MCP manager: {e}")
|
|
272
|
+
self._mcp_manager = None
|
|
273
|
+
|
|
274
|
+
def get_mcp_manager(self):
|
|
275
|
+
"""Get the MCP manager instance.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
MCPServerManager or None if not initialized
|
|
279
|
+
"""
|
|
280
|
+
return self._mcp_manager
|
|
281
|
+
|
|
282
|
+
def register_tool(self, tool: BaseTool) -> None:
|
|
283
|
+
"""Register a tool.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
tool: Tool instance to register
|
|
287
|
+
"""
|
|
288
|
+
self._tools[tool.name] = tool
|
|
289
|
+
|
|
290
|
+
def set_emitter(self, emitter) -> None:
|
|
291
|
+
"""Inject emitter into tools that need it.
|
|
292
|
+
|
|
293
|
+
This should be called by the runner after toolkit creation
|
|
294
|
+
to enable event streaming from tools like TaskTool.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
emitter: AgentEventEmitter for streaming events
|
|
298
|
+
"""
|
|
299
|
+
# Inject emitter into TaskTool for sub-agent event streaming
|
|
300
|
+
task_tool = self.get_tool("task")
|
|
301
|
+
if task_tool and hasattr(task_tool, "emitter"):
|
|
302
|
+
task_tool.emitter = emitter
|
|
303
|
+
log.debug("Injected emitter into TaskTool")
|
|
304
|
+
|
|
305
|
+
def get_tool(self, name: str) -> Optional[BaseTool]:
|
|
306
|
+
"""Get a tool by name.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
name: Tool name
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Tool instance or None if not found
|
|
313
|
+
"""
|
|
314
|
+
return self._tools.get(name)
|
|
315
|
+
|
|
316
|
+
def list_tools(self) -> list[dict]:
|
|
317
|
+
"""List all available tools.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of tool info dicts with name, description, category
|
|
321
|
+
"""
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
"name": tool.name,
|
|
325
|
+
"description": tool.description,
|
|
326
|
+
"category": tool.category.value,
|
|
327
|
+
}
|
|
328
|
+
for tool in self._tools.values()
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
def execute(self, tool_name: str, **params) -> ToolResult:
|
|
332
|
+
"""Execute a tool by name with parameters.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
tool_name: Name of the tool to execute
|
|
336
|
+
**params: Tool-specific parameters
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ToolResult with success/data or error
|
|
340
|
+
"""
|
|
341
|
+
tool = self.get_tool(tool_name)
|
|
342
|
+
|
|
343
|
+
if not tool:
|
|
344
|
+
return ToolResult.error_result(
|
|
345
|
+
f"Unknown tool: {tool_name}",
|
|
346
|
+
suggestions=[f"Available tools: {list(self._tools.keys())}"],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
result = tool.execute(**params)
|
|
351
|
+
|
|
352
|
+
# Track in session if enabled
|
|
353
|
+
if self.session:
|
|
354
|
+
self.session.record_action(tool_name, params, result)
|
|
355
|
+
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
log.exception(f"Tool execution error: {tool_name}")
|
|
360
|
+
return ToolResult.error_result(
|
|
361
|
+
f"Tool execution failed: {str(e)}",
|
|
362
|
+
suggestions=["Check the parameters and try again"],
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def get_all_schemas(self) -> list[dict]:
|
|
366
|
+
"""Get OpenAI function calling schemas for all tools.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
List of OpenAI function schemas
|
|
370
|
+
"""
|
|
371
|
+
return [tool.get_schema() for tool in self._tools.values()]
|
|
372
|
+
|
|
373
|
+
def get_schemas_by_category(self, category: str) -> list[dict]:
|
|
374
|
+
"""Get schemas for tools in a specific category.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
category: Category name (search, traversal, analytics, history, planning)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
List of OpenAI function schemas for that category
|
|
381
|
+
"""
|
|
382
|
+
return [
|
|
383
|
+
tool.get_schema()
|
|
384
|
+
for tool in self._tools.values()
|
|
385
|
+
if tool.category.value == category
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
def get_tools_by_category(self, category: str) -> list[BaseTool]:
|
|
389
|
+
"""Get all tools in a category.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
category: Category name
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of tool instances
|
|
396
|
+
"""
|
|
397
|
+
return [
|
|
398
|
+
tool
|
|
399
|
+
for tool in self._tools.values()
|
|
400
|
+
if tool.category.value == category
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
def get_session_context(self) -> Optional[dict]:
|
|
404
|
+
"""Get current session context summary.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Session context dict or None if session disabled
|
|
408
|
+
"""
|
|
409
|
+
if self.session:
|
|
410
|
+
return self.session.get_context_summary()
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def get_exploration_steps(self) -> list:
|
|
414
|
+
"""Get exploration steps from the current session.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of ExplorationStep objects or empty list if session disabled
|
|
418
|
+
"""
|
|
419
|
+
if self.session:
|
|
420
|
+
return self.session.steps
|
|
421
|
+
return []
|
|
422
|
+
|
|
423
|
+
def reset_session(self) -> None:
|
|
424
|
+
"""Reset the exploration session state."""
|
|
425
|
+
if self.session:
|
|
426
|
+
self.session.reset()
|
|
427
|
+
# Also reset task state
|
|
428
|
+
from .tools.tasks import TaskState
|
|
429
|
+
TaskState.reset()
|
|
430
|
+
# Also reset spec state if in plan mode
|
|
431
|
+
if self.plan_mode:
|
|
432
|
+
from .tools.spec import SpecState
|
|
433
|
+
SpecState.reset()
|
|
434
|
+
|
|
435
|
+
# Convenience methods for common operations
|
|
436
|
+
|
|
437
|
+
def search(self, query: str, **kwargs) -> ToolResult:
|
|
438
|
+
"""Convenience method for semantic search.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
query: Natural language search query
|
|
442
|
+
**kwargs: Additional parameters (entity_types, limit, min_score)
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
ToolResult with matching entities
|
|
446
|
+
"""
|
|
447
|
+
return self.execute("semantic_search", query=query, **kwargs)
|
|
448
|
+
|
|
449
|
+
def expand(
|
|
450
|
+
self,
|
|
451
|
+
node_type: str,
|
|
452
|
+
identifier: str,
|
|
453
|
+
**kwargs,
|
|
454
|
+
) -> ToolResult:
|
|
455
|
+
"""Convenience method for node expansion.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
node_type: Type of node (Function, Class, File)
|
|
459
|
+
identifier: Qualified name or file path
|
|
460
|
+
**kwargs: Additional parameters (max_hops)
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
ToolResult with expanded graph context
|
|
464
|
+
"""
|
|
465
|
+
return self.execute(
|
|
466
|
+
"expand_node",
|
|
467
|
+
node_type=node_type,
|
|
468
|
+
identifier=identifier,
|
|
469
|
+
**kwargs,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# def plan(self, goal: str, **kwargs) -> ToolResult:
|
|
473
|
+
# """Convenience method for exploration planning.
|
|
474
|
+
#
|
|
475
|
+
# Args:
|
|
476
|
+
# goal: What you're trying to understand or accomplish
|
|
477
|
+
# **kwargs: Additional parameters (context, constraints, exploration_depth)
|
|
478
|
+
#
|
|
479
|
+
# Returns:
|
|
480
|
+
# ToolResult with exploration plan
|
|
481
|
+
# """
|
|
482
|
+
# return self.execute("plan_exploration", goal=goal, **kwargs) # Disabled
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Toolkit registry for sub-agents.
|
|
2
|
+
|
|
3
|
+
Provides specialized toolkits for different agent types.
|
|
4
|
+
Each toolkit contains a curated set of tools appropriate for the agent's purpose.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, Type
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .base import BaseToolkit
|
|
12
|
+
|
|
13
|
+
# Registry for easy extension - just add new toolkits here
|
|
14
|
+
# Imported lazily to avoid circular imports
|
|
15
|
+
TOOLKIT_REGISTRY: Dict[str, str] = {
|
|
16
|
+
"Explore": "emdash_core.agent.toolkits.explore:ExploreToolkit",
|
|
17
|
+
"Plan": "emdash_core.agent.toolkits.plan:PlanToolkit",
|
|
18
|
+
# Future: "Bash": "emdash_core.agent.toolkits.bash:BashToolkit",
|
|
19
|
+
# Future: "Research": "emdash_core.agent.toolkits.research:ResearchToolkit",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_toolkit(subagent_type: str, repo_root: Path) -> "BaseToolkit":
|
|
24
|
+
"""Get toolkit for agent type.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
subagent_type: Type of agent (e.g., "Explore", "Plan")
|
|
28
|
+
repo_root: Root directory of the repository
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Toolkit instance
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If agent type is not registered
|
|
35
|
+
"""
|
|
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
|
+
)
|
|
41
|
+
|
|
42
|
+
# Import lazily to avoid circular imports
|
|
43
|
+
import importlib
|
|
44
|
+
|
|
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)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def list_agent_types() -> list[str]:
|
|
52
|
+
"""List all available agent types.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of agent type names
|
|
56
|
+
"""
|
|
57
|
+
return list(TOOLKIT_REGISTRY.keys())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"get_toolkit",
|
|
62
|
+
"list_agent_types",
|
|
63
|
+
"TOOLKIT_REGISTRY",
|
|
64
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Base class for sub-agent toolkits."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..tools.base import BaseTool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseToolkit(ABC):
|
|
11
|
+
"""Abstract base class for sub-agent toolkits.
|
|
12
|
+
|
|
13
|
+
Each toolkit provides a curated set of tools appropriate for a specific
|
|
14
|
+
agent type. Toolkits are responsible for:
|
|
15
|
+
- Registering appropriate tools
|
|
16
|
+
- Providing OpenAI function schemas
|
|
17
|
+
- Executing tools by name
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# List of tool names this toolkit provides (for documentation)
|
|
21
|
+
TOOLS: list[str] = []
|
|
22
|
+
|
|
23
|
+
def __init__(self, repo_root: Path):
|
|
24
|
+
"""Initialize the toolkit.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
repo_root: Root directory of the repository
|
|
28
|
+
"""
|
|
29
|
+
self.repo_root = repo_root.resolve()
|
|
30
|
+
self._tools: dict[str, BaseTool] = {}
|
|
31
|
+
self._register_tools()
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def _register_tools(self) -> None:
|
|
35
|
+
"""Register tools for this toolkit.
|
|
36
|
+
|
|
37
|
+
Subclasses must implement this to register their specific tools.
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def register_tool(self, tool: BaseTool) -> None:
|
|
42
|
+
"""Register a tool.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tool: Tool instance to register
|
|
46
|
+
"""
|
|
47
|
+
self._tools[tool.name] = tool
|
|
48
|
+
|
|
49
|
+
def get_tool(self, name: str) -> Optional[BaseTool]:
|
|
50
|
+
"""Get a tool by name.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: Tool name
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tool instance or None if not found
|
|
57
|
+
"""
|
|
58
|
+
return self._tools.get(name)
|
|
59
|
+
|
|
60
|
+
def list_tools(self) -> list[str]:
|
|
61
|
+
"""List all tool names in this toolkit.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of tool names
|
|
65
|
+
"""
|
|
66
|
+
return list(self._tools.keys())
|
|
67
|
+
|
|
68
|
+
def execute(self, tool_name: str, **params) -> ToolResult:
|
|
69
|
+
"""Execute a tool by name.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
tool_name: Name of the tool
|
|
73
|
+
**params: Tool parameters
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
ToolResult
|
|
77
|
+
"""
|
|
78
|
+
tool = self.get_tool(tool_name)
|
|
79
|
+
if not tool:
|
|
80
|
+
return ToolResult.error_result(
|
|
81
|
+
f"Unknown tool: {tool_name}",
|
|
82
|
+
suggestions=[f"Available tools: {self.list_tools()}"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return tool.execute(**params)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return ToolResult.error_result(f"Tool execution failed: {str(e)}")
|
|
89
|
+
|
|
90
|
+
def get_all_schemas(self) -> list[dict]:
|
|
91
|
+
"""Get OpenAI function calling schemas for all tools.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of function schemas
|
|
95
|
+
"""
|
|
96
|
+
return [tool.get_schema() for tool in self._tools.values()]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Explorer toolkit - read-only tools for fast codebase exploration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .base import BaseToolkit
|
|
6
|
+
from ..tools.coding import ReadFileTool, ListFilesTool
|
|
7
|
+
from ..tools.search import SemanticSearchTool, GrepTool, GlobTool
|
|
8
|
+
from ...utils.logger import log
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExploreToolkit(BaseToolkit):
|
|
12
|
+
"""Read-only toolkit for fast codebase exploration.
|
|
13
|
+
|
|
14
|
+
Provides tools for:
|
|
15
|
+
- Reading files
|
|
16
|
+
- Listing directory contents
|
|
17
|
+
- Searching with patterns (grep, glob)
|
|
18
|
+
- Semantic code search
|
|
19
|
+
|
|
20
|
+
All tools are read-only - no file modifications allowed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
TOOLS = [
|
|
24
|
+
"read_file",
|
|
25
|
+
"list_files",
|
|
26
|
+
"glob",
|
|
27
|
+
"grep",
|
|
28
|
+
"semantic_search",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def _register_tools(self) -> None:
|
|
32
|
+
"""Register read-only exploration tools."""
|
|
33
|
+
# File reading
|
|
34
|
+
self.register_tool(ReadFileTool(repo_root=self.repo_root))
|
|
35
|
+
self.register_tool(ListFilesTool(repo_root=self.repo_root))
|
|
36
|
+
|
|
37
|
+
# Pattern-based search
|
|
38
|
+
self.register_tool(GlobTool(connection=None))
|
|
39
|
+
self.register_tool(GrepTool(connection=None))
|
|
40
|
+
|
|
41
|
+
# Semantic search (if available)
|
|
42
|
+
try:
|
|
43
|
+
self.register_tool(SemanticSearchTool(connection=None))
|
|
44
|
+
except Exception as e:
|
|
45
|
+
log.debug(f"Semantic search not available: {e}")
|
|
46
|
+
|
|
47
|
+
log.debug(f"ExploreToolkit registered {len(self._tools)} tools")
|