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,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
|
+
)
|