tunacode-cli 0.0.40__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.

Files changed (50) hide show
  1. tunacode/cli/commands/__init__.py +2 -0
  2. tunacode/cli/commands/implementations/__init__.py +3 -0
  3. tunacode/cli/commands/implementations/debug.py +1 -1
  4. tunacode/cli/commands/implementations/todo.py +217 -0
  5. tunacode/cli/commands/registry.py +2 -0
  6. tunacode/cli/main.py +12 -5
  7. tunacode/cli/repl.py +205 -136
  8. tunacode/configuration/defaults.py +2 -0
  9. tunacode/configuration/models.py +6 -0
  10. tunacode/constants.py +27 -3
  11. tunacode/context.py +7 -3
  12. tunacode/core/agents/dspy_integration.py +223 -0
  13. tunacode/core/agents/dspy_tunacode.py +458 -0
  14. tunacode/core/agents/main.py +182 -12
  15. tunacode/core/agents/utils.py +54 -6
  16. tunacode/core/recursive/__init__.py +18 -0
  17. tunacode/core/recursive/aggregator.py +467 -0
  18. tunacode/core/recursive/budget.py +414 -0
  19. tunacode/core/recursive/decomposer.py +398 -0
  20. tunacode/core/recursive/executor.py +467 -0
  21. tunacode/core/recursive/hierarchy.py +487 -0
  22. tunacode/core/setup/config_setup.py +5 -0
  23. tunacode/core/state.py +91 -1
  24. tunacode/core/token_usage/api_response_parser.py +44 -0
  25. tunacode/core/token_usage/cost_calculator.py +58 -0
  26. tunacode/core/token_usage/usage_tracker.py +98 -0
  27. tunacode/exceptions.py +23 -0
  28. tunacode/prompts/dspy_task_planning.md +45 -0
  29. tunacode/prompts/dspy_tool_selection.md +58 -0
  30. tunacode/prompts/system.md +69 -5
  31. tunacode/tools/todo.py +343 -0
  32. tunacode/types.py +20 -1
  33. tunacode/ui/console.py +1 -1
  34. tunacode/ui/input.py +1 -1
  35. tunacode/ui/output.py +38 -1
  36. tunacode/ui/panels.py +4 -1
  37. tunacode/ui/recursive_progress.py +380 -0
  38. tunacode/ui/tool_ui.py +24 -6
  39. tunacode/ui/utils.py +1 -1
  40. tunacode/utils/message_utils.py +17 -0
  41. tunacode/utils/retry.py +163 -0
  42. tunacode/utils/token_counter.py +78 -8
  43. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/METADATA +4 -1
  44. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/RECORD +48 -32
  45. tunacode/cli/textual_app.py +0 -420
  46. tunacode/cli/textual_bridge.py +0 -161
  47. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/WHEEL +0 -0
  48. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/entry_points.txt +0 -0
  49. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
  50. {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ Handles agent creation, configuration, and request processing.
6
6
 
7
7
  import asyncio
8
8
  import json
9
+ import logging
9
10
  import os
10
11
  import re
11
12
  from datetime import datetime, timezone
@@ -29,7 +30,12 @@ except ImportError:
29
30
  STREAMING_AVAILABLE = False
30
31
 
31
32
  from tunacode.constants import READ_ONLY_TOOLS
33
+ from tunacode.core.agents.dspy_integration import DSPyIntegration
34
+ from tunacode.core.recursive import RecursiveTaskExecutor
32
35
  from tunacode.core.state import StateManager
36
+ from tunacode.core.token_usage.api_response_parser import ApiResponseParser
37
+ from tunacode.core.token_usage.cost_calculator import CostCalculator
38
+ from tunacode.exceptions import ToolBatchingJSONError
33
39
  from tunacode.services.mcp import get_mcp_servers
34
40
  from tunacode.tools.bash import bash
35
41
  from tunacode.tools.glob import glob
@@ -37,6 +43,7 @@ from tunacode.tools.grep import grep
37
43
  from tunacode.tools.list_dir import list_dir
38
44
  from tunacode.tools.read_file import read_file
39
45
  from tunacode.tools.run_command import run_command
46
+ from tunacode.tools.todo import TodoTool
40
47
  from tunacode.tools.update_file import update_file
41
48
  from tunacode.tools.write_file import write_file
42
49
  from tunacode.types import (
@@ -50,8 +57,12 @@ from tunacode.types import (
50
57
  ToolCallback,
51
58
  ToolCallId,
52
59
  ToolName,
60
+ UsageTrackerProtocol,
53
61
  )
54
62
 
63
+ # Configure logging
64
+ logger = logging.getLogger(__name__)
65
+
55
66
 
56
67
  class ToolBuffer:
57
68
  """Buffer for collecting read-only tool calls to execute in parallel."""
@@ -110,6 +121,7 @@ async def execute_tools_parallel(
110
121
  try:
111
122
  return await callback(part, node)
112
123
  except Exception as e:
124
+ logger.error(f"Error executing parallel tool: {e}", exc_info=True)
113
125
  return e
114
126
 
115
127
  # If we have more tools than max_parallel, execute in batches
@@ -214,6 +226,7 @@ async def _process_node(
214
226
  state_manager: StateManager,
215
227
  tool_buffer: Optional[ToolBuffer] = None,
216
228
  streaming_callback: Optional[callable] = None,
229
+ usage_tracker: Optional[UsageTrackerProtocol] = None,
217
230
  ):
218
231
  from tunacode.ui import console as ui
219
232
  from tunacode.utils.token_counter import estimate_tokens
@@ -233,6 +246,9 @@ async def _process_node(
233
246
  if hasattr(node, "model_response"):
234
247
  state_manager.session.messages.append(node.model_response)
235
248
 
249
+ if usage_tracker:
250
+ await usage_tracker.track_and_display(node.model_response)
251
+
236
252
  # Stream content to callback if provided
237
253
  # Use this as fallback when true token streaming is not available
238
254
  if streaming_callback and not STREAMING_AVAILABLE:
@@ -313,8 +329,8 @@ async def _process_node(
313
329
  thought_obj = json.loads(content)
314
330
  if "thought" in thought_obj:
315
331
  await ui.muted(f"REASONING: {thought_obj['thought']}")
316
- except (json.JSONDecodeError, KeyError):
317
- pass
332
+ except (json.JSONDecodeError, KeyError) as e:
333
+ logger.debug(f"Failed to parse thought JSON: {e}")
318
334
 
319
335
  # Pattern 3: Multi-line thoughts with context
320
336
  multiline_pattern = r'\{"thought":\s*"([^"]+(?:\\.[^"]*)*?)"\}'
@@ -442,8 +458,9 @@ async def _process_node(
442
458
  # Handle tool returns
443
459
  for part in node.model_response.parts:
444
460
  if part.part_kind == "tool-return":
445
- obs_msg = f"OBSERVATION[{part.tool_name}]: {part.content[:2_000]}"
446
- state_manager.session.messages.append(obs_msg)
461
+ state_manager.session.messages.append(
462
+ f"OBSERVATION[{part.tool_name}]: {part.content}"
463
+ )
447
464
 
448
465
  # Display tool return when thoughts are enabled
449
466
  if state_manager.session.show_thoughts:
@@ -457,9 +474,17 @@ async def _process_node(
457
474
  if not has_tool_calls and buffering_callback:
458
475
  for part in node.model_response.parts:
459
476
  if hasattr(part, "content") and isinstance(part.content, str):
460
- await extract_and_execute_tool_calls(
461
- part.content, buffering_callback, state_manager
462
- )
477
+ try:
478
+ await extract_and_execute_tool_calls(
479
+ part.content, buffering_callback, state_manager
480
+ )
481
+ except ToolBatchingJSONError as e:
482
+ # Handle JSON parsing failure after retries
483
+ logger.error(f"Tool batching JSON error: {e}")
484
+ if state_manager.session.show_thoughts:
485
+ await ui.error(str(e))
486
+ # Continue processing other parts instead of failing completely
487
+ continue
463
488
 
464
489
  # Final flush: disabled temporarily while fixing the parallel execution design
465
490
  # The buffer is not being used in the current implementation
@@ -494,6 +519,18 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
494
519
  # Use a default system prompt if neither file exists
495
520
  system_prompt = "You are a helpful AI assistant for software development tasks."
496
521
 
522
+ # Enhance with DSPy optimization if enabled
523
+ use_dspy = state_manager.session.user_config.get("settings", {}).get(
524
+ "use_dspy_optimization", True
525
+ )
526
+ if use_dspy:
527
+ try:
528
+ dspy_integration = DSPyIntegration(state_manager)
529
+ system_prompt = dspy_integration.enhance_system_prompt(system_prompt)
530
+ logger.info("Enhanced system prompt with DSPy optimizations")
531
+ except Exception as e:
532
+ logger.warning(f"Failed to enhance prompt with DSPy: {e}")
533
+
497
534
  # Load TUNACODE.md context
498
535
  # Use sync version of get_code_style to avoid nested event loop issues
499
536
  try:
@@ -510,9 +547,22 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
510
547
  else:
511
548
  # Log that TUNACODE.md was not found
512
549
  print("📄 TUNACODE.md not found: Using default context")
513
- except Exception:
514
- # Ignore errors loading TUNACODE.md
515
- pass
550
+ except Exception as e:
551
+ # Log errors loading TUNACODE.md at debug level
552
+ logger.debug(f"Error loading TUNACODE.md: {e}")
553
+
554
+ todo_tool = TodoTool(state_manager=state_manager)
555
+
556
+ try:
557
+ # Only add todo section if there are actual todos
558
+ current_todos = todo_tool.get_current_todos_sync()
559
+ if current_todos != "No todos found":
560
+ system_prompt += f'\n\n# Current Todo List\n\nYou have existing todos that need attention:\n\n{current_todos}\n\nRemember to check progress on these todos and update them as you work. Use todo("list") to see current status anytime.'
561
+ except Exception as e:
562
+ # Log error but don't fail agent creation
563
+ import sys
564
+
565
+ print(f"Warning: Failed to load todos: {e}", file=sys.stderr)
516
566
 
517
567
  state_manager.session.agents[model] = Agent(
518
568
  model=model,
@@ -524,6 +574,7 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
524
574
  Tool(list_dir, max_retries=max_retries),
525
575
  Tool(read_file, max_retries=max_retries),
526
576
  Tool(run_command, max_retries=max_retries),
577
+ Tool(todo_tool._execute, max_retries=max_retries),
527
578
  Tool(update_file, max_retries=max_retries),
528
579
  Tool(write_file, max_retries=max_retries),
529
580
  ],
@@ -622,7 +673,9 @@ async def parse_json_tool_calls(
622
673
  if isinstance(parsed, dict) and "tool" in parsed and "args" in parsed:
623
674
  potential_jsons.append((parsed["tool"], parsed["args"]))
624
675
  except json.JSONDecodeError:
625
- pass
676
+ logger.debug(
677
+ f"Failed to parse potential JSON tool call: {potential_json[:50]}..."
678
+ )
626
679
  start_pos = -1
627
680
 
628
681
  matches = potential_jsons
@@ -720,6 +773,116 @@ async def process_request(
720
773
  "fallback_response", True
721
774
  )
722
775
 
776
+ # Check if DSPy optimization is enabled and if this is a complex task
777
+ use_dspy = state_manager.session.user_config.get("settings", {}).get(
778
+ "use_dspy_optimization", True
779
+ )
780
+ dspy_integration = None
781
+ task_breakdown = None
782
+
783
+ # Check if recursive execution is enabled
784
+ use_recursive = state_manager.session.user_config.get("settings", {}).get(
785
+ "use_recursive_execution", True
786
+ )
787
+ recursive_threshold = state_manager.session.user_config.get("settings", {}).get(
788
+ "recursive_complexity_threshold", 0.7
789
+ )
790
+
791
+ if use_dspy:
792
+ try:
793
+ dspy_integration = DSPyIntegration(state_manager)
794
+
795
+ # Check if this is a complex task that needs planning
796
+ if dspy_integration.should_use_task_planner(message):
797
+ task_breakdown = dspy_integration.get_task_breakdown(message)
798
+ if task_breakdown and task_breakdown.get("requires_todo"):
799
+ # Auto-create todos for complex tasks
800
+ from tunacode.tools.todo import TodoTool
801
+
802
+ todo_tool = TodoTool(state_manager=state_manager)
803
+
804
+ if state_manager.session.show_thoughts:
805
+ from tunacode.ui import console as ui
806
+
807
+ await ui.muted("DSPy: Detected complex task - creating todo list")
808
+
809
+ # Create todos from subtasks
810
+ todos = []
811
+ for subtask in task_breakdown["subtasks"][:5]: # Limit to first 5
812
+ todos.append(
813
+ {
814
+ "content": subtask["task"],
815
+ "priority": subtask.get("priority", "medium"),
816
+ }
817
+ )
818
+
819
+ if todos:
820
+ await todo_tool._execute(action="add_multiple", todos=todos)
821
+ except Exception as e:
822
+ logger.warning(f"DSPy task planning failed: {e}")
823
+
824
+ # Check if recursive execution should be used
825
+ if use_recursive and state_manager.session.current_recursion_depth == 0:
826
+ try:
827
+ # Initialize recursive executor
828
+ recursive_executor = RecursiveTaskExecutor(
829
+ state_manager=state_manager,
830
+ max_depth=state_manager.session.max_recursion_depth,
831
+ min_complexity_threshold=recursive_threshold,
832
+ default_iteration_budget=max_iterations,
833
+ )
834
+
835
+ # Analyze task complexity
836
+ complexity_result = await recursive_executor.decomposer.analyze_and_decompose(message)
837
+
838
+ if (
839
+ complexity_result.should_decompose
840
+ and complexity_result.total_complexity >= recursive_threshold
841
+ ):
842
+ if state_manager.session.show_thoughts:
843
+ from tunacode.ui import console as ui
844
+
845
+ await ui.muted(
846
+ f"\n🔄 RECURSIVE EXECUTION: Task complexity {complexity_result.total_complexity:.2f} >= {recursive_threshold}"
847
+ )
848
+ await ui.muted(f"Reasoning: {complexity_result.reasoning}")
849
+ await ui.muted(f"Subtasks: {len(complexity_result.subtasks)}")
850
+
851
+ # Execute recursively
852
+ success, result, error = await recursive_executor.execute_task(
853
+ request=message, parent_task_id=None, depth=0
854
+ )
855
+
856
+ # Create AgentRun response
857
+ from datetime import datetime
858
+
859
+ if success:
860
+ return AgentRun(
861
+ messages=[{"role": "assistant", "content": str(result)}],
862
+ timestamp=datetime.now(),
863
+ model=model,
864
+ iterations=1,
865
+ status="success",
866
+ )
867
+ else:
868
+ return AgentRun(
869
+ messages=[{"role": "assistant", "content": f"Task failed: {error}"}],
870
+ timestamp=datetime.now(),
871
+ model=model,
872
+ iterations=1,
873
+ status="error",
874
+ )
875
+ except Exception as e:
876
+ logger.warning(f"Recursive execution failed, falling back to normal: {e}")
877
+ # Continue with normal execution
878
+
879
+ from tunacode.configuration.models import ModelRegistry
880
+ from tunacode.core.token_usage.usage_tracker import UsageTracker
881
+
882
+ parser = ApiResponseParser()
883
+ registry = ModelRegistry()
884
+ calculator = CostCalculator(registry)
885
+ usage_tracker = UsageTracker(parser, calculator, state_manager)
723
886
  response_state = ResponseState()
724
887
 
725
888
  # Reset iteration tracking for this request
@@ -763,7 +926,14 @@ async def process_request(
763
926
  if event.delta.content_delta:
764
927
  await streaming_callback(event.delta.content_delta)
765
928
 
766
- await _process_node(node, tool_callback, state_manager, tool_buffer, streaming_callback)
929
+ await _process_node(
930
+ node,
931
+ tool_callback,
932
+ state_manager,
933
+ tool_buffer,
934
+ streaming_callback,
935
+ usage_tracker,
936
+ )
767
937
  if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
768
938
  if node.result.output:
769
939
  response_state.has_user_response = True
@@ -1,13 +1,20 @@
1
1
  import asyncio
2
2
  import importlib
3
3
  import json
4
+ import logging
4
5
  import os
5
6
  import re
6
7
  from collections.abc import Iterator
7
8
  from datetime import datetime, timezone
8
9
  from typing import Any
9
10
 
10
- from tunacode.constants import READ_ONLY_TOOLS
11
+ from tunacode.constants import (
12
+ JSON_PARSE_BASE_DELAY,
13
+ JSON_PARSE_MAX_DELAY,
14
+ JSON_PARSE_MAX_RETRIES,
15
+ READ_ONLY_TOOLS,
16
+ )
17
+ from tunacode.exceptions import ToolBatchingJSONError
11
18
  from tunacode.types import (
12
19
  ErrorMessage,
13
20
  StateManager,
@@ -16,6 +23,9 @@ from tunacode.types import (
16
23
  ToolName,
17
24
  )
18
25
  from tunacode.ui import console as ui
26
+ from tunacode.utils.retry import retry_json_parse_async
27
+
28
+ logger = logging.getLogger(__name__)
19
29
 
20
30
 
21
31
  # Lazy import for Agent and Tool
@@ -167,11 +177,28 @@ async def parse_json_tool_calls(
167
177
  if brace_count == 0 and start_pos != -1:
168
178
  potential_json = text[start_pos : i + 1]
169
179
  try:
170
- parsed = json.loads(potential_json)
180
+ # Use retry logic for JSON parsing
181
+ parsed = await retry_json_parse_async(
182
+ potential_json,
183
+ max_retries=JSON_PARSE_MAX_RETRIES,
184
+ base_delay=JSON_PARSE_BASE_DELAY,
185
+ max_delay=JSON_PARSE_MAX_DELAY,
186
+ )
171
187
  if isinstance(parsed, dict) and "tool" in parsed and "args" in parsed:
172
188
  potential_jsons.append((parsed["tool"], parsed["args"]))
173
- except json.JSONDecodeError:
174
- pass
189
+ except json.JSONDecodeError as e:
190
+ # After all retries failed
191
+ logger.error(f"JSON parsing failed after {JSON_PARSE_MAX_RETRIES} retries: {e}")
192
+ if state_manager.session.show_thoughts:
193
+ await ui.error(
194
+ f"Failed to parse tool JSON after {JSON_PARSE_MAX_RETRIES} retries"
195
+ )
196
+ # Raise custom exception for better error handling
197
+ raise ToolBatchingJSONError(
198
+ json_content=potential_json,
199
+ retry_count=JSON_PARSE_MAX_RETRIES,
200
+ original_error=e,
201
+ ) from e
175
202
  start_pos = -1
176
203
 
177
204
  matches = potential_jsons
@@ -220,7 +247,13 @@ async def extract_and_execute_tool_calls(
220
247
 
221
248
  for match in code_matches:
222
249
  try:
223
- tool_data = json.loads(match)
250
+ # Use retry logic for JSON parsing in code blocks
251
+ tool_data = await retry_json_parse_async(
252
+ match,
253
+ max_retries=JSON_PARSE_MAX_RETRIES,
254
+ base_delay=JSON_PARSE_BASE_DELAY,
255
+ max_delay=JSON_PARSE_MAX_DELAY,
256
+ )
224
257
  if "tool" in tool_data and "args" in tool_data:
225
258
 
226
259
  class MockToolCall:
@@ -240,7 +273,22 @@ async def extract_and_execute_tool_calls(
240
273
  if state_manager.session.show_thoughts:
241
274
  await ui.muted(f"FALLBACK: Executed {tool_data['tool']} from code block")
242
275
 
243
- except (json.JSONDecodeError, KeyError, Exception) as e:
276
+ except json.JSONDecodeError as e:
277
+ # After all retries failed
278
+ logger.error(
279
+ f"Code block JSON parsing failed after {JSON_PARSE_MAX_RETRIES} retries: {e}"
280
+ )
281
+ if state_manager.session.show_thoughts:
282
+ await ui.error(
283
+ f"Failed to parse code block tool JSON after {JSON_PARSE_MAX_RETRIES} retries"
284
+ )
285
+ # Raise custom exception for better error handling
286
+ raise ToolBatchingJSONError(
287
+ json_content=match,
288
+ retry_count=JSON_PARSE_MAX_RETRIES,
289
+ original_error=e,
290
+ ) from e
291
+ except (KeyError, Exception) as e:
244
292
  if state_manager.session.show_thoughts:
245
293
  await ui.error(f"Error parsing code block tool call: {e!s}")
246
294
 
@@ -0,0 +1,18 @@
1
+ """Module: tunacode.core.recursive
2
+
3
+ Recursive task execution system for complex task decomposition and execution.
4
+ """
5
+
6
+ from .aggregator import ResultAggregator
7
+ from .budget import BudgetManager
8
+ from .decomposer import TaskDecomposer
9
+ from .executor import RecursiveTaskExecutor
10
+ from .hierarchy import TaskHierarchy
11
+
12
+ __all__ = [
13
+ "RecursiveTaskExecutor",
14
+ "TaskDecomposer",
15
+ "TaskHierarchy",
16
+ "BudgetManager",
17
+ "ResultAggregator",
18
+ ]