tunacode-cli 0.0.48__py3-none-any.whl → 0.0.49__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- api/auth.py +13 -0
- api/users.py +8 -0
- tunacode/__init__.py +4 -0
- tunacode/cli/main.py +4 -0
- tunacode/cli/repl.py +39 -6
- tunacode/configuration/defaults.py +0 -1
- tunacode/constants.py +7 -1
- tunacode/core/agents/main.py +268 -245
- tunacode/core/agents/utils.py +54 -6
- tunacode/core/logging/__init__.py +29 -0
- tunacode/core/logging/config.py +28 -0
- tunacode/core/logging/formatters.py +48 -0
- tunacode/core/logging/handlers.py +83 -0
- tunacode/core/logging/logger.py +8 -0
- tunacode/core/recursive/__init__.py +18 -0
- tunacode/core/recursive/aggregator.py +467 -0
- tunacode/core/recursive/budget.py +414 -0
- tunacode/core/recursive/decomposer.py +398 -0
- tunacode/core/recursive/executor.py +470 -0
- tunacode/core/recursive/hierarchy.py +488 -0
- tunacode/core/state.py +45 -0
- tunacode/exceptions.py +23 -0
- tunacode/tools/base.py +7 -1
- tunacode/types.py +1 -1
- tunacode/ui/completers.py +2 -2
- tunacode/ui/console.py +30 -9
- tunacode/ui/input.py +2 -1
- tunacode/ui/keybindings.py +58 -1
- tunacode/ui/logging_compat.py +44 -0
- tunacode/ui/output.py +7 -6
- tunacode/ui/panels.py +30 -5
- tunacode/ui/recursive_progress.py +380 -0
- tunacode/utils/retry.py +163 -0
- tunacode/utils/security.py +3 -2
- tunacode/utils/token_counter.py +1 -2
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/METADATA +2 -2
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/RECORD +41 -29
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/top_level.txt +1 -0
- tunacode/core/agents/dspy_integration.py +0 -223
- tunacode/core/agents/dspy_tunacode.py +0 -458
- tunacode/prompts/dspy_task_planning.md +0 -45
- tunacode/prompts/dspy_tool_selection.md +0 -58
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Module: tunacode.core.recursive.executor
|
|
2
|
+
|
|
3
|
+
Main RecursiveTaskExecutor class for orchestrating recursive task decomposition and execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from tunacode.core.state import StateManager
|
|
14
|
+
|
|
15
|
+
from .aggregator import AggregationStrategy, ResultAggregator, TaskResult
|
|
16
|
+
from .budget import BudgetManager
|
|
17
|
+
from .decomposer import TaskDecomposer
|
|
18
|
+
from .hierarchy import TaskHierarchy
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TaskNode:
|
|
25
|
+
"""Represents a single task in the recursive execution tree."""
|
|
26
|
+
|
|
27
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
28
|
+
parent_id: Optional[str] = None
|
|
29
|
+
title: str = ""
|
|
30
|
+
description: str = ""
|
|
31
|
+
complexity_score: float = 0.0
|
|
32
|
+
iteration_budget: int = 10
|
|
33
|
+
subtasks: List["TaskNode"] = field(default_factory=list)
|
|
34
|
+
status: str = "pending" # pending, in_progress, completed, failed
|
|
35
|
+
result: Optional[object] = None
|
|
36
|
+
error: Optional[str] = None
|
|
37
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
depth: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TaskComplexityResult(BaseModel):
|
|
42
|
+
"""Result of task complexity analysis."""
|
|
43
|
+
|
|
44
|
+
is_complex: bool
|
|
45
|
+
complexity_score: float
|
|
46
|
+
reasoning: str
|
|
47
|
+
suggested_subtasks: List[str] = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RecursiveTaskExecutor:
|
|
51
|
+
"""Orchestrates recursive task decomposition and execution."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
state_manager: StateManager,
|
|
56
|
+
max_depth: int = 5,
|
|
57
|
+
min_complexity_threshold: float = 0.7,
|
|
58
|
+
default_iteration_budget: int = 10,
|
|
59
|
+
):
|
|
60
|
+
"""Initialize the RecursiveTaskExecutor.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
state_manager: The StateManager instance for accessing agents and state
|
|
64
|
+
max_depth: Maximum recursion depth allowed
|
|
65
|
+
min_complexity_threshold: Minimum complexity score to trigger decomposition
|
|
66
|
+
default_iteration_budget: Default iteration budget for tasks
|
|
67
|
+
"""
|
|
68
|
+
self.state_manager = state_manager
|
|
69
|
+
self.max_depth = max_depth
|
|
70
|
+
self.min_complexity_threshold = min_complexity_threshold
|
|
71
|
+
self.default_iteration_budget = default_iteration_budget
|
|
72
|
+
self._task_hierarchy: Dict[str, TaskNode] = {}
|
|
73
|
+
self._execution_stack: List[str] = []
|
|
74
|
+
|
|
75
|
+
# Initialize decomposer and hierarchy manager
|
|
76
|
+
self.decomposer = TaskDecomposer(state_manager)
|
|
77
|
+
self.hierarchy = TaskHierarchy()
|
|
78
|
+
self.budget_manager = BudgetManager(
|
|
79
|
+
total_budget=default_iteration_budget * 10, # Scale based on default
|
|
80
|
+
min_task_budget=2,
|
|
81
|
+
)
|
|
82
|
+
self.aggregator = ResultAggregator(state_manager)
|
|
83
|
+
|
|
84
|
+
async def execute_task(
|
|
85
|
+
self,
|
|
86
|
+
request: str,
|
|
87
|
+
parent_task_id: Optional[str] = None,
|
|
88
|
+
depth: int = 0,
|
|
89
|
+
inherited_context: Optional[Dict[str, Any]] = None,
|
|
90
|
+
) -> Tuple[bool, Any, Optional[str]]:
|
|
91
|
+
"""Execute a task, potentially decomposing it into subtasks.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
request: The task request/description
|
|
95
|
+
parent_task_id: ID of parent task if this is a subtask
|
|
96
|
+
depth: Current recursion depth
|
|
97
|
+
inherited_context: Context inherited from parent task
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Tuple of (success, result, error_message)
|
|
101
|
+
"""
|
|
102
|
+
# Check recursion depth
|
|
103
|
+
if depth >= self.max_depth:
|
|
104
|
+
logger.warning(f"Max recursion depth {self.max_depth} reached")
|
|
105
|
+
return False, None, f"Maximum recursion depth ({self.max_depth}) exceeded"
|
|
106
|
+
|
|
107
|
+
# Create task node
|
|
108
|
+
task_node = TaskNode(
|
|
109
|
+
title=request[:100], # First 100 chars as title
|
|
110
|
+
description=request,
|
|
111
|
+
parent_id=parent_task_id,
|
|
112
|
+
depth=depth,
|
|
113
|
+
context=inherited_context or {},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._task_hierarchy[task_node.id] = task_node
|
|
117
|
+
self._execution_stack.append(task_node.id)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Analyze task complexity
|
|
121
|
+
complexity_result = await self._analyze_task_complexity(request)
|
|
122
|
+
task_node.complexity_score = complexity_result.complexity_score
|
|
123
|
+
|
|
124
|
+
# Decide whether to decompose or execute directly
|
|
125
|
+
if (
|
|
126
|
+
complexity_result.is_complex
|
|
127
|
+
and complexity_result.complexity_score >= self.min_complexity_threshold
|
|
128
|
+
and depth < self.max_depth - 1
|
|
129
|
+
):
|
|
130
|
+
# Decompose into subtasks
|
|
131
|
+
if self.state_manager.session.show_thoughts:
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Decomposing complex task (score: {complexity_result.complexity_score:.2f})"
|
|
134
|
+
)
|
|
135
|
+
return await self._execute_with_decomposition(task_node, complexity_result)
|
|
136
|
+
else:
|
|
137
|
+
# Execute directly
|
|
138
|
+
if self.state_manager.session.show_thoughts:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"Executing task directly (score: {complexity_result.complexity_score:.2f})"
|
|
141
|
+
)
|
|
142
|
+
return await self._execute_directly(task_node)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Error executing task {task_node.id}: {str(e)}")
|
|
146
|
+
task_node.status = "failed"
|
|
147
|
+
task_node.error = str(e)
|
|
148
|
+
return False, None, str(e)
|
|
149
|
+
finally:
|
|
150
|
+
self._execution_stack.pop()
|
|
151
|
+
|
|
152
|
+
async def _analyze_task_complexity(self, request: str) -> TaskComplexityResult:
|
|
153
|
+
"""Analyze task complexity using the main agent.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
request: The task description
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
TaskComplexityResult with analysis
|
|
160
|
+
"""
|
|
161
|
+
# Get the main agent from state manager
|
|
162
|
+
agent = self.state_manager.session.agents.get("main")
|
|
163
|
+
if not agent:
|
|
164
|
+
# Simple heuristic if agent not available
|
|
165
|
+
word_count = len(request.split())
|
|
166
|
+
has_multiple_parts = any(
|
|
167
|
+
word in request.lower() for word in ["and", "then", "also", "plus"]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
complexity_score = min(1.0, (word_count / 50) + (0.3 if has_multiple_parts else 0))
|
|
171
|
+
is_complex = complexity_score >= self.min_complexity_threshold
|
|
172
|
+
|
|
173
|
+
return TaskComplexityResult(
|
|
174
|
+
is_complex=is_complex,
|
|
175
|
+
complexity_score=complexity_score,
|
|
176
|
+
reasoning="Heuristic analysis based on request length and structure",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Use agent to analyze complexity
|
|
180
|
+
complexity_prompt = f"""Analyze the complexity of this task and determine if it should be broken down into subtasks.
|
|
181
|
+
|
|
182
|
+
Task: {request}
|
|
183
|
+
|
|
184
|
+
Provide:
|
|
185
|
+
1. A complexity score from 0.0 to 1.0 (0 = trivial, 1 = extremely complex)
|
|
186
|
+
2. Whether this task should be decomposed (true/false)
|
|
187
|
+
3. Brief reasoning for your assessment
|
|
188
|
+
4. If complex, suggest 2-5 subtasks
|
|
189
|
+
|
|
190
|
+
Consider factors like:
|
|
191
|
+
- Number of distinct operations required
|
|
192
|
+
- Dependencies between operations
|
|
193
|
+
- Technical complexity
|
|
194
|
+
- Estimated time/effort needed
|
|
195
|
+
|
|
196
|
+
Respond in JSON format:
|
|
197
|
+
{{
|
|
198
|
+
"is_complex": boolean,
|
|
199
|
+
"complexity_score": float,
|
|
200
|
+
"reasoning": "string",
|
|
201
|
+
"suggested_subtasks": ["subtask1", "subtask2", ...]
|
|
202
|
+
}}"""
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
await agent.run(complexity_prompt)
|
|
206
|
+
# Parse the response (simplified - in production would use proper JSON parsing)
|
|
207
|
+
# For now, return a default result
|
|
208
|
+
return TaskComplexityResult(
|
|
209
|
+
is_complex=True,
|
|
210
|
+
complexity_score=0.8,
|
|
211
|
+
reasoning="Task requires multiple distinct operations",
|
|
212
|
+
suggested_subtasks=[],
|
|
213
|
+
)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Error analyzing task complexity: {str(e)}")
|
|
216
|
+
# Fallback to heuristic
|
|
217
|
+
return TaskComplexityResult(
|
|
218
|
+
is_complex=False, complexity_score=0.5, reasoning="Error in analysis, using default"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def _execute_with_decomposition(
|
|
222
|
+
self, task_node: TaskNode, complexity_result: TaskComplexityResult
|
|
223
|
+
) -> Tuple[bool, Any, Optional[str]]:
|
|
224
|
+
"""Execute a task by decomposing it into subtasks.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
task_node: The task node to execute
|
|
228
|
+
complexity_result: The complexity analysis result
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Tuple of (success, aggregated_result, error_message)
|
|
232
|
+
"""
|
|
233
|
+
task_node.status = "in_progress"
|
|
234
|
+
|
|
235
|
+
# Generate subtasks
|
|
236
|
+
subtasks = await self._generate_subtasks(task_node, complexity_result)
|
|
237
|
+
|
|
238
|
+
# Allocate iteration budgets
|
|
239
|
+
subtask_budgets = self._allocate_iteration_budgets(
|
|
240
|
+
task_node.iteration_budget, len(subtasks)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Execute subtasks
|
|
244
|
+
results = []
|
|
245
|
+
errors = []
|
|
246
|
+
|
|
247
|
+
# Show UI feedback if thoughts are enabled
|
|
248
|
+
if self.state_manager.session.show_thoughts:
|
|
249
|
+
from tunacode.ui import console as ui_console
|
|
250
|
+
from tunacode.ui.recursive_progress import show_recursive_progress
|
|
251
|
+
|
|
252
|
+
await show_recursive_progress(
|
|
253
|
+
ui_console.console,
|
|
254
|
+
f"Executing {len(subtasks)} subtasks",
|
|
255
|
+
task_id=task_node.id,
|
|
256
|
+
depth=task_node.depth,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
for i, (subtask_desc, budget) in enumerate(zip(subtasks, subtask_budgets)):
|
|
260
|
+
if self.state_manager.session.show_thoughts:
|
|
261
|
+
logger.info(f"Executing subtask {i + 1}/{len(subtasks)}: {subtask_desc[:50]}...")
|
|
262
|
+
|
|
263
|
+
# Create subtask context
|
|
264
|
+
subtask_context = {
|
|
265
|
+
**task_node.context,
|
|
266
|
+
"parent_task": task_node.description,
|
|
267
|
+
"subtask_index": i,
|
|
268
|
+
"total_subtasks": len(subtasks),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Execute subtask recursively
|
|
272
|
+
success, result, error = await self.execute_task(
|
|
273
|
+
subtask_desc,
|
|
274
|
+
parent_task_id=task_node.id,
|
|
275
|
+
depth=task_node.depth + 1,
|
|
276
|
+
inherited_context=subtask_context,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if success:
|
|
280
|
+
results.append(result)
|
|
281
|
+
else:
|
|
282
|
+
errors.append(f"Subtask {i + 1} failed: {error}")
|
|
283
|
+
|
|
284
|
+
# Show completion status
|
|
285
|
+
if self.state_manager.session.show_thoughts:
|
|
286
|
+
from tunacode.ui import console as ui_console
|
|
287
|
+
from tunacode.ui.recursive_progress import show_task_completion
|
|
288
|
+
|
|
289
|
+
await show_task_completion(
|
|
290
|
+
ui_console.console,
|
|
291
|
+
task_node.id + f".{i + 1}",
|
|
292
|
+
success,
|
|
293
|
+
depth=task_node.depth + 1,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Aggregate results
|
|
297
|
+
if errors:
|
|
298
|
+
task_node.status = "failed"
|
|
299
|
+
task_node.error = "; ".join(errors)
|
|
300
|
+
return False, results, f"Some subtasks failed: {'; '.join(errors)}"
|
|
301
|
+
|
|
302
|
+
task_node.status = "completed"
|
|
303
|
+
aggregated_result = await self._aggregate_results(task_node, results)
|
|
304
|
+
task_node.result = aggregated_result
|
|
305
|
+
|
|
306
|
+
return True, aggregated_result, None
|
|
307
|
+
|
|
308
|
+
async def _execute_directly(self, task_node: TaskNode) -> Tuple[bool, Any, Optional[str]]:
|
|
309
|
+
"""Execute a task directly using the main agent.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
task_node: The task node to execute
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Tuple of (success, result, error_message)
|
|
316
|
+
"""
|
|
317
|
+
task_node.status = "in_progress"
|
|
318
|
+
|
|
319
|
+
# Get the main agent
|
|
320
|
+
agent = self.state_manager.session.agents.get("main")
|
|
321
|
+
if not agent:
|
|
322
|
+
error_msg = "Main agent not available"
|
|
323
|
+
task_node.status = "failed"
|
|
324
|
+
task_node.error = error_msg
|
|
325
|
+
return False, None, error_msg
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
# Execute with the agent
|
|
329
|
+
result = await agent.run(task_node.description)
|
|
330
|
+
task_node.status = "completed"
|
|
331
|
+
task_node.result = result
|
|
332
|
+
return True, result, None
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
error_msg = f"Error executing task: {str(e)}"
|
|
336
|
+
logger.error(error_msg)
|
|
337
|
+
task_node.status = "failed"
|
|
338
|
+
task_node.error = error_msg
|
|
339
|
+
return False, None, error_msg
|
|
340
|
+
|
|
341
|
+
async def _generate_subtasks(
|
|
342
|
+
self, task_node: TaskNode, complexity_result: TaskComplexityResult
|
|
343
|
+
) -> List[str]:
|
|
344
|
+
"""Generate subtasks for a complex task.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
task_node: The parent task node
|
|
348
|
+
complexity_result: The complexity analysis result
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
List of subtask descriptions
|
|
352
|
+
"""
|
|
353
|
+
# If we have suggested subtasks from complexity analysis, use them
|
|
354
|
+
if complexity_result.suggested_subtasks:
|
|
355
|
+
return complexity_result.suggested_subtasks
|
|
356
|
+
|
|
357
|
+
# Otherwise, use agent to generate subtasks
|
|
358
|
+
agent = self.state_manager.session.agents.get("main")
|
|
359
|
+
if not agent:
|
|
360
|
+
# Fallback to simple decomposition
|
|
361
|
+
return [f"Part 1 of: {task_node.description}", f"Part 2 of: {task_node.description}"]
|
|
362
|
+
|
|
363
|
+
decompose_prompt = f"""Break down this task into 2-5 logical subtasks that can be executed independently:
|
|
364
|
+
|
|
365
|
+
Task: {task_node.description}
|
|
366
|
+
|
|
367
|
+
Provide a list of clear, actionable subtasks. Each subtask should:
|
|
368
|
+
- Be self-contained and executable
|
|
369
|
+
- Have clear success criteria
|
|
370
|
+
- Contribute to completing the overall task
|
|
371
|
+
|
|
372
|
+
Return ONLY a JSON array of subtask descriptions:
|
|
373
|
+
["subtask 1 description", "subtask 2 description", ...]"""
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
await agent.run(decompose_prompt)
|
|
377
|
+
# Parse subtasks from result (simplified)
|
|
378
|
+
# In production, would parse actual JSON response
|
|
379
|
+
return [
|
|
380
|
+
f"Step 1: Analyze requirements for {task_node.title}",
|
|
381
|
+
f"Step 2: Implement core functionality for {task_node.title}",
|
|
382
|
+
f"Step 3: Test and validate {task_node.title}",
|
|
383
|
+
]
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(f"Error generating subtasks: {str(e)}")
|
|
386
|
+
return [f"Execute: {task_node.description}"]
|
|
387
|
+
|
|
388
|
+
def _allocate_iteration_budgets(self, total_budget: int, num_subtasks: int) -> List[int]:
|
|
389
|
+
"""Allocate iteration budget across subtasks.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
total_budget: Total iteration budget available
|
|
393
|
+
num_subtasks: Number of subtasks
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
List of budgets for each subtask
|
|
397
|
+
"""
|
|
398
|
+
if num_subtasks == 0:
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
# Simple equal allocation with remainder distribution
|
|
402
|
+
base_budget = total_budget // num_subtasks
|
|
403
|
+
remainder = total_budget % num_subtasks
|
|
404
|
+
|
|
405
|
+
budgets = [base_budget] * num_subtasks
|
|
406
|
+
# Distribute remainder to first tasks
|
|
407
|
+
for i in range(remainder):
|
|
408
|
+
budgets[i] += 1
|
|
409
|
+
|
|
410
|
+
return budgets
|
|
411
|
+
|
|
412
|
+
async def _aggregate_results(self, task_node: TaskNode, subtask_results: List[Any]) -> Any:
|
|
413
|
+
"""Aggregate results from subtasks into a final result.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
task_node: The parent task node
|
|
417
|
+
subtask_results: List of results from subtasks
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Aggregated result
|
|
421
|
+
"""
|
|
422
|
+
if not subtask_results:
|
|
423
|
+
return "Task completed with no results"
|
|
424
|
+
|
|
425
|
+
# Convert subtask results to TaskResult objects
|
|
426
|
+
task_results = []
|
|
427
|
+
for i, (subtask, result) in enumerate(zip(task_node.subtasks, subtask_results)):
|
|
428
|
+
task_results.append(
|
|
429
|
+
TaskResult(
|
|
430
|
+
task_id=subtask.id,
|
|
431
|
+
task_title=subtask.title,
|
|
432
|
+
result_data=result,
|
|
433
|
+
status=subtask.status,
|
|
434
|
+
error=subtask.error,
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Use intelligent aggregation
|
|
439
|
+
aggregated = await self.aggregator.aggregate_results(
|
|
440
|
+
task_results=task_results,
|
|
441
|
+
parent_task={
|
|
442
|
+
"id": task_node.id,
|
|
443
|
+
"title": task_node.title,
|
|
444
|
+
"description": task_node.description,
|
|
445
|
+
},
|
|
446
|
+
strategy=AggregationStrategy.INTELLIGENT,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return aggregated.primary_result
|
|
450
|
+
|
|
451
|
+
def get_task_hierarchy(self) -> Dict[str, TaskNode]:
|
|
452
|
+
"""Get the current task hierarchy.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dictionary mapping task IDs to TaskNode objects
|
|
456
|
+
"""
|
|
457
|
+
return self._task_hierarchy.copy()
|
|
458
|
+
|
|
459
|
+
def get_execution_stack(self) -> List[str]:
|
|
460
|
+
"""Get the current execution stack of task IDs.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of task IDs in execution order
|
|
464
|
+
"""
|
|
465
|
+
return self._execution_stack.copy()
|
|
466
|
+
|
|
467
|
+
def clear_hierarchy(self):
|
|
468
|
+
"""Clear the task hierarchy and execution stack."""
|
|
469
|
+
self._task_hierarchy.clear()
|
|
470
|
+
self._execution_stack.clear()
|