tunacode-cli 0.0.51__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 +1 -1
  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.51.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.51.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.51.dist-info/RECORD +0 -107
  85. {tunacode_cli-0.0.51.dist-info → tunacode_cli-0.0.53.dist-info}/WHEEL +0 -0
  86. {tunacode_cli-0.0.51.dist-info → tunacode_cli-0.0.53.dist-info}/entry_points.txt +0 -0
  87. {tunacode_cli-0.0.51.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, TOOL_READ_FILE
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": [TOOL_READ_FILE],
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
- APP_NAME,
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
- TOOL_READ_FILE,
36
- TOOL_RUN_COMMAND,
37
- TOOL_UPDATE_FILE,
38
- TOOL_WRITE_FILE,
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.51"
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
- # Tool names
33
- TOOL_READ_FILE = "read_file"
34
- TOOL_WRITE_FILE = "write_file"
35
- TOOL_UPDATE_FILE = "update_file"
36
- TOOL_RUN_COMMAND = "run_command"
37
- TOOL_BASH = "bash"
38
- TOOL_GREP = "grep"
39
- TOOL_LIST_DIR = "list_dir"
40
- TOOL_GLOB = "glob"
41
- TOOL_TODO = "todo"
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 = [TOOL_READ_FILE, TOOL_GREP, TOOL_LIST_DIR, TOOL_GLOB]
45
- WRITE_TOOLS = [TOOL_WRITE_FILE, TOOL_UPDATE_FILE]
46
- EXECUTE_TOOLS = [TOOL_BASH, TOOL_RUN_COMMAND]
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
- TODO_PRIORITY_HIGH = "high"
152
- TODO_PRIORITY_MEDIUM = "medium"
153
- TODO_PRIORITY_LOW = "low"
154
- TODO_PRIORITIES = [TODO_PRIORITY_HIGH, TODO_PRIORITY_MEDIUM, TODO_PRIORITY_LOW]
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