tunacode-cli 0.0.41__py3-none-any.whl → 0.0.42__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.

@@ -0,0 +1,467 @@
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[Any] = 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
+ logger.info(
132
+ f"Decomposing complex task (score: {complexity_result.complexity_score:.2f})"
133
+ )
134
+ return await self._execute_with_decomposition(task_node, complexity_result)
135
+ else:
136
+ # Execute directly
137
+ logger.info(
138
+ f"Executing task directly (score: {complexity_result.complexity_score:.2f})"
139
+ )
140
+ return await self._execute_directly(task_node)
141
+
142
+ except Exception as e:
143
+ logger.error(f"Error executing task {task_node.id}: {str(e)}")
144
+ task_node.status = "failed"
145
+ task_node.error = str(e)
146
+ return False, None, str(e)
147
+ finally:
148
+ self._execution_stack.pop()
149
+
150
+ async def _analyze_task_complexity(self, request: str) -> TaskComplexityResult:
151
+ """Analyze task complexity using the main agent.
152
+
153
+ Args:
154
+ request: The task description
155
+
156
+ Returns:
157
+ TaskComplexityResult with analysis
158
+ """
159
+ # Get the main agent from state manager
160
+ agent = self.state_manager.session.agents.get("main")
161
+ if not agent:
162
+ # Simple heuristic if agent not available
163
+ word_count = len(request.split())
164
+ has_multiple_parts = any(
165
+ word in request.lower() for word in ["and", "then", "also", "plus"]
166
+ )
167
+
168
+ complexity_score = min(1.0, (word_count / 50) + (0.3 if has_multiple_parts else 0))
169
+ is_complex = complexity_score >= self.min_complexity_threshold
170
+
171
+ return TaskComplexityResult(
172
+ is_complex=is_complex,
173
+ complexity_score=complexity_score,
174
+ reasoning="Heuristic analysis based on request length and structure",
175
+ )
176
+
177
+ # Use agent to analyze complexity
178
+ complexity_prompt = f"""Analyze the complexity of this task and determine if it should be broken down into subtasks.
179
+
180
+ Task: {request}
181
+
182
+ Provide:
183
+ 1. A complexity score from 0.0 to 1.0 (0 = trivial, 1 = extremely complex)
184
+ 2. Whether this task should be decomposed (true/false)
185
+ 3. Brief reasoning for your assessment
186
+ 4. If complex, suggest 2-5 subtasks
187
+
188
+ Consider factors like:
189
+ - Number of distinct operations required
190
+ - Dependencies between operations
191
+ - Technical complexity
192
+ - Estimated time/effort needed
193
+
194
+ Respond in JSON format:
195
+ {{
196
+ "is_complex": boolean,
197
+ "complexity_score": float,
198
+ "reasoning": "string",
199
+ "suggested_subtasks": ["subtask1", "subtask2", ...]
200
+ }}"""
201
+
202
+ try:
203
+ await agent.run(complexity_prompt)
204
+ # Parse the response (simplified - in production would use proper JSON parsing)
205
+ # For now, return a default result
206
+ return TaskComplexityResult(
207
+ is_complex=True,
208
+ complexity_score=0.8,
209
+ reasoning="Task requires multiple distinct operations",
210
+ suggested_subtasks=[],
211
+ )
212
+ except Exception as e:
213
+ logger.error(f"Error analyzing task complexity: {str(e)}")
214
+ # Fallback to heuristic
215
+ return TaskComplexityResult(
216
+ is_complex=False, complexity_score=0.5, reasoning="Error in analysis, using default"
217
+ )
218
+
219
+ async def _execute_with_decomposition(
220
+ self, task_node: TaskNode, complexity_result: TaskComplexityResult
221
+ ) -> Tuple[bool, Any, Optional[str]]:
222
+ """Execute a task by decomposing it into subtasks.
223
+
224
+ Args:
225
+ task_node: The task node to execute
226
+ complexity_result: The complexity analysis result
227
+
228
+ Returns:
229
+ Tuple of (success, aggregated_result, error_message)
230
+ """
231
+ task_node.status = "in_progress"
232
+
233
+ # Generate subtasks
234
+ subtasks = await self._generate_subtasks(task_node, complexity_result)
235
+
236
+ # Allocate iteration budgets
237
+ subtask_budgets = self._allocate_iteration_budgets(
238
+ task_node.iteration_budget, len(subtasks)
239
+ )
240
+
241
+ # Execute subtasks
242
+ results = []
243
+ errors = []
244
+
245
+ # Show UI feedback if thoughts are enabled
246
+ if self.state_manager.session.show_thoughts:
247
+ from tunacode.ui import console as ui_console
248
+ from tunacode.ui.recursive_progress import show_recursive_progress
249
+
250
+ await show_recursive_progress(
251
+ ui_console.console,
252
+ f"Executing {len(subtasks)} subtasks",
253
+ task_id=task_node.id,
254
+ depth=task_node.depth,
255
+ )
256
+
257
+ for i, (subtask_desc, budget) in enumerate(zip(subtasks, subtask_budgets)):
258
+ logger.info(f"Executing subtask {i + 1}/{len(subtasks)}: {subtask_desc[:50]}...")
259
+
260
+ # Create subtask context
261
+ subtask_context = {
262
+ **task_node.context,
263
+ "parent_task": task_node.description,
264
+ "subtask_index": i,
265
+ "total_subtasks": len(subtasks),
266
+ }
267
+
268
+ # Execute subtask recursively
269
+ success, result, error = await self.execute_task(
270
+ subtask_desc,
271
+ parent_task_id=task_node.id,
272
+ depth=task_node.depth + 1,
273
+ inherited_context=subtask_context,
274
+ )
275
+
276
+ if success:
277
+ results.append(result)
278
+ else:
279
+ errors.append(f"Subtask {i + 1} failed: {error}")
280
+
281
+ # Show completion status
282
+ if self.state_manager.session.show_thoughts:
283
+ from tunacode.ui import console as ui_console
284
+ from tunacode.ui.recursive_progress import show_task_completion
285
+
286
+ await show_task_completion(
287
+ ui_console.console,
288
+ task_node.id + f".{i + 1}",
289
+ success,
290
+ depth=task_node.depth + 1,
291
+ )
292
+
293
+ # Aggregate results
294
+ if errors:
295
+ task_node.status = "failed"
296
+ task_node.error = "; ".join(errors)
297
+ return False, results, f"Some subtasks failed: {'; '.join(errors)}"
298
+
299
+ task_node.status = "completed"
300
+ aggregated_result = await self._aggregate_results(task_node, results)
301
+ task_node.result = aggregated_result
302
+
303
+ return True, aggregated_result, None
304
+
305
+ async def _execute_directly(self, task_node: TaskNode) -> Tuple[bool, Any, Optional[str]]:
306
+ """Execute a task directly using the main agent.
307
+
308
+ Args:
309
+ task_node: The task node to execute
310
+
311
+ Returns:
312
+ Tuple of (success, result, error_message)
313
+ """
314
+ task_node.status = "in_progress"
315
+
316
+ # Get the main agent
317
+ agent = self.state_manager.session.agents.get("main")
318
+ if not agent:
319
+ error_msg = "Main agent not available"
320
+ task_node.status = "failed"
321
+ task_node.error = error_msg
322
+ return False, None, error_msg
323
+
324
+ try:
325
+ # Execute with the agent
326
+ result = await agent.run(task_node.description)
327
+ task_node.status = "completed"
328
+ task_node.result = result
329
+ return True, result, None
330
+
331
+ except Exception as e:
332
+ error_msg = f"Error executing task: {str(e)}"
333
+ logger.error(error_msg)
334
+ task_node.status = "failed"
335
+ task_node.error = error_msg
336
+ return False, None, error_msg
337
+
338
+ async def _generate_subtasks(
339
+ self, task_node: TaskNode, complexity_result: TaskComplexityResult
340
+ ) -> List[str]:
341
+ """Generate subtasks for a complex task.
342
+
343
+ Args:
344
+ task_node: The parent task node
345
+ complexity_result: The complexity analysis result
346
+
347
+ Returns:
348
+ List of subtask descriptions
349
+ """
350
+ # If we have suggested subtasks from complexity analysis, use them
351
+ if complexity_result.suggested_subtasks:
352
+ return complexity_result.suggested_subtasks
353
+
354
+ # Otherwise, use agent to generate subtasks
355
+ agent = self.state_manager.session.agents.get("main")
356
+ if not agent:
357
+ # Fallback to simple decomposition
358
+ return [f"Part 1 of: {task_node.description}", f"Part 2 of: {task_node.description}"]
359
+
360
+ decompose_prompt = f"""Break down this task into 2-5 logical subtasks that can be executed independently:
361
+
362
+ Task: {task_node.description}
363
+
364
+ Provide a list of clear, actionable subtasks. Each subtask should:
365
+ - Be self-contained and executable
366
+ - Have clear success criteria
367
+ - Contribute to completing the overall task
368
+
369
+ Return ONLY a JSON array of subtask descriptions:
370
+ ["subtask 1 description", "subtask 2 description", ...]"""
371
+
372
+ try:
373
+ await agent.run(decompose_prompt)
374
+ # Parse subtasks from result (simplified)
375
+ # In production, would parse actual JSON response
376
+ return [
377
+ f"Step 1: Analyze requirements for {task_node.title}",
378
+ f"Step 2: Implement core functionality for {task_node.title}",
379
+ f"Step 3: Test and validate {task_node.title}",
380
+ ]
381
+ except Exception as e:
382
+ logger.error(f"Error generating subtasks: {str(e)}")
383
+ return [f"Execute: {task_node.description}"]
384
+
385
+ def _allocate_iteration_budgets(self, total_budget: int, num_subtasks: int) -> List[int]:
386
+ """Allocate iteration budget across subtasks.
387
+
388
+ Args:
389
+ total_budget: Total iteration budget available
390
+ num_subtasks: Number of subtasks
391
+
392
+ Returns:
393
+ List of budgets for each subtask
394
+ """
395
+ if num_subtasks == 0:
396
+ return []
397
+
398
+ # Simple equal allocation with remainder distribution
399
+ base_budget = total_budget // num_subtasks
400
+ remainder = total_budget % num_subtasks
401
+
402
+ budgets = [base_budget] * num_subtasks
403
+ # Distribute remainder to first tasks
404
+ for i in range(remainder):
405
+ budgets[i] += 1
406
+
407
+ return budgets
408
+
409
+ async def _aggregate_results(self, task_node: TaskNode, subtask_results: List[Any]) -> Any:
410
+ """Aggregate results from subtasks into a final result.
411
+
412
+ Args:
413
+ task_node: The parent task node
414
+ subtask_results: List of results from subtasks
415
+
416
+ Returns:
417
+ Aggregated result
418
+ """
419
+ if not subtask_results:
420
+ return "Task completed with no results"
421
+
422
+ # Convert subtask results to TaskResult objects
423
+ task_results = []
424
+ for i, (subtask, result) in enumerate(zip(task_node.subtasks, subtask_results)):
425
+ task_results.append(
426
+ TaskResult(
427
+ task_id=subtask.id,
428
+ task_title=subtask.title,
429
+ result_data=result,
430
+ status=subtask.status,
431
+ error=subtask.error,
432
+ )
433
+ )
434
+
435
+ # Use intelligent aggregation
436
+ aggregated = await self.aggregator.aggregate_results(
437
+ task_results=task_results,
438
+ parent_task={
439
+ "id": task_node.id,
440
+ "title": task_node.title,
441
+ "description": task_node.description,
442
+ },
443
+ strategy=AggregationStrategy.INTELLIGENT,
444
+ )
445
+
446
+ return aggregated.primary_result
447
+
448
+ def get_task_hierarchy(self) -> Dict[str, TaskNode]:
449
+ """Get the current task hierarchy.
450
+
451
+ Returns:
452
+ Dictionary mapping task IDs to TaskNode objects
453
+ """
454
+ return self._task_hierarchy.copy()
455
+
456
+ def get_execution_stack(self) -> List[str]:
457
+ """Get the current execution stack of task IDs.
458
+
459
+ Returns:
460
+ List of task IDs in execution order
461
+ """
462
+ return self._execution_stack.copy()
463
+
464
+ def clear_hierarchy(self):
465
+ """Clear the task hierarchy and execution stack."""
466
+ self._task_hierarchy.clear()
467
+ self._execution_stack.clear()