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

Files changed (87) hide show
  1. tunacode/cli/commands/base.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +7 -1
  3. tunacode/cli/commands/implementations/conversation.py +1 -1
  4. tunacode/cli/commands/implementations/debug.py +1 -1
  5. tunacode/cli/commands/implementations/development.py +4 -1
  6. tunacode/cli/commands/implementations/template.py +132 -0
  7. tunacode/cli/commands/registry.py +28 -1
  8. tunacode/cli/commands/template_shortcut.py +93 -0
  9. tunacode/cli/main.py +6 -0
  10. tunacode/cli/repl.py +29 -174
  11. tunacode/cli/repl_components/__init__.py +10 -0
  12. tunacode/cli/repl_components/command_parser.py +34 -0
  13. tunacode/cli/repl_components/error_recovery.py +88 -0
  14. tunacode/cli/repl_components/output_display.py +33 -0
  15. tunacode/cli/repl_components/tool_executor.py +84 -0
  16. tunacode/configuration/defaults.py +2 -2
  17. tunacode/configuration/settings.py +11 -14
  18. tunacode/constants.py +57 -23
  19. tunacode/context.py +0 -14
  20. tunacode/core/agents/agent_components/__init__.py +27 -0
  21. tunacode/core/agents/agent_components/agent_config.py +109 -0
  22. tunacode/core/agents/agent_components/json_tool_parser.py +109 -0
  23. tunacode/core/agents/agent_components/message_handler.py +100 -0
  24. tunacode/core/agents/agent_components/node_processor.py +480 -0
  25. tunacode/core/agents/agent_components/response_state.py +13 -0
  26. tunacode/core/agents/agent_components/result_wrapper.py +50 -0
  27. tunacode/core/agents/agent_components/task_completion.py +28 -0
  28. tunacode/core/agents/agent_components/tool_buffer.py +24 -0
  29. tunacode/core/agents/agent_components/tool_executor.py +49 -0
  30. tunacode/core/agents/main.py +421 -778
  31. tunacode/core/agents/utils.py +42 -2
  32. tunacode/core/background/manager.py +3 -3
  33. tunacode/core/logging/__init__.py +4 -3
  34. tunacode/core/logging/config.py +29 -16
  35. tunacode/core/logging/formatters.py +1 -1
  36. tunacode/core/logging/handlers.py +41 -7
  37. tunacode/core/setup/__init__.py +2 -0
  38. tunacode/core/setup/agent_setup.py +2 -2
  39. tunacode/core/setup/base.py +2 -2
  40. tunacode/core/setup/config_setup.py +10 -6
  41. tunacode/core/setup/git_safety_setup.py +13 -2
  42. tunacode/core/setup/template_setup.py +75 -0
  43. tunacode/core/state.py +13 -2
  44. tunacode/core/token_usage/api_response_parser.py +6 -2
  45. tunacode/core/token_usage/usage_tracker.py +37 -7
  46. tunacode/core/tool_handler.py +24 -1
  47. tunacode/prompts/system.md +289 -4
  48. tunacode/setup.py +2 -0
  49. tunacode/templates/__init__.py +9 -0
  50. tunacode/templates/loader.py +210 -0
  51. tunacode/tools/glob.py +3 -3
  52. tunacode/tools/grep.py +26 -276
  53. tunacode/tools/grep_components/__init__.py +9 -0
  54. tunacode/tools/grep_components/file_filter.py +93 -0
  55. tunacode/tools/grep_components/pattern_matcher.py +152 -0
  56. tunacode/tools/grep_components/result_formatter.py +45 -0
  57. tunacode/tools/grep_components/search_result.py +35 -0
  58. tunacode/tools/todo.py +27 -21
  59. tunacode/types.py +19 -4
  60. tunacode/ui/completers.py +6 -1
  61. tunacode/ui/decorators.py +2 -2
  62. tunacode/ui/keybindings.py +1 -1
  63. tunacode/ui/panels.py +13 -5
  64. tunacode/ui/prompt_manager.py +1 -1
  65. tunacode/ui/tool_ui.py +8 -2
  66. tunacode/utils/bm25.py +4 -4
  67. tunacode/utils/file_utils.py +2 -2
  68. tunacode/utils/message_utils.py +3 -1
  69. tunacode/utils/system.py +0 -4
  70. tunacode/utils/text_utils.py +1 -1
  71. tunacode/utils/token_counter.py +2 -2
  72. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/METADATA +146 -1
  73. tunacode_cli-0.0.53.dist-info/RECORD +123 -0
  74. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/top_level.txt +0 -1
  75. api/auth.py +0 -13
  76. api/users.py +0 -8
  77. tunacode/core/recursive/__init__.py +0 -18
  78. tunacode/core/recursive/aggregator.py +0 -467
  79. tunacode/core/recursive/budget.py +0 -414
  80. tunacode/core/recursive/decomposer.py +0 -398
  81. tunacode/core/recursive/executor.py +0 -470
  82. tunacode/core/recursive/hierarchy.py +0 -488
  83. tunacode/ui/recursive_progress.py +0 -380
  84. tunacode_cli-0.0.50.dist-info/RECORD +0 -107
  85. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/WHEEL +0 -0
  86. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/entry_points.txt +0 -0
  87. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/licenses/LICENSE +0 -0
@@ -1,470 +0,0 @@
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()