tunacode-cli 0.0.70__py3-none-any.whl → 0.0.78.6__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 +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Dict, Tuple
|
|
5
5
|
|
|
6
|
+
from httpx import AsyncClient, HTTPStatusError
|
|
6
7
|
from pydantic_ai import Agent
|
|
8
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
9
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
10
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
11
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
12
|
+
from pydantic_ai.retries import AsyncTenacityTransport, RetryConfig, wait_retry_after
|
|
13
|
+
from tenacity import retry_if_exception_type, stop_after_attempt
|
|
7
14
|
|
|
8
15
|
from tunacode.core.logging.logger import get_logger
|
|
9
16
|
from tunacode.core.state import StateManager
|
|
10
|
-
from tunacode.services.mcp import get_mcp_servers
|
|
17
|
+
from tunacode.services.mcp import get_mcp_servers, register_mcp_agent
|
|
11
18
|
from tunacode.tools.bash import bash
|
|
12
19
|
from tunacode.tools.glob import glob
|
|
13
20
|
from tunacode.tools.grep import grep
|
|
14
21
|
from tunacode.tools.list_dir import list_dir
|
|
15
|
-
from tunacode.tools.present_plan import create_present_plan_tool
|
|
16
22
|
from tunacode.tools.read_file import read_file
|
|
17
23
|
from tunacode.tools.run_command import run_command
|
|
18
|
-
from tunacode.tools.todo import TodoTool
|
|
19
24
|
from tunacode.tools.update_file import update_file
|
|
20
25
|
from tunacode.tools.write_file import write_file
|
|
21
26
|
from tunacode.types import ModelName, PydanticAgent
|
|
@@ -46,55 +51,55 @@ def get_agent_tool():
|
|
|
46
51
|
return Agent, Tool
|
|
47
52
|
|
|
48
53
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
51
|
-
prompt_path = base_path / "prompts" / "system.md"
|
|
54
|
+
def _read_prompt_from_path(prompt_path: Path) -> str:
|
|
55
|
+
"""Return prompt content from disk, leveraging the cache when possible."""
|
|
52
56
|
cache_key = str(prompt_path)
|
|
53
57
|
|
|
54
|
-
# Check cache with file modification time
|
|
55
58
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if current_mtime == cached_mtime:
|
|
60
|
-
return cached_content
|
|
59
|
+
current_mtime = prompt_path.stat().st_mtime
|
|
60
|
+
except FileNotFoundError as error:
|
|
61
|
+
raise FileNotFoundError from error
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
if cache_key in _PROMPT_CACHE:
|
|
64
|
+
cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
|
|
65
|
+
if current_mtime == cached_mtime:
|
|
66
|
+
return cached_content
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
content = prompt_path.read_text(encoding="utf-8").strip()
|
|
70
|
+
except FileNotFoundError as error:
|
|
71
|
+
raise FileNotFoundError from error
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
prompt_path = base_path / "prompts" / "system.txt"
|
|
71
|
-
cache_key = str(prompt_path)
|
|
73
|
+
_PROMPT_CACHE[cache_key] = (content, current_mtime)
|
|
74
|
+
return content
|
|
72
75
|
|
|
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
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
def load_system_prompt(base_path: Path) -> str:
|
|
78
|
+
"""Load the system prompt from system.xml file with caching.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
FileNotFoundError: If system.xml does not exist in the prompts directory.
|
|
82
|
+
"""
|
|
83
|
+
prompts_dir = base_path / "prompts"
|
|
84
|
+
prompt_path = prompts_dir / "system.xml"
|
|
85
|
+
|
|
86
|
+
if not prompt_path.exists():
|
|
87
|
+
raise FileNotFoundError(
|
|
88
|
+
f"Required system prompt file not found: {prompt_path}. "
|
|
89
|
+
"The system.xml file must exist in the prompts directory."
|
|
90
|
+
)
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
# Use a default system prompt if neither file exists
|
|
87
|
-
return "You are a helpful AI assistant."
|
|
92
|
+
return _read_prompt_from_path(prompt_path)
|
|
88
93
|
|
|
89
94
|
|
|
90
95
|
def load_tunacode_context() -> str:
|
|
91
|
-
"""Load
|
|
96
|
+
"""Load AGENTS.md context if it exists with caching."""
|
|
92
97
|
try:
|
|
93
|
-
tunacode_path = Path.cwd() / "
|
|
98
|
+
tunacode_path = Path.cwd() / "AGENTS.md"
|
|
94
99
|
cache_key = str(tunacode_path)
|
|
95
100
|
|
|
96
101
|
if not tunacode_path.exists():
|
|
97
|
-
logger.info("📄
|
|
102
|
+
logger.info("📄 AGENTS.md not found: Using default context")
|
|
98
103
|
return ""
|
|
99
104
|
|
|
100
105
|
# Check cache with file modification time
|
|
@@ -107,20 +112,82 @@ def load_tunacode_context() -> str:
|
|
|
107
112
|
# Load from file and cache
|
|
108
113
|
tunacode_content = tunacode_path.read_text(encoding="utf-8")
|
|
109
114
|
if tunacode_content.strip():
|
|
110
|
-
logger.info("📄
|
|
111
|
-
result = "\n\n# Project Context from
|
|
115
|
+
logger.info("📄 AGENTS.md located: Loading context...")
|
|
116
|
+
result = "\n\n# Project Context from AGENTS.md\n" + tunacode_content
|
|
112
117
|
_TUNACODE_CACHE[cache_key] = (result, tunacode_path.stat().st_mtime)
|
|
113
118
|
return result
|
|
114
119
|
else:
|
|
115
|
-
logger.info("📄
|
|
120
|
+
logger.info("📄 AGENTS.md not found: Using default context")
|
|
116
121
|
_TUNACODE_CACHE[cache_key] = ("", tunacode_path.stat().st_mtime)
|
|
117
122
|
return ""
|
|
118
123
|
|
|
119
124
|
except Exception as e:
|
|
120
|
-
logger.debug(f"Error loading
|
|
125
|
+
logger.debug(f"Error loading AGENTS.md: {e}")
|
|
121
126
|
return ""
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
def _create_model_with_retry(
|
|
130
|
+
model_string: str, http_client: AsyncClient, state_manager: StateManager
|
|
131
|
+
):
|
|
132
|
+
"""Create a model instance with retry-enabled HTTP client.
|
|
133
|
+
|
|
134
|
+
Parses model string in format 'provider:model_name' and creates
|
|
135
|
+
appropriate provider and model instances with the retry-enabled HTTP client.
|
|
136
|
+
"""
|
|
137
|
+
# Extract environment config
|
|
138
|
+
env = state_manager.session.user_config.get("env", {})
|
|
139
|
+
|
|
140
|
+
# Provider configuration: API key names and base URLs
|
|
141
|
+
PROVIDER_CONFIG = {
|
|
142
|
+
"anthropic": {"api_key_name": "ANTHROPIC_API_KEY", "base_url": None},
|
|
143
|
+
"openai": {"api_key_name": "OPENAI_API_KEY", "base_url": None},
|
|
144
|
+
"openrouter": {
|
|
145
|
+
"api_key_name": "OPENROUTER_API_KEY",
|
|
146
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
147
|
+
},
|
|
148
|
+
"azure": {
|
|
149
|
+
"api_key_name": "AZURE_OPENAI_API_KEY",
|
|
150
|
+
"base_url": env.get("AZURE_OPENAI_ENDPOINT"),
|
|
151
|
+
},
|
|
152
|
+
"deepseek": {"api_key_name": "DEEPSEEK_API_KEY", "base_url": None},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Parse model string
|
|
156
|
+
if ":" in model_string:
|
|
157
|
+
provider_name, model_name = model_string.split(":", 1)
|
|
158
|
+
else:
|
|
159
|
+
# Auto-detect provider from model name
|
|
160
|
+
model_name = model_string
|
|
161
|
+
if model_name.startswith("claude"):
|
|
162
|
+
provider_name = "anthropic"
|
|
163
|
+
elif model_name.startswith(("gpt", "o1", "o3")):
|
|
164
|
+
provider_name = "openai"
|
|
165
|
+
else:
|
|
166
|
+
# Default to treating as model string (pydantic-ai will auto-detect)
|
|
167
|
+
return model_string
|
|
168
|
+
|
|
169
|
+
# Create provider with api_key + base_url + http_client
|
|
170
|
+
if provider_name == "anthropic":
|
|
171
|
+
api_key = env.get("ANTHROPIC_API_KEY")
|
|
172
|
+
provider = AnthropicProvider(api_key=api_key, http_client=http_client)
|
|
173
|
+
return AnthropicModel(model_name, provider=provider)
|
|
174
|
+
elif provider_name in ("openai", "openrouter", "azure", "deepseek"):
|
|
175
|
+
# OpenAI-compatible providers all use OpenAIChatModel
|
|
176
|
+
config = PROVIDER_CONFIG.get(provider_name, {})
|
|
177
|
+
api_key = env.get(config.get("api_key_name"))
|
|
178
|
+
base_url = config.get("base_url")
|
|
179
|
+
provider = OpenAIProvider(api_key=api_key, base_url=base_url, http_client=http_client)
|
|
180
|
+
return OpenAIChatModel(model_name, provider=provider)
|
|
181
|
+
else:
|
|
182
|
+
# Unsupported provider, return string and let pydantic-ai handle it
|
|
183
|
+
# (won't have retry support but won't break)
|
|
184
|
+
logger.warning(
|
|
185
|
+
f"Provider '{provider_name}' not configured for HTTP retries. "
|
|
186
|
+
f"Falling back to default behavior."
|
|
187
|
+
)
|
|
188
|
+
return model_string
|
|
189
|
+
|
|
190
|
+
|
|
124
191
|
def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
|
|
125
192
|
"""Get existing agent or create new one for the specified model."""
|
|
126
193
|
import logging
|
|
@@ -135,10 +202,11 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
135
202
|
# Check module-level cache
|
|
136
203
|
if model in _AGENT_CACHE:
|
|
137
204
|
# Verify cache is still valid (check for config changes)
|
|
205
|
+
settings = state_manager.session.user_config.get("settings", {})
|
|
138
206
|
current_version = hash(
|
|
139
207
|
(
|
|
140
|
-
|
|
141
|
-
str(
|
|
208
|
+
str(settings.get("max_retries", 3)),
|
|
209
|
+
str(settings.get("tool_strict_validation", False)),
|
|
142
210
|
str(state_manager.session.user_config.get("mcpServers", {})),
|
|
143
211
|
)
|
|
144
212
|
)
|
|
@@ -152,9 +220,7 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
152
220
|
del _AGENT_CACHE_VERSION[model]
|
|
153
221
|
|
|
154
222
|
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
|
-
)
|
|
223
|
+
logger.debug(f"Creating new agent for model {model}")
|
|
158
224
|
max_retries = state_manager.session.user_config.get("settings", {}).get("max_retries", 3)
|
|
159
225
|
|
|
160
226
|
# Lazy import Agent and Tool
|
|
@@ -164,133 +230,71 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
164
230
|
base_path = Path(__file__).parent.parent.parent.parent
|
|
165
231
|
system_prompt = load_system_prompt(base_path)
|
|
166
232
|
|
|
167
|
-
# Load
|
|
233
|
+
# Load AGENTS.md context
|
|
168
234
|
system_prompt += load_tunacode_context()
|
|
169
235
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"TUNACODE_TASK_COMPLETE", "PLAN_MODE_TASK_PLACEHOLDER"
|
|
175
|
-
)
|
|
176
|
-
# Remove the completion guidance that conflicts with plan mode
|
|
177
|
-
lines_to_remove = [
|
|
178
|
-
"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",
|
|
180
|
-
"**How to signal completion:**",
|
|
181
|
-
"TUNACODE_TASK_COMPLETE",
|
|
182
|
-
"[Your summary of what was accomplished]",
|
|
183
|
-
"**IMPORTANT**: Always evaluate if you've completed the task. If yes, use TUNACODE_TASK_COMPLETE.",
|
|
184
|
-
"This prevents wasting iterations and API calls.",
|
|
185
|
-
]
|
|
186
|
-
for line in lines_to_remove:
|
|
187
|
-
system_prompt = system_prompt.replace(line, "")
|
|
188
|
-
# COMPLETELY REPLACE system prompt in plan mode - nuclear option
|
|
189
|
-
system_prompt = """
|
|
190
|
-
🔧 PLAN MODE - TOOL EXECUTION ONLY 🔧
|
|
191
|
-
|
|
192
|
-
You are a planning assistant that ONLY communicates through tool execution.
|
|
193
|
-
|
|
194
|
-
CRITICAL: You cannot respond with text. You MUST use tools for everything.
|
|
195
|
-
|
|
196
|
-
AVAILABLE TOOLS:
|
|
197
|
-
- read_file(filepath): Read file contents
|
|
198
|
-
- grep(pattern): Search for text patterns
|
|
199
|
-
- list_dir(directory): List directory contents
|
|
200
|
-
- glob(pattern): Find files matching patterns
|
|
201
|
-
- present_plan(title, overview, steps, files_to_create, success_criteria): Present structured plan
|
|
202
|
-
|
|
203
|
-
MANDATORY WORKFLOW:
|
|
204
|
-
1. User asks you to plan something
|
|
205
|
-
2. You research using read-only tools (if needed)
|
|
206
|
-
3. You EXECUTE present_plan tool with structured data
|
|
207
|
-
4. DONE
|
|
208
|
-
|
|
209
|
-
FORBIDDEN:
|
|
210
|
-
- Text responses
|
|
211
|
-
- Showing function calls as code
|
|
212
|
-
- Saying "here is the plan"
|
|
213
|
-
- Any text completion
|
|
214
|
-
|
|
215
|
-
EXAMPLE:
|
|
216
|
-
User: "plan a markdown file"
|
|
217
|
-
You: [Call read_file or grep for research if needed]
|
|
218
|
-
[Call present_plan tool with actual parameters - NOT as text]
|
|
219
|
-
|
|
220
|
-
The present_plan tool takes these parameters:
|
|
221
|
-
- title: Brief title string
|
|
222
|
-
- overview: What the plan accomplishes
|
|
223
|
-
- steps: List of implementation steps
|
|
224
|
-
- files_to_create: List of files to create
|
|
225
|
-
- success_criteria: List of success criteria
|
|
226
|
-
|
|
227
|
-
YOU MUST EXECUTE present_plan TOOL TO COMPLETE ANY PLANNING TASK.
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
# Initialize tools that need state manager
|
|
231
|
-
todo_tool = TodoTool(state_manager=state_manager)
|
|
232
|
-
present_plan = create_present_plan_tool(state_manager)
|
|
233
|
-
logger.debug(f"Tools initialized, present_plan available: {present_plan is not None}")
|
|
234
|
-
|
|
235
|
-
# Add todo context if available
|
|
236
|
-
try:
|
|
237
|
-
current_todos = todo_tool.get_current_todos_sync()
|
|
238
|
-
if current_todos != "No todos found":
|
|
239
|
-
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.'
|
|
240
|
-
except Exception as e:
|
|
241
|
-
logger.warning(f"Warning: Failed to load todos: {e}")
|
|
242
|
-
|
|
243
|
-
# Create tool list based on mode
|
|
244
|
-
if state_manager.is_plan_mode():
|
|
245
|
-
# Plan mode: Only read-only tools + present_plan
|
|
246
|
-
tools_list = [
|
|
247
|
-
Tool(present_plan, max_retries=max_retries),
|
|
248
|
-
Tool(glob, max_retries=max_retries),
|
|
249
|
-
Tool(grep, max_retries=max_retries),
|
|
250
|
-
Tool(list_dir, max_retries=max_retries),
|
|
251
|
-
Tool(read_file, max_retries=max_retries),
|
|
252
|
-
]
|
|
253
|
-
else:
|
|
254
|
-
# Normal mode: All tools
|
|
255
|
-
tools_list = [
|
|
256
|
-
Tool(bash, max_retries=max_retries),
|
|
257
|
-
Tool(present_plan, max_retries=max_retries),
|
|
258
|
-
Tool(glob, max_retries=max_retries),
|
|
259
|
-
Tool(grep, max_retries=max_retries),
|
|
260
|
-
Tool(list_dir, max_retries=max_retries),
|
|
261
|
-
Tool(read_file, max_retries=max_retries),
|
|
262
|
-
Tool(run_command, max_retries=max_retries),
|
|
263
|
-
Tool(todo_tool._execute, max_retries=max_retries),
|
|
264
|
-
Tool(update_file, max_retries=max_retries),
|
|
265
|
-
Tool(write_file, max_retries=max_retries),
|
|
266
|
-
]
|
|
267
|
-
|
|
268
|
-
# Log which tools are being registered
|
|
269
|
-
logger.debug(
|
|
270
|
-
f"Creating agent: plan_mode={state_manager.is_plan_mode()}, tools={len(tools_list)}"
|
|
236
|
+
# Get tool strict validation setting from config (default to False for backward
|
|
237
|
+
# compatibility)
|
|
238
|
+
tool_strict_validation = state_manager.session.user_config.get("settings", {}).get(
|
|
239
|
+
"tool_strict_validation", False
|
|
271
240
|
)
|
|
272
|
-
if state_manager.is_plan_mode():
|
|
273
|
-
logger.debug(f"PLAN MODE TOOLS: {[str(tool) for tool in tools_list]}")
|
|
274
|
-
logger.debug(f"present_plan tool type: {type(present_plan)}")
|
|
275
241
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
242
|
+
# Create tool list
|
|
243
|
+
tools_list = [
|
|
244
|
+
Tool(bash, max_retries=max_retries, strict=tool_strict_validation),
|
|
245
|
+
Tool(glob, max_retries=max_retries, strict=tool_strict_validation),
|
|
246
|
+
Tool(grep, max_retries=max_retries, strict=tool_strict_validation),
|
|
247
|
+
Tool(list_dir, max_retries=max_retries, strict=tool_strict_validation),
|
|
248
|
+
Tool(read_file, max_retries=max_retries, strict=tool_strict_validation),
|
|
249
|
+
Tool(run_command, max_retries=max_retries, strict=tool_strict_validation),
|
|
250
|
+
Tool(update_file, max_retries=max_retries, strict=tool_strict_validation),
|
|
251
|
+
Tool(write_file, max_retries=max_retries, strict=tool_strict_validation),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
logger.debug(f"Creating agent with {len(tools_list)} tools")
|
|
255
|
+
|
|
256
|
+
mcp_servers = get_mcp_servers(state_manager)
|
|
257
|
+
|
|
258
|
+
# Configure HTTP client with retry logic at transport layer
|
|
259
|
+
# This handles retries BEFORE node creation, avoiding pydantic-ai's
|
|
260
|
+
# single-stream-per-node constraint violations
|
|
261
|
+
# https://ai.pydantic.dev/api/retries/#pydantic_ai.retries.wait_retry_after
|
|
262
|
+
transport = AsyncTenacityTransport(
|
|
263
|
+
config=RetryConfig(
|
|
264
|
+
retry=retry_if_exception_type(HTTPStatusError),
|
|
265
|
+
wait=wait_retry_after(max_wait=60),
|
|
266
|
+
stop=stop_after_attempt(max_retries),
|
|
267
|
+
reraise=True,
|
|
268
|
+
),
|
|
269
|
+
validate_response=lambda r: r.raise_for_status(),
|
|
270
|
+
)
|
|
271
|
+
http_client = AsyncClient(transport=transport)
|
|
272
|
+
|
|
273
|
+
# Create model instance with retry-enabled HTTP client
|
|
274
|
+
model_instance = _create_model_with_retry(model, http_client, state_manager)
|
|
280
275
|
|
|
281
276
|
agent = Agent(
|
|
282
|
-
model=
|
|
277
|
+
model=model_instance,
|
|
283
278
|
system_prompt=system_prompt,
|
|
284
279
|
tools=tools_list,
|
|
285
|
-
mcp_servers=
|
|
280
|
+
mcp_servers=mcp_servers,
|
|
286
281
|
)
|
|
287
282
|
|
|
283
|
+
# Register agent for MCP cleanup tracking
|
|
284
|
+
mcp_server_names = state_manager.session.user_config.get("mcpServers", {}).keys()
|
|
285
|
+
for server_name in mcp_server_names:
|
|
286
|
+
register_mcp_agent(server_name, agent)
|
|
287
|
+
|
|
288
288
|
# Store in both caches
|
|
289
289
|
_AGENT_CACHE[model] = agent
|
|
290
290
|
_AGENT_CACHE_VERSION[model] = hash(
|
|
291
291
|
(
|
|
292
|
-
state_manager.is_plan_mode(),
|
|
293
292
|
str(state_manager.session.user_config.get("settings", {}).get("max_retries", 3)),
|
|
293
|
+
str(
|
|
294
|
+
state_manager.session.user_config.get("settings", {}).get(
|
|
295
|
+
"tool_strict_validation", False
|
|
296
|
+
)
|
|
297
|
+
),
|
|
294
298
|
str(state_manager.session.user_config.get("mcpServers", {})),
|
|
295
299
|
)
|
|
296
300
|
)
|
|
@@ -97,7 +97,8 @@ def create_empty_response_message(
|
|
|
97
97
|
"""Create a constructive message for handling empty responses."""
|
|
98
98
|
tools_context = get_recent_tools_context(tool_calls)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
reason = empty_reason if empty_reason != "empty" else "empty"
|
|
101
|
+
content = f"""Response appears {reason} or incomplete. Let's troubleshoot and try again.
|
|
101
102
|
|
|
102
103
|
Task: {message[:200]}...
|
|
103
104
|
{tools_context}
|
|
@@ -106,7 +107,7 @@ Attempt: {iteration}
|
|
|
106
107
|
Please take one of these specific actions:
|
|
107
108
|
|
|
108
109
|
1. **Search yielded no results?** → Try alternative search terms or broader patterns
|
|
109
|
-
2. **Found what you need?** → Use
|
|
110
|
+
2. **Found what you need?** → Use TUNACODE DONE: to finalize
|
|
110
111
|
3. **Encountering a blocker?** → Explain the specific issue preventing progress
|
|
111
112
|
4. **Need more context?** → Use list_dir or expand your search scope
|
|
112
113
|
|
|
@@ -201,6 +202,34 @@ def create_fallback_response(
|
|
|
201
202
|
return fallback
|
|
202
203
|
|
|
203
204
|
|
|
205
|
+
async def handle_empty_response(
|
|
206
|
+
message: str,
|
|
207
|
+
reason: str,
|
|
208
|
+
iter_index: int,
|
|
209
|
+
state: Any,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Handle empty responses by creating a synthetic user message with retry guidance."""
|
|
212
|
+
from tunacode.ui import console as ui
|
|
213
|
+
|
|
214
|
+
force_action_content = create_empty_response_message(
|
|
215
|
+
message,
|
|
216
|
+
reason,
|
|
217
|
+
getattr(state.sm.session, "tool_calls", []),
|
|
218
|
+
iter_index,
|
|
219
|
+
state.sm,
|
|
220
|
+
)
|
|
221
|
+
create_user_message(force_action_content, state.sm)
|
|
222
|
+
|
|
223
|
+
if state.show_thoughts:
|
|
224
|
+
await ui.warning("\nEMPTY RESPONSE FAILURE - AGGRESSIVE RETRY TRIGGERED")
|
|
225
|
+
await ui.muted(f" Reason: {reason}")
|
|
226
|
+
await ui.muted(
|
|
227
|
+
f" Recent tools: "
|
|
228
|
+
f"{get_recent_tools_context(getattr(state.sm.session, 'tool_calls', []))}"
|
|
229
|
+
)
|
|
230
|
+
await ui.muted(" Injecting retry guidance prompt")
|
|
231
|
+
|
|
232
|
+
|
|
204
233
|
def format_fallback_output(fallback: FallbackResponse) -> str:
|
|
205
234
|
"""Format a fallback response into a comprehensive output string."""
|
|
206
235
|
output_parts = [fallback.summary, ""]
|