tunacode-cli 0.0.56__py3-none-any.whl → 0.0.60__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/implementations/plan.py +8 -8
- tunacode/cli/commands/registry.py +2 -2
- tunacode/cli/repl.py +214 -407
- tunacode/cli/repl_components/command_parser.py +37 -4
- tunacode/cli/repl_components/error_recovery.py +79 -1
- tunacode/cli/repl_components/output_display.py +14 -11
- tunacode/cli/repl_components/tool_executor.py +7 -4
- tunacode/configuration/defaults.py +8 -0
- tunacode/constants.py +8 -2
- tunacode/core/agents/agent_components/agent_config.py +128 -65
- tunacode/core/agents/agent_components/node_processor.py +6 -2
- tunacode/core/code_index.py +83 -29
- tunacode/core/state.py +1 -1
- tunacode/core/token_usage/usage_tracker.py +2 -2
- tunacode/core/tool_handler.py +3 -3
- tunacode/prompts/system.md +117 -490
- tunacode/services/mcp.py +29 -7
- tunacode/tools/base.py +110 -0
- tunacode/tools/bash.py +96 -1
- tunacode/tools/exit_plan_mode.py +114 -32
- tunacode/tools/glob.py +366 -33
- tunacode/tools/grep.py +226 -77
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/present_plan.py +111 -31
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +99 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/todo.py +108 -1
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/types.py +10 -9
- tunacode/ui/input.py +1 -0
- tunacode/ui/keybindings.py +1 -0
- tunacode/ui/panels.py +49 -27
- tunacode/ui/prompt_manager.py +13 -7
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/ripgrep.py +332 -9
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/METADATA +7 -2
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/RECORD +44 -43
- tunacode/tools/read_file_async_poc.py +0 -196
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/top_level.txt +0 -0
|
@@ -5,14 +5,24 @@ Command parsing utilities for the REPL.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import logging
|
|
8
9
|
|
|
10
|
+
from tunacode.constants import (
|
|
11
|
+
JSON_PARSE_BASE_DELAY,
|
|
12
|
+
JSON_PARSE_MAX_DELAY,
|
|
13
|
+
JSON_PARSE_MAX_RETRIES,
|
|
14
|
+
)
|
|
9
15
|
from tunacode.exceptions import ValidationError
|
|
10
16
|
from tunacode.types import ToolArgs
|
|
17
|
+
from tunacode.utils.json_utils import safe_json_parse
|
|
18
|
+
from tunacode.utils.retry import retry_json_parse
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
def parse_args(args) -> ToolArgs:
|
|
14
24
|
"""
|
|
15
|
-
Parse tool arguments from a JSON string or dictionary.
|
|
25
|
+
Parse tool arguments from a JSON string or dictionary with retry logic.
|
|
16
26
|
|
|
17
27
|
Args:
|
|
18
28
|
args (str or dict): A JSON-formatted string or a dictionary containing tool arguments.
|
|
@@ -21,12 +31,35 @@ def parse_args(args) -> ToolArgs:
|
|
|
21
31
|
dict: The parsed arguments.
|
|
22
32
|
|
|
23
33
|
Raises:
|
|
24
|
-
|
|
34
|
+
ValidationError: If 'args' is not a string or dictionary, or if the string is not valid JSON.
|
|
25
35
|
"""
|
|
26
36
|
if isinstance(args, str):
|
|
27
37
|
try:
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
# First attempt: Use retry logic for transient failures
|
|
39
|
+
return retry_json_parse(
|
|
40
|
+
args,
|
|
41
|
+
max_retries=JSON_PARSE_MAX_RETRIES,
|
|
42
|
+
base_delay=JSON_PARSE_BASE_DELAY,
|
|
43
|
+
max_delay=JSON_PARSE_MAX_DELAY,
|
|
44
|
+
)
|
|
45
|
+
except json.JSONDecodeError as e:
|
|
46
|
+
# Check if this is an "Extra data" error (concatenated JSON objects)
|
|
47
|
+
if "Extra data" in str(e):
|
|
48
|
+
logger.warning(f"Detected concatenated JSON objects in args: {args[:200]}...")
|
|
49
|
+
try:
|
|
50
|
+
# Use the new safe JSON parser with concatenation support
|
|
51
|
+
result = safe_json_parse(args, allow_concatenated=True)
|
|
52
|
+
if isinstance(result, dict):
|
|
53
|
+
return result
|
|
54
|
+
elif isinstance(result, list) and result:
|
|
55
|
+
# Multiple objects - return first one
|
|
56
|
+
logger.warning("Multiple JSON objects detected, using first object only")
|
|
57
|
+
return result[0]
|
|
58
|
+
except Exception:
|
|
59
|
+
# If safe parsing also fails, fall through to original error
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Original error - no recovery possible
|
|
30
63
|
raise ValidationError(f"Invalid JSON: {args}")
|
|
31
64
|
elif isinstance(args, dict):
|
|
32
65
|
return args
|
|
@@ -14,6 +14,77 @@ from .tool_executor import tool_handler
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
MSG_JSON_RECOVERY = "Recovered using JSON tool parsing"
|
|
17
|
+
MSG_JSON_ARGS_RECOVERY = "Recovered from malformed tool arguments"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def attempt_json_args_recovery(e: Exception, state_manager: StateManager) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Attempt to recover from JSON parsing errors in tool arguments.
|
|
23
|
+
|
|
24
|
+
This handles cases where the model emits concatenated JSON objects
|
|
25
|
+
or other malformed JSON in tool call arguments.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: True if recovery was successful, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
error_str = str(e).lower()
|
|
31
|
+
|
|
32
|
+
# Check if this is a JSON parsing error with tool arguments
|
|
33
|
+
if not any(
|
|
34
|
+
keyword in error_str for keyword in ["invalid json", "extra data", "jsondecodeerror"]
|
|
35
|
+
):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
if not state_manager.session.messages:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
last_msg = state_manager.session.messages[-1]
|
|
42
|
+
if not hasattr(last_msg, "parts"):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# Look for tool call parts with malformed args
|
|
46
|
+
for part in last_msg.parts:
|
|
47
|
+
if hasattr(part, "tool_name") and hasattr(part, "args"):
|
|
48
|
+
# This is a structured tool call with potentially malformed args
|
|
49
|
+
try:
|
|
50
|
+
from tunacode.utils.json_utils import split_concatenated_json
|
|
51
|
+
|
|
52
|
+
# Try to split concatenated JSON objects in the args
|
|
53
|
+
if isinstance(part.args, str):
|
|
54
|
+
logger.info(f"Attempting to recover malformed args for tool {part.tool_name}")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
json_objects = split_concatenated_json(part.args)
|
|
58
|
+
if json_objects:
|
|
59
|
+
# Use the first object as the args
|
|
60
|
+
part.args = json_objects[0]
|
|
61
|
+
|
|
62
|
+
# Execute the recovered tool call
|
|
63
|
+
await tool_handler(part, state_manager)
|
|
64
|
+
|
|
65
|
+
await ui.warning(f"Warning: {MSG_JSON_ARGS_RECOVERY}")
|
|
66
|
+
logger.info(
|
|
67
|
+
f"Successfully recovered tool {part.tool_name} with split JSON args",
|
|
68
|
+
extra={
|
|
69
|
+
"original_args": part.args,
|
|
70
|
+
"recovered_args": json_objects[0],
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
except Exception as split_exc:
|
|
76
|
+
logger.debug(f"Failed to split JSON args: {split_exc}")
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
except Exception as recovery_exc:
|
|
80
|
+
logger.error(
|
|
81
|
+
f"Error during JSON args recovery for tool {getattr(part, 'tool_name', 'unknown')}",
|
|
82
|
+
exc_info=True,
|
|
83
|
+
extra={"recovery_exception": str(recovery_exc)},
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
return False
|
|
17
88
|
|
|
18
89
|
|
|
19
90
|
async def attempt_tool_recovery(e: Exception, state_manager: StateManager) -> bool:
|
|
@@ -25,9 +96,16 @@ async def attempt_tool_recovery(e: Exception, state_manager: StateManager) -> bo
|
|
|
25
96
|
"""
|
|
26
97
|
error_str = str(e).lower()
|
|
27
98
|
tool_keywords = ["tool", "function", "call", "schema"]
|
|
28
|
-
|
|
99
|
+
json_keywords = ["json", "invalid json", "jsondecodeerror", "extra data", "validation"]
|
|
100
|
+
recovery_keywords = tool_keywords + json_keywords
|
|
101
|
+
|
|
102
|
+
if not any(keyword in error_str for keyword in recovery_keywords):
|
|
29
103
|
return False
|
|
30
104
|
|
|
105
|
+
# First, try JSON args recovery for structured tool calls with malformed args
|
|
106
|
+
if await attempt_json_args_recovery(e, state_manager):
|
|
107
|
+
return True
|
|
108
|
+
|
|
31
109
|
if not state_manager.session.messages:
|
|
32
110
|
return False
|
|
33
111
|
|
|
@@ -29,22 +29,25 @@ async def display_agent_output(res, enable_streaming: bool, state_manager=None)
|
|
|
29
29
|
|
|
30
30
|
if '"tool_uses"' in output:
|
|
31
31
|
return
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
# In plan mode, don't display any agent text output at all
|
|
34
34
|
# The plan will be displayed via the present_plan tool
|
|
35
35
|
if state_manager and state_manager.is_plan_mode():
|
|
36
36
|
return
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
# Filter out plan mode system prompts and tool definitions
|
|
39
|
-
if any(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
if any(
|
|
40
|
+
phrase in output
|
|
41
|
+
for phrase in [
|
|
42
|
+
"PLAN MODE - TOOL EXECUTION ONLY",
|
|
43
|
+
"🔧 PLAN MODE",
|
|
44
|
+
"TOOL EXECUTION ONLY 🔧",
|
|
45
|
+
"planning assistant that ONLY communicates",
|
|
46
|
+
"namespace functions {",
|
|
47
|
+
"namespace multi_tool_use {",
|
|
48
|
+
"You are trained on data up to",
|
|
49
|
+
]
|
|
50
|
+
):
|
|
48
51
|
return
|
|
49
52
|
|
|
50
53
|
await ui.agent(output)
|
|
@@ -62,12 +62,15 @@ async def tool_handler(part, state_manager: StateManager):
|
|
|
62
62
|
# Check if tool is blocked in plan mode first
|
|
63
63
|
if tool_handler_instance.is_tool_blocked_in_plan_mode(part.tool_name):
|
|
64
64
|
from tunacode.constants import READ_ONLY_TOOLS
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
|
|
66
|
+
error_msg = (
|
|
67
|
+
f"🔍 Plan Mode: Tool '{part.tool_name}' is not available in Plan Mode.\n"
|
|
68
|
+
f"Only read-only tools are allowed: {', '.join(READ_ONLY_TOOLS)}\n"
|
|
69
|
+
f"Use 'exit_plan_mode' tool to present your plan and exit Plan Mode."
|
|
70
|
+
)
|
|
68
71
|
print(f"\n❌ {error_msg}\n")
|
|
69
72
|
return True # Abort the tool
|
|
70
|
-
|
|
73
|
+
|
|
71
74
|
if not tool_handler_instance.should_confirm(part.tool_name):
|
|
72
75
|
return False
|
|
73
76
|
request = tool_handler_instance.create_confirmation_request(part.tool_name, args)
|
|
@@ -24,6 +24,14 @@ DEFAULT_USER_CONFIG: UserConfig = {
|
|
|
24
24
|
"fallback_response": True,
|
|
25
25
|
"fallback_verbosity": "normal", # Options: minimal, normal, detailed
|
|
26
26
|
"context_window_size": 200000,
|
|
27
|
+
"ripgrep": {
|
|
28
|
+
"use_bundled": False, # Use system ripgrep binary
|
|
29
|
+
"timeout": 10, # Search timeout in seconds
|
|
30
|
+
"max_buffer_size": 1048576, # 1MB max output buffer
|
|
31
|
+
"max_results": 100, # Maximum results per search
|
|
32
|
+
"enable_metrics": False, # Enable performance metrics collection
|
|
33
|
+
"debug": False, # Enable debug logging for ripgrep operations
|
|
34
|
+
},
|
|
27
35
|
},
|
|
28
36
|
"mcpServers": {},
|
|
29
37
|
}
|
tunacode/constants.py
CHANGED
|
@@ -9,7 +9,7 @@ from enum import Enum
|
|
|
9
9
|
|
|
10
10
|
# Application info
|
|
11
11
|
APP_NAME = "TunaCode"
|
|
12
|
-
APP_VERSION = "0.0.
|
|
12
|
+
APP_VERSION = "0.0.57"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
# File patterns
|
|
@@ -60,7 +60,13 @@ TOOL_TODO = ToolName.TODO
|
|
|
60
60
|
TOOL_EXIT_PLAN_MODE = ToolName.EXIT_PLAN_MODE
|
|
61
61
|
|
|
62
62
|
# Tool categorization
|
|
63
|
-
READ_ONLY_TOOLS = [
|
|
63
|
+
READ_ONLY_TOOLS = [
|
|
64
|
+
ToolName.READ_FILE,
|
|
65
|
+
ToolName.GREP,
|
|
66
|
+
ToolName.LIST_DIR,
|
|
67
|
+
ToolName.GLOB,
|
|
68
|
+
ToolName.EXIT_PLAN_MODE,
|
|
69
|
+
]
|
|
64
70
|
WRITE_TOOLS = [ToolName.WRITE_FILE, ToolName.UPDATE_FILE]
|
|
65
71
|
EXECUTE_TOOLS = [ToolName.BASH, ToolName.RUN_COMMAND]
|
|
66
72
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Agent configuration and creation utilities."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Dict, Tuple
|
|
4
5
|
|
|
5
6
|
from pydantic_ai import Agent
|
|
6
7
|
|
|
@@ -8,10 +9,10 @@ from tunacode.core.logging.logger import get_logger
|
|
|
8
9
|
from tunacode.core.state import StateManager
|
|
9
10
|
from tunacode.services.mcp import get_mcp_servers
|
|
10
11
|
from tunacode.tools.bash import bash
|
|
11
|
-
from tunacode.tools.present_plan import create_present_plan_tool
|
|
12
12
|
from tunacode.tools.glob import glob
|
|
13
13
|
from tunacode.tools.grep import grep
|
|
14
14
|
from tunacode.tools.list_dir import list_dir
|
|
15
|
+
from tunacode.tools.present_plan import create_present_plan_tool
|
|
15
16
|
from tunacode.tools.read_file import read_file
|
|
16
17
|
from tunacode.tools.run_command import run_command
|
|
17
18
|
from tunacode.tools.todo import TodoTool
|
|
@@ -21,6 +22,22 @@ from tunacode.types import ModelName, PydanticAgent
|
|
|
21
22
|
|
|
22
23
|
logger = get_logger(__name__)
|
|
23
24
|
|
|
25
|
+
# Module-level caches for system prompts
|
|
26
|
+
_PROMPT_CACHE: Dict[str, Tuple[str, float]] = {}
|
|
27
|
+
_TUNACODE_CACHE: Dict[str, Tuple[str, float]] = {}
|
|
28
|
+
|
|
29
|
+
# Module-level cache for agents to persist across requests
|
|
30
|
+
_AGENT_CACHE: Dict[ModelName, PydanticAgent] = {}
|
|
31
|
+
_AGENT_CACHE_VERSION: Dict[ModelName, int] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clear_all_caches():
|
|
35
|
+
"""Clear all module-level caches. Useful for testing."""
|
|
36
|
+
_PROMPT_CACHE.clear()
|
|
37
|
+
_TUNACODE_CACHE.clear()
|
|
38
|
+
_AGENT_CACHE.clear()
|
|
39
|
+
_AGENT_CACHE_VERSION.clear()
|
|
40
|
+
|
|
24
41
|
|
|
25
42
|
def get_agent_tool():
|
|
26
43
|
"""Lazy import for Agent and Tool to avoid circular imports."""
|
|
@@ -30,47 +47,114 @@ def get_agent_tool():
|
|
|
30
47
|
|
|
31
48
|
|
|
32
49
|
def load_system_prompt(base_path: Path) -> str:
|
|
33
|
-
"""Load the system prompt from file."""
|
|
50
|
+
"""Load the system prompt from file with caching."""
|
|
34
51
|
prompt_path = base_path / "prompts" / "system.md"
|
|
52
|
+
cache_key = str(prompt_path)
|
|
53
|
+
|
|
54
|
+
# Check cache with file modification time
|
|
35
55
|
try:
|
|
56
|
+
if cache_key in _PROMPT_CACHE:
|
|
57
|
+
cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
|
|
58
|
+
current_mtime = prompt_path.stat().st_mtime
|
|
59
|
+
if current_mtime == cached_mtime:
|
|
60
|
+
return cached_content
|
|
61
|
+
|
|
62
|
+
# Load from file and cache
|
|
36
63
|
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
37
|
-
|
|
64
|
+
content = f.read().strip()
|
|
65
|
+
_PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
|
|
66
|
+
return content
|
|
67
|
+
|
|
38
68
|
except FileNotFoundError:
|
|
39
69
|
# Fallback to system.txt if system.md not found
|
|
40
70
|
prompt_path = base_path / "prompts" / "system.txt"
|
|
71
|
+
cache_key = str(prompt_path)
|
|
72
|
+
|
|
41
73
|
try:
|
|
74
|
+
if cache_key in _PROMPT_CACHE:
|
|
75
|
+
cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
|
|
76
|
+
current_mtime = prompt_path.stat().st_mtime
|
|
77
|
+
if current_mtime == cached_mtime:
|
|
78
|
+
return cached_content
|
|
79
|
+
|
|
42
80
|
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
43
|
-
|
|
81
|
+
content = f.read().strip()
|
|
82
|
+
_PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
|
|
83
|
+
return content
|
|
84
|
+
|
|
44
85
|
except FileNotFoundError:
|
|
45
86
|
# Use a default system prompt if neither file exists
|
|
46
|
-
return "You are a helpful AI assistant
|
|
87
|
+
return "You are a helpful AI assistant."
|
|
47
88
|
|
|
48
89
|
|
|
49
90
|
def load_tunacode_context() -> str:
|
|
50
|
-
"""Load TUNACODE.md context if it exists."""
|
|
91
|
+
"""Load TUNACODE.md context if it exists with caching."""
|
|
51
92
|
try:
|
|
52
93
|
tunacode_path = Path.cwd() / "TUNACODE.md"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
cache_key = str(tunacode_path)
|
|
95
|
+
|
|
96
|
+
if not tunacode_path.exists():
|
|
97
|
+
logger.info("📄 TUNACODE.md not found: Using default context")
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
# Check cache with file modification time
|
|
101
|
+
if cache_key in _TUNACODE_CACHE:
|
|
102
|
+
cached_content, cached_mtime = _TUNACODE_CACHE[cache_key]
|
|
103
|
+
current_mtime = tunacode_path.stat().st_mtime
|
|
104
|
+
if current_mtime == cached_mtime:
|
|
105
|
+
return cached_content
|
|
106
|
+
|
|
107
|
+
# Load from file and cache
|
|
108
|
+
tunacode_content = tunacode_path.read_text(encoding="utf-8")
|
|
109
|
+
if tunacode_content.strip():
|
|
110
|
+
logger.info("📄 TUNACODE.md located: Loading context...")
|
|
111
|
+
result = "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
|
|
112
|
+
_TUNACODE_CACHE[cache_key] = (result, tunacode_path.stat().st_mtime)
|
|
113
|
+
return result
|
|
60
114
|
else:
|
|
61
115
|
logger.info("📄 TUNACODE.md not found: Using default context")
|
|
116
|
+
_TUNACODE_CACHE[cache_key] = ("", tunacode_path.stat().st_mtime)
|
|
117
|
+
return ""
|
|
118
|
+
|
|
62
119
|
except Exception as e:
|
|
63
120
|
logger.debug(f"Error loading TUNACODE.md: {e}")
|
|
64
|
-
|
|
121
|
+
return ""
|
|
65
122
|
|
|
66
123
|
|
|
67
124
|
def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
|
|
68
125
|
"""Get existing agent or create new one for the specified model."""
|
|
69
126
|
import logging
|
|
127
|
+
|
|
70
128
|
logger = logging.getLogger(__name__)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
|
|
130
|
+
# Check session-level cache first (for backward compatibility with tests)
|
|
131
|
+
if model in state_manager.session.agents:
|
|
132
|
+
logger.debug(f"Using session-cached agent for model {model}")
|
|
133
|
+
return state_manager.session.agents[model]
|
|
134
|
+
|
|
135
|
+
# Check module-level cache
|
|
136
|
+
if model in _AGENT_CACHE:
|
|
137
|
+
# Verify cache is still valid (check for config changes)
|
|
138
|
+
current_version = hash(
|
|
139
|
+
(
|
|
140
|
+
state_manager.is_plan_mode(),
|
|
141
|
+
str(state_manager.session.user_config.get("settings", {}).get("max_retries", 3)),
|
|
142
|
+
str(state_manager.session.user_config.get("mcpServers", {})),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
if _AGENT_CACHE_VERSION.get(model) == current_version:
|
|
146
|
+
logger.debug(f"Using module-cached agent for model {model}")
|
|
147
|
+
state_manager.session.agents[model] = _AGENT_CACHE[model]
|
|
148
|
+
return _AGENT_CACHE[model]
|
|
149
|
+
else:
|
|
150
|
+
logger.debug(f"Cache invalidated for model {model} due to config change")
|
|
151
|
+
del _AGENT_CACHE[model]
|
|
152
|
+
del _AGENT_CACHE_VERSION[model]
|
|
153
|
+
|
|
154
|
+
if model not in _AGENT_CACHE:
|
|
155
|
+
logger.debug(
|
|
156
|
+
f"Creating new agent for model {model}, plan_mode={state_manager.is_plan_mode()}"
|
|
157
|
+
)
|
|
74
158
|
max_retries = state_manager.session.user_config.get("settings", {}).get("max_retries", 3)
|
|
75
159
|
|
|
76
160
|
# Lazy import Agent and Tool
|
|
@@ -86,56 +170,21 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
86
170
|
# Add plan mode context if in plan mode
|
|
87
171
|
if state_manager.is_plan_mode():
|
|
88
172
|
# REMOVE all TUNACODE_TASK_COMPLETE instructions from the system prompt
|
|
89
|
-
system_prompt = system_prompt.replace(
|
|
173
|
+
system_prompt = system_prompt.replace(
|
|
174
|
+
"TUNACODE_TASK_COMPLETE", "PLAN_MODE_TASK_PLACEHOLDER"
|
|
175
|
+
)
|
|
90
176
|
# Remove the completion guidance that conflicts with plan mode
|
|
91
177
|
lines_to_remove = [
|
|
92
178
|
"When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE",
|
|
93
|
-
"4. When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE",
|
|
179
|
+
"4. When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE",
|
|
94
180
|
"**How to signal completion:**",
|
|
95
181
|
"TUNACODE_TASK_COMPLETE",
|
|
96
182
|
"[Your summary of what was accomplished]",
|
|
97
183
|
"**IMPORTANT**: Always evaluate if you've completed the task. If yes, use TUNACODE_TASK_COMPLETE.",
|
|
98
|
-
"This prevents wasting iterations and API calls."
|
|
184
|
+
"This prevents wasting iterations and API calls.",
|
|
99
185
|
]
|
|
100
186
|
for line in lines_to_remove:
|
|
101
187
|
system_prompt = system_prompt.replace(line, "")
|
|
102
|
-
plan_mode_override = """
|
|
103
|
-
🔍 PLAN MODE - YOU MUST USE THE present_plan TOOL 🔍
|
|
104
|
-
|
|
105
|
-
CRITICAL: You are in Plan Mode. You MUST execute the present_plan TOOL, not show it as text.
|
|
106
|
-
|
|
107
|
-
❌ WRONG - DO NOT SHOW THE FUNCTION AS TEXT:
|
|
108
|
-
```
|
|
109
|
-
present_plan(title="...", ...) # THIS IS WRONG - DON'T SHOW AS CODE
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
✅ CORRECT - ACTUALLY EXECUTE THE TOOL:
|
|
113
|
-
You must EXECUTE present_plan as a tool call, just like you execute read_file or grep.
|
|
114
|
-
|
|
115
|
-
CRITICAL RULES:
|
|
116
|
-
1. DO NOT show present_plan() as code or text
|
|
117
|
-
2. DO NOT write "Here's the plan" or any text description
|
|
118
|
-
3. DO NOT use TUNACODE_TASK_COMPLETE
|
|
119
|
-
4. DO NOT use markdown code blocks for present_plan
|
|
120
|
-
|
|
121
|
-
YOU MUST EXECUTE THE TOOL:
|
|
122
|
-
When the user asks you to "plan" something, you must:
|
|
123
|
-
1. Research using read_only tools (optional)
|
|
124
|
-
2. EXECUTE present_plan tool with the plan data
|
|
125
|
-
3. The tool will handle displaying the plan
|
|
126
|
-
|
|
127
|
-
Example of CORRECT behavior:
|
|
128
|
-
User: "plan a markdown file"
|
|
129
|
-
You: [Execute read_file/grep if needed for research]
|
|
130
|
-
[Then EXECUTE present_plan tool - not as text but as an actual tool call]
|
|
131
|
-
|
|
132
|
-
Remember: present_plan is a TOOL like read_file or grep. You must EXECUTE it, not SHOW it.
|
|
133
|
-
|
|
134
|
-
Available tools:
|
|
135
|
-
- read_file, grep, list_dir, glob: For research
|
|
136
|
-
- present_plan: EXECUTE this tool to present the plan (DO NOT show as text)
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
188
|
# COMPLETELY REPLACE system prompt in plan mode - nuclear option
|
|
140
189
|
system_prompt = """
|
|
141
190
|
🔧 PLAN MODE - TOOL EXECUTION ONLY 🔧
|
|
@@ -146,7 +195,7 @@ CRITICAL: You cannot respond with text. You MUST use tools for everything.
|
|
|
146
195
|
|
|
147
196
|
AVAILABLE TOOLS:
|
|
148
197
|
- read_file(filepath): Read file contents
|
|
149
|
-
- grep(pattern): Search for text patterns
|
|
198
|
+
- grep(pattern): Search for text patterns
|
|
150
199
|
- list_dir(directory): List directory contents
|
|
151
200
|
- glob(pattern): Find files matching patterns
|
|
152
201
|
- present_plan(title, overview, steps, files_to_create, success_criteria): Present structured plan
|
|
@@ -170,7 +219,7 @@ You: [Call read_file or grep for research if needed]
|
|
|
170
219
|
|
|
171
220
|
The present_plan tool takes these parameters:
|
|
172
221
|
- title: Brief title string
|
|
173
|
-
- overview: What the plan accomplishes
|
|
222
|
+
- overview: What the plan accomplishes
|
|
174
223
|
- steps: List of implementation steps
|
|
175
224
|
- files_to_create: List of files to create
|
|
176
225
|
- success_criteria: List of success criteria
|
|
@@ -215,22 +264,36 @@ YOU MUST EXECUTE present_plan TOOL TO COMPLETE ANY PLANNING TASK.
|
|
|
215
264
|
Tool(update_file, max_retries=max_retries),
|
|
216
265
|
Tool(write_file, max_retries=max_retries),
|
|
217
266
|
]
|
|
218
|
-
|
|
267
|
+
|
|
219
268
|
# Log which tools are being registered
|
|
220
|
-
logger.debug(
|
|
269
|
+
logger.debug(
|
|
270
|
+
f"Creating agent: plan_mode={state_manager.is_plan_mode()}, tools={len(tools_list)}"
|
|
271
|
+
)
|
|
221
272
|
if state_manager.is_plan_mode():
|
|
222
273
|
logger.debug(f"PLAN MODE TOOLS: {[str(tool) for tool in tools_list]}")
|
|
223
274
|
logger.debug(f"present_plan tool type: {type(present_plan)}")
|
|
224
|
-
|
|
275
|
+
|
|
225
276
|
if "PLAN MODE - YOU MUST USE THE present_plan TOOL" in system_prompt:
|
|
226
277
|
logger.debug("✅ Plan mode instructions ARE in system prompt")
|
|
227
278
|
else:
|
|
228
279
|
logger.debug("❌ Plan mode instructions NOT in system prompt")
|
|
229
|
-
|
|
230
|
-
|
|
280
|
+
|
|
281
|
+
agent = Agent(
|
|
231
282
|
model=model,
|
|
232
283
|
system_prompt=system_prompt,
|
|
233
284
|
tools=tools_list,
|
|
234
285
|
mcp_servers=get_mcp_servers(state_manager),
|
|
235
286
|
)
|
|
236
|
-
|
|
287
|
+
|
|
288
|
+
# Store in both caches
|
|
289
|
+
_AGENT_CACHE[model] = agent
|
|
290
|
+
_AGENT_CACHE_VERSION[model] = hash(
|
|
291
|
+
(
|
|
292
|
+
state_manager.is_plan_mode(),
|
|
293
|
+
str(state_manager.session.user_config.get("settings", {}).get("max_retries", 3)),
|
|
294
|
+
str(state_manager.session.user_config.get("mcpServers", {})),
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
state_manager.session.agents[model] = agent
|
|
298
|
+
|
|
299
|
+
return _AGENT_CACHE[model]
|
|
@@ -366,7 +366,9 @@ async def _process_tool_calls(
|
|
|
366
366
|
buffered_part.tool_name == "grep"
|
|
367
367
|
and "pattern" in buffered_part.args
|
|
368
368
|
):
|
|
369
|
-
tool_desc +=
|
|
369
|
+
tool_desc += (
|
|
370
|
+
f" → pattern: '{buffered_part.args['pattern']}'"
|
|
371
|
+
)
|
|
370
372
|
if "include_files" in buffered_part.args:
|
|
371
373
|
tool_desc += (
|
|
372
374
|
f", files: '{buffered_part.args['include_files']}'"
|
|
@@ -380,7 +382,9 @@ async def _process_tool_calls(
|
|
|
380
382
|
buffered_part.tool_name == "glob"
|
|
381
383
|
and "pattern" in buffered_part.args
|
|
382
384
|
):
|
|
383
|
-
tool_desc +=
|
|
385
|
+
tool_desc += (
|
|
386
|
+
f" → pattern: '{buffered_part.args['pattern']}'"
|
|
387
|
+
)
|
|
384
388
|
await ui.muted(tool_desc)
|
|
385
389
|
await ui.muted("=" * 60)
|
|
386
390
|
|