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.
- tunacode/cli/commands/__init__.py +2 -0
- tunacode/cli/commands/implementations/__init__.py +3 -0
- tunacode/cli/commands/implementations/debug.py +1 -1
- tunacode/cli/commands/implementations/todo.py +217 -0
- tunacode/cli/commands/registry.py +2 -0
- tunacode/cli/main.py +12 -5
- tunacode/cli/repl.py +205 -136
- tunacode/configuration/defaults.py +2 -0
- tunacode/configuration/models.py +6 -0
- tunacode/constants.py +27 -3
- tunacode/context.py +7 -3
- tunacode/core/agents/dspy_integration.py +223 -0
- tunacode/core/agents/dspy_tunacode.py +458 -0
- tunacode/core/agents/main.py +182 -12
- tunacode/core/agents/utils.py +54 -6
- 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 +467 -0
- tunacode/core/recursive/hierarchy.py +487 -0
- tunacode/core/setup/config_setup.py +5 -0
- tunacode/core/state.py +91 -1
- tunacode/core/token_usage/api_response_parser.py +44 -0
- tunacode/core/token_usage/cost_calculator.py +58 -0
- tunacode/core/token_usage/usage_tracker.py +98 -0
- tunacode/exceptions.py +23 -0
- tunacode/prompts/dspy_task_planning.md +45 -0
- tunacode/prompts/dspy_tool_selection.md +58 -0
- tunacode/prompts/system.md +69 -5
- tunacode/tools/todo.py +343 -0
- tunacode/types.py +20 -1
- tunacode/ui/console.py +1 -1
- tunacode/ui/input.py +1 -1
- tunacode/ui/output.py +38 -1
- tunacode/ui/panels.py +4 -1
- tunacode/ui/recursive_progress.py +380 -0
- tunacode/ui/tool_ui.py +24 -6
- tunacode/ui/utils.py +1 -1
- tunacode/utils/message_utils.py +17 -0
- tunacode/utils/retry.py +163 -0
- tunacode/utils/token_counter.py +78 -8
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/METADATA +4 -1
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/RECORD +48 -32
- tunacode/cli/textual_app.py +0 -420
- tunacode/cli/textual_bridge.py +0 -161
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/top_level.txt +0 -0
tunacode/core/agents/main.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
#
|
|
515
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
tunacode/core/agents/utils.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
]
|