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