Coverage for src / osiris_cli / minimal_cli.py: 0%

831 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-27 17:41 +0200

1#!/usr/bin/env python3 

2""" 

3Osiris CLI v4.0 - Minimal Terminal Interface 

4 

5Design Philosophy: 

6- Minimal & Clean: No boxes, no borders, pure terminal 

7- Memory-First: Agents persist and learn over time 

8- Context-Aware: OSIRIS.md files for project context 

9- Tool Safety: Permission system before dangerous operations 

10- Extensible: MCP, hooks, subagents support 

11- Fast: Streaming responses, instant startup 

12- Scriptable: Non-interactive mode for automation 

13""" 

14 

15import sys 

16import os 

17import asyncio 

18from pathlib import Path 

19from typing import Optional, List, Dict, Any 

20import readline 

21from datetime import datetime 

22import json 

23import traceback 

24from openai import APIError, RateLimitError, APITimeoutError, BadRequestError, NotFoundError 

25 

26from rich.console import Console 

27from rich.markdown import Markdown 

28from rich.syntax import Syntax 

29from rich.live import Live 

30from rich.spinner import Spinner 

31from rich.panel import Panel 

32from rich.prompt import Confirm 

33from rich.table import Table 

34 

35# Interactive menu support 

36from prompt_toolkit import PromptSession 

37from prompt_toolkit.completion import Completer, Completion 

38from prompt_toolkit.key_binding import KeyBindings 

39from prompt_toolkit.formatted_text import HTML 

40from prompt_toolkit.styles import Style 

41 

42from .config import settings 

43from .session import session 

44from .context import context 

45from .tools import tools 

46from .client import async_client 

47from .scenarios import detect_scenario, ScenarioDefinition, resolve_context_files 

48from .event_history import event_history 

49from .session_summary import write_session_summary 

50 

51console = Console() 

52 

53class ResponseSanitizer: 

54 """Robust stateful filter to remove ALL XML-like tags from user output.""" 

55 def __init__(self): 

56 self.in_tag = False 

57 self.tag_buffer = "" 

58 self.in_hide_block = False # For blocks where we hide BOTH tag and content (like tool_calls) 

59 self.hide_tags = ["tool_call", "function", "parameter", "thought", "thinking", "call", "scratchpad", "reasoning", "logs"] 

60 

61 def sanitize(self, text: str) -> str: 

62 if not text: return "" 

63 

64 result = "" 

65 for char in text: 

66 if char == '<': 

67 self.in_tag = True 

68 self.tag_buffer = "<" 

69 continue 

70 

71 if self.in_tag: 

72 self.tag_buffer += char 

73 if char == '>': 

74 # Check if this tag starts a hidden block 

75 tag_content = self.tag_buffer[1:-1].lower() 

76 

77 # Handle closing tags 

78 if tag_content.startswith('/'): 

79 base_tag = tag_content[1:].split()[0] 

80 if base_tag in self.hide_tags: 

81 self.in_hide_block = False 

82 else: 

83 # Handle opening tags 

84 base_tag = tag_content.split('=')[0].split()[0] 

85 if base_tag in self.hide_tags: 

86 self.in_hide_block = True 

87 

88 # Reset tag state 

89 self.in_tag = False 

90 self.tag_buffer = "" 

91 continue 

92 

93 if not self.in_hide_block: 

94 result += char 

95 

96 return result 

97 

98 

99 

100# ASCII Logo 

101OSIRIS_LOGO = """ 

102 ░░░ ░░░░░░░░░ 

103 ░░░ ░░░ ░░░ 

104 ░░░ ░░░ 

105 ███ ░░░ █████████░░░ 

106 ███ ░░░ ███░░ ███░░ 

107 ███ ███░░ ░░░ 

108 ░░░ ███ ███░░░░████░ 

109 ███ ███ ███ 

110 ███ ███ ███ 

111 ███ █████████ 

112""" 

113 

114 

115# Command definitions for interactive palette 

116SLASH_COMMANDS = [ 

117 {"cmd": "/help", "desc": "Show command reference", "category": "Core"}, 

118 {"cmd": "/exit", "desc": "Exit Osiris", "category": "Core"}, 

119 {"cmd": "/version", "desc": "Show Osiris version", "category": "Core"}, 

120 

121 {"cmd": "/model", "desc": "Change AI model", "category": "Model & Provider"}, 

122 {"cmd": "/provider", "desc": "Switch provider", "category": "Model & Provider"}, 

123 {"cmd": "/yolo", "desc": "Toggle autonomous mode", "category": "Model & Provider"}, 

124 

125 {"cmd": "/remember", "desc": "Add to agent memory (key:value)", "category": "Memory"}, 

126 {"cmd": "/forget", "desc": "Remove from memory", "category": "Memory"}, 

127 {"cmd": "/memory", "desc": "Show agent memory", "category": "Memory"}, 

128 {"cmd": "/init", "desc": "Initialize agent memory", "category": "Memory"}, 

129 {"cmd": "/skill", "desc": "Learn a new skill", "category": "Memory"}, 

130 

131 {"cmd": "/session", "desc": "Manage sessions", "category": "Session"}, 

132 {"cmd": "/clear", "desc": "Clear screen", "category": "Session"}, 

133 {"cmd": "/reset", "desc": "Reset session (keep memory)", "category": "Session"}, 

134 

135 {"cmd": "/context", "desc": "Show context files", "category": "Context & Tools"}, 

136 {"cmd": "/tools", "desc": "List available tools", "category": "Context & Tools"}, 

137 

138 {"cmd": "/cost", "desc": "Show cost for current session", "category": "Analytics"}, 

139 {"cmd": "/usage", "desc": "Show usage statistics", "category": "Analytics"}, 

140 {"cmd": "/budget", "desc": "Set and check budget", "category": "Analytics"}, 

141 

142 {"cmd": "/shadow", "desc": "Toggle shadow mode", "category": "Advanced"}, 

143 {"cmd": "/vim", "desc": "Toggle Vim keybindings", "category": "Advanced"}, 

144 {"cmd": "/plan", "desc": "Plan before execution", "category": "Advanced"}, 

145] 

146 

147 

148class SlashCommandCompleter(Completer): 

149 """ 

150 Interactive command completer with arrow key navigation and mouse support. 

151 Shows a popup menu when user types '/' with command descriptions. 

152 """ 

153 

154 def get_completions(self, document, complete_event): 

155 """Generate completions for slash commands""" 

156 text = document.text_before_cursor 

157 

158 # Only show completions if we're typing a slash command 

159 if text.startswith('/'): 

160 search_text = text[1:].lower() # Remove the '/' for searching 

161 

162 for cmd_info in SLASH_COMMANDS: 

163 cmd = cmd_info["cmd"] 

164 desc = cmd_info["desc"] 

165 category = cmd_info["category"] 

166 

167 # If just "/", show all commands 

168 # Otherwise, match command name (without the /) 

169 if not search_text or search_text in cmd[1:].lower(): 

170 # Create formatted display with category and description 

171 display = HTML(f'<b>{cmd}</b> <style fg="ansibrightblack">│</style> <i>{desc}</i>') 

172 display_meta = HTML(f'<style fg="ansicyan">{category}</style>') 

173 

174 yield Completion( 

175 cmd, 

176 start_position=-len(text), 

177 display=display, 

178 display_meta=display_meta, 

179 ) 

180 

181 

182class AgentMemory: 

183 """ 

184 Memory-first agent system. 

185 Agents persist across sessions and learn over time. 

186 """ 

187 

188 def __init__(self, agent_name: str = "main"): 

189 self.agent_name = agent_name 

190 self.memory_file = Path.home() / ".osiris" / "memory" / f"{agent_name}.json" 

191 self.memory_file.parent.mkdir(parents=True, exist_ok=True) 

192 self.core_memory: Dict[str, Any] = {} 

193 self.skills: List[str] = [] 

194 self.load() 

195 

196 def load(self): 

197 """Load agent memory from disk""" 

198 if self.memory_file.exists(): 

199 import json 

200 try: 

201 with open(self.memory_file, 'r') as f: 

202 data = json.load(f) 

203 self.core_memory = data.get('core_memory', {}) 

204 self.skills = data.get('skills', []) 

205 except Exception as e: 

206 console.print(f"[yellow]Warning: Could not load memory: {e}[/yellow]") 

207 

208 def save(self): 

209 """Save agent memory to disk""" 

210 import json 

211 try: 

212 with open(self.memory_file, 'w') as f: 

213 json.dump({ 

214 'core_memory': self.core_memory, 

215 'skills': self.skills, 

216 'last_updated': datetime.now().isoformat() 

217 }, f, indent=2) 

218 except Exception as e: 

219 console.print(f"[yellow]Warning: Could not save memory: {e}[/yellow]") 

220 

221 def remember(self, key: str, value: str): 

222 """Add to long-term memory""" 

223 self.core_memory[key] = value 

224 self.save() 

225 console.print(f"[green]✓[/green] Remembered: {key}") 

226 

227 def forget(self, key: str): 

228 """Remove from memory""" 

229 if key in self.core_memory: 

230 del self.core_memory[key] 

231 self.save() 

232 console.print(f"[green]✓[/green] Forgot: {key}") 

233 else: 

234 console.print(f"[yellow]No memory found for: {key}[/yellow]") 

235 

236 def recall(self) -> str: 

237 """Get memory as formatted string for AI context""" 

238 if not self.core_memory and not self.skills: 

239 return "" 

240 

241 memory_str = "\n## Agent Memory\n" 

242 if self.core_memory: 

243 memory_str += "\n### Core Memory:\n" 

244 for key, value in self.core_memory.items(): 

245 memory_str += f"- **{key}**: {value}\n" 

246 

247 if self.skills: 

248 memory_str += "\n### Learned Skills:\n" 

249 for skill in self.skills: 

250 memory_str += f"- {skill}\n" 

251 

252 return memory_str 

253 

254 def learn_skill(self, skill: str): 

255 """Learn a new skill""" 

256 if skill not in self.skills: 

257 self.skills.append(skill) 

258 self.save() 

259 console.print(f"[green]✓[/green] Learned skill: {skill}") 

260 else: 

261 console.print(f"[yellow]Already know: {skill}[/yellow]") 

262 

263 

264class MinimalCLI: 

265 """ 

266 Minimal CLI interface with advanced features: 

267 - Simple prompt and streaming responses 

268 - Permission system and hooks for safety 

269 - Memory-first agents with skill learning 

270 - Subagents, shadow mode, and tool control 

271 """ 

272 

273 def __init__(self, agent_name: str = "main", non_interactive: bool = False): 

274 self.agent_name = agent_name 

275 self.non_interactive = non_interactive 

276 self.running = True 

277 self.memory = AgentMemory(agent_name) 

278 self.shadow_mode = False 

279 self.vim_mode = False 

280 self.active_scenario: Optional[ScenarioDefinition] = None 

281 self.pending_summary_prompt: Optional[str] = None 

282 self.last_user_prompt: str = "" 

283 

284 # Initialize conversation with strong System Prompt 

285 self.system_prompt = ( 

286 f"{settings.system_prompt}\n\n" 

287 "## OPERATIONAL DIRECTIVES:\n" 

288 "1. You are a Sovereign AGI Terminal with direct access to the host system via TOOLS.\n" 

289 "2. When a user asks for real-time information (time, weather, search) or system operations (files, shell), " 

290 "you MUST NOT apologize or say you cannot do it. You must use the relevant TOOL immediately.\n" 

291 "3. If a tool fails, analyze the error and try a different approach or tool or run a known command that achieves the objective requested..\n" 

292 "4. Your goal is to be an autonomous operator, not just a chat assistant.\n" 

293 "5. Maintain a clean, professional, and efficient tone." 

294 ) 

295 

296 # Add memory context 

297 memory_context = self.memory.recall() 

298 if memory_context: 

299 self.system_prompt += f"\n\n{memory_context}" 

300 

301 self.conversation = [{"role": "system", "content": self.system_prompt}] 

302 

303 # Load OSIRIS.md context if exists 

304 self.load_context_file() 

305 

306 # Auto-load project context files 

307 self.auto_load_contexts() 

308 

309 def auto_load_contexts(self): 

310 """Auto-load project context files""" 

311 from .context_manager import context_manager 

312 

313 loaded = context_manager.auto_load() 

314 if loaded > 0: 

315 console.print(f"[dim]📚 Loaded {loaded} project context file(s)[/dim]") 

316 

317 def _extract_hallucinated_tools(self, text: str) -> dict: 

318 import re 

319 import json 

320 from datetime import datetime 

321 tool_calls = {} 

322 blocks = re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL) 

323 if not blocks: 

324 last_start = text.rfind('<tool_call>') 

325 if last_start != -1: 

326 blocks = [text[last_start:]] 

327 for i, block in enumerate(blocks): 

328 name_match = re.search(r'function=(["\'])?(.*?)(?:\1|>|\s)', block) 

329 if not name_match: name_match = re.search(r'<function=(.*?)>', block) 

330 if name_match: 

331 name = name_match.group(2) if len(name_match.groups()) > 1 else name_match.group(1) 

332 name = (name or "").strip() 

333 if not name: continue 

334 params = {} 

335 param_matches = re.finditer(r'<parameter=(["\'])?(.*?)(?:\1|>)\s*(.*?)\s*</parameter>', block, re.DOTALL) 

336 for pm in param_matches: params[pm.group(2).strip()] = pm.group(3).strip() 

337 if not params and "command" in block: 

338 cmd_match = re.search(r'<parameter=command>(.*?)</parameter>', block, re.DOTALL) 

339 if cmd_match: params["command"] = cmd_match.group(1).strip() 

340 tool_calls[i + 1000] = { 

341 "id": f"hallucinated-{i}-{datetime.now().strftime("%H%M%S")}", 

342 "name": name, "arguments": json.dumps(params) 

343 } 

344 return tool_calls 

345 

346 def _prepare_prompt(self, user_input: str) -> str: 

347 """Detect scenarios and enrich the prompt when necessary.""" 

348 event_history.log("user_prompt", {"text": user_input}) 

349 self.last_user_prompt = user_input 

350 self.pending_summary_prompt = None 

351 scenario = detect_scenario(user_input) 

352 self.active_scenario = scenario 

353 

354 if scenario: 

355 self._load_scenario_context(scenario) 

356 self.pending_summary_prompt = scenario.summary_prompt 

357 console.print(f"[dim]🧭 Scenario detected: {scenario.title}[/dim]") 

358 return f"{scenario.instructions}\n\nUser request: {user_input}" 

359 

360 return user_input 

361 

362 def _load_scenario_context(self, scenario: ScenarioDefinition): 

363 """Load scenario-specific context files if they exist.""" 

364 from .context_manager import context_manager 

365 

366 loaded_paths = resolve_context_files(scenario) 

367 if not loaded_paths: 

368 console.print("[dim]No scenario context files found for this repository[/dim]") 

369 return 

370 

371 for path in loaded_paths: 

372 context_manager.add_file(path) 

373 

374 def _should_auto_extend_tools(self) -> bool: 

375 """Auto-extend tool rounds when scenarios are active.""" 

376 return bool(self.active_scenario and self.active_scenario.auto_continue) 

377 

378 async def _request_summary(self, prompt_text: str) -> str: 

379 """Ask the model for a closing summary without invoking tools.""" 

380 from .client import get_async_client 

381 

382 summary_messages = self.conversation + [{"role": "user", "content": prompt_text}] 

383 summary_stream = await get_async_client().create_completion( 

384 model=settings.default_model, 

385 messages=summary_messages, 

386 temperature=settings.temperature, 

387 stream=True, 

388 tools=None, 

389 ) 

390 

391 summary = "" 

392 sanitizer = ResponseSanitizer() 

393 async for chunk in summary_stream: 

394 if not chunk.choices: 

395 continue 

396 delta = chunk.choices[0].delta 

397 if delta.content: 

398 cleaned = sanitizer.sanitize(delta.content) 

399 if cleaned: 

400 summary += cleaned 

401 return summary.strip() 

402 

403 async def _maybe_emit_summary(self, latest_response: str): 

404 """Ensure a final response is provided even after extensive tool use.""" 

405 if latest_response and latest_response.strip(): 

406 return 

407 if not self.pending_summary_prompt: 

408 return 

409 

410 summary = await self._request_summary(self.pending_summary_prompt) 

411 if summary: 

412 console.print(f"\n{summary}\n") 

413 self.conversation.append({"role": "assistant", "content": summary}) 

414 session.add_message("assistant", summary) 

415 event_history.log("assistant_summary", {"text": summary[:400]}) 

416 

417 

418 def _save_history(self, history_file: Path): 

419 """Save readline history""" 

420 try: 

421 readline.write_history_file(str(history_file)) 

422 except (OSError, IOError): 

423 pass 

424 

425 def load_context_file(self): 

426 """Load OSIRIS.md context file if it exists""" 

427 osiris_md = Path.cwd() / "OSIRIS.md" 

428 if osiris_md.exists(): 

429 try: 

430 with open(osiris_md, 'r') as f: 

431 content = f.read() 

432 self.conversation.append({ 

433 "role": "system", 

434 "content": f"## Project Context (from OSIRIS.md)\n\n{content}" 

435 }) 

436 console.print("[dim]Loaded OSIRIS.md context[/dim]") 

437 except Exception as e: 

438 console.print(f"[yellow]Warning: Could not load OSIRIS.md: {e}[/yellow]") 

439 

440 def show_welcome(self): 

441 """Display welcome message""" 

442 # OSIRIS ASCII Logo - Techy, clear design 

443 logo = """ 

444 ██████╗ ███████╗██╗██████╗ ██╗███████╗ 

445██╔═══██╗██╔════╝██║██╔══██╗██║██╔════╝ 

446██║ ██║███████╗██║██████╔╝██║███████╗ 

447██║ ██║╚════██║██║██╔══██╗██║╚════██║ 

448╚██████╔╝███████║██║██║ ██║██║███████║ 

449 ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═╝╚═╝╚══════╝ 

450""" 

451 console.print(f"[cyan]{logo}[/cyan]") 

452 console.print() 

453 

454 # Status line 

455 yolo_status = "[red]YOLO MODE[/red]" if settings.yolo_mode else "" 

456 console.print(f"Osiris CLI v4.0 | Agent: {self.agent_name} | Model: {settings.default_model} | {yolo_status}") 

457 console.print() 

458 

459 # Instructions 

460 console.print("[bold]Commands:[/bold]") 

461 console.print(" Type / for command menu") 

462 console.print(" /help for all commands") 

463 console.print() 

464 console.print("[bold]Keyboard Shortcuts:[/bold]") 

465 console.print(" [cyan]Ctrl+R[/cyan] - Retry last prompt") 

466 console.print(" [cyan]Ctrl+H[/cyan] - Show history") 

467 console.print(" [cyan]Ctrl+T[/cyan] - Toggle YOLO mode") 

468 console.print(" [cyan]Ctrl+C[/cyan] - Stop generation / Exit") 

469 console.print() 

470 

471 def show_help(self): 

472 """Display help information""" 

473 console.print("\n[bold cyan]Osiris CLI v4.0 - Command Reference[/bold cyan]\n") 

474 

475 console.print("[bold]Core Commands:[/bold]") 

476 console.print(" /help - Show this help message") 

477 console.print(" /status - Show current configuration") 

478 console.print(" /config - Show detailed configuration") 

479 console.print(" /history - Show conversation history") 

480 console.print(" /clear - Clear screen") 

481 console.print(" /reset - Reset conversation (keep memory)") 

482 console.print(" /exit, /quit - Exit Osiris") 

483 console.print() 

484 

485 console.print("[bold]Configuration:[/bold]") 

486 console.print(" /setup - Run setup wizard") 

487 console.print(" /provider - Select AI provider") 

488 console.print(" /model - Select model") 

489 console.print(" /yolo - Toggle YOLO mode") 

490 console.print() 

491 

492 console.print("[bold]Memory & Sessions:[/bold]") 

493 console.print(" /memory - Manage agent memory") 

494 console.print(" /session - Manage sessions") 

495 console.print(" /remember - Add to memory") 

496 console.print(" /forget - Remove from memory") 

497 console.print(" /recall - View memory") 

498 console.print() 

499 

500 console.print("[bold]Advanced Features:[/bold]") 

501 console.print(" /cost - Show cost tracking & statistics") 

502 console.print(" /plan - Plan mode for complex tasks") 

503 console.print(" /checkpoint - Save/restore conversation states") 

504 console.print(" /database - Database operations menu") 

505 console.print(" /cloud - Cloud operations menu") 

506 console.print() 

507 

508 console.print("[bold]Tools:[/bold]") 

509 console.print(" /tools - Show available tools") 

510 console.print() 

511 

512 console.print("[bold]Advanced Features:[/bold]") 

513 console.print(" /context - Manage project context files") 

514 console.print(" /analyze - Analyze code quality") 

515 console.print(" /refactor - Get refactoring suggestions") 

516 console.print(" /performance - Show performance stats") 

517 console.print(" /cache - Manage response cache") 

518 console.print() 

519 

520 console.print("[bold]Keyboard Shortcuts:[/bold]") 

521 console.print(" [cyan]Arrow Up/Down[/cyan] - Navigate prompt history") 

522 console.print(" [cyan]Ctrl+C[/cyan] - Stop generation / Exit") 

523 console.print(" [cyan]Ctrl+D[/cyan] - Exit") 

524 console.print() 

525 

526 console.print("[dim]Tip: Type / to see the interactive command menu[/dim]\n") 

527 

528 async def get_prompt(self) -> str: 

529 """Get user input with LIVE real-time command palette""" 

530 from .live_palette import get_live_input 

531 

532 try: 

533 prompt_symbol = ":" if self.vim_mode else ">" 

534 user_input = await get_live_input(prompt_symbol) 

535 return user_input 

536 except (KeyboardInterrupt, EOFError): 

537 return "/exit" 

538 

539 async def handle_slash_command(self, command: str) -> bool: 

540 """Handle slash commands with live interactive menus, return True if handled""" 

541 parts = command.split(maxsplit=1) 

542 cmd = parts[0] 

543 args = parts[1] if len(parts) > 1 else "" 

544 

545 if cmd == "/help": 

546 self.show_help() 

547 return True 

548 

549 elif cmd == "/history": 

550 # Show conversation history 

551 from .conversation_history import conversation_history 

552 

553 prompts = conversation_history.get_all() 

554 if not prompts: 

555 console.print("[yellow]No conversation history yet[/yellow]") 

556 return True 

557 

558 console.print(f"\n[bold]Conversation History ({len(prompts)} prompts):[/bold]\n") 

559 

560 # Show last 20 prompts 

561 recent_prompts = prompts[-20:] 

562 for i, prompt in enumerate(recent_prompts, 1): 

563 # Truncate long prompts 

564 display_prompt = prompt[:100] + "..." if len(prompt) > 100 else prompt 

565 console.print(f" [dim]{i}.[/dim] {display_prompt}") 

566 

567 if len(prompts) > 20: 

568 console.print(f"\n[dim]Showing last 20 of {len(prompts)} prompts[/dim]") 

569 

570 console.print("\n[dim]Tip: Use arrow up/down to navigate history, Ctrl+R to retry last prompt[/dim]\n") 

571 return True 

572 

573 

574 elif cmd == "/setup": 

575 # Live setup menu 

576 from .setup import show_setup_menu 

577 await show_setup_menu() 

578 return True 

579 

580 elif cmd == "/status": 

581 # Show system status 

582 from .status import show_status 

583 show_status() 

584 return True 

585 

586 elif cmd == "/config": 

587 # Show full configuration 

588 from .status import show_config 

589 show_config() 

590 return True 

591 

592 elif cmd in ["/exit", "/quit"]: 

593 self.running = False 

594 return True 

595 

596 elif cmd == "/clear": 

597 os.system('clear' if os.name != 'nt' else 'cls') 

598 return True 

599 

600 elif cmd == "/reset": 

601 self.conversation = [] 

602 console.print("[green]✓[/green] Session reset (memory preserved)") 

603 return True 

604 

605 elif cmd == "/provider": 

606 # Provider selection with API key management and auto model loading 

607 from .live_menus import select_provider 

608 selected = await select_provider() 

609 if selected: 

610 # Reinitialize async client with new provider 

611 from .client import get_async_client 

612 get_async_client().reinitialize() 

613 

614 # Show current status 

615 console.print() 

616 console.print(f"[bold]Current Configuration:[/bold]") 

617 console.print(f" Provider: [cyan]{settings.provider}[/cyan]") 

618 console.print(f" Model: [cyan]{settings.default_model}[/cyan]") 

619 console.print() 

620 return True 

621 

622 elif cmd == "/model": 

623 # Global model selection (updates provider automatically) 

624 from .live_menus import select_model 

625 selected = await select_model() 

626 if selected: 

627 # Reinitialize async client with new provider/config 

628 from .client import get_async_client 

629 get_async_client().reinitialize() 

630 

631 # Show confirmation 

632 console.print() 

633 console.print(f"[bold]Current Configuration:[/bold]") 

634 console.print(f" Provider: [cyan]{settings.provider}[/cyan]") 

635 console.print(f" Model: [cyan]{settings.default_model}[/cyan]") 

636 if settings.provider_base_url: 

637 console.print(f" Base URL: [dim]{settings.provider_base_url}[/dim]") 

638 console.print() 

639 return True 

640 

641 

642 elif cmd == "/yolo": 

643 settings.yolo_mode = not settings.yolo_mode 

644 settings.save() 

645 status = "enabled" if settings.yolo_mode else "disabled" 

646 console.print(f"[green]✓[/green] YOLO mode {status}") 

647 return True 

648 

649 elif cmd == "/context": 

650 # Context file management 

651 from .context_manager import context_manager 

652 

653 if not args: 

654 # Show loaded contexts 

655 contexts = context_manager.list_contexts() 

656 if not contexts: 

657 console.print("[yellow]No context files loaded[/yellow]") 

658 console.print("\n[dim]Usage:[/dim]") 

659 console.print(" /context add <file> - Add context file") 

660 console.print(" /context list - List loaded contexts") 

661 console.print(" /context clear - Clear all contexts") 

662 else: 

663 console.print(f"\n{context_manager.get_context_summary()}") 

664 elif args.startswith("add "): 

665 file_path = Path(args[4:].strip()) 

666 context_manager.add_file(file_path) 

667 elif args == "list": 

668 console.print(f"\n{context_manager.get_context_summary()}") 

669 elif args == "clear": 

670 context_manager.clear() 

671 elif args.startswith("search "): 

672 query = args[7:].strip() 

673 results = context_manager.search(query) 

674 if results: 

675 console.print(f"\n[bold]Found {len(results)} result(s):[/bold]\n") 

676 for file_name, line_num, line_text in results[:20]: 

677 console.print(f" {file_name}:{line_num} - {line_text}") 

678 else: 

679 console.print("[yellow]No results found[/yellow]") 

680 else: 

681 console.print("[yellow]Unknown context command[/yellow]") 

682 return True 

683 

684 elif cmd == "/analyze": 

685 # Code analysis 

686 from .code_analyzer import code_analyzer 

687 

688 if not args: 

689 console.print("[yellow]Usage: /analyze <file>[/yellow]") 

690 else: 

691 file_path = Path(args.strip()) 

692 code_analyzer.display_analysis(file_path) 

693 return True 

694 

695 elif cmd == "/refactor": 

696 # Refactoring suggestions 

697 from .code_analyzer import code_analyzer 

698 

699 if not args: 

700 console.print("[yellow]Usage: /refactor <file>[/yellow]") 

701 else: 

702 file_path = Path(args.strip()) 

703 suggestions = code_analyzer.suggest_refactoring(file_path) 

704 console.print(f"\n[bold]Refactoring Suggestions for {file_path.name}:[/bold]\n") 

705 for suggestion in suggestions: 

706 console.print(f"{suggestion}") 

707 console.print() 

708 return True 

709 

710 elif cmd == "/performance": 

711 # Show performance statistics 

712 from .performance import performance_optimizer 

713 

714 stats = performance_optimizer.get_stats() 

715 console.print("\n[bold]Performance Statistics:[/bold]\n") 

716 console.print(f" API Calls: {stats['api_calls']}") 

717 console.print(f" Cache Hits: {stats['cache_hits']}") 

718 console.print(f" Cache Misses: {stats['cache_misses']}") 

719 console.print(f" Cache Hit Rate: {stats['cache_hit_rate']}") 

720 console.print(f" Total Tokens: {stats['total_tokens']:,}") 

721 console.print(f" Avg Response Time: {stats['avg_response_time']}") 

722 console.print(f" Cache Size: {stats['cache_size_kb']}") 

723 console.print() 

724 return True 

725 

726 elif cmd == "/cache": 

727 # Cache management 

728 from .performance import performance_optimizer 

729 

730 if args == "clear": 

731 performance_optimizer.clear_cache() 

732 elif args == "stats": 

733 stats = performance_optimizer.get_stats() 

734 console.print(f"\n[bold]Cache Statistics:[/bold]\n") 

735 console.print(f" Entries: {stats['cache_entries']}") 

736 console.print(f" Size: {stats['cache_size_kb']}") 

737 console.print(f" Hit Rate: {stats['cache_hit_rate']}") 

738 console.print() 

739 else: 

740 console.print("[yellow]Usage: /cache [clear|stats][/yellow]") 

741 return True 

742 

743 

744 elif cmd == "/remember": 

745 if args: 

746 # Parse key:value format 

747 if ':' in args: 

748 key, value = args.split(':', 1) 

749 self.memory.remember(key.strip(), value.strip()) 

750 else: 

751 console.print("[yellow]Usage: /remember key:value[/yellow]") 

752 else: 

753 console.print("[yellow]Usage: /remember key:value[/yellow]") 

754 return True 

755 

756 elif cmd == "/forget": 

757 if args: 

758 self.memory.forget(args) 

759 else: 

760 console.print("[yellow]Usage: /forget key[/yellow]") 

761 return True 

762 

763 elif cmd == "/memory": 

764 # Live memory management menu 

765 from .live_menus import show_memory_menu 

766 action = await show_memory_menu(self.agent_name) 

767 if action == "view": 

768 self.show_memory() 

769 elif action == "add": 

770 # Interactive add 

771 key = input("Memory key: ").strip() 

772 if key: 

773 value = input("Memory value: ").strip() 

774 if value: 

775 self.memory.remember(key, value) 

776 else: 

777 console.print("[yellow]Value required[/yellow]") 

778 else: 

779 console.print("[yellow]Key required[/yellow]") 

780 elif action == "remove": 

781 # Interactive remove 

782 if self.memory.core_memory: 

783 console.print("\n[bold]Current memories:[/bold]") 

784 for key in self.memory.core_memory.keys(): 

785 console.print(f"{key}") 

786 key_to_remove = input("\nKey to remove: ").strip() 

787 if key_to_remove: 

788 self.memory.forget(key_to_remove) 

789 else: 

790 console.print("[dim]No memories to remove[/dim]") 

791 elif action == "init": 

792 self.init_memory() 

793 elif action == "skill": 

794 skill_name = input("Skill name: ").strip() 

795 if skill_name: 

796 self.memory.learn_skill(skill_name) 

797 else: 

798 console.print("[yellow]Skill name required[/yellow]") 

799 return True 

800 

801 elif cmd == "/init": 

802 self.init_memory() 

803 return True 

804 

805 elif cmd == "/skill": 

806 if args: 

807 self.memory.learn_skill(args) 

808 else: 

809 console.print("[yellow]Usage: /skill skill_name[/yellow]") 

810 return True 

811 

812 elif cmd == "/shadow": 

813 self.shadow_mode = not self.shadow_mode 

814 status = "enabled" if self.shadow_mode else "disabled" 

815 console.print(f"[green]✓[/green] Shadow mode {status}") 

816 if self.shadow_mode: 

817 console.print("[dim]Changes will be tested before applying[/dim]") 

818 return True 

819 

820 elif cmd == "/vim": 

821 self.vim_mode = not self.vim_mode 

822 status = "enabled" if self.vim_mode else "disabled" 

823 console.print(f"[green]✓[/green] Vim mode {status}") 

824 return True 

825 

826 elif cmd == "/session": 

827 # Live session selection menu 

828 from .live_menus import select_session 

829 selected = await select_session() 

830 if selected and selected != "__delete__": 

831 # Load the session 

832 from .session import session 

833 if session.load_session(selected): 

834 # Update conversation with loaded messages 

835 self.conversation = session.messages.copy() 

836 console.print(f"[green]✓[/green] Loaded session: {selected}") 

837 console.print(f"[dim]Messages: {len(self.conversation)} | Tokens: {session.total_tokens}[/dim]") 

838 else: 

839 console.print(f"[red]Failed to load session: {selected}[/red]") 

840 return True 

841 

842 elif cmd == "/context": 

843 self.show_context() 

844 return True 

845 

846 elif cmd == "/tools": 

847 # Live tools menu 

848 from .live_menus import show_tools_menu 

849 await show_tools_menu() 

850 return True 

851 

852 elif cmd == "/plan": 

853 console.print("[cyan]Plan Mode:[/cyan] I'll outline my strategy before executing.") 

854 return False # Let AI handle the planning 

855 

856 return False 

857 

858 def show_help(self): 

859 """Display help message""" 

860 help_text = """ 

861# Osiris CLI v4.0 - Command Reference 

862 

863## Core Commands 

864- `/help` - Show this help message 

865- `/exit` - Exit Osiris 

866 

867## Model & Provider 

868- `/model [name]` - Change AI model 

869- `/provider [name]` - Switch provider 

870- `/yolo` - Toggle autonomous mode 

871 

872## Memory System (Agent Learning) 

873- `/remember key:value` - Add to agent memory 

874- `/forget key` - Remove from memory 

875- `/memory` - Show agent memory 

876- `/init` - Initialize agent memory 

877- `/skill name` - Learn a new skill 

878 

879## Session Management 

880- `/session` - Manage sessions 

881- `/clear` - Clear screen 

882- `/reset` - Reset session (keep memory) 

883 

884## Context & Tools 

885- `/context` - Show context files 

886- `/tools` - List available tools 

887 

888## Advanced Features 

889- `/shadow` - Toggle shadow mode (test before apply) 

890- `/vim` - Toggle Vim keybindings 

891- `/plan` - Plan before execution 

892 

893## Tips 

894- Create `OSIRIS.md` in your project for context 

895- Use `/remember` to teach the agent about your project 

896- Memory persists across sessions 

897 """ 

898 console.print(Markdown(help_text)) 

899 

900 def show_memory(self): 

901 """Display agent memory""" 

902 if not self.memory.core_memory and not self.memory.skills: 

903 console.print("[dim]No memory stored yet. Use /remember or /init[/dim]") 

904 return 

905 

906 console.print(f"\n[bold cyan]Agent Memory: {self.agent_name}[/bold cyan]\n") 

907 

908 if self.memory.core_memory: 

909 console.print("[bold]Core Memory:[/bold]") 

910 for key, value in self.memory.core_memory.items(): 

911 console.print(f" • [cyan]{key}[/cyan]: {value}") 

912 console.print() 

913 

914 if self.memory.skills: 

915 console.print("[bold]Learned Skills:[/bold]") 

916 for skill in self.memory.skills: 

917 console.print(f"{skill}") 

918 console.print() 

919 

920 def init_memory(self): 

921 """Initialize agent memory interactively""" 

922 console.print("[cyan]Initializing agent memory...[/cyan]\n") 

923 console.print("Tell me about this project (or press Enter to skip):") 

924 

925 project_desc = input("Project description: ").strip() 

926 if project_desc: 

927 self.memory.remember("project_description", project_desc) 

928 

929 tech_stack = input("Tech stack: ").strip() 

930 if tech_stack: 

931 self.memory.remember("tech_stack", tech_stack) 

932 

933 coding_style = input("Coding style preferences: ").strip() 

934 if coding_style: 

935 self.memory.remember("coding_style", coding_style) 

936 

937 console.print("\n[green]✓[/green] Memory initialized!") 

938 

939 def show_sessions(self): 

940 """Show available sessions""" 

941 sessions_dir = Path.home() / ".osiris" / "sessions" 

942 if not sessions_dir.exists(): 

943 console.print("[dim]No saved sessions[/dim]") 

944 return 

945 

946 sessions = list(sessions_dir.glob("*.json")) 

947 if not sessions: 

948 console.print("[dim]No saved sessions[/dim]") 

949 return 

950 

951 console.print("\n[bold cyan]Saved Sessions:[/bold cyan]\n") 

952 for i, session_file in enumerate(sessions, 1): 

953 console.print(f"{i}. {session_file.stem}") 

954 console.print() 

955 

956 def show_context(self): 

957 """Show context files""" 

958 osiris_md = Path.cwd() / "OSIRIS.md" 

959 if osiris_md.exists(): 

960 console.print("[green]✓[/green] OSIRIS.md found") 

961 with open(osiris_md, 'r') as f: 

962 preview = f.read()[:200] 

963 console.print(f"[dim]{preview}...[/dim]") 

964 else: 

965 console.print("[yellow]No OSIRIS.md found in current directory[/yellow]") 

966 console.print("[dim]Create one to provide project context[/dim]") 

967 

968 def show_tools(self): 

969 """Show available tools""" 

970 tool_defs = tools.get_definitions() 

971 console.print(f"\n[bold cyan]Available Tools ({len(tool_defs)}):[/bold cyan]\n") 

972 for name, tool in tool_defs.items(): 

973 console.print(f" • [cyan]{name}[/cyan]: {tool['description']}") 

974 console.print() 

975 

976 async def confirm_tool_execution(self, tool_name: str, args: Dict) -> bool: 

977 """Ask user to confirm tool execution""" 

978 if settings.yolo_mode or tool_name in settings.auto_allow_tools: 

979 return True 

980 

981 console.print(f"\n[yellow]Tool Request:[/yellow] {tool_name}") 

982 console.print(f"[dim]Arguments: {args}[/dim]") 

983 

984 return Confirm.ask("Execute this tool?", default=False) 

985 

986 async def stream_response(self, user_input: str, use_tools: bool = True): 

987 """Stream AI response with tool support""" 

988 from .client import get_async_client 

989 from .tools import get_tools, execute_tool 

990 from .activity_monitor import activity_monitor 

991 from .session import session 

992 import json 

993 

994 # Add user message to conversation 

995 self.conversation.append({"role": "user", "content": user_input}) 

996 session.add_message("user", user_input) 

997 

998 # Prepare messages 

999 messages = self.conversation.copy() 

1000 

1001 # Add memory context if available 

1002 memory_context = self.memory.recall() 

1003 if memory_context and memory_context != "No memory stored yet.": 

1004 messages.insert(0, {"role": "system", "content": memory_context}) 

1005 

1006 # Add system prompt 

1007 if settings.system_prompt: 

1008 messages.insert(0, {"role": "system", "content": settings.system_prompt}) 

1009 

1010 # Get tools if YOLO mode is enabled and use_tools is True 

1011 tools = None 

1012 if settings.yolo_mode and use_tools: 

1013 tools = get_tools() 

1014 if tools: 

1015 console.print("[dim]🔧 YOLO Mode: Tools enabled and ready[/dim]\n") 

1016 

1017 # Start activity monitor 

1018 # Note: ActivityMonitor doesn't have start() method, it tracks tools directly 

1019 

1020 console.print("\n[bold]Response:[/bold]\n") 

1021 

1022 # Stream the response 

1023 full_response = "" 

1024 tool_calls_data = {} 

1025 sanitizer = ResponseSanitizer() 

1026 

1027 

1028 try: 

1029 # Make initial API call with tools 

1030 response = await get_async_client().create_completion( 

1031 model=settings.default_model, 

1032 messages=messages, 

1033 temperature=settings.temperature, 

1034 stream=True, 

1035 tools=tools if tools else None, 

1036 ) 

1037 

1038 full_response = "" 

1039 raw_response = "" 

1040 tool_calls_data = {} 

1041 

1042 

1043 async for chunk in response: 

1044 if not chunk.choices: 

1045 continue 

1046 

1047 delta = chunk.choices[0].delta 

1048 

1049 # Handle content 

1050 if delta.content: 

1051 raw_response += delta.content 

1052 clean_content = sanitizer.sanitize(delta.content) 

1053 if clean_content: 

1054 console.print(clean_content, end="") 

1055 full_response += clean_content 

1056 

1057 

1058 

1059 # Handle tool calls 

1060 if delta.tool_calls: 

1061 for tc in delta.tool_calls: 

1062 idx = tc.index 

1063 if idx not in tool_calls_data: 

1064 tool_calls_data[idx] = { 

1065 'id': tc.id or '', 

1066 'name': '', 

1067 'arguments': '' 

1068 } 

1069 

1070 if tc.id: 

1071 tool_calls_data[idx]['id'] = tc.id 

1072 if tc.function: 

1073 if tc.function.name: 

1074 tool_calls_data[idx]['name'] = tc.function.name 

1075 if tc.function.arguments: 

1076 tool_calls_data[idx]['arguments'] += tc.function.arguments 

1077 

1078 console.print("\n") 

1079 

1080 # Check for hallucinated tool calls if no official ones found 

1081 if not tool_calls_data: 

1082 tool_calls_data = self._extract_hallucinated_tools(raw_response) 

1083 

1084 

1085 # Execute tool calls if any 

1086 if tool_calls_data and settings.yolo_mode: 

1087 from rich.panel import Panel 

1088 from rich.table import Table 

1089 

1090 # Track iterations to optionally prevent infinite loops 

1091 user_defined_limit = getattr(settings, "max_iterations", None) 

1092 if user_defined_limit is not None and user_defined_limit <= 0: 

1093 user_defined_limit = None 

1094 

1095 default_rounds = getattr(settings, "default_tool_rounds", 5) or 0 

1096 if default_rounds <= 0: 

1097 default_rounds = None 

1098 extension_rounds = getattr(settings, "tool_round_extension", 5) or 0 

1099 if extension_rounds <= 0: 

1100 extension_rounds = default_rounds or 5 

1101 

1102 max_iterations = user_defined_limit or default_rounds 

1103 dynamic_limit = user_defined_limit is None and max_iterations is not None 

1104 

1105 current_iteration = 0 

1106 

1107 while True: 

1108 current_iteration += 1 

1109 

1110 # Show Round Header 

1111 round_label = ( 

1112 f"[cyan]Agent Action (Round {current_iteration}/{max_iterations})[/cyan]" 

1113 if max_iterations 

1114 else f"[cyan]Agent Action (Round {current_iteration})[/cyan]" 

1115 ) 

1116 console.print() 

1117 console.print(Panel( 

1118 round_label, 

1119 border_style="cyan", 

1120 padding=(0, 1) 

1121 )) 

1122 console.print() 

1123 

1124 execute_results = [] 

1125 

1126 # 1. Execute all tool calls in the current turn 

1127 for idx, tool_call in tool_calls_data.items(): 

1128 tool_name = tool_call['name'] 

1129 tool_args_str = tool_call['arguments'] 

1130 

1131 activity_monitor.start_tool(tool_name, {}) 

1132 console.print(f" [bold cyan]→[/bold cyan] {tool_name}") 

1133 

1134 try: 

1135 args_dict = json.loads(tool_args_str) if tool_args_str else {} 

1136 if args_dict: 

1137 args_str = ", ".join(f"{k}={v}" for k, v in args_dict.items()) 

1138 console.print(f" [dim]{args_str[:70]}{'...' if len(args_str) > 70 else ''}[/dim]") 

1139 event_history.log("tool_invocation", {"name": tool_name, "arguments": args_dict}) 

1140 

1141 # Execute the tool 

1142 result = execute_tool(tool_name, args_dict) 

1143 result_str = str(result) 

1144 

1145 console.print(f" [green]✓[/green] [dim]{result_str[:150]}{'...' if len(result_str) > 150 else ''}[/dim]") 

1146 activity_monitor.end_tool(result_str[:200], success=True) 

1147 event_history.log("tool_result", {"name": tool_name, "result": result_str[:200]}) 

1148 

1149 execute_results.append({ 

1150 'id': tool_call['id'], 

1151 'name': tool_name, 

1152 'result': result_str 

1153 }) 

1154 except Exception as e: 

1155 console.print(f" [red]✗ Error: {e}[/red]") 

1156 activity_monitor.end_tool(str(e), success=False) 

1157 event_history.log("tool_error", {"name": tool_name, "error": str(e)}) 

1158 execute_results.append({ 

1159 'id': tool_call['id'], 

1160 'name': tool_name, 

1161 'result': f"Error: {str(e)}" 

1162 }) 

1163 

1164 # 2. Add turn to conversation history 

1165 # Add content if any was generated before tools 

1166 self.conversation.append({ 

1167 "role": "assistant", 

1168 "content": raw_response or None, 

1169 "tool_calls": [{ 

1170 "id": tc['id'], 

1171 "type": "function", 

1172 "function": {"name": tc['name'], "arguments": tool_calls_data[idx]['arguments']} 

1173 } for idx, tc in enumerate(execute_results)] 

1174 }) 

1175 

1176 for res in execute_results: 

1177 self.conversation.append({ 

1178 "role": "tool", 

1179 "tool_call_id": res['id'], 

1180 "content": res['result'] 

1181 }) 

1182 

1183 # 3. Get NEXT response from model 

1184 console.print(f"\n[dim]Analyzing results...[/dim]\n") 

1185 

1186 # Reset data for next turn 

1187 raw_response = "" 

1188 tool_calls_data = {} 

1189 sanitizer = ResponseSanitizer() 

1190 

1191 # Call model again 

1192 async_client = get_async_client() 

1193 next_response = await async_client.create_completion( 

1194 model=settings.default_model, 

1195 messages=self.conversation, 

1196 temperature=settings.temperature, 

1197 tools=get_tools(), 

1198 stream=True, 

1199 ) 

1200 

1201 async for chunk in next_response: 

1202 if not chunk.choices: continue 

1203 delta = chunk.choices[0].delta 

1204 

1205 if delta.content: 

1206 raw_response += delta.content 

1207 clean = sanitizer.sanitize(delta.content) 

1208 if clean: 

1209 console.print(clean, end="") 

1210 

1211 if delta.tool_calls: 

1212 for tc in delta.tool_calls: 

1213 tidx = tc.index 

1214 if tidx not in tool_calls_data: 

1215 tool_calls_data[tidx] = {'id': tc.id or '', 'name': '', 'arguments': ''} 

1216 if tc.id: tool_calls_data[tidx]['id'] = tc.id 

1217 if tc.function: 

1218 if tc.function.name: tool_calls_data[tidx]['name'] = tc.function.name 

1219 if tc.function.arguments: tool_calls_data[tidx]['arguments'] += tc.function.arguments 

1220 

1221 # Extract hallucinated tools from net text 

1222 if not tool_calls_data: 

1223 tool_calls_data = self._extract_hallucinated_tools(raw_response) 

1224 

1225 # If NO tools were called in this turn, we are DONE 

1226 if not tool_calls_data: 

1227 self.conversation.append({"role": "assistant", "content": raw_response}) 

1228 session.add_message("assistant", raw_response) 

1229 event_history.log("assistant_response", {"text": (raw_response or "")[:400]}) 

1230 break 

1231 

1232 if max_iterations and current_iteration >= max_iterations: 

1233 if dynamic_limit: 

1234 if self._should_auto_extend_tools(): 

1235 max_iterations += extension_rounds 

1236 console.print(f"[green]Auto-extending action budget to {max_iterations} rounds.[/green]\n") 

1237 continue 

1238 console.print(f"\n[yellow]⚠ Reached default action budget ({max_iterations} rounds).[/yellow]") 

1239 try: 

1240 allow_more = Confirm.ask( 

1241 f"Need more time? Continue with +{extension_rounds} rounds?", 

1242 default=True 

1243 ) 

1244 except Exception: 

1245 allow_more = False 

1246 

1247 if allow_more: 

1248 max_iterations += extension_rounds 

1249 console.print(f"[green]Extending action budget to {max_iterations} rounds.[/green]\n") 

1250 else: 

1251 console.print("[yellow]Stopping tool execution at user's request.[/yellow]\n") 

1252 break 

1253 else: 

1254 console.print(f"\n[yellow]⚠ Max iterations ({max_iterations}) reached. Stopping.[/yellow]\n") 

1255 break 

1256 

1257 else: 

1258 # No tool calls, just add the response 

1259 self.conversation.append({"role": "assistant", "content": raw_response or full_response}) 

1260 session.add_message("assistant", full_response) 

1261 event_history.log("assistant_response", {"text": (raw_response or full_response or "")[:400]}) 

1262 

1263 await self._maybe_emit_summary(raw_response or full_response) 

1264 

1265 except KeyboardInterrupt: 

1266 console.print("\n\n[yellow]⚠ Generation stopped by user[/yellow]") 

1267 except NotFoundError as e: 

1268 err_msg = str(e) 

1269 if ("tool" in err_msg.lower() or "endpoint" in err_msg.lower()) and use_tools: 

1270 console.print(f"\n[bold yellow]⚠ Model '{settings.default_model}' does not support tool use.[/bold yellow]") 

1271 console.print("[dim]Retrying without tools...[/dim]\n") 

1272 await self.stream_response(user_input, use_tools=False) 

1273 else: 

1274 console.print(f"\n\n[red]Error: {e}[/red]") 

1275 except RateLimitError: 

1276 console.print(f"\n\n[bold yellow]⏱ Rate limit exceeded for {settings.provider}.[/bold yellow]") 

1277 console.print("[dim]Try a different model or wait a few seconds.[/dim]") 

1278 except BadRequestError as e: 

1279 err_msg = str(e).lower() 

1280 if ("tool" in err_msg or "function" in err_msg) and use_tools: 

1281 console.print(f"\n[bold yellow]⚠ Model '{settings.default_model}' reported a tool-calling error.[/bold yellow]") 

1282 console.print("[dim]Retrying without tools...[/dim]\n") 

1283 await self.stream_response(user_input, use_tools=False) 

1284 else: 

1285 console.print(f"\n\n[bold red]Invalid Request:[/bold red] {e}") 

1286 if "model" in str(e).lower(): 

1287 console.print("[yellow]Hint: The model might be deprecated. Try selecting a new one with /model[/yellow]") 

1288 except Exception as e: 

1289 console.print(f"\n\n[bold red]Error:[/bold red] {e}") 

1290 # Log full traceback to hidden file for debugging instead of terminal 

1291 try: 

1292 with open("/root/.osiris/error.log", "a") as f: 

1293 f.write(f"\n--- {datetime.now()} ---\n") 

1294 f.write(traceback.format_exc()) 

1295 except: 

1296 pass 

1297 finally: 

1298 # Reset scenario state after each response 

1299 self.active_scenario = None 

1300 self.pending_summary_prompt = None 

1301 

1302 

1303 async def run_interactive(self): 

1304 """Main interactive CLI loop""" 

1305 self.show_welcome() 

1306 

1307 while self.running: 

1308 user_input = await self.get_prompt() 

1309 

1310 if not user_input: 

1311 continue 

1312 

1313 # Handle slash commands 

1314 if user_input.startswith("/"): 

1315 if await self.handle_slash_command(user_input): 

1316 continue 

1317 

1318 prepared_input = self._prepare_prompt(user_input) 

1319 

1320 # Stream AI response 

1321 try: 

1322 await self.stream_response(prepared_input) 

1323 except KeyboardInterrupt: 

1324 console.print("\n[yellow]Interrupted[/yellow]") 

1325 continue 

1326 except Exception as e: 

1327 console.print(f"\n[red]Error: {e}[/red]") 

1328 continue 

1329 

1330 summary_path = write_session_summary(event_history, self.agent_name) 

1331 if summary_path: 

1332 console.print(f"\n[dim]Session summary saved to {summary_path}[/dim]") 

1333 event_history.clear() 

1334 console.print("\n[cyan]Goodbye![/cyan]\n") 

1335 

1336 async def run_non_interactive(self, prompt: str): 

1337 """Run in non-interactive mode for scripting""" 

1338 prepared_input = self._prepare_prompt(prompt) 

1339 await self.stream_response(prepared_input) 

1340 summary_path = write_session_summary(event_history, self.agent_name) 

1341 if summary_path: 

1342 console.print(f"[dim]Session summary saved to {summary_path}[/dim]") 

1343 event_history.clear() 

1344 

1345 

1346async def run_minimal_cli(agent_name: str = "main", prompt: Optional[str] = None): 

1347 """Entry point for minimal CLI""" 

1348 cli = MinimalCLI(agent_name=agent_name, non_interactive=bool(prompt)) 

1349 

1350 if prompt: 

1351 await cli.run_non_interactive(prompt) 

1352 else: 

1353 await cli.run_interactive()