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.

Files changed (90) hide show
  1. tunacode/cli/commands/__init__.py +0 -2
  2. tunacode/cli/commands/implementations/__init__.py +0 -3
  3. tunacode/cli/commands/implementations/debug.py +2 -2
  4. tunacode/cli/commands/implementations/development.py +10 -8
  5. tunacode/cli/commands/implementations/model.py +357 -29
  6. tunacode/cli/commands/implementations/system.py +3 -2
  7. tunacode/cli/commands/implementations/template.py +0 -2
  8. tunacode/cli/commands/registry.py +8 -7
  9. tunacode/cli/commands/slash/loader.py +2 -1
  10. tunacode/cli/commands/slash/validator.py +2 -1
  11. tunacode/cli/main.py +19 -1
  12. tunacode/cli/repl.py +90 -229
  13. tunacode/cli/repl_components/command_parser.py +2 -1
  14. tunacode/cli/repl_components/error_recovery.py +8 -5
  15. tunacode/cli/repl_components/output_display.py +1 -10
  16. tunacode/cli/repl_components/tool_executor.py +1 -13
  17. tunacode/configuration/defaults.py +2 -2
  18. tunacode/configuration/key_descriptions.py +284 -0
  19. tunacode/configuration/settings.py +0 -1
  20. tunacode/constants.py +6 -42
  21. tunacode/core/agents/__init__.py +43 -2
  22. tunacode/core/agents/agent_components/__init__.py +7 -0
  23. tunacode/core/agents/agent_components/agent_config.py +162 -158
  24. tunacode/core/agents/agent_components/agent_helpers.py +31 -2
  25. tunacode/core/agents/agent_components/node_processor.py +180 -146
  26. tunacode/core/agents/agent_components/response_state.py +123 -6
  27. tunacode/core/agents/agent_components/state_transition.py +116 -0
  28. tunacode/core/agents/agent_components/streaming.py +296 -0
  29. tunacode/core/agents/agent_components/task_completion.py +19 -6
  30. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  31. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  32. tunacode/core/agents/main.py +522 -370
  33. tunacode/core/agents/main_legact.py +538 -0
  34. tunacode/core/agents/prompts.py +66 -0
  35. tunacode/core/agents/utils.py +29 -122
  36. tunacode/core/setup/__init__.py +0 -2
  37. tunacode/core/setup/config_setup.py +88 -227
  38. tunacode/core/setup/config_wizard.py +230 -0
  39. tunacode/core/setup/coordinator.py +2 -1
  40. tunacode/core/state.py +16 -64
  41. tunacode/core/token_usage/usage_tracker.py +3 -1
  42. tunacode/core/tool_authorization.py +352 -0
  43. tunacode/core/tool_handler.py +67 -60
  44. tunacode/prompts/system.xml +751 -0
  45. tunacode/services/mcp.py +97 -1
  46. tunacode/setup.py +0 -23
  47. tunacode/tools/base.py +54 -1
  48. tunacode/tools/bash.py +14 -0
  49. tunacode/tools/glob.py +4 -2
  50. tunacode/tools/grep.py +7 -17
  51. tunacode/tools/prompts/glob_prompt.xml +1 -1
  52. tunacode/tools/prompts/grep_prompt.xml +1 -0
  53. tunacode/tools/prompts/list_dir_prompt.xml +1 -1
  54. tunacode/tools/prompts/react_prompt.xml +23 -0
  55. tunacode/tools/prompts/read_file_prompt.xml +1 -1
  56. tunacode/tools/react.py +153 -0
  57. tunacode/tools/run_command.py +15 -0
  58. tunacode/types.py +14 -79
  59. tunacode/ui/completers.py +434 -50
  60. tunacode/ui/config_dashboard.py +585 -0
  61. tunacode/ui/console.py +63 -11
  62. tunacode/ui/input.py +8 -3
  63. tunacode/ui/keybindings.py +0 -18
  64. tunacode/ui/model_selector.py +395 -0
  65. tunacode/ui/output.py +40 -19
  66. tunacode/ui/panels.py +173 -49
  67. tunacode/ui/path_heuristics.py +91 -0
  68. tunacode/ui/prompt_manager.py +1 -20
  69. tunacode/ui/tool_ui.py +30 -8
  70. tunacode/utils/api_key_validation.py +93 -0
  71. tunacode/utils/config_comparator.py +340 -0
  72. tunacode/utils/models_registry.py +593 -0
  73. tunacode/utils/text_utils.py +18 -1
  74. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
  75. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
  76. tunacode/cli/commands/implementations/plan.py +0 -50
  77. tunacode/cli/commands/implementations/todo.py +0 -217
  78. tunacode/context.py +0 -71
  79. tunacode/core/setup/git_safety_setup.py +0 -186
  80. tunacode/prompts/system.md +0 -359
  81. tunacode/prompts/system.md.bak +0 -487
  82. tunacode/tools/exit_plan_mode.py +0 -273
  83. tunacode/tools/present_plan.py +0 -288
  84. tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
  85. tunacode/tools/prompts/present_plan_prompt.xml +0 -20
  86. tunacode/tools/prompts/todo_prompt.xml +0 -96
  87. tunacode/tools/todo.py +0 -456
  88. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
  89. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  90. {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 load_system_prompt(base_path: Path) -> str:
50
- """Load the system prompt from file with caching."""
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
- 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
59
+ current_mtime = prompt_path.stat().st_mtime
60
+ except FileNotFoundError as error:
61
+ raise FileNotFoundError from error
61
62
 
62
- # Load from file and cache
63
- with open(prompt_path, "r", encoding="utf-8") as f:
64
- content = f.read().strip()
65
- _PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
66
- return content
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
- except FileNotFoundError:
69
- # Fallback to system.txt if system.md not found
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
- with open(prompt_path, "r", encoding="utf-8") as f:
81
- content = f.read().strip()
82
- _PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
83
- return content
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
- except FileNotFoundError:
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 TUNACODE.md context if it exists with caching."""
96
+ """Load AGENTS.md context if it exists with caching."""
92
97
  try:
93
- tunacode_path = Path.cwd() / "TUNACODE.md"
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("📄 TUNACODE.md not found: Using default context")
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("📄 TUNACODE.md located: Loading context...")
111
- result = "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
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("📄 TUNACODE.md not found: Using default context")
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 TUNACODE.md: {e}")
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
- state_manager.is_plan_mode(),
141
- str(state_manager.session.user_config.get("settings", {}).get("max_retries", 3)),
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 TUNACODE.md context
233
+ # Load AGENTS.md context
168
234
  system_prompt += load_tunacode_context()
169
235
 
170
- # Add plan mode context if in plan mode
171
- if state_manager.is_plan_mode():
172
- # REMOVE all TUNACODE_TASK_COMPLETE instructions from the system prompt
173
- system_prompt = system_prompt.replace(
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
- if "PLAN MODE - YOU MUST USE THE present_plan TOOL" in system_prompt:
277
- logger.debug("✅ Plan mode instructions ARE in system prompt")
278
- else:
279
- logger.debug("❌ Plan mode instructions NOT in system prompt")
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=model,
277
+ model=model_instance,
283
278
  system_prompt=system_prompt,
284
279
  tools=tools_list,
285
- mcp_servers=get_mcp_servers(state_manager),
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
- content = f"""Response appears {empty_reason if empty_reason != "empty" else "empty"} or incomplete. Let's troubleshoot and try again.
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 TUNACODE_TASK_COMPLETE to finalize
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, ""]