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,341 @@
1
+ """Spec planning tools for feature specifications."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from .base import BaseTool, ToolResult, ToolCategory
8
+ from ..spec_schema import Spec
9
+ from ...utils.logger import log
10
+
11
+
12
+ @dataclass
13
+ class SpecState:
14
+ """Singleton state for spec management."""
15
+
16
+ current_spec: Optional[Spec] = None
17
+ save_path: Optional[Path] = None
18
+ history: list[Spec] = field(default_factory=list)
19
+
20
+ _instance: Optional["SpecState"] = None
21
+
22
+ @classmethod
23
+ def get_instance(cls) -> "SpecState":
24
+ """Get the singleton instance."""
25
+ if cls._instance is None:
26
+ cls._instance = cls()
27
+ return cls._instance
28
+
29
+ @classmethod
30
+ def reset(cls) -> None:
31
+ """Reset the singleton instance."""
32
+ cls._instance = None
33
+
34
+ def configure(self, save_path: Optional[Path] = None) -> None:
35
+ """Configure the spec state.
36
+
37
+ Args:
38
+ save_path: Path to save specs to
39
+ """
40
+ self.save_path = save_path
41
+
42
+
43
+ class SubmitSpecTool(BaseTool):
44
+ """Tool for submitting a new specification."""
45
+
46
+ name = "submit_spec"
47
+ description = """Submit a feature specification for review.
48
+ Creates a structured spec document with requirements and acceptance criteria."""
49
+ category = ToolCategory.PLANNING
50
+
51
+ def __init__(self, connection=None):
52
+ """Initialize without requiring connection."""
53
+ self.connection = connection
54
+
55
+ def execute(
56
+ self,
57
+ title: str,
58
+ summary: str,
59
+ requirements: list[str],
60
+ acceptance_criteria: list[str],
61
+ technical_notes: Optional[list[str]] = None,
62
+ dependencies: Optional[list[str]] = None,
63
+ open_questions: Optional[list[str]] = None,
64
+ ) -> ToolResult:
65
+ """Submit a specification.
66
+
67
+ Args:
68
+ title: Spec title
69
+ summary: Brief summary
70
+ requirements: List of requirements
71
+ acceptance_criteria: Acceptance criteria
72
+ technical_notes: Optional technical notes
73
+ dependencies: Optional dependencies
74
+ open_questions: Optional open questions
75
+
76
+ Returns:
77
+ ToolResult with spec info
78
+ """
79
+ try:
80
+ spec = Spec(
81
+ title=title,
82
+ summary=summary,
83
+ requirements=requirements,
84
+ acceptance_criteria=acceptance_criteria,
85
+ technical_notes=technical_notes or [],
86
+ dependencies=dependencies or [],
87
+ open_questions=open_questions or [],
88
+ )
89
+
90
+ # Validate
91
+ errors = spec.validate()
92
+ if errors:
93
+ return ToolResult.error_result(
94
+ f"Spec validation failed: {', '.join(errors)}",
95
+ suggestions=["Ensure all required fields are provided"],
96
+ )
97
+
98
+ # Store in state
99
+ state = SpecState.get_instance()
100
+ if state.current_spec:
101
+ state.history.append(state.current_spec)
102
+ state.current_spec = spec
103
+
104
+ # Save to file if configured
105
+ if state.save_path:
106
+ try:
107
+ state.save_path.write_text(spec.to_markdown())
108
+ log.info(f"Saved spec to {state.save_path}")
109
+ except Exception as e:
110
+ log.warning(f"Failed to save spec: {e}")
111
+
112
+ return ToolResult.success_result(
113
+ data={
114
+ "title": title,
115
+ "requirements_count": len(requirements),
116
+ "acceptance_criteria_count": len(acceptance_criteria),
117
+ "markdown": spec.to_markdown(),
118
+ },
119
+ )
120
+
121
+ except Exception as e:
122
+ log.exception("Submit spec failed")
123
+ return ToolResult.error_result(f"Failed to submit spec: {str(e)}")
124
+
125
+ def get_schema(self) -> dict:
126
+ """Get OpenAI function schema."""
127
+ return self._make_schema(
128
+ properties={
129
+ "title": {
130
+ "type": "string",
131
+ "description": "Spec title",
132
+ },
133
+ "summary": {
134
+ "type": "string",
135
+ "description": "Brief summary of the feature",
136
+ },
137
+ "requirements": {
138
+ "type": "array",
139
+ "items": {"type": "string"},
140
+ "description": "List of requirements",
141
+ },
142
+ "acceptance_criteria": {
143
+ "type": "array",
144
+ "items": {"type": "string"},
145
+ "description": "Acceptance criteria for completion",
146
+ },
147
+ "technical_notes": {
148
+ "type": "array",
149
+ "items": {"type": "string"},
150
+ "description": "Technical implementation notes",
151
+ },
152
+ "dependencies": {
153
+ "type": "array",
154
+ "items": {"type": "string"},
155
+ "description": "Dependencies on other features/specs",
156
+ },
157
+ "open_questions": {
158
+ "type": "array",
159
+ "items": {"type": "string"},
160
+ "description": "Open questions to resolve",
161
+ },
162
+ },
163
+ required=["title", "summary", "requirements", "acceptance_criteria"],
164
+ )
165
+
166
+
167
+ class GetSpecTool(BaseTool):
168
+ """Tool for getting the current specification."""
169
+
170
+ name = "get_spec"
171
+ description = """Get the current feature specification.
172
+ Returns the spec in markdown format."""
173
+ category = ToolCategory.PLANNING
174
+
175
+ def __init__(self, connection=None):
176
+ """Initialize without requiring connection."""
177
+ self.connection = connection
178
+
179
+ def execute(self) -> ToolResult:
180
+ """Get the current spec.
181
+
182
+ Returns:
183
+ ToolResult with spec content
184
+ """
185
+ state = SpecState.get_instance()
186
+
187
+ if not state.current_spec:
188
+ return ToolResult.error_result(
189
+ "No spec has been submitted yet",
190
+ suggestions=["Use submit_spec to create a specification"],
191
+ )
192
+
193
+ spec = state.current_spec
194
+
195
+ return ToolResult.success_result(
196
+ data={
197
+ "title": spec.title,
198
+ "summary": spec.summary,
199
+ "requirements": spec.requirements,
200
+ "acceptance_criteria": spec.acceptance_criteria,
201
+ "technical_notes": spec.technical_notes,
202
+ "dependencies": spec.dependencies,
203
+ "open_questions": spec.open_questions,
204
+ "markdown": spec.to_markdown(),
205
+ "is_complete": spec.is_complete(),
206
+ },
207
+ )
208
+
209
+ def get_schema(self) -> dict:
210
+ """Get OpenAI function schema."""
211
+ return self._make_schema(properties={}, required=[])
212
+
213
+
214
+ class UpdateSpecTool(BaseTool):
215
+ """Tool for updating the current specification."""
216
+
217
+ name = "update_spec"
218
+ description = """Update the current feature specification.
219
+ Add or modify requirements, criteria, or other fields."""
220
+ category = ToolCategory.PLANNING
221
+
222
+ def __init__(self, connection=None):
223
+ """Initialize without requiring connection."""
224
+ self.connection = connection
225
+
226
+ def execute(
227
+ self,
228
+ add_requirements: Optional[list[str]] = None,
229
+ add_acceptance_criteria: Optional[list[str]] = None,
230
+ add_technical_notes: Optional[list[str]] = None,
231
+ add_dependencies: Optional[list[str]] = None,
232
+ add_open_questions: Optional[list[str]] = None,
233
+ resolve_questions: Optional[list[str]] = None,
234
+ update_summary: Optional[str] = None,
235
+ ) -> ToolResult:
236
+ """Update the current spec.
237
+
238
+ Args:
239
+ add_requirements: Requirements to add
240
+ add_acceptance_criteria: Criteria to add
241
+ add_technical_notes: Notes to add
242
+ add_dependencies: Dependencies to add
243
+ add_open_questions: Questions to add
244
+ resolve_questions: Questions to mark as resolved
245
+ update_summary: New summary text
246
+
247
+ Returns:
248
+ ToolResult with updated spec
249
+ """
250
+ state = SpecState.get_instance()
251
+
252
+ if not state.current_spec:
253
+ return ToolResult.error_result(
254
+ "No spec to update",
255
+ suggestions=["Use submit_spec first"],
256
+ )
257
+
258
+ spec = state.current_spec
259
+
260
+ # Add new items
261
+ if add_requirements:
262
+ spec.requirements.extend(add_requirements)
263
+ if add_acceptance_criteria:
264
+ spec.acceptance_criteria.extend(add_acceptance_criteria)
265
+ if add_technical_notes:
266
+ spec.technical_notes.extend(add_technical_notes)
267
+ if add_dependencies:
268
+ spec.dependencies.extend(add_dependencies)
269
+ if add_open_questions:
270
+ spec.open_questions.extend(add_open_questions)
271
+
272
+ # Resolve questions
273
+ if resolve_questions:
274
+ spec.open_questions = [
275
+ q for q in spec.open_questions
276
+ if q not in resolve_questions
277
+ ]
278
+
279
+ # Update summary
280
+ if update_summary:
281
+ spec.summary = update_summary
282
+
283
+ # Save if configured
284
+ if state.save_path:
285
+ try:
286
+ state.save_path.write_text(spec.to_markdown())
287
+ except Exception as e:
288
+ log.warning(f"Failed to save spec: {e}")
289
+
290
+ return ToolResult.success_result(
291
+ data={
292
+ "title": spec.title,
293
+ "requirements_count": len(spec.requirements),
294
+ "acceptance_criteria_count": len(spec.acceptance_criteria),
295
+ "open_questions_count": len(spec.open_questions),
296
+ "is_complete": spec.is_complete(),
297
+ "markdown": spec.to_markdown(),
298
+ },
299
+ )
300
+
301
+ def get_schema(self) -> dict:
302
+ """Get OpenAI function schema."""
303
+ return self._make_schema(
304
+ properties={
305
+ "add_requirements": {
306
+ "type": "array",
307
+ "items": {"type": "string"},
308
+ "description": "Requirements to add",
309
+ },
310
+ "add_acceptance_criteria": {
311
+ "type": "array",
312
+ "items": {"type": "string"},
313
+ "description": "Acceptance criteria to add",
314
+ },
315
+ "add_technical_notes": {
316
+ "type": "array",
317
+ "items": {"type": "string"},
318
+ "description": "Technical notes to add",
319
+ },
320
+ "add_dependencies": {
321
+ "type": "array",
322
+ "items": {"type": "string"},
323
+ "description": "Dependencies to add",
324
+ },
325
+ "add_open_questions": {
326
+ "type": "array",
327
+ "items": {"type": "string"},
328
+ "description": "Open questions to add",
329
+ },
330
+ "resolve_questions": {
331
+ "type": "array",
332
+ "items": {"type": "string"},
333
+ "description": "Questions to mark as resolved",
334
+ },
335
+ "update_summary": {
336
+ "type": "string",
337
+ "description": "New summary text",
338
+ },
339
+ },
340
+ required=[],
341
+ )
@@ -0,0 +1,262 @@
1
+ """Task tool for spawning sub-agents.
2
+
3
+ Follows Claude Code's Task tool pattern - spawns specialized sub-agents
4
+ for focused tasks like exploration and planning.
5
+
6
+ Uses in-process execution for better UX (real-time events) while
7
+ maintaining isolated message histories per sub-agent.
8
+ """
9
+
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from .base import BaseTool, ToolResult, ToolCategory
15
+ from ..toolkits import list_agent_types
16
+ from ..inprocess_subagent import run_subagent, run_subagent_async
17
+ from ...utils.logger import log
18
+
19
+
20
+ class TaskTool(BaseTool):
21
+ """Spawn a sub-agent to handle complex, multi-step tasks autonomously.
22
+
23
+ The Task tool launches specialized agents in-process with isolated
24
+ message histories. Each agent type has specific capabilities:
25
+
26
+ - **Explore**: Fast codebase exploration using read_file, glob, grep, semantic_search
27
+ - **Plan**: Design implementation plans, can write to .emdash/plans/*.md
28
+
29
+ Sub-agents run with their own context and tools, returning a summary when done.
30
+ Events are tagged with agent_id to prevent mixing in the UI.
31
+ """
32
+
33
+ name = "task"
34
+ description = """Launch a specialized sub-agent for focused tasks.
35
+
36
+ Use this to spawn lightweight agents for:
37
+ - Fast codebase exploration (Explore agent)
38
+ - Implementation planning (Plan agent)
39
+
40
+ Sub-agents run autonomously and return structured results.
41
+ Multiple sub-agents can be launched in parallel."""
42
+ category = ToolCategory.PLANNING
43
+
44
+ def __init__(self, repo_root: Path, connection=None, emitter=None):
45
+ """Initialize with repo root.
46
+
47
+ Args:
48
+ repo_root: Root directory of the repository
49
+ connection: Optional connection (not used)
50
+ emitter: Optional event emitter for progress events
51
+ """
52
+ self.repo_root = repo_root.resolve()
53
+ self.connection = connection
54
+ self.emitter = emitter
55
+
56
+ def execute(
57
+ self,
58
+ description: str = "",
59
+ prompt: str = "",
60
+ subagent_type: str = "Explore",
61
+ model_tier: str = "fast",
62
+ max_turns: int = 10,
63
+ run_in_background: bool = False,
64
+ resume: Optional[str] = None,
65
+ **kwargs,
66
+ ) -> ToolResult:
67
+ """Spawn a sub-agent to perform a task.
68
+
69
+ Args:
70
+ description: Short (3-5 word) description of the task
71
+ prompt: The task for the agent to perform
72
+ subagent_type: Type of agent (Explore, Plan)
73
+ model_tier: Model tier (fast, standard, powerful)
74
+ max_turns: Maximum API round-trips
75
+ run_in_background: Run asynchronously
76
+ resume: Agent ID to resume from
77
+
78
+ Returns:
79
+ ToolResult with agent results or background task info
80
+ """
81
+ # Validate inputs
82
+ if not prompt:
83
+ return ToolResult.error_result(
84
+ "Prompt is required",
85
+ suggestions=["Provide a clear task description in 'prompt'"],
86
+ )
87
+
88
+ available_types = list_agent_types()
89
+ if subagent_type not in available_types:
90
+ return ToolResult.error_result(
91
+ f"Unknown agent type: {subagent_type}",
92
+ suggestions=[f"Available types: {available_types}"],
93
+ )
94
+
95
+ log.info(
96
+ "Spawning sub-agent type={} model={} prompt={}",
97
+ subagent_type,
98
+ model_tier,
99
+ prompt[:50] + "..." if len(prompt) > 50 else prompt,
100
+ )
101
+
102
+ if run_in_background:
103
+ return self._run_background(subagent_type, prompt, max_turns)
104
+ else:
105
+ return self._run_sync(subagent_type, prompt, max_turns)
106
+
107
+ def _run_sync(
108
+ self,
109
+ subagent_type: str,
110
+ prompt: str,
111
+ max_turns: int,
112
+ ) -> ToolResult:
113
+ """Run sub-agent synchronously in the same process.
114
+
115
+ Args:
116
+ subagent_type: Agent type
117
+ prompt: Task prompt
118
+ max_turns: Maximum API round-trips
119
+
120
+ Returns:
121
+ ToolResult with agent results
122
+ """
123
+ try:
124
+ result = run_subagent(
125
+ subagent_type=subagent_type,
126
+ prompt=prompt,
127
+ repo_root=self.repo_root,
128
+ emitter=self.emitter,
129
+ max_turns=max_turns,
130
+ )
131
+
132
+ if result.success:
133
+ return ToolResult.success_result(
134
+ data=result.to_dict(),
135
+ suggestions=self._generate_suggestions(result.to_dict()),
136
+ )
137
+ else:
138
+ return ToolResult.error_result(
139
+ f"Sub-agent failed: {result.error}",
140
+ suggestions=["Check the prompt and try again"],
141
+ )
142
+
143
+ except Exception as e:
144
+ log.exception("Failed to run sub-agent")
145
+ return ToolResult.error_result(f"Failed to run sub-agent: {e}")
146
+
147
+ def _run_background(
148
+ self,
149
+ subagent_type: str,
150
+ prompt: str,
151
+ max_turns: int,
152
+ ) -> ToolResult:
153
+ """Run sub-agent in background using a thread.
154
+
155
+ Args:
156
+ subagent_type: Agent type
157
+ prompt: Task prompt
158
+ max_turns: Maximum API round-trips
159
+
160
+ Returns:
161
+ ToolResult with task info
162
+ """
163
+ agent_id = str(uuid.uuid4())[:8]
164
+
165
+ # Output file for results
166
+ output_dir = self.repo_root / ".emdash" / "agents"
167
+ output_dir.mkdir(parents=True, exist_ok=True)
168
+ output_file = output_dir / f"{agent_id}.output"
169
+
170
+ try:
171
+ # Start async execution
172
+ future = run_subagent_async(
173
+ subagent_type=subagent_type,
174
+ prompt=prompt,
175
+ repo_root=self.repo_root,
176
+ emitter=self.emitter,
177
+ max_turns=max_turns,
178
+ )
179
+
180
+ # Store future for later retrieval (attach to class for now)
181
+ if not hasattr(self, "_background_tasks"):
182
+ self._background_tasks = {}
183
+ self._background_tasks[agent_id] = {
184
+ "future": future,
185
+ "output_file": output_file,
186
+ }
187
+
188
+ log.info(f"Started background agent {agent_id}")
189
+
190
+ return ToolResult.success_result(
191
+ data={
192
+ "agent_id": agent_id,
193
+ "status": "running",
194
+ "agent_type": subagent_type,
195
+ "output_file": str(output_file),
196
+ },
197
+ suggestions=[
198
+ f"Use task_output(agent_id='{agent_id}') to check results",
199
+ ],
200
+ )
201
+
202
+ except Exception as e:
203
+ log.exception("Failed to start background agent")
204
+ return ToolResult.error_result(f"Failed to start background agent: {e}")
205
+
206
+ def _generate_suggestions(self, data: dict) -> list[str]:
207
+ """Generate follow-up suggestions based on results."""
208
+ suggestions = []
209
+
210
+ files = data.get("files_explored", [])
211
+ if files:
212
+ suggestions.append(f"Found {len(files)} relevant files")
213
+
214
+ if data.get("agent_type") == "Plan":
215
+ suggestions.append("Review the plan in .emdash/plans/")
216
+
217
+ if data.get("agent_id"):
218
+ suggestions.append(f"Agent ID: {data['agent_id']} (can resume later)")
219
+
220
+ return suggestions
221
+
222
+ def get_schema(self) -> dict:
223
+ """Get OpenAI function schema."""
224
+ return self._make_schema(
225
+ properties={
226
+ "description": {
227
+ "type": "string",
228
+ "description": "Short (3-5 word) description of the task",
229
+ },
230
+ "prompt": {
231
+ "type": "string",
232
+ "description": "The task for the agent to perform",
233
+ },
234
+ "subagent_type": {
235
+ "type": "string",
236
+ "enum": ["Explore", "Plan"],
237
+ "description": "Type of specialized agent",
238
+ "default": "Explore",
239
+ },
240
+ "model_tier": {
241
+ "type": "string",
242
+ "enum": ["fast", "model"],
243
+ "description": "Model tier (fast=cheap/quick, model=standard)",
244
+ "default": "fast",
245
+ },
246
+ "max_turns": {
247
+ "type": "integer",
248
+ "description": "Maximum API round-trips",
249
+ "default": 10,
250
+ },
251
+ "run_in_background": {
252
+ "type": "boolean",
253
+ "description": "Run agent asynchronously",
254
+ "default": False,
255
+ },
256
+ "resume": {
257
+ "type": "string",
258
+ "description": "Agent ID to resume from previous execution",
259
+ },
260
+ },
261
+ required=["prompt"],
262
+ )