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,204 @@
|
|
|
1
|
+
"""TaskOutput tool for retrieving sub-agent results.
|
|
2
|
+
|
|
3
|
+
Retrieves output from background sub-agents started with run_in_background=True.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
12
|
+
from ...utils.logger import log
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskOutputTool(BaseTool):
|
|
16
|
+
"""Get output from a running or completed background sub-agent.
|
|
17
|
+
|
|
18
|
+
Use this to check on sub-agents that were started with run_in_background=True.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "task_output"
|
|
22
|
+
description = """Get output from a background sub-agent.
|
|
23
|
+
|
|
24
|
+
Use this to check the status and results of sub-agents started with
|
|
25
|
+
run_in_background=True. Can wait for completion or check immediately."""
|
|
26
|
+
category = ToolCategory.PLANNING
|
|
27
|
+
|
|
28
|
+
def __init__(self, repo_root: Path, connection=None):
|
|
29
|
+
"""Initialize with repo root.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
repo_root: Root directory of the repository
|
|
33
|
+
connection: Optional connection (not used)
|
|
34
|
+
"""
|
|
35
|
+
self.repo_root = repo_root.resolve()
|
|
36
|
+
self.agents_dir = repo_root / ".emdash" / "agents"
|
|
37
|
+
self.connection = connection
|
|
38
|
+
|
|
39
|
+
def execute(
|
|
40
|
+
self,
|
|
41
|
+
agent_id: str = "",
|
|
42
|
+
block: bool = True,
|
|
43
|
+
timeout: int = 60,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> ToolResult:
|
|
46
|
+
"""Get output from a background agent.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
agent_id: Agent ID to get output from
|
|
50
|
+
block: Whether to wait for completion
|
|
51
|
+
timeout: Max wait time in seconds (if blocking)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
ToolResult with agent output or status
|
|
55
|
+
"""
|
|
56
|
+
if not agent_id:
|
|
57
|
+
return ToolResult.error_result(
|
|
58
|
+
"agent_id is required",
|
|
59
|
+
suggestions=["Provide the agent_id from the task() call"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
output_file = self.agents_dir / f"{agent_id}.output"
|
|
63
|
+
transcript_file = self.agents_dir / f"{agent_id}.jsonl"
|
|
64
|
+
|
|
65
|
+
if block:
|
|
66
|
+
return self._wait_for_completion(
|
|
67
|
+
agent_id, output_file, transcript_file, timeout
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
return self._check_status(agent_id, output_file, transcript_file)
|
|
71
|
+
|
|
72
|
+
def _wait_for_completion(
|
|
73
|
+
self,
|
|
74
|
+
agent_id: str,
|
|
75
|
+
output_file: Path,
|
|
76
|
+
transcript_file: Path,
|
|
77
|
+
timeout: int,
|
|
78
|
+
) -> ToolResult:
|
|
79
|
+
"""Wait for agent to complete.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
agent_id: Agent ID
|
|
83
|
+
output_file: Path to output file
|
|
84
|
+
transcript_file: Path to transcript file
|
|
85
|
+
timeout: Max wait time in seconds
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
ToolResult with results
|
|
89
|
+
"""
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
|
|
92
|
+
while time.time() - start_time < timeout:
|
|
93
|
+
if output_file.exists():
|
|
94
|
+
content = output_file.read_text().strip()
|
|
95
|
+
|
|
96
|
+
# Check if output is complete JSON
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(content)
|
|
99
|
+
if isinstance(data, dict) and "success" in data:
|
|
100
|
+
return ToolResult.success_result(
|
|
101
|
+
data=data,
|
|
102
|
+
metadata={"agent_id": agent_id, "status": "completed"},
|
|
103
|
+
)
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# Still running if output exists but isn't complete JSON
|
|
108
|
+
if content:
|
|
109
|
+
return ToolResult.success_result(
|
|
110
|
+
data={
|
|
111
|
+
"status": "running",
|
|
112
|
+
"agent_id": agent_id,
|
|
113
|
+
"partial_output": content[-2000:], # Last 2KB
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
time.sleep(1)
|
|
118
|
+
|
|
119
|
+
# Timeout
|
|
120
|
+
return ToolResult.success_result(
|
|
121
|
+
data={
|
|
122
|
+
"status": "timeout",
|
|
123
|
+
"agent_id": agent_id,
|
|
124
|
+
"message": f"Agent did not complete within {timeout}s",
|
|
125
|
+
},
|
|
126
|
+
suggestions=["Use block=false to check status without waiting"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _check_status(
|
|
130
|
+
self,
|
|
131
|
+
agent_id: str,
|
|
132
|
+
output_file: Path,
|
|
133
|
+
transcript_file: Path,
|
|
134
|
+
) -> ToolResult:
|
|
135
|
+
"""Check agent status without waiting.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
agent_id: Agent ID
|
|
139
|
+
output_file: Path to output file
|
|
140
|
+
transcript_file: Path to transcript file
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
ToolResult with status
|
|
144
|
+
"""
|
|
145
|
+
# Check if output file exists
|
|
146
|
+
if not output_file.exists():
|
|
147
|
+
# Check if transcript exists (agent was started)
|
|
148
|
+
if transcript_file.exists():
|
|
149
|
+
return ToolResult.success_result(
|
|
150
|
+
data={
|
|
151
|
+
"status": "running",
|
|
152
|
+
"agent_id": agent_id,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
return ToolResult.error_result(
|
|
157
|
+
f"Agent {agent_id} not found",
|
|
158
|
+
suggestions=["Check the agent_id is correct"],
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Output exists, check if complete
|
|
162
|
+
try:
|
|
163
|
+
content = output_file.read_text().strip()
|
|
164
|
+
data = json.loads(content)
|
|
165
|
+
|
|
166
|
+
if isinstance(data, dict) and "success" in data:
|
|
167
|
+
return ToolResult.success_result(
|
|
168
|
+
data=data,
|
|
169
|
+
metadata={"agent_id": agent_id, "status": "completed"},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
except json.JSONDecodeError:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# Partial output
|
|
176
|
+
return ToolResult.success_result(
|
|
177
|
+
data={
|
|
178
|
+
"status": "running",
|
|
179
|
+
"agent_id": agent_id,
|
|
180
|
+
"has_output": True,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def get_schema(self) -> dict:
|
|
185
|
+
"""Get OpenAI function schema."""
|
|
186
|
+
return self._make_schema(
|
|
187
|
+
properties={
|
|
188
|
+
"agent_id": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": "Agent ID to get output from",
|
|
191
|
+
},
|
|
192
|
+
"block": {
|
|
193
|
+
"type": "boolean",
|
|
194
|
+
"description": "Wait for completion (default: true)",
|
|
195
|
+
"default": True,
|
|
196
|
+
},
|
|
197
|
+
"timeout": {
|
|
198
|
+
"type": "integer",
|
|
199
|
+
"description": "Max wait time in seconds if blocking",
|
|
200
|
+
"default": 60,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required=["agent_id"],
|
|
204
|
+
)
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""Task management tools for agent workflows."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskStatus(Enum):
|
|
11
|
+
"""Status of a task."""
|
|
12
|
+
|
|
13
|
+
PENDING = "pending"
|
|
14
|
+
IN_PROGRESS = "in_progress"
|
|
15
|
+
COMPLETED = "completed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Task:
|
|
20
|
+
"""A task in the todo list."""
|
|
21
|
+
|
|
22
|
+
id: str
|
|
23
|
+
title: str
|
|
24
|
+
description: str = ""
|
|
25
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
26
|
+
complexity: str = "M" # T-shirt size: S, M, L, XL
|
|
27
|
+
files: list[str] = field(default_factory=list) # Target files for this task
|
|
28
|
+
checklist: list[dict] = field(default_factory=list) # Subtasks: [{text, done}]
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"id": self.id,
|
|
33
|
+
"title": self.title,
|
|
34
|
+
"description": self.description,
|
|
35
|
+
"status": self.status.value,
|
|
36
|
+
"complexity": self.complexity,
|
|
37
|
+
"files": self.files,
|
|
38
|
+
"checklist": self.checklist,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TaskState:
|
|
43
|
+
"""Singleton state for task management."""
|
|
44
|
+
|
|
45
|
+
_instance: Optional["TaskState"] = None
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self.tasks: list[Task] = []
|
|
49
|
+
self.completed: bool = False
|
|
50
|
+
self.completion_summary: Optional[str] = None
|
|
51
|
+
self.pending_question: Optional[str] = None
|
|
52
|
+
self.user_response: Optional[str] = None
|
|
53
|
+
self._next_id: int = 1
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_instance(cls) -> "TaskState":
|
|
57
|
+
"""Get the singleton instance."""
|
|
58
|
+
if cls._instance is None:
|
|
59
|
+
cls._instance = cls()
|
|
60
|
+
return cls._instance
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def reset(cls) -> None:
|
|
64
|
+
"""Reset the singleton instance."""
|
|
65
|
+
cls._instance = None
|
|
66
|
+
|
|
67
|
+
def add_task(
|
|
68
|
+
self,
|
|
69
|
+
title: str,
|
|
70
|
+
description: str = "",
|
|
71
|
+
complexity: str = "M",
|
|
72
|
+
files: list[str] = None,
|
|
73
|
+
checklist: list[str] = None,
|
|
74
|
+
) -> Task:
|
|
75
|
+
"""Add a new task.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
title: Task title
|
|
79
|
+
description: Detailed description
|
|
80
|
+
complexity: T-shirt size (S, M, L, XL)
|
|
81
|
+
files: List of target file paths
|
|
82
|
+
checklist: List of subtask strings (converted to {text, done} dicts)
|
|
83
|
+
"""
|
|
84
|
+
task = Task(
|
|
85
|
+
id=str(self._next_id),
|
|
86
|
+
title=title,
|
|
87
|
+
description=description,
|
|
88
|
+
complexity=complexity,
|
|
89
|
+
files=files or [],
|
|
90
|
+
checklist=[{"text": item, "done": False} for item in (checklist or [])],
|
|
91
|
+
)
|
|
92
|
+
self._next_id += 1
|
|
93
|
+
self.tasks.append(task)
|
|
94
|
+
return task
|
|
95
|
+
|
|
96
|
+
def get_task(self, task_id: str) -> Optional[Task]:
|
|
97
|
+
"""Get task by ID."""
|
|
98
|
+
for task in self.tasks:
|
|
99
|
+
if task.id == task_id:
|
|
100
|
+
return task
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def update_status(self, task_id: str, status: TaskStatus) -> bool:
|
|
104
|
+
"""Update task status."""
|
|
105
|
+
task = self.get_task(task_id)
|
|
106
|
+
if task:
|
|
107
|
+
task.status = status
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def get_all_tasks(self) -> list[dict]:
|
|
112
|
+
"""Get all tasks as dicts."""
|
|
113
|
+
return [t.to_dict() for t in self.tasks]
|
|
114
|
+
|
|
115
|
+
def mark_completed(self, summary: str):
|
|
116
|
+
"""Mark the overall task as completed."""
|
|
117
|
+
self.completed = True
|
|
118
|
+
self.completion_summary = summary
|
|
119
|
+
|
|
120
|
+
def ask_question(self, question: str):
|
|
121
|
+
"""Set a pending question for the user."""
|
|
122
|
+
self.pending_question = question
|
|
123
|
+
self.user_response = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TaskManagementTool(BaseTool):
|
|
127
|
+
"""Base class for task management tools."""
|
|
128
|
+
|
|
129
|
+
category = ToolCategory.PLANNING
|
|
130
|
+
|
|
131
|
+
def __init__(self, connection=None):
|
|
132
|
+
"""Initialize without requiring connection."""
|
|
133
|
+
self.connection = connection
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def state(self) -> TaskState:
|
|
137
|
+
"""Get current TaskState instance (always fresh to handle resets)."""
|
|
138
|
+
return TaskState.get_instance()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class WriteTodoTool(TaskManagementTool):
|
|
142
|
+
"""Create a new sub-task for complex work breakdown."""
|
|
143
|
+
|
|
144
|
+
name = "write_todo"
|
|
145
|
+
description = "Create a new task with target files and checklist items. Use reset=true to clear all existing tasks first."
|
|
146
|
+
|
|
147
|
+
def execute(
|
|
148
|
+
self,
|
|
149
|
+
title: str,
|
|
150
|
+
description: str = "",
|
|
151
|
+
complexity: str = "M",
|
|
152
|
+
files: list[str] = None,
|
|
153
|
+
checklist: list[str] = None,
|
|
154
|
+
reset: bool = False,
|
|
155
|
+
**kwargs,
|
|
156
|
+
) -> ToolResult:
|
|
157
|
+
"""Create a new task.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
title: Short task title
|
|
161
|
+
description: Detailed description (optional)
|
|
162
|
+
complexity: T-shirt size (S, M, L, XL)
|
|
163
|
+
files: Target file paths this task will modify
|
|
164
|
+
checklist: List of subtask items to track
|
|
165
|
+
reset: If true, clear all existing tasks before adding this one
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ToolResult with task info
|
|
169
|
+
"""
|
|
170
|
+
if not title or not title.strip():
|
|
171
|
+
return ToolResult.error_result("Task title is required")
|
|
172
|
+
|
|
173
|
+
# Reset all tasks if requested
|
|
174
|
+
if reset:
|
|
175
|
+
TaskState.reset()
|
|
176
|
+
|
|
177
|
+
task = self.state.add_task(
|
|
178
|
+
title=title.strip(),
|
|
179
|
+
description=description.strip() if description else "",
|
|
180
|
+
complexity=complexity,
|
|
181
|
+
files=files or [],
|
|
182
|
+
checklist=checklist or [],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return ToolResult.success_result({
|
|
186
|
+
"task": task.to_dict(),
|
|
187
|
+
"total_tasks": len(self.state.tasks),
|
|
188
|
+
"all_tasks": self.state.get_all_tasks(),
|
|
189
|
+
"was_reset": reset,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
def get_schema(self) -> dict:
|
|
193
|
+
"""Get OpenAI function schema."""
|
|
194
|
+
return self._make_schema(
|
|
195
|
+
properties={
|
|
196
|
+
"title": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Short task title",
|
|
199
|
+
},
|
|
200
|
+
"description": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Detailed description of what needs to be done",
|
|
203
|
+
},
|
|
204
|
+
"complexity": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"enum": ["S", "M", "L", "XL"],
|
|
207
|
+
"description": "T-shirt size complexity: S (trivial), M (moderate), L (significant), XL (complex)",
|
|
208
|
+
},
|
|
209
|
+
"files": {
|
|
210
|
+
"type": "array",
|
|
211
|
+
"items": {"type": "string"},
|
|
212
|
+
"description": "Target file paths this task will modify",
|
|
213
|
+
},
|
|
214
|
+
"checklist": {
|
|
215
|
+
"type": "array",
|
|
216
|
+
"items": {"type": "string"},
|
|
217
|
+
"description": "List of subtask items to track (e.g., ['Add imports', 'Update function', 'Add tests'])",
|
|
218
|
+
},
|
|
219
|
+
"reset": {
|
|
220
|
+
"type": "boolean",
|
|
221
|
+
"description": "Set to true to clear all existing tasks before adding this one",
|
|
222
|
+
"default": False,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
required=["title"],
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class UpdateTodoListTool(TaskManagementTool):
|
|
230
|
+
"""Update task status or checklist items."""
|
|
231
|
+
|
|
232
|
+
name = "update_todo_list"
|
|
233
|
+
description = "Update task status, mark checklist items done, or add files. Auto-creates tasks if they don't exist."
|
|
234
|
+
|
|
235
|
+
def execute(
|
|
236
|
+
self,
|
|
237
|
+
task_id: str,
|
|
238
|
+
status: str = None,
|
|
239
|
+
checklist_done: list[int] = None,
|
|
240
|
+
checklist: list[int] = None, # Alias for checklist_done
|
|
241
|
+
files: list[str] = None,
|
|
242
|
+
title: str = "",
|
|
243
|
+
description: str = "",
|
|
244
|
+
**kwargs, # Ignore unexpected params from LLM
|
|
245
|
+
) -> ToolResult:
|
|
246
|
+
"""Update task status or checklist items.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
task_id: Task ID to update (e.g., "1", "2")
|
|
250
|
+
status: New status (pending, in_progress, completed)
|
|
251
|
+
checklist_done: Indices of checklist items to mark done (0-based)
|
|
252
|
+
checklist: Alias for checklist_done
|
|
253
|
+
files: Additional files to add to the task
|
|
254
|
+
title: Optional title for auto-created tasks
|
|
255
|
+
description: Optional description for auto-created tasks
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
ToolResult with updated task list
|
|
259
|
+
"""
|
|
260
|
+
# Handle checklist alias
|
|
261
|
+
if checklist and not checklist_done:
|
|
262
|
+
checklist_done = checklist
|
|
263
|
+
task = self.state.get_task(task_id)
|
|
264
|
+
|
|
265
|
+
# Auto-create task if not found
|
|
266
|
+
if not task:
|
|
267
|
+
new_status = TaskStatus.PENDING
|
|
268
|
+
if status:
|
|
269
|
+
try:
|
|
270
|
+
new_status = TaskStatus(status.lower())
|
|
271
|
+
except ValueError:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
task = Task(
|
|
275
|
+
id=str(task_id),
|
|
276
|
+
title=title or f"Task {task_id}",
|
|
277
|
+
status=new_status,
|
|
278
|
+
description=description,
|
|
279
|
+
files=files or [],
|
|
280
|
+
)
|
|
281
|
+
self.state.tasks.append(task)
|
|
282
|
+
return ToolResult.success_result({
|
|
283
|
+
"task_id": task_id,
|
|
284
|
+
"auto_created": True,
|
|
285
|
+
"task": task.to_dict(),
|
|
286
|
+
"all_tasks": self.state.get_all_tasks(),
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
# Update status if provided
|
|
290
|
+
if status:
|
|
291
|
+
try:
|
|
292
|
+
task.status = TaskStatus(status.lower())
|
|
293
|
+
except ValueError:
|
|
294
|
+
return ToolResult.error_result(
|
|
295
|
+
f"Invalid status: {status}. Use: pending, in_progress, completed"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Mark checklist items as done
|
|
299
|
+
if checklist_done:
|
|
300
|
+
for idx in checklist_done:
|
|
301
|
+
if 0 <= idx < len(task.checklist):
|
|
302
|
+
task.checklist[idx]["done"] = True
|
|
303
|
+
|
|
304
|
+
# Add files
|
|
305
|
+
if files:
|
|
306
|
+
for f in files:
|
|
307
|
+
if f not in task.files:
|
|
308
|
+
task.files.append(f)
|
|
309
|
+
|
|
310
|
+
return ToolResult.success_result({
|
|
311
|
+
"task_id": task_id,
|
|
312
|
+
"task": task.to_dict(),
|
|
313
|
+
"all_tasks": self.state.get_all_tasks(),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
def get_schema(self) -> dict:
|
|
317
|
+
"""Get OpenAI function schema."""
|
|
318
|
+
return self._make_schema(
|
|
319
|
+
properties={
|
|
320
|
+
"task_id": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "ID of the task to update (e.g., '1', '2')",
|
|
323
|
+
},
|
|
324
|
+
"status": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"enum": ["pending", "in_progress", "completed"],
|
|
327
|
+
"description": "New status for the task",
|
|
328
|
+
},
|
|
329
|
+
"checklist_done": {
|
|
330
|
+
"type": "array",
|
|
331
|
+
"items": {"type": "integer"},
|
|
332
|
+
"description": "Indices of checklist items to mark done (0-based)",
|
|
333
|
+
},
|
|
334
|
+
"files": {
|
|
335
|
+
"type": "array",
|
|
336
|
+
"items": {"type": "string"},
|
|
337
|
+
"description": "Additional file paths to add to the task",
|
|
338
|
+
},
|
|
339
|
+
"title": {
|
|
340
|
+
"type": "string",
|
|
341
|
+
"description": "Task title (used if auto-creating)",
|
|
342
|
+
},
|
|
343
|
+
"description": {
|
|
344
|
+
"type": "string",
|
|
345
|
+
"description": "Task description (used if auto-creating)",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required=["task_id"],
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class AskFollowupQuestionTool(TaskManagementTool):
|
|
353
|
+
"""Request clarification from the user."""
|
|
354
|
+
|
|
355
|
+
name = "ask_followup_question"
|
|
356
|
+
description = "Ask the user a clarifying question. Use this when you need more information to proceed."
|
|
357
|
+
|
|
358
|
+
def execute(
|
|
359
|
+
self,
|
|
360
|
+
question: str,
|
|
361
|
+
options: Optional[list[str]] = None,
|
|
362
|
+
) -> ToolResult:
|
|
363
|
+
"""Ask a followup question.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
question: Question to ask the user
|
|
367
|
+
options: Optional list of suggested answers
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
ToolResult indicating question was set
|
|
371
|
+
"""
|
|
372
|
+
if not question or not question.strip():
|
|
373
|
+
return ToolResult.error_result("Question is required")
|
|
374
|
+
|
|
375
|
+
self.state.ask_question(question.strip())
|
|
376
|
+
|
|
377
|
+
return ToolResult.success_result({
|
|
378
|
+
"question": question,
|
|
379
|
+
"options": options,
|
|
380
|
+
"status": "awaiting_response",
|
|
381
|
+
"message": "Question will be shown to user. Agent loop will pause.",
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
def get_schema(self) -> dict:
|
|
385
|
+
"""Get OpenAI function schema."""
|
|
386
|
+
return self._make_schema(
|
|
387
|
+
properties={
|
|
388
|
+
"question": {
|
|
389
|
+
"type": "string",
|
|
390
|
+
"description": "The question to ask the user",
|
|
391
|
+
},
|
|
392
|
+
"options": {
|
|
393
|
+
"type": "array",
|
|
394
|
+
"items": {"type": "string"},
|
|
395
|
+
"description": "Suggested answer options",
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
required=["question"],
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class AttemptCompletionTool(TaskManagementTool):
|
|
403
|
+
"""Signal task completion with summary."""
|
|
404
|
+
|
|
405
|
+
name = "attempt_completion"
|
|
406
|
+
description = "Signal that the task is complete. Provide a summary of what was accomplished and list files that were modified."
|
|
407
|
+
|
|
408
|
+
def execute(
|
|
409
|
+
self,
|
|
410
|
+
summary: str,
|
|
411
|
+
files_modified: list[str] = None,
|
|
412
|
+
) -> ToolResult:
|
|
413
|
+
"""Signal task completion.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
summary: Summary of what was accomplished
|
|
417
|
+
files_modified: List of files that were modified
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
ToolResult with completion info
|
|
421
|
+
"""
|
|
422
|
+
if not summary or not summary.strip():
|
|
423
|
+
return ToolResult.error_result("Summary is required")
|
|
424
|
+
|
|
425
|
+
self.state.mark_completed(summary.strip())
|
|
426
|
+
|
|
427
|
+
# Count completed vs total tasks
|
|
428
|
+
completed = sum(1 for t in self.state.tasks if t.status == TaskStatus.COMPLETED)
|
|
429
|
+
total = len(self.state.tasks)
|
|
430
|
+
|
|
431
|
+
return ToolResult.success_result({
|
|
432
|
+
"status": "completed",
|
|
433
|
+
"summary": summary,
|
|
434
|
+
"files_modified": files_modified or [],
|
|
435
|
+
"tasks_completed": f"{completed}/{total}" if total > 0 else "No subtasks",
|
|
436
|
+
"message": "Task marked as complete. Agent loop will terminate.",
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
def get_schema(self) -> dict:
|
|
440
|
+
"""Get OpenAI function schema."""
|
|
441
|
+
return self._make_schema(
|
|
442
|
+
properties={
|
|
443
|
+
"summary": {
|
|
444
|
+
"type": "string",
|
|
445
|
+
"description": "Summary of what was accomplished",
|
|
446
|
+
},
|
|
447
|
+
"files_modified": {
|
|
448
|
+
"type": "array",
|
|
449
|
+
"items": {"type": "string"},
|
|
450
|
+
"description": "List of file paths that were modified",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
required=["summary"],
|
|
454
|
+
)
|