tunacode-cli 0.0.55__py3-none-any.whl → 0.0.56__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.

@@ -8,6 +8,7 @@ from tunacode.core.logging.logger import get_logger
8
8
  from tunacode.core.state import StateManager
9
9
  from tunacode.services.mcp import get_mcp_servers
10
10
  from tunacode.tools.bash import bash
11
+ from tunacode.tools.present_plan import create_present_plan_tool
11
12
  from tunacode.tools.glob import glob
12
13
  from tunacode.tools.grep import grep
13
14
  from tunacode.tools.list_dir import list_dir
@@ -65,7 +66,11 @@ def load_tunacode_context() -> str:
65
66
 
66
67
  def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
67
68
  """Get existing agent or create new one for the specified model."""
69
+ import logging
70
+ logger = logging.getLogger(__name__)
71
+
68
72
  if model not in state_manager.session.agents:
73
+ logger.debug(f"Creating new agent for model {model}, plan_mode={state_manager.is_plan_mode()}")
69
74
  max_retries = state_manager.session.user_config.get("settings", {}).get("max_retries", 3)
70
75
 
71
76
  # Lazy import Agent and Tool
@@ -78,8 +83,105 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
78
83
  # Load TUNACODE.md context
79
84
  system_prompt += load_tunacode_context()
80
85
 
81
- # Initialize todo tool
86
+ # Add plan mode context if in plan mode
87
+ if state_manager.is_plan_mode():
88
+ # REMOVE all TUNACODE_TASK_COMPLETE instructions from the system prompt
89
+ system_prompt = system_prompt.replace("TUNACODE_TASK_COMPLETE", "PLAN_MODE_TASK_PLACEHOLDER")
90
+ # Remove the completion guidance that conflicts with plan mode
91
+ lines_to_remove = [
92
+ "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",
94
+ "**How to signal completion:**",
95
+ "TUNACODE_TASK_COMPLETE",
96
+ "[Your summary of what was accomplished]",
97
+ "**IMPORTANT**: Always evaluate if you've completed the task. If yes, use TUNACODE_TASK_COMPLETE.",
98
+ "This prevents wasting iterations and API calls."
99
+ ]
100
+ for line in lines_to_remove:
101
+ 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
+ # COMPLETELY REPLACE system prompt in plan mode - nuclear option
140
+ system_prompt = """
141
+ 🔧 PLAN MODE - TOOL EXECUTION ONLY 🔧
142
+
143
+ You are a planning assistant that ONLY communicates through tool execution.
144
+
145
+ CRITICAL: You cannot respond with text. You MUST use tools for everything.
146
+
147
+ AVAILABLE TOOLS:
148
+ - read_file(filepath): Read file contents
149
+ - grep(pattern): Search for text patterns
150
+ - list_dir(directory): List directory contents
151
+ - glob(pattern): Find files matching patterns
152
+ - present_plan(title, overview, steps, files_to_create, success_criteria): Present structured plan
153
+
154
+ MANDATORY WORKFLOW:
155
+ 1. User asks you to plan something
156
+ 2. You research using read-only tools (if needed)
157
+ 3. You EXECUTE present_plan tool with structured data
158
+ 4. DONE
159
+
160
+ FORBIDDEN:
161
+ - Text responses
162
+ - Showing function calls as code
163
+ - Saying "here is the plan"
164
+ - Any text completion
165
+
166
+ EXAMPLE:
167
+ User: "plan a markdown file"
168
+ You: [Call read_file or grep for research if needed]
169
+ [Call present_plan tool with actual parameters - NOT as text]
170
+
171
+ The present_plan tool takes these parameters:
172
+ - title: Brief title string
173
+ - overview: What the plan accomplishes
174
+ - steps: List of implementation steps
175
+ - files_to_create: List of files to create
176
+ - success_criteria: List of success criteria
177
+
178
+ YOU MUST EXECUTE present_plan TOOL TO COMPLETE ANY PLANNING TASK.
179
+ """
180
+
181
+ # Initialize tools that need state manager
82
182
  todo_tool = TodoTool(state_manager=state_manager)
183
+ present_plan = create_present_plan_tool(state_manager)
184
+ logger.debug(f"Tools initialized, present_plan available: {present_plan is not None}")
83
185
 
84
186
  # Add todo context if available
85
187
  try:
@@ -89,12 +191,21 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
89
191
  except Exception as e:
90
192
  logger.warning(f"Warning: Failed to load todos: {e}")
91
193
 
92
- # Create agent with all tools
93
- state_manager.session.agents[model] = Agent(
94
- model=model,
95
- system_prompt=system_prompt,
96
- tools=[
194
+ # Create tool list based on mode
195
+ if state_manager.is_plan_mode():
196
+ # Plan mode: Only read-only tools + present_plan
197
+ tools_list = [
198
+ Tool(present_plan, max_retries=max_retries),
199
+ Tool(glob, max_retries=max_retries),
200
+ Tool(grep, max_retries=max_retries),
201
+ Tool(list_dir, max_retries=max_retries),
202
+ Tool(read_file, max_retries=max_retries),
203
+ ]
204
+ else:
205
+ # Normal mode: All tools
206
+ tools_list = [
97
207
  Tool(bash, max_retries=max_retries),
208
+ Tool(present_plan, max_retries=max_retries),
98
209
  Tool(glob, max_retries=max_retries),
99
210
  Tool(grep, max_retries=max_retries),
100
211
  Tool(list_dir, max_retries=max_retries),
@@ -103,7 +214,23 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
103
214
  Tool(todo_tool._execute, max_retries=max_retries),
104
215
  Tool(update_file, max_retries=max_retries),
105
216
  Tool(write_file, max_retries=max_retries),
106
- ],
217
+ ]
218
+
219
+ # Log which tools are being registered
220
+ logger.debug(f"Creating agent: plan_mode={state_manager.is_plan_mode()}, tools={len(tools_list)}")
221
+ if state_manager.is_plan_mode():
222
+ logger.debug(f"PLAN MODE TOOLS: {[str(tool) for tool in tools_list]}")
223
+ logger.debug(f"present_plan tool type: {type(present_plan)}")
224
+
225
+ if "PLAN MODE - YOU MUST USE THE present_plan TOOL" in system_prompt:
226
+ logger.debug("✅ Plan mode instructions ARE in system prompt")
227
+ else:
228
+ logger.debug("❌ Plan mode instructions NOT in system prompt")
229
+
230
+ state_manager.session.agents[model] = Agent(
231
+ model=model,
232
+ system_prompt=system_prompt,
233
+ tools=tools_list,
107
234
  mcp_servers=get_mcp_servers(state_manager),
108
235
  )
109
236
  return state_manager.session.agents[model]
@@ -343,45 +343,46 @@ async def _process_tool_calls(
343
343
  f"[bold #00d7ff]{batch_msg}...[/bold #00d7ff]", state_manager
344
344
  )
345
345
 
346
- # Enhanced visual feedback for parallel execution
347
- await ui.muted("\n" + "=" * 60)
348
- await ui.muted(
349
- f"🚀 PARALLEL BATCH #{batch_id}: Executing {len(buffered_tasks)} read-only tools concurrently"
350
- )
351
- await ui.muted("=" * 60)
352
-
353
- # Display details of what's being executed
354
- for idx, (buffered_part, _) in enumerate(buffered_tasks, 1):
355
- tool_desc = f" [{idx}] {buffered_part.tool_name}"
356
- if hasattr(buffered_part, "args") and isinstance(
357
- buffered_part.args, dict
358
- ):
359
- if (
360
- buffered_part.tool_name == "read_file"
361
- and "file_path" in buffered_part.args
362
- ):
363
- tool_desc += f" → {buffered_part.args['file_path']}"
364
- elif (
365
- buffered_part.tool_name == "grep"
366
- and "pattern" in buffered_part.args
367
- ):
368
- tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
369
- if "include_files" in buffered_part.args:
370
- tool_desc += (
371
- f", files: '{buffered_part.args['include_files']}'"
372
- )
373
- elif (
374
- buffered_part.tool_name == "list_dir"
375
- and "directory" in buffered_part.args
376
- ):
377
- tool_desc += f" → {buffered_part.args['directory']}"
378
- elif (
379
- buffered_part.tool_name == "glob"
380
- and "pattern" in buffered_part.args
346
+ # Enhanced visual feedback for parallel execution (suppress in plan mode)
347
+ if not state_manager.is_plan_mode():
348
+ await ui.muted("\n" + "=" * 60)
349
+ await ui.muted(
350
+ f"🚀 PARALLEL BATCH #{batch_id}: Executing {len(buffered_tasks)} read-only tools concurrently"
351
+ )
352
+ await ui.muted("=" * 60)
353
+
354
+ # Display details of what's being executed
355
+ for idx, (buffered_part, _) in enumerate(buffered_tasks, 1):
356
+ tool_desc = f" [{idx}] {buffered_part.tool_name}"
357
+ if hasattr(buffered_part, "args") and isinstance(
358
+ buffered_part.args, dict
381
359
  ):
382
- tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
383
- await ui.muted(tool_desc)
384
- await ui.muted("=" * 60)
360
+ if (
361
+ buffered_part.tool_name == "read_file"
362
+ and "file_path" in buffered_part.args
363
+ ):
364
+ tool_desc += f" → {buffered_part.args['file_path']}"
365
+ elif (
366
+ buffered_part.tool_name == "grep"
367
+ and "pattern" in buffered_part.args
368
+ ):
369
+ tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
370
+ if "include_files" in buffered_part.args:
371
+ tool_desc += (
372
+ f", files: '{buffered_part.args['include_files']}'"
373
+ )
374
+ elif (
375
+ buffered_part.tool_name == "list_dir"
376
+ and "directory" in buffered_part.args
377
+ ):
378
+ tool_desc += f" → {buffered_part.args['directory']}"
379
+ elif (
380
+ buffered_part.tool_name == "glob"
381
+ and "pattern" in buffered_part.args
382
+ ):
383
+ tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
384
+ await ui.muted(tool_desc)
385
+ await ui.muted("=" * 60)
385
386
 
386
387
  await execute_tools_parallel(buffered_tasks, tool_callback)
387
388
 
@@ -391,10 +392,11 @@ async def _process_tool_calls(
391
392
  ) # Assume 100ms per tool average
392
393
  speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
393
394
 
394
- await ui.muted(
395
- f"✅ Parallel batch completed in {elapsed_time:.0f}ms "
396
- f"(~{speedup:.1f}x faster than sequential)\n"
397
- )
395
+ if not state_manager.is_plan_mode():
396
+ await ui.muted(
397
+ f"✅ Parallel batch completed in {elapsed_time:.0f}ms "
398
+ f"(~{speedup:.1f}x faster than sequential)\n"
399
+ )
398
400
 
399
401
  # Reset spinner message back to thinking
400
402
  from tunacode.constants import UI_THINKING_MESSAGE
tunacode/core/state.py CHANGED
@@ -15,6 +15,8 @@ from tunacode.types import (
15
15
  InputSessions,
16
16
  MessageHistory,
17
17
  ModelName,
18
+ PlanDoc,
19
+ PlanPhase,
18
20
  SessionId,
19
21
  TodoItem,
20
22
  ToolName,
@@ -84,6 +86,12 @@ class SessionState:
84
86
  task_hierarchy: dict[str, Any] = field(default_factory=dict)
85
87
  iteration_budgets: dict[str, int] = field(default_factory=dict)
86
88
  recursive_context_stack: list[dict[str, Any]] = field(default_factory=list)
89
+
90
+ # Plan Mode state tracking
91
+ plan_mode: bool = False
92
+ plan_phase: Optional[PlanPhase] = None
93
+ current_plan: Optional[PlanDoc] = None
94
+ plan_approved: bool = False
87
95
 
88
96
  def update_token_count(self):
89
97
  """Calculates the total token count from messages and files in context."""
@@ -167,3 +175,39 @@ class StateManager:
167
175
  def reset_session(self) -> None:
168
176
  """Reset the session to a fresh state."""
169
177
  self._session = SessionState()
178
+
179
+ # Plan Mode methods
180
+ def enter_plan_mode(self) -> None:
181
+ """Enter plan mode - restricts to read-only operations."""
182
+ self._session.plan_mode = True
183
+ self._session.plan_phase = PlanPhase.PLANNING_RESEARCH
184
+ self._session.current_plan = None
185
+ self._session.plan_approved = False
186
+ # Clear agent cache to force recreation with plan mode tools
187
+ self._session.agents.clear()
188
+
189
+ def exit_plan_mode(self, plan: Optional[PlanDoc] = None) -> None:
190
+ """Exit plan mode with optional plan data."""
191
+ self._session.plan_mode = False
192
+ self._session.plan_phase = None
193
+ self._session.current_plan = plan
194
+ self._session.plan_approved = False
195
+ # Clear agent cache to force recreation without plan mode tools
196
+ self._session.agents.clear()
197
+
198
+ def approve_plan(self) -> None:
199
+ """Mark current plan as approved for execution."""
200
+ self._session.plan_approved = True
201
+ self._session.plan_mode = False
202
+
203
+ def is_plan_mode(self) -> bool:
204
+ """Check if currently in plan mode."""
205
+ return self._session.plan_mode
206
+
207
+ def set_current_plan(self, plan: PlanDoc) -> None:
208
+ """Set the current plan data."""
209
+ self._session.current_plan = plan
210
+
211
+ def get_current_plan(self) -> Optional[PlanDoc]:
212
+ """Get the current plan data."""
213
+ return self._session.current_plan
@@ -41,6 +41,14 @@ class ToolHandler:
41
41
  Returns:
42
42
  bool: True if confirmation is required, False otherwise.
43
43
  """
44
+ # Never confirm present_plan - it has its own approval flow
45
+ if tool_name == "present_plan":
46
+ return False
47
+
48
+ # Block write tools in plan mode
49
+ if self.is_tool_blocked_in_plan_mode(tool_name):
50
+ return True # Force confirmation for blocked tools
51
+
44
52
  # Skip confirmation for read-only tools
45
53
  if is_read_only_tool(tool_name):
46
54
  return False
@@ -52,6 +60,18 @@ class ToolHandler:
52
60
 
53
61
  return not (self.state.session.yolo or tool_name in self.state.session.tool_ignore)
54
62
 
63
+ def is_tool_blocked_in_plan_mode(self, tool_name: ToolName) -> bool:
64
+ """Check if tool is blocked in plan mode."""
65
+ if not self.state.is_plan_mode():
66
+ return False
67
+
68
+ # Allow present_plan tool to end planning phase
69
+ if tool_name == "present_plan":
70
+ return False
71
+
72
+ # Allow read-only tools
73
+ return not is_read_only_tool(tool_name)
74
+
55
75
  def process_confirmation(self, response: ToolConfirmationResponse, tool_name: ToolName) -> bool:
56
76
  """
57
77
  Process the confirmation response.
@@ -0,0 +1,191 @@
1
+ """Tool for exiting plan mode and presenting implementation plan."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from tunacode.tools.base import BaseTool
6
+ from tunacode.ui import console as ui
7
+ from tunacode.types import ToolResult
8
+
9
+
10
+ class ExitPlanModeTool(BaseTool):
11
+ """Present implementation plan and exit plan mode."""
12
+
13
+ def __init__(self, state_manager, ui_logger=None):
14
+ """Initialize the exit plan mode tool.
15
+
16
+ Args:
17
+ state_manager: StateManager instance for controlling plan mode state
18
+ ui_logger: UI logger instance for displaying messages
19
+ """
20
+ super().__init__(ui_logger)
21
+ self.state_manager = state_manager
22
+
23
+ @property
24
+ def tool_name(self) -> str:
25
+ return "exit_plan_mode"
26
+
27
+ async def _execute(
28
+ self,
29
+ plan_title: str,
30
+ overview: str,
31
+ implementation_steps: List[str],
32
+ files_to_modify: List[str] = None,
33
+ files_to_create: List[str] = None,
34
+ risks_and_considerations: List[str] = None,
35
+ testing_approach: str = None,
36
+ success_criteria: List[str] = None,
37
+ ) -> ToolResult:
38
+ """Present the implementation plan and get user approval."""
39
+
40
+ plan = {
41
+ "title": plan_title,
42
+ "overview": overview,
43
+ "files_to_modify": files_to_modify or [],
44
+ "files_to_create": files_to_create or [],
45
+ "implementation_steps": implementation_steps,
46
+ "risks_and_considerations": risks_and_considerations or [],
47
+ "testing_approach": testing_approach or "Manual testing of functionality",
48
+ "success_criteria": success_criteria or []
49
+ }
50
+
51
+ # Present plan to user
52
+ await self._present_plan(plan)
53
+
54
+ # Get user approval
55
+ approved = await self._get_user_approval()
56
+
57
+ # Update state based on user approval
58
+ if approved:
59
+ # Store the plan and exit plan mode
60
+ self.state_manager.set_current_plan(plan)
61
+ self.state_manager.exit_plan_mode(plan)
62
+ await ui.success("✅ Plan approved! Exiting Plan Mode.")
63
+ return "Plan approved and Plan Mode exited. You can now execute the implementation using write tools (write_file, update_file, bash, run_command)."
64
+ else:
65
+ # Keep the plan but stay in plan mode
66
+ self.state_manager.set_current_plan(plan)
67
+ await ui.warning("❌ Plan rejected. Staying in Plan Mode for further research.")
68
+ return "Plan rejected. Continue researching and refine your approach. You remain in Plan Mode - only read-only tools are available."
69
+
70
+ async def _present_plan(self, plan: Dict[str, Any]) -> None:
71
+ """Present the plan in a formatted way."""
72
+ # Build the entire plan output as a single string to avoid UI flooding
73
+ output = []
74
+ output.append("")
75
+ output.append("╭─────────────────────────────────────────────────────────╮")
76
+ output.append("│ 📋 IMPLEMENTATION PLAN │")
77
+ output.append("╰─────────────────────────────────────────────────────────╯")
78
+ output.append("")
79
+ output.append(f"🎯 {plan['title']}")
80
+ output.append("")
81
+
82
+ if plan["overview"]:
83
+ output.append(f"📝 Overview: {plan['overview']}")
84
+ output.append("")
85
+
86
+ # Files section
87
+ if plan["files_to_modify"]:
88
+ output.append("📝 Files to Modify:")
89
+ for f in plan["files_to_modify"]:
90
+ output.append(f" • {f}")
91
+ output.append("")
92
+
93
+ if plan["files_to_create"]:
94
+ output.append("📄 Files to Create:")
95
+ for f in plan["files_to_create"]:
96
+ output.append(f" • {f}")
97
+ output.append("")
98
+
99
+ # Implementation steps
100
+ output.append("🔧 Implementation Steps:")
101
+ for i, step in enumerate(plan["implementation_steps"], 1):
102
+ output.append(f" {i}. {step}")
103
+ output.append("")
104
+
105
+ # Testing approach
106
+ if plan["testing_approach"]:
107
+ output.append(f"🧪 Testing Approach: {plan['testing_approach']}")
108
+ output.append("")
109
+
110
+ # Success criteria
111
+ if plan["success_criteria"]:
112
+ output.append("✅ Success Criteria:")
113
+ for criteria in plan["success_criteria"]:
114
+ output.append(f" • {criteria}")
115
+ output.append("")
116
+
117
+ # Risks and considerations
118
+ if plan["risks_and_considerations"]:
119
+ output.append("⚠️ Risks & Considerations:")
120
+ for risk in plan["risks_and_considerations"]:
121
+ output.append(f" • {risk}")
122
+ output.append("")
123
+
124
+ # Print everything at once
125
+ await ui.info("\n".join(output))
126
+
127
+ async def _get_user_approval(self) -> bool:
128
+ """Get user approval for the plan."""
129
+ try:
130
+ from prompt_toolkit import PromptSession
131
+ from prompt_toolkit.patch_stdout import patch_stdout
132
+
133
+ session = PromptSession()
134
+
135
+ with patch_stdout():
136
+ response = await session.prompt_async("\n🤔 Approve this implementation plan? (y/n): ")
137
+
138
+ return response.strip().lower() in ['y', 'yes', 'approve']
139
+ except (KeyboardInterrupt, EOFError):
140
+ return False
141
+
142
+
143
+ def create_exit_plan_mode_tool(state_manager):
144
+ """
145
+ Factory function to create exit_plan_mode tool with the correct state manager.
146
+
147
+ Args:
148
+ state_manager: The StateManager instance to use
149
+
150
+ Returns:
151
+ Callable: The exit_plan_mode function bound to the provided state manager
152
+ """
153
+ async def exit_plan_mode(
154
+ plan_title: str,
155
+ overview: str,
156
+ implementation_steps: List[str],
157
+ files_to_modify: List[str] = None,
158
+ files_to_create: List[str] = None,
159
+ risks_and_considerations: List[str] = None,
160
+ testing_approach: str = None,
161
+ success_criteria: List[str] = None,
162
+ ) -> str:
163
+ """
164
+ Present implementation plan and exit plan mode.
165
+
166
+ Args:
167
+ plan_title: Brief title for the implementation plan
168
+ overview: High-level overview of the changes needed
169
+ implementation_steps: Ordered list of implementation steps
170
+ files_to_modify: List of files that need to be modified
171
+ files_to_create: List of new files to be created
172
+ risks_and_considerations: Potential risks or important considerations
173
+ testing_approach: Approach for testing the implementation
174
+ success_criteria: Criteria for considering the implementation successful
175
+
176
+ Returns:
177
+ str: Result message indicating plan approval status
178
+ """
179
+ tool = ExitPlanModeTool(state_manager=state_manager)
180
+ return await tool._execute(
181
+ plan_title=plan_title,
182
+ overview=overview,
183
+ implementation_steps=implementation_steps,
184
+ files_to_modify=files_to_modify,
185
+ files_to_create=files_to_create,
186
+ risks_and_considerations=risks_and_considerations,
187
+ testing_approach=testing_approach,
188
+ success_criteria=success_criteria,
189
+ )
190
+
191
+ return exit_plan_mode