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.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. 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")