massgen 0.1.2__py3-none-any.whl → 0.1.4__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 massgen might be problematic. Click here for more details.

Files changed (82) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +33 -7
  3. massgen/api_params_handler/_api_params_handler_base.py +3 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +4 -0
  5. massgen/api_params_handler/_claude_api_params_handler.py +4 -0
  6. massgen/api_params_handler/_gemini_api_params_handler.py +4 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +4 -0
  8. massgen/backend/azure_openai.py +9 -1
  9. massgen/backend/base.py +4 -0
  10. massgen/backend/base_with_custom_tool_and_mcp.py +25 -5
  11. massgen/backend/claude_code.py +9 -1
  12. massgen/backend/docs/permissions_and_context_files.md +2 -2
  13. massgen/backend/gemini.py +35 -6
  14. massgen/backend/gemini_utils.py +30 -0
  15. massgen/backend/response.py +2 -0
  16. massgen/chat_agent.py +9 -3
  17. massgen/cli.py +291 -43
  18. massgen/config_builder.py +163 -18
  19. massgen/configs/README.md +69 -14
  20. massgen/configs/debug/restart_test_controlled.yaml +60 -0
  21. massgen/configs/debug/restart_test_controlled_filesystem.yaml +73 -0
  22. massgen/configs/tools/code-execution/docker_with_sudo.yaml +35 -0
  23. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +56 -0
  24. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +65 -0
  25. massgen/configs/tools/custom_tools/computer_use_example.yaml +50 -0
  26. massgen/configs/tools/custom_tools/crawl4ai_example.yaml +55 -0
  27. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_multi.yaml +61 -0
  28. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_single.yaml +29 -0
  29. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_multi.yaml +51 -0
  30. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_single.yaml +33 -0
  31. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_multi.yaml +55 -0
  32. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_single.yaml +33 -0
  33. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_multi.yaml +47 -0
  34. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_single.yaml +29 -0
  35. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +33 -0
  36. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +34 -0
  37. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +33 -0
  38. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +34 -0
  39. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +59 -0
  40. massgen/docker/README.md +83 -0
  41. massgen/filesystem_manager/_code_execution_server.py +22 -7
  42. massgen/filesystem_manager/_docker_manager.py +21 -1
  43. massgen/filesystem_manager/_filesystem_manager.py +9 -0
  44. massgen/filesystem_manager/_path_permission_manager.py +148 -0
  45. massgen/filesystem_manager/_workspace_tools_server.py +0 -997
  46. massgen/formatter/_gemini_formatter.py +73 -0
  47. massgen/frontend/coordination_ui.py +175 -257
  48. massgen/frontend/displays/base_display.py +29 -0
  49. massgen/frontend/displays/rich_terminal_display.py +155 -9
  50. massgen/frontend/displays/simple_display.py +21 -0
  51. massgen/frontend/displays/terminal_display.py +22 -2
  52. massgen/logger_config.py +50 -6
  53. massgen/message_templates.py +283 -15
  54. massgen/orchestrator.py +335 -38
  55. massgen/tests/test_binary_file_blocking.py +274 -0
  56. massgen/tests/test_case_studies.md +12 -12
  57. massgen/tests/test_code_execution.py +178 -0
  58. massgen/tests/test_multimodal_size_limits.py +407 -0
  59. massgen/tests/test_orchestration_restart.py +204 -0
  60. massgen/tool/__init__.py +4 -0
  61. massgen/tool/_manager.py +7 -2
  62. massgen/tool/_multimodal_tools/image_to_image_generation.py +293 -0
  63. massgen/tool/_multimodal_tools/text_to_file_generation.py +455 -0
  64. massgen/tool/_multimodal_tools/text_to_image_generation.py +222 -0
  65. massgen/tool/_multimodal_tools/text_to_speech_continue_generation.py +226 -0
  66. massgen/tool/_multimodal_tools/text_to_speech_transcription_generation.py +217 -0
  67. massgen/tool/_multimodal_tools/text_to_video_generation.py +223 -0
  68. massgen/tool/_multimodal_tools/understand_audio.py +211 -0
  69. massgen/tool/_multimodal_tools/understand_file.py +555 -0
  70. massgen/tool/_multimodal_tools/understand_image.py +316 -0
  71. massgen/tool/_multimodal_tools/understand_video.py +340 -0
  72. massgen/tool/_web_tools/crawl4ai_tool.py +718 -0
  73. massgen/tool/docs/multimodal_tools.md +1368 -0
  74. massgen/tool/workflow_toolkits/__init__.py +26 -0
  75. massgen/tool/workflow_toolkits/post_evaluation.py +216 -0
  76. massgen/utils.py +1 -0
  77. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/METADATA +101 -69
  78. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/RECORD +82 -46
  79. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/WHEEL +0 -0
  80. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/entry_points.txt +0 -0
  81. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/licenses/LICENSE +0 -0
  82. {massgen-0.1.2.dist-info → massgen-0.1.4.dist-info}/top_level.txt +0 -0
@@ -193,6 +193,7 @@ class RichTerminalDisplay(TerminalDisplay):
193
193
  self._key_handler = None
194
194
  self._input_thread = None
195
195
  self._stop_input_thread = False
196
+ self._user_quit_requested = False # Flag to signal user wants to quit
196
197
  self._original_settings = None
197
198
  self._agent_selector_active = False # Flag to prevent duplicate agent selector calls
198
199
 
@@ -207,6 +208,15 @@ class RichTerminalDisplay(TerminalDisplay):
207
208
  self._final_presentation_agent = None
208
209
  self._final_presentation_vote_results = None
209
210
 
211
+ # Post-evaluation display state
212
+ self._post_evaluation_active = False
213
+ self._post_evaluation_content = ""
214
+ self._post_evaluation_agent = None
215
+
216
+ # Restart context state (for attempt 2+)
217
+ self._restart_context_reason = None
218
+ self._restart_context_instructions = None
219
+
210
220
  # Code detection patterns
211
221
  self.code_patterns = [
212
222
  r"```(\w+)?\n(.*?)\n```", # Markdown code blocks
@@ -1077,12 +1087,25 @@ class RichTerminalDisplay(TerminalDisplay):
1077
1087
  Layout(footer, name="footer", size=8),
1078
1088
  )
1079
1089
  else:
1080
- # Arrange layout without final presentation
1081
- layout.split_column(
1082
- Layout(header, name="header", size=5),
1083
- Layout(agent_columns, name="main"),
1084
- Layout(footer, name="footer", size=8),
1085
- )
1090
+ # Build layout components
1091
+ layout_components = []
1092
+
1093
+ # Add header
1094
+ layout_components.append(Layout(header, name="header", size=5))
1095
+
1096
+ # Add agent columns
1097
+ layout_components.append(Layout(agent_columns, name="main"))
1098
+
1099
+ # Add post-evaluation panel if active (below agents)
1100
+ post_eval_panel = self._create_post_evaluation_panel()
1101
+ if post_eval_panel:
1102
+ layout_components.append(Layout(post_eval_panel, name="post_eval", size=6))
1103
+
1104
+ # Add footer
1105
+ layout_components.append(Layout(footer, name="footer", size=8))
1106
+
1107
+ # Arrange layout
1108
+ layout.split_column(*layout_components)
1086
1109
 
1087
1110
  return layout
1088
1111
 
@@ -1343,7 +1366,10 @@ class RichTerminalDisplay(TerminalDisplay):
1343
1366
  elif key == "q":
1344
1367
  # Quit the application - restore terminal and stop
1345
1368
  self._stop_input_thread = True
1369
+ self._user_quit_requested = True
1346
1370
  self._restore_terminal_settings()
1371
+ # Print quit message
1372
+ self.console.print("\n[yellow]Exiting coordination...[/yellow]")
1347
1373
 
1348
1374
  def _open_agent_in_default_text_editor(self, agent_id: str) -> None:
1349
1375
  """Open agent's txt file in default text editor."""
@@ -2013,6 +2039,56 @@ class RichTerminalDisplay(TerminalDisplay):
2013
2039
  expand=True, # Full width
2014
2040
  )
2015
2041
 
2042
+ def _create_post_evaluation_panel(self) -> Optional[Panel]:
2043
+ """Create a panel for post-evaluation display (below agent columns)."""
2044
+ if not self._post_evaluation_active:
2045
+ return None
2046
+
2047
+ content_text = Text()
2048
+
2049
+ if not self._post_evaluation_content:
2050
+ content_text.append("Evaluating answer...", style=self.colors["text"])
2051
+ else:
2052
+ # Show last few lines of post-eval content
2053
+ lines = self._post_evaluation_content.split("\n")
2054
+ display_lines = lines[-5:] if len(lines) > 5 else lines
2055
+
2056
+ for line in display_lines:
2057
+ if line.strip():
2058
+ formatted_line = self._format_content_line(line)
2059
+ content_text.append(formatted_line)
2060
+ content_text.append("\n")
2061
+
2062
+ title = f"🔍 Post-Evaluation by {self._post_evaluation_agent}"
2063
+
2064
+ return Panel(
2065
+ content_text,
2066
+ title=f"[{self.colors['info']}]{title}[/{self.colors['info']}]",
2067
+ border_style=self.colors["info"],
2068
+ box=ROUNDED,
2069
+ expand=True,
2070
+ height=6, # Fixed height to not take too much space
2071
+ )
2072
+
2073
+ def _create_restart_context_panel(self) -> Optional[Panel]:
2074
+ """Create restart context panel for attempt 2+ (yellow warning at top)."""
2075
+ if not self._restart_context_reason or not self._restart_context_instructions:
2076
+ return None
2077
+
2078
+ content_text = Text()
2079
+ content_text.append("Reason: ", style="bold bright_yellow")
2080
+ content_text.append(f"{self._restart_context_reason}\n\n", style="bright_yellow")
2081
+ content_text.append("Instructions: ", style="bold bright_yellow")
2082
+ content_text.append(f"{self._restart_context_instructions}", style="bright_yellow")
2083
+
2084
+ return Panel(
2085
+ content_text,
2086
+ title="[bold bright_yellow]⚠️ PREVIOUS ATTEMPT FEEDBACK[/bold bright_yellow]",
2087
+ border_style="bright_yellow",
2088
+ box=ROUNDED,
2089
+ expand=True,
2090
+ )
2091
+
2016
2092
  def _format_presentation_content(self, content: str) -> Text:
2017
2093
  """Format presentation content with enhanced styling for orchestrator queries."""
2018
2094
  formatted = Text()
@@ -3174,7 +3250,7 @@ class RichTerminalDisplay(TerminalDisplay):
3174
3250
  title="[bold bright_green]🎯 FINAL COORDINATED ANSWER[/bold bright_green]",
3175
3251
  border_style=self.colors["success"],
3176
3252
  box=DOUBLE,
3177
- expand=False,
3253
+ expand=True,
3178
3254
  )
3179
3255
 
3180
3256
  self.console.print(final_panel)
@@ -3244,10 +3320,80 @@ class RichTerminalDisplay(TerminalDisplay):
3244
3320
  )
3245
3321
  self.console.print(error_text)
3246
3322
 
3247
- # Show interactive options for viewing agent details (only if not in safe mode)
3248
- if self._keyboard_interactive_mode and hasattr(self, "_agent_keys") and not self._safe_keyboard_mode:
3323
+ # Show interactive options for viewing agent details (only if not in safe mode and not restarting)
3324
+ # Don't show inspection menu if orchestration is restarting
3325
+ is_restarting = hasattr(self, "orchestrator") and hasattr(self.orchestrator, "restart_pending") and self.orchestrator.restart_pending
3326
+ if self._keyboard_interactive_mode and hasattr(self, "_agent_keys") and not self._safe_keyboard_mode and not is_restarting:
3249
3327
  self.show_agent_selector()
3250
3328
 
3329
+ def show_post_evaluation_content(self, content: str, agent_id: str):
3330
+ """Display post-evaluation streaming content in a panel below agents."""
3331
+ self._post_evaluation_active = True
3332
+ self._post_evaluation_agent = agent_id
3333
+ self._post_evaluation_content += content
3334
+ # Panel will be created/updated in _update_display via layout
3335
+
3336
+ def show_restart_banner(self, reason: str, instructions: str, attempt: int, max_attempts: int):
3337
+ """Display restart decision banner prominently (like final presentation)."""
3338
+ # Stop live display temporarily for static banner
3339
+ self.live is not None
3340
+ if self.live:
3341
+ self.live.stop()
3342
+ self.live = None
3343
+
3344
+ # Create restart banner content
3345
+ banner_content = Text()
3346
+ banner_content.append("\nREASON:\n", style="bold bright_yellow")
3347
+ banner_content.append(f"{reason}\n\n", style="bright_yellow")
3348
+ banner_content.append("INSTRUCTIONS FOR NEXT ATTEMPT:\n", style="bold bright_yellow")
3349
+ banner_content.append(f"{instructions}\n", style="bright_yellow")
3350
+
3351
+ restart_panel = Panel(
3352
+ banner_content,
3353
+ title=f"[bold bright_yellow]🔄 ORCHESTRATION RESTART (Attempt {attempt}/{max_attempts})[/bold bright_yellow]",
3354
+ border_style="bright_yellow",
3355
+ box=DOUBLE,
3356
+ expand=True,
3357
+ )
3358
+
3359
+ self.console.print(restart_panel)
3360
+ time.sleep(2.0) # Allow user to read restart banner
3361
+
3362
+ # Reset state for fresh attempt - clear all agent content and status
3363
+ for agent_id in self.agent_ids:
3364
+ self.agent_outputs[agent_id] = []
3365
+ self.agent_status[agent_id] = "waiting"
3366
+ # Clear text buffers
3367
+ if hasattr(self, "_text_buffers") and agent_id in self._text_buffers:
3368
+ self._text_buffers[agent_id] = ""
3369
+
3370
+ # Clear cached panels and ALL cached state
3371
+ self._agent_panels_cache.clear()
3372
+ self._footer_cache = None
3373
+ self._header_cache = None
3374
+
3375
+ # Clear orchestrator events (from base class)
3376
+ self.orchestrator_events = []
3377
+
3378
+ # Clear presentation state
3379
+ self._final_presentation_active = False
3380
+ self._final_presentation_content = ""
3381
+ self._post_evaluation_active = False
3382
+ self._post_evaluation_content = ""
3383
+
3384
+ # Clear restart context state (so it doesn't show on next attempt)
3385
+ self._restart_context_reason = None
3386
+ self._restart_context_instructions = None
3387
+
3388
+ # DON'T restart live display here - let the next coordinate() call handle it
3389
+ # The CLI will create a fresh UI instance which will initialize its own display
3390
+
3391
+ def show_restart_context_panel(self, reason: str, instructions: str):
3392
+ """Display restart context panel at top of UI (for attempt 2+)."""
3393
+ self._restart_context_reason = reason
3394
+ self._restart_context_instructions = instructions
3395
+ # Panel will be displayed in initialize() method before agent columns
3396
+
3251
3397
  def _display_answer_with_flush(self, answer: str) -> None:
3252
3398
  """Display answer with flush output effect - streaming character by character."""
3253
3399
  import sys
@@ -90,6 +90,27 @@ class SimpleDisplay(BaseDisplay):
90
90
  print(f"🗳️ Vote results: {vote_summary}")
91
91
  print("=" * 50)
92
92
 
93
+ def show_post_evaluation_content(self, content: str, agent_id: str):
94
+ """Display post-evaluation streaming content."""
95
+ print(f"🔍 [{agent_id}] {content}", end="", flush=True)
96
+
97
+ def show_restart_banner(self, reason: str, instructions: str, attempt: int, max_attempts: int):
98
+ """Display restart decision banner."""
99
+ print("\n" + "🔄" * 40)
100
+ print(f"ORCHESTRATION RESTART - Attempt {attempt}/{max_attempts}")
101
+ print("🔄" * 40)
102
+ print(f"\n{reason}\n")
103
+ print(f"Instructions: {instructions}\n")
104
+ print("🔄" * 40 + "\n")
105
+
106
+ def show_restart_context_panel(self, reason: str, instructions: str):
107
+ """Display restart context panel at top of UI (for attempt 2+)."""
108
+ print("\n" + "⚠️ " * 30)
109
+ print("PREVIOUS ATTEMPT FEEDBACK")
110
+ print(f"Reason: {reason}")
111
+ print(f"Instructions: {instructions}")
112
+ print("⚠️ " * 30 + "\n")
113
+
93
114
  def cleanup(self):
94
115
  """Clean up resources."""
95
116
  print(f"\n✅ Coordination completed with {len(self.agent_ids)} agents")
@@ -220,8 +220,6 @@ class TerminalDisplay(BaseDisplay):
220
220
 
221
221
  # Add working indicator if transitioning to working
222
222
  if old_status != "working" and status == "working":
223
- agent_prefix = f"[{agent_id}] " if self.num_agents > 1 else ""
224
- print(f"\n{agent_prefix}⚡ Working...")
225
223
  if not self.agent_outputs[agent_id] or not self.agent_outputs[agent_id][-1].startswith("⚡"):
226
224
  self.agent_outputs[agent_id].append("⚡ Working...")
227
225
 
@@ -245,6 +243,28 @@ class TerminalDisplay(BaseDisplay):
245
243
  print(f"🗳️ Vote results: {vote_summary}")
246
244
  print("=" * 60)
247
245
 
246
+ def show_post_evaluation_content(self, content: str, agent_id: str):
247
+ """Display post-evaluation streaming content."""
248
+ print(f"🔍 Post-Evaluation [{agent_id}]: {content}", end="", flush=True)
249
+
250
+ def show_restart_banner(self, reason: str, instructions: str, attempt: int, max_attempts: int):
251
+ """Display restart decision banner."""
252
+ print("\n" + "=" * 80)
253
+ print(f"🔄 ORCHESTRATION RESTART (Attempt {attempt}/{max_attempts})")
254
+ print("=" * 80)
255
+ print(f"\nREASON:\n{reason}")
256
+ print(f"\nINSTRUCTIONS FOR NEXT ATTEMPT:\n{instructions}")
257
+ print("\n" + "=" * 80 + "\n")
258
+
259
+ def show_restart_context_panel(self, reason: str, instructions: str):
260
+ """Display restart context panel at top of UI (for attempt 2+)."""
261
+ print("\n" + "⚠" * 40)
262
+ print("⚠️ PREVIOUS ATTEMPT FEEDBACK")
263
+ print("⚠" * 40)
264
+ print(f"\nReason: {reason}")
265
+ print(f"\nInstructions: {instructions}")
266
+ print("\n" + "⚠" * 40 + "\n")
267
+
248
268
  def cleanup(self):
249
269
  """Clean up display resources."""
250
270
  # No special cleanup needed for terminal display
massgen/logger_config.py CHANGED
@@ -41,6 +41,7 @@ _DEBUG_MODE = False
41
41
  _LOG_SESSION_DIR = None
42
42
  _LOG_BASE_SESSION_DIR = None # Base session dir (without turn subdirectory)
43
43
  _CURRENT_TURN = None
44
+ _CURRENT_ATTEMPT = None # Current attempt number for restart tracking
44
45
 
45
46
  # Console logging suppression (for Rich Live display compatibility)
46
47
  _CONSOLE_HANDLER_ID = None
@@ -48,15 +49,15 @@ _CONSOLE_SUPPRESSED = False
48
49
 
49
50
 
50
51
  def get_log_session_dir(turn: Optional[int] = None) -> Path:
51
- """Get the current log session directory.
52
+ """Get the current log session directory, including attempt subdirectory if set.
52
53
 
53
54
  Args:
54
55
  turn: Optional turn number for multi-turn conversations
55
56
 
56
57
  Returns:
57
- Path to the log directory
58
+ Path to the log directory (includes attempt subdirectory if _CURRENT_ATTEMPT is set)
58
59
  """
59
- global _LOG_SESSION_DIR, _LOG_BASE_SESSION_DIR, _CURRENT_TURN
60
+ global _LOG_SESSION_DIR, _LOG_BASE_SESSION_DIR, _CURRENT_TURN, _CURRENT_ATTEMPT
60
61
 
61
62
  # Initialize base session dir once per session
62
63
  if _LOG_BASE_SESSION_DIR is None:
@@ -88,19 +89,62 @@ def get_log_session_dir(turn: Optional[int] = None) -> Path:
88
89
  _LOG_SESSION_DIR = None # Force recreation
89
90
 
90
91
  if _LOG_SESSION_DIR is None:
91
- # Create directory structure based on turn
92
+ # Build directory structure based on turn and attempt
92
93
  if _CURRENT_TURN and _CURRENT_TURN > 0:
93
94
  # Multi-turn conversation: organize by turn within session
94
- _LOG_SESSION_DIR = _LOG_BASE_SESSION_DIR / f"turn_{_CURRENT_TURN}"
95
+ base_dir = _LOG_BASE_SESSION_DIR / f"turn_{_CURRENT_TURN}"
95
96
  else:
96
97
  # First execution or single execution: use base session dir
97
- _LOG_SESSION_DIR = _LOG_BASE_SESSION_DIR
98
+ base_dir = _LOG_BASE_SESSION_DIR
99
+
100
+ # Add attempt subdirectory if attempt is set
101
+ if _CURRENT_ATTEMPT and _CURRENT_ATTEMPT > 0:
102
+ _LOG_SESSION_DIR = base_dir / f"attempt_{_CURRENT_ATTEMPT}"
103
+ else:
104
+ _LOG_SESSION_DIR = base_dir
98
105
 
99
106
  _LOG_SESSION_DIR.mkdir(parents=True, exist_ok=True)
100
107
 
101
108
  return _LOG_SESSION_DIR
102
109
 
103
110
 
111
+ def set_log_attempt(attempt: int) -> None:
112
+ """Set the current attempt number for restart tracking.
113
+
114
+ This forces the log directory to be recreated with the new attempt subdirectory.
115
+
116
+ Args:
117
+ attempt: Attempt number (1-indexed)
118
+ """
119
+ global _LOG_SESSION_DIR, _CURRENT_ATTEMPT
120
+ _CURRENT_ATTEMPT = attempt
121
+ _LOG_SESSION_DIR = None # Force recreation with new attempt subdirectory
122
+
123
+
124
+ def get_log_session_dir_base() -> Path:
125
+ """Get the base log session directory without attempt subdirectory.
126
+
127
+ This is useful for copying final results to the root level after all attempts complete.
128
+
129
+ Returns:
130
+ Path to the base log directory (turn level or session root, without attempt)
131
+ """
132
+ global _LOG_BASE_SESSION_DIR, _CURRENT_TURN
133
+
134
+ # Ensure base session dir is initialized
135
+ if _LOG_BASE_SESSION_DIR is None:
136
+ # Initialize by calling get_log_session_dir
137
+ get_log_session_dir()
138
+
139
+ # Build base directory based on turn (without attempt)
140
+ if _CURRENT_TURN and _CURRENT_TURN > 0:
141
+ # Multi-turn conversation: return turn directory
142
+ return _LOG_BASE_SESSION_DIR / f"turn_{_CURRENT_TURN}"
143
+ else:
144
+ # Single turn: return base session dir
145
+ return _LOG_BASE_SESSION_DIR
146
+
147
+
104
148
  def save_execution_metadata(
105
149
  query: str,
106
150
  config_path: Optional[str] = None,