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.
- tunacode/cli/commands/base.py +2 -2
- tunacode/cli/commands/implementations/__init__.py +7 -1
- tunacode/cli/commands/implementations/conversation.py +1 -1
- tunacode/cli/commands/implementations/debug.py +1 -1
- tunacode/cli/commands/implementations/development.py +4 -1
- tunacode/cli/commands/implementations/template.py +132 -0
- tunacode/cli/commands/registry.py +28 -1
- tunacode/cli/commands/template_shortcut.py +93 -0
- tunacode/cli/main.py +6 -0
- tunacode/cli/repl.py +29 -174
- tunacode/cli/repl_components/__init__.py +10 -0
- tunacode/cli/repl_components/command_parser.py +34 -0
- tunacode/cli/repl_components/error_recovery.py +88 -0
- tunacode/cli/repl_components/output_display.py +33 -0
- tunacode/cli/repl_components/tool_executor.py +84 -0
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/settings.py +11 -14
- tunacode/constants.py +57 -23
- tunacode/context.py +0 -14
- tunacode/core/agents/agent_components/__init__.py +27 -0
- tunacode/core/agents/agent_components/agent_config.py +109 -0
- tunacode/core/agents/agent_components/json_tool_parser.py +109 -0
- tunacode/core/agents/agent_components/message_handler.py +100 -0
- tunacode/core/agents/agent_components/node_processor.py +480 -0
- tunacode/core/agents/agent_components/response_state.py +13 -0
- tunacode/core/agents/agent_components/result_wrapper.py +50 -0
- tunacode/core/agents/agent_components/task_completion.py +28 -0
- tunacode/core/agents/agent_components/tool_buffer.py +24 -0
- tunacode/core/agents/agent_components/tool_executor.py +49 -0
- tunacode/core/agents/main.py +421 -778
- tunacode/core/agents/utils.py +42 -2
- tunacode/core/background/manager.py +3 -3
- tunacode/core/logging/__init__.py +4 -3
- tunacode/core/logging/config.py +29 -16
- tunacode/core/logging/formatters.py +1 -1
- tunacode/core/logging/handlers.py +41 -7
- tunacode/core/setup/__init__.py +2 -0
- tunacode/core/setup/agent_setup.py +2 -2
- tunacode/core/setup/base.py +2 -2
- tunacode/core/setup/config_setup.py +10 -6
- tunacode/core/setup/git_safety_setup.py +13 -2
- tunacode/core/setup/template_setup.py +75 -0
- tunacode/core/state.py +13 -2
- tunacode/core/token_usage/api_response_parser.py +6 -2
- tunacode/core/token_usage/usage_tracker.py +37 -7
- tunacode/core/tool_handler.py +24 -1
- tunacode/prompts/system.md +289 -4
- tunacode/setup.py +2 -0
- tunacode/templates/__init__.py +9 -0
- tunacode/templates/loader.py +210 -0
- tunacode/tools/glob.py +3 -3
- tunacode/tools/grep.py +26 -276
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +152 -0
- tunacode/tools/grep_components/result_formatter.py +45 -0
- tunacode/tools/grep_components/search_result.py +35 -0
- tunacode/tools/todo.py +27 -21
- tunacode/types.py +19 -4
- tunacode/ui/completers.py +6 -1
- tunacode/ui/decorators.py +2 -2
- tunacode/ui/keybindings.py +1 -1
- tunacode/ui/panels.py +13 -5
- tunacode/ui/prompt_manager.py +1 -1
- tunacode/ui/tool_ui.py +8 -2
- tunacode/utils/bm25.py +4 -4
- tunacode/utils/file_utils.py +2 -2
- tunacode/utils/message_utils.py +3 -1
- tunacode/utils/system.py +0 -4
- tunacode/utils/text_utils.py +1 -1
- tunacode/utils/token_counter.py +2 -2
- {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/METADATA +146 -1
- tunacode_cli-0.0.53.dist-info/RECORD +123 -0
- {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/top_level.txt +0 -1
- api/auth.py +0 -13
- api/users.py +0 -8
- tunacode/core/recursive/__init__.py +0 -18
- tunacode/core/recursive/aggregator.py +0 -467
- tunacode/core/recursive/budget.py +0 -414
- tunacode/core/recursive/decomposer.py +0 -398
- tunacode/core/recursive/executor.py +0 -470
- tunacode/core/recursive/hierarchy.py +0 -488
- tunacode/ui/recursive_progress.py +0 -380
- tunacode_cli-0.0.50.dist-info/RECORD +0 -107
- {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REPL components package for modular REPL functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .command_parser import parse_args
|
|
6
|
+
from .error_recovery import attempt_tool_recovery
|
|
7
|
+
from .output_display import display_agent_output
|
|
8
|
+
from .tool_executor import tool_handler
|
|
9
|
+
|
|
10
|
+
__all__ = ["parse_args", "attempt_tool_recovery", "display_agent_output", "tool_handler"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.cli.repl_components.command_parser
|
|
3
|
+
|
|
4
|
+
Command parsing utilities for the REPL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from tunacode.exceptions import ValidationError
|
|
10
|
+
from tunacode.types import ToolArgs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args(args) -> ToolArgs:
|
|
14
|
+
"""
|
|
15
|
+
Parse tool arguments from a JSON string or dictionary.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
args (str or dict): A JSON-formatted string or a dictionary containing tool arguments.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict: The parsed arguments.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If 'args' is not a string or dictionary, or if the string is not valid JSON.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(args, str):
|
|
27
|
+
try:
|
|
28
|
+
return json.loads(args)
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
raise ValidationError(f"Invalid JSON: {args}")
|
|
31
|
+
elif isinstance(args, dict):
|
|
32
|
+
return args
|
|
33
|
+
else:
|
|
34
|
+
raise ValidationError(f"Invalid args type: {type(args)}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.cli.repl_components.error_recovery
|
|
3
|
+
|
|
4
|
+
Error recovery utilities for the REPL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from tunacode.types import StateManager
|
|
10
|
+
from tunacode.ui import console as ui
|
|
11
|
+
|
|
12
|
+
from .tool_executor import tool_handler
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
MSG_JSON_RECOVERY = "Recovered using JSON tool parsing"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def attempt_tool_recovery(e: Exception, state_manager: StateManager) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Attempt to recover from tool calling failures by parsing raw JSON from the last message.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
bool: True if recovery was successful, False otherwise
|
|
25
|
+
"""
|
|
26
|
+
error_str = str(e).lower()
|
|
27
|
+
tool_keywords = ["tool", "function", "call", "schema"]
|
|
28
|
+
if not any(keyword in error_str for keyword in tool_keywords):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
if not state_manager.session.messages:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
last_msg = state_manager.session.messages[-1]
|
|
35
|
+
if not hasattr(last_msg, "parts"):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
for part in last_msg.parts:
|
|
39
|
+
content_to_parse = getattr(part, "content", None)
|
|
40
|
+
if not isinstance(content_to_parse, str) or not content_to_parse.strip():
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
logger.debug(
|
|
44
|
+
"Attempting JSON tool recovery on content",
|
|
45
|
+
extra={
|
|
46
|
+
"content_preview": content_to_parse[:200],
|
|
47
|
+
"original_error": str(e),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
await ui.muted(
|
|
51
|
+
f"⚠️ Model response error. Attempting to recover by parsing tools from text: {str(e)[:100]}..."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
56
|
+
|
|
57
|
+
def tool_callback_with_state(tool_part, _node):
|
|
58
|
+
return tool_handler(tool_part, state_manager)
|
|
59
|
+
|
|
60
|
+
# This function now returns the number of tools found
|
|
61
|
+
tools_found = await extract_and_execute_tool_calls(
|
|
62
|
+
content_to_parse, tool_callback_with_state, state_manager
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Treat any truthy return value as success – we don't depend on an exact count.
|
|
66
|
+
if tools_found:
|
|
67
|
+
await ui.warning(f" {MSG_JSON_RECOVERY}")
|
|
68
|
+
logger.info(
|
|
69
|
+
"Successfully recovered from JSON tool parsing error.",
|
|
70
|
+
extra={"tools_executed": tools_found},
|
|
71
|
+
)
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
logger.debug("Recovery attempted, but no tools were found in content.")
|
|
75
|
+
|
|
76
|
+
except Exception as recovery_exc:
|
|
77
|
+
logger.error(
|
|
78
|
+
"Exception during JSON tool recovery attempt",
|
|
79
|
+
exc_info=True,
|
|
80
|
+
extra={"recovery_exception": str(recovery_exc)},
|
|
81
|
+
)
|
|
82
|
+
continue # Try next part if available
|
|
83
|
+
|
|
84
|
+
# If we attempted recovery but could not execute any tools, simply
|
|
85
|
+
# return False so that the caller can handle the original error. We avoid
|
|
86
|
+
# emitting an additional error message here to prevent duplicate UI
|
|
87
|
+
# notifications which would otherwise break expectations in unit tests.
|
|
88
|
+
return False
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.cli.repl_components.output_display
|
|
3
|
+
|
|
4
|
+
Output formatting and display utilities for the REPL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tunacode.ui import console as ui
|
|
8
|
+
|
|
9
|
+
# MSG_REQUEST_COMPLETED is used in repl.py
|
|
10
|
+
MSG_REQUEST_COMPLETED = "Request completed"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def display_agent_output(res, enable_streaming: bool) -> None:
|
|
14
|
+
"""Display agent output using guard clauses to flatten nested conditionals."""
|
|
15
|
+
if enable_streaming:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
if not hasattr(res, "result") or res.result is None or not hasattr(res.result, "output"):
|
|
19
|
+
await ui.muted(MSG_REQUEST_COMPLETED)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
output = res.result.output
|
|
23
|
+
|
|
24
|
+
if not isinstance(output, str):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
if output.strip().startswith('{"thought"'):
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if '"tool_uses"' in output:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
await ui.agent(output)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.cli.repl_components.tool_executor
|
|
3
|
+
|
|
4
|
+
Tool execution and confirmation handling for the REPL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from asyncio.exceptions import CancelledError
|
|
9
|
+
|
|
10
|
+
from prompt_toolkit.application import run_in_terminal
|
|
11
|
+
|
|
12
|
+
from tunacode.core.agents.main import patch_tool_messages
|
|
13
|
+
from tunacode.core.tool_handler import ToolHandler
|
|
14
|
+
from tunacode.exceptions import UserAbortError
|
|
15
|
+
from tunacode.types import StateManager
|
|
16
|
+
from tunacode.ui import console as ui
|
|
17
|
+
from tunacode.ui.tool_ui import ToolUI
|
|
18
|
+
|
|
19
|
+
from .command_parser import parse_args
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_tool_ui = ToolUI()
|
|
24
|
+
|
|
25
|
+
MSG_OPERATION_ABORTED_BY_USER = "Operation aborted by user."
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def tool_handler(part, state_manager: StateManager):
|
|
29
|
+
"""Handle tool execution with separated business logic and UI."""
|
|
30
|
+
# Check for cancellation before tool execution (only if explicitly set to True)
|
|
31
|
+
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
32
|
+
if operation_cancelled is True:
|
|
33
|
+
logger.debug("Tool execution cancelled")
|
|
34
|
+
raise CancelledError("Operation was cancelled")
|
|
35
|
+
|
|
36
|
+
# Get or create tool handler
|
|
37
|
+
if state_manager.tool_handler is None:
|
|
38
|
+
tool_handler_instance = ToolHandler(state_manager)
|
|
39
|
+
state_manager.set_tool_handler(tool_handler_instance)
|
|
40
|
+
else:
|
|
41
|
+
tool_handler_instance = state_manager.tool_handler
|
|
42
|
+
|
|
43
|
+
if tool_handler_instance.should_confirm(part.tool_name):
|
|
44
|
+
await ui.info(f"Tool({part.tool_name})")
|
|
45
|
+
|
|
46
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
47
|
+
state_manager.session.spinner.stop()
|
|
48
|
+
|
|
49
|
+
streaming_panel = None
|
|
50
|
+
if state_manager.session.is_streaming_active and hasattr(
|
|
51
|
+
state_manager.session, "streaming_panel"
|
|
52
|
+
):
|
|
53
|
+
streaming_panel = state_manager.session.streaming_panel
|
|
54
|
+
if streaming_panel and tool_handler_instance.should_confirm(part.tool_name):
|
|
55
|
+
await streaming_panel.stop()
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
args = parse_args(part.args)
|
|
59
|
+
|
|
60
|
+
def confirm_func():
|
|
61
|
+
if not tool_handler_instance.should_confirm(part.tool_name):
|
|
62
|
+
return False
|
|
63
|
+
request = tool_handler_instance.create_confirmation_request(part.tool_name, args)
|
|
64
|
+
|
|
65
|
+
response = _tool_ui.show_sync_confirmation(request)
|
|
66
|
+
|
|
67
|
+
if not tool_handler_instance.process_confirmation(response, part.tool_name):
|
|
68
|
+
return True # Abort
|
|
69
|
+
return False # Continue
|
|
70
|
+
|
|
71
|
+
should_abort = await run_in_terminal(confirm_func)
|
|
72
|
+
|
|
73
|
+
if should_abort:
|
|
74
|
+
raise UserAbortError("User aborted.")
|
|
75
|
+
|
|
76
|
+
except UserAbortError:
|
|
77
|
+
patch_tool_messages(MSG_OPERATION_ABORTED_BY_USER, state_manager)
|
|
78
|
+
raise
|
|
79
|
+
finally:
|
|
80
|
+
if streaming_panel and tool_handler_instance.should_confirm(part.tool_name):
|
|
81
|
+
await streaming_panel.start()
|
|
82
|
+
|
|
83
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
84
|
+
state_manager.session.spinner.start()
|
|
@@ -5,7 +5,7 @@ Default configuration values for the TunaCode CLI.
|
|
|
5
5
|
Provides sensible defaults for user configuration and environment variables.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from tunacode.constants import GUIDE_FILE_NAME,
|
|
8
|
+
from tunacode.constants import GUIDE_FILE_NAME, ToolName
|
|
9
9
|
from tunacode.types import UserConfig
|
|
10
10
|
|
|
11
11
|
DEFAULT_USER_CONFIG: UserConfig = {
|
|
@@ -19,7 +19,7 @@ DEFAULT_USER_CONFIG: UserConfig = {
|
|
|
19
19
|
"settings": {
|
|
20
20
|
"max_retries": 10,
|
|
21
21
|
"max_iterations": 40,
|
|
22
|
-
"tool_ignore": [
|
|
22
|
+
"tool_ignore": [ToolName.READ_FILE],
|
|
23
23
|
"guide_file": GUIDE_FILE_NAME,
|
|
24
24
|
"fallback_response": True,
|
|
25
25
|
"fallback_verbosity": "normal", # Options: minimal, normal, detailed
|
|
@@ -7,16 +7,8 @@ Handles configuration paths, model registries, and application metadata.
|
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
from tunacode.constants import
|
|
11
|
-
|
|
12
|
-
APP_VERSION,
|
|
13
|
-
CONFIG_FILE_NAME,
|
|
14
|
-
TOOL_READ_FILE,
|
|
15
|
-
TOOL_RUN_COMMAND,
|
|
16
|
-
TOOL_UPDATE_FILE,
|
|
17
|
-
TOOL_WRITE_FILE,
|
|
18
|
-
)
|
|
19
|
-
from tunacode.types import ConfigFile, ConfigPath, ToolName
|
|
10
|
+
from tunacode.constants import APP_NAME, APP_VERSION, CONFIG_FILE_NAME, ToolName
|
|
11
|
+
from tunacode.types import ConfigFile, ConfigPath
|
|
20
12
|
|
|
21
13
|
|
|
22
14
|
class PathConfig:
|
|
@@ -32,8 +24,13 @@ class ApplicationSettings:
|
|
|
32
24
|
self.guide_file = f"{self.name.upper()}.md"
|
|
33
25
|
self.paths = PathConfig()
|
|
34
26
|
self.internal_tools: list[ToolName] = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
ToolName.BASH,
|
|
28
|
+
ToolName.GLOB,
|
|
29
|
+
ToolName.GREP,
|
|
30
|
+
ToolName.LIST_DIR,
|
|
31
|
+
ToolName.READ_FILE,
|
|
32
|
+
ToolName.RUN_COMMAND,
|
|
33
|
+
ToolName.TODO,
|
|
34
|
+
ToolName.UPDATE_FILE,
|
|
35
|
+
ToolName.WRITE_FILE,
|
|
39
36
|
]
|
tunacode/constants.py
CHANGED
|
@@ -5,9 +5,11 @@ Global constants and configuration values for the TunaCode CLI application.
|
|
|
5
5
|
Centralizes all magic strings, UI text, error messages, and application constants.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
8
10
|
# Application info
|
|
9
11
|
APP_NAME = "TunaCode"
|
|
10
|
-
APP_VERSION = "0.0.
|
|
12
|
+
APP_VERSION = "0.0.53"
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
# File patterns
|
|
@@ -29,21 +31,36 @@ COMMAND_OUTPUT_THRESHOLD = 3500 # Length threshold for truncation
|
|
|
29
31
|
COMMAND_OUTPUT_START_INDEX = 2500 # Where to start showing content
|
|
30
32
|
COMMAND_OUTPUT_END_SIZE = 1000 # How much to show from the end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
|
|
35
|
+
class ToolName(str, Enum):
|
|
36
|
+
"""Enumeration of tool names."""
|
|
37
|
+
|
|
38
|
+
READ_FILE = "read_file"
|
|
39
|
+
WRITE_FILE = "write_file"
|
|
40
|
+
UPDATE_FILE = "update_file"
|
|
41
|
+
RUN_COMMAND = "run_command"
|
|
42
|
+
BASH = "bash"
|
|
43
|
+
GREP = "grep"
|
|
44
|
+
LIST_DIR = "list_dir"
|
|
45
|
+
GLOB = "glob"
|
|
46
|
+
TODO = "todo"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Tool names (backward compatibility)
|
|
50
|
+
TOOL_READ_FILE = ToolName.READ_FILE
|
|
51
|
+
TOOL_WRITE_FILE = ToolName.WRITE_FILE
|
|
52
|
+
TOOL_UPDATE_FILE = ToolName.UPDATE_FILE
|
|
53
|
+
TOOL_RUN_COMMAND = ToolName.RUN_COMMAND
|
|
54
|
+
TOOL_BASH = ToolName.BASH
|
|
55
|
+
TOOL_GREP = ToolName.GREP
|
|
56
|
+
TOOL_LIST_DIR = ToolName.LIST_DIR
|
|
57
|
+
TOOL_GLOB = ToolName.GLOB
|
|
58
|
+
TOOL_TODO = ToolName.TODO
|
|
42
59
|
|
|
43
60
|
# Tool categorization
|
|
44
|
-
READ_ONLY_TOOLS = [
|
|
45
|
-
WRITE_TOOLS = [
|
|
46
|
-
EXECUTE_TOOLS = [
|
|
61
|
+
READ_ONLY_TOOLS = [ToolName.READ_FILE, ToolName.GREP, ToolName.LIST_DIR, ToolName.GLOB]
|
|
62
|
+
WRITE_TOOLS = [ToolName.WRITE_FILE, ToolName.UPDATE_FILE]
|
|
63
|
+
EXECUTE_TOOLS = [ToolName.BASH, ToolName.RUN_COMMAND]
|
|
47
64
|
|
|
48
65
|
# Commands
|
|
49
66
|
CMD_HELP = "/help"
|
|
@@ -142,16 +159,33 @@ MSG_UPDATE_INSTRUCTION = "Exit, and run: [bold]pip install --upgrade tunacode-cl
|
|
|
142
159
|
MSG_VERSION_DISPLAY = "TunaCode CLI {version}"
|
|
143
160
|
MSG_FILE_SIZE_LIMIT = " Please specify a smaller file or use other tools to process it."
|
|
144
161
|
|
|
145
|
-
# Todo-related constants
|
|
146
|
-
TODO_STATUS_PENDING = "pending"
|
|
147
|
-
TODO_STATUS_IN_PROGRESS = "in_progress"
|
|
148
|
-
TODO_STATUS_COMPLETED = "completed"
|
|
149
|
-
TODO_STATUSES = [TODO_STATUS_PENDING, TODO_STATUS_IN_PROGRESS, TODO_STATUS_COMPLETED]
|
|
150
162
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
class TodoStatus(str, Enum):
|
|
164
|
+
"""Enumeration of todo statuses."""
|
|
165
|
+
|
|
166
|
+
PENDING = "pending"
|
|
167
|
+
IN_PROGRESS = "in_progress"
|
|
168
|
+
COMPLETED = "completed"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TodoPriority(str, Enum):
|
|
172
|
+
"""Enumeration of todo priorities."""
|
|
173
|
+
|
|
174
|
+
HIGH = "high"
|
|
175
|
+
MEDIUM = "medium"
|
|
176
|
+
LOW = "low"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Todo-related constants (backward compatibility)
|
|
180
|
+
TODO_STATUS_PENDING = TodoStatus.PENDING
|
|
181
|
+
TODO_STATUS_IN_PROGRESS = TodoStatus.IN_PROGRESS
|
|
182
|
+
TODO_STATUS_COMPLETED = TodoStatus.COMPLETED
|
|
183
|
+
TODO_STATUSES = [TodoStatus.PENDING, TodoStatus.IN_PROGRESS, TodoStatus.COMPLETED]
|
|
184
|
+
|
|
185
|
+
TODO_PRIORITY_HIGH = TodoPriority.HIGH
|
|
186
|
+
TODO_PRIORITY_MEDIUM = TodoPriority.MEDIUM
|
|
187
|
+
TODO_PRIORITY_LOW = TodoPriority.LOW
|
|
188
|
+
TODO_PRIORITIES = [TodoPriority.HIGH, TodoPriority.MEDIUM, TodoPriority.LOW]
|
|
155
189
|
|
|
156
190
|
# Maximum number of todos allowed per session
|
|
157
191
|
MAX_TODOS_PER_SESSION = 100
|
tunacode/context.py
CHANGED
|
@@ -69,17 +69,3 @@ async def get_code_style() -> str:
|
|
|
69
69
|
async def get_claude_files() -> List[str]:
|
|
70
70
|
"""Return a list of additional TUNACODE.md files in the repo."""
|
|
71
71
|
return ripgrep("TUNACODE.md", ".")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async def get_context() -> Dict[str, object]:
|
|
75
|
-
"""Gather repository context."""
|
|
76
|
-
git = await get_git_status()
|
|
77
|
-
directory = await get_directory_structure()
|
|
78
|
-
style = await get_code_style()
|
|
79
|
-
claude_files = await get_claude_files()
|
|
80
|
-
return {
|
|
81
|
-
"git": git,
|
|
82
|
-
"directory": directory,
|
|
83
|
-
"codeStyle": style,
|
|
84
|
-
"claudeFiles": claude_files,
|
|
85
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent components package for modular agent functionality."""
|
|
2
|
+
|
|
3
|
+
from .agent_config import get_or_create_agent
|
|
4
|
+
from .json_tool_parser import extract_and_execute_tool_calls, parse_json_tool_calls
|
|
5
|
+
from .message_handler import get_model_messages, patch_tool_messages
|
|
6
|
+
from .node_processor import _process_node
|
|
7
|
+
from .response_state import ResponseState
|
|
8
|
+
from .result_wrapper import AgentRunWithState, AgentRunWrapper, SimpleResult
|
|
9
|
+
from .task_completion import check_task_completion
|
|
10
|
+
from .tool_buffer import ToolBuffer
|
|
11
|
+
from .tool_executor import execute_tools_parallel
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"get_or_create_agent",
|
|
15
|
+
"extract_and_execute_tool_calls",
|
|
16
|
+
"parse_json_tool_calls",
|
|
17
|
+
"get_model_messages",
|
|
18
|
+
"patch_tool_messages",
|
|
19
|
+
"_process_node",
|
|
20
|
+
"ResponseState",
|
|
21
|
+
"AgentRunWithState",
|
|
22
|
+
"AgentRunWrapper",
|
|
23
|
+
"SimpleResult",
|
|
24
|
+
"check_task_completion",
|
|
25
|
+
"ToolBuffer",
|
|
26
|
+
"execute_tools_parallel",
|
|
27
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Agent configuration and creation utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic_ai import Agent
|
|
6
|
+
|
|
7
|
+
from tunacode.core.logging.logger import get_logger
|
|
8
|
+
from tunacode.core.state import StateManager
|
|
9
|
+
from tunacode.services.mcp import get_mcp_servers
|
|
10
|
+
from tunacode.tools.bash import bash
|
|
11
|
+
from tunacode.tools.glob import glob
|
|
12
|
+
from tunacode.tools.grep import grep
|
|
13
|
+
from tunacode.tools.list_dir import list_dir
|
|
14
|
+
from tunacode.tools.read_file import read_file
|
|
15
|
+
from tunacode.tools.run_command import run_command
|
|
16
|
+
from tunacode.tools.todo import TodoTool
|
|
17
|
+
from tunacode.tools.update_file import update_file
|
|
18
|
+
from tunacode.tools.write_file import write_file
|
|
19
|
+
from tunacode.types import ModelName, PydanticAgent
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_agent_tool():
|
|
25
|
+
"""Lazy import for Agent and Tool to avoid circular imports."""
|
|
26
|
+
from pydantic_ai import Tool
|
|
27
|
+
|
|
28
|
+
return Agent, Tool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_system_prompt(base_path: Path) -> str:
|
|
32
|
+
"""Load the system prompt from file."""
|
|
33
|
+
prompt_path = base_path / "prompts" / "system.md"
|
|
34
|
+
try:
|
|
35
|
+
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
36
|
+
return f.read().strip()
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
# Fallback to system.txt if system.md not found
|
|
39
|
+
prompt_path = base_path / "prompts" / "system.txt"
|
|
40
|
+
try:
|
|
41
|
+
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
42
|
+
return f.read().strip()
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
# Use a default system prompt if neither file exists
|
|
45
|
+
return "You are a helpful AI assistant for software development tasks."
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_tunacode_context() -> str:
|
|
49
|
+
"""Load TUNACODE.md context if it exists."""
|
|
50
|
+
try:
|
|
51
|
+
tunacode_path = Path.cwd() / "TUNACODE.md"
|
|
52
|
+
if tunacode_path.exists():
|
|
53
|
+
tunacode_content = tunacode_path.read_text(encoding="utf-8")
|
|
54
|
+
if tunacode_content.strip():
|
|
55
|
+
logger.info("📄 TUNACODE.md located: Loading context...")
|
|
56
|
+
return "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
|
|
57
|
+
else:
|
|
58
|
+
logger.info("📄 TUNACODE.md not found: Using default context")
|
|
59
|
+
else:
|
|
60
|
+
logger.info("📄 TUNACODE.md not found: Using default context")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.debug(f"Error loading TUNACODE.md: {e}")
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
|
|
67
|
+
"""Get existing agent or create new one for the specified model."""
|
|
68
|
+
if model not in state_manager.session.agents:
|
|
69
|
+
max_retries = state_manager.session.user_config.get("settings", {}).get("max_retries", 3)
|
|
70
|
+
|
|
71
|
+
# Lazy import Agent and Tool
|
|
72
|
+
Agent, Tool = get_agent_tool()
|
|
73
|
+
|
|
74
|
+
# Load system prompt
|
|
75
|
+
base_path = Path(__file__).parent.parent.parent.parent
|
|
76
|
+
system_prompt = load_system_prompt(base_path)
|
|
77
|
+
|
|
78
|
+
# Load TUNACODE.md context
|
|
79
|
+
system_prompt += load_tunacode_context()
|
|
80
|
+
|
|
81
|
+
# Initialize todo tool
|
|
82
|
+
todo_tool = TodoTool(state_manager=state_manager)
|
|
83
|
+
|
|
84
|
+
# Add todo context if available
|
|
85
|
+
try:
|
|
86
|
+
current_todos = todo_tool.get_current_todos_sync()
|
|
87
|
+
if current_todos != "No todos found":
|
|
88
|
+
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.'
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning(f"Warning: Failed to load todos: {e}")
|
|
91
|
+
|
|
92
|
+
# Create agent with all tools
|
|
93
|
+
state_manager.session.agents[model] = Agent(
|
|
94
|
+
model=model,
|
|
95
|
+
system_prompt=system_prompt,
|
|
96
|
+
tools=[
|
|
97
|
+
Tool(bash, max_retries=max_retries),
|
|
98
|
+
Tool(glob, max_retries=max_retries),
|
|
99
|
+
Tool(grep, max_retries=max_retries),
|
|
100
|
+
Tool(list_dir, max_retries=max_retries),
|
|
101
|
+
Tool(read_file, max_retries=max_retries),
|
|
102
|
+
Tool(run_command, max_retries=max_retries),
|
|
103
|
+
Tool(todo_tool._execute, max_retries=max_retries),
|
|
104
|
+
Tool(update_file, max_retries=max_retries),
|
|
105
|
+
Tool(write_file, max_retries=max_retries),
|
|
106
|
+
],
|
|
107
|
+
mcp_servers=get_mcp_servers(state_manager),
|
|
108
|
+
)
|
|
109
|
+
return state_manager.session.agents[model]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""JSON tool call parsing utilities for fallback functionality."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
6
|
+
|
|
7
|
+
from tunacode.core.logging.logger import get_logger
|
|
8
|
+
from tunacode.core.state import StateManager
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
ToolCallback = Callable[[Any, Any], Awaitable[Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def parse_json_tool_calls(
|
|
16
|
+
text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager
|
|
17
|
+
) -> int:
|
|
18
|
+
"""
|
|
19
|
+
Parse JSON tool calls from text when structured tool calling fails.
|
|
20
|
+
Fallback for when API providers don't support proper tool calling.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
int: Number of tools successfully executed
|
|
24
|
+
"""
|
|
25
|
+
if not tool_callback:
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
# Pattern for JSON tool calls: {"tool": "tool_name", "args": {...}}
|
|
29
|
+
# Find potential JSON objects and parse them
|
|
30
|
+
potential_jsons = []
|
|
31
|
+
brace_count = 0
|
|
32
|
+
start_pos = -1
|
|
33
|
+
|
|
34
|
+
for i, char in enumerate(text):
|
|
35
|
+
if char == "{":
|
|
36
|
+
if brace_count == 0:
|
|
37
|
+
start_pos = i
|
|
38
|
+
brace_count += 1
|
|
39
|
+
elif char == "}":
|
|
40
|
+
brace_count -= 1
|
|
41
|
+
if brace_count == 0 and start_pos != -1:
|
|
42
|
+
potential_json = text[start_pos : i + 1]
|
|
43
|
+
try:
|
|
44
|
+
parsed = json.loads(potential_json)
|
|
45
|
+
if isinstance(parsed, dict) and "tool" in parsed and "args" in parsed:
|
|
46
|
+
potential_jsons.append((parsed["tool"], parsed["args"]))
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Failed to parse potential JSON tool call: {potential_json[:50]}..."
|
|
50
|
+
)
|
|
51
|
+
start_pos = -1
|
|
52
|
+
|
|
53
|
+
matches = potential_jsons
|
|
54
|
+
tools_executed = 0
|
|
55
|
+
|
|
56
|
+
for tool_name, args in matches:
|
|
57
|
+
try:
|
|
58
|
+
# Create a mock tool call object
|
|
59
|
+
class MockToolCall:
|
|
60
|
+
def __init__(self, tool_name: str, args: dict):
|
|
61
|
+
self.tool_name = tool_name
|
|
62
|
+
self.args = args
|
|
63
|
+
self.tool_call_id = f"fallback_{datetime.now().timestamp()}"
|
|
64
|
+
|
|
65
|
+
class MockNode:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Execute the tool through the callback
|
|
69
|
+
mock_call = MockToolCall(tool_name, args)
|
|
70
|
+
mock_node = MockNode()
|
|
71
|
+
|
|
72
|
+
await tool_callback(mock_call, mock_node)
|
|
73
|
+
tools_executed += 1
|
|
74
|
+
|
|
75
|
+
if state_manager.session.show_thoughts:
|
|
76
|
+
from tunacode.ui import console as ui
|
|
77
|
+
|
|
78
|
+
await ui.muted(f"FALLBACK: Executed {tool_name} via JSON parsing")
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
if state_manager.session.show_thoughts:
|
|
82
|
+
from tunacode.ui import console as ui
|
|
83
|
+
|
|
84
|
+
await ui.error(f"Error executing fallback tool {tool_name}: {str(e)}")
|
|
85
|
+
|
|
86
|
+
return tools_executed
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def extract_and_execute_tool_calls(
|
|
90
|
+
text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager
|
|
91
|
+
) -> int:
|
|
92
|
+
"""
|
|
93
|
+
Extract tool calls from text content and execute them.
|
|
94
|
+
Supports multiple formats for maximum compatibility.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
int: Number of tools successfully executed
|
|
98
|
+
"""
|
|
99
|
+
if not tool_callback:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
tools_executed = 0
|
|
103
|
+
|
|
104
|
+
# First try JSON format
|
|
105
|
+
tools_executed += await parse_json_tool_calls(text, tool_callback, state_manager)
|
|
106
|
+
|
|
107
|
+
# Add more formats here if needed in the future
|
|
108
|
+
|
|
109
|
+
return tools_executed
|