omnimancer-cli 0.1.0__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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,668 @@
1
+ """
2
+ Agent Switcher for managing persona transitions and state preservation.
3
+
4
+ This module provides the switching mechanism that preserves session state
5
+ and handles transitions between personas.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ import logging
11
+ import pickle
12
+ from dataclasses import asdict, dataclass, field
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from threading import Lock
17
+ from typing import Any, Callable, Dict, List, Optional, Tuple
18
+
19
+
20
+ from .config import AgentConfig
21
+ from .persona import AgentPersona, PersonaManager, PersonaStatus
22
+ from .status_core import AgentStatus
23
+ from .status_manager import UnifiedStatusManager as AgentStatusManager
24
+ from .status_manager import (
25
+ get_status_manager,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class SwitchState(Enum):
32
+ """States for the switching process."""
33
+
34
+ IDLE = "idle"
35
+ PREPARING = "preparing"
36
+ SWITCHING = "switching"
37
+ VALIDATING = "validating"
38
+ COMPLETE = "complete"
39
+ ERROR = "error"
40
+ ROLLBACK = "rollback"
41
+
42
+
43
+ class SwitchValidationError(Exception):
44
+ """Exception raised when switch validation fails."""
45
+
46
+ pass
47
+
48
+
49
+ class SwitchOperationError(Exception):
50
+ """Exception raised when switch operation fails."""
51
+
52
+ pass
53
+
54
+
55
+ @dataclass
56
+ class SessionState:
57
+ """
58
+ Session state that needs to be preserved during persona switches.
59
+ """
60
+
61
+ # Core conversation state
62
+ conversation_history: List[Dict[str, Any]] = field(default_factory=list)
63
+ current_context: Dict[str, Any] = field(default_factory=dict)
64
+
65
+ # User preferences and settings
66
+ user_preferences: Dict[str, Any] = field(default_factory=dict)
67
+ active_tools: List[str] = field(default_factory=list)
68
+ disabled_tools: List[str] = field(default_factory=list)
69
+
70
+ # Session metadata
71
+ session_id: str = ""
72
+ created_at: datetime = field(default_factory=datetime.now)
73
+ last_modified: datetime = field(default_factory=datetime.now)
74
+
75
+ # Persona-specific data
76
+ persona_data: Dict[str, Any] = field(default_factory=dict)
77
+
78
+ # Operation state
79
+ pending_operations: List[Dict[str, Any]] = field(default_factory=list)
80
+ active_operations: List[Dict[str, Any]] = field(default_factory=list)
81
+
82
+ # Security and permissions
83
+ permission_overrides: Dict[str, Any] = field(default_factory=dict)
84
+ approval_history: List[Dict[str, Any]] = field(default_factory=list)
85
+
86
+ def update_timestamp(self) -> None:
87
+ """Update the last modified timestamp."""
88
+ self.last_modified = datetime.now()
89
+
90
+ def to_json(self) -> str:
91
+ """Serialize state to JSON."""
92
+ data = asdict(self)
93
+ # Convert datetime objects to ISO format
94
+ data["created_at"] = self.created_at.isoformat()
95
+ data["last_modified"] = self.last_modified.isoformat()
96
+ return json.dumps(data, default=str)
97
+
98
+ @classmethod
99
+ def from_json(cls, json_str: str) -> "SessionState":
100
+ """Deserialize state from JSON."""
101
+ data = json.loads(json_str)
102
+ # Convert ISO format strings back to datetime objects
103
+ if "created_at" in data:
104
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
105
+ if "last_modified" in data:
106
+ data["last_modified"] = datetime.fromisoformat(data["last_modified"])
107
+ return cls(**data)
108
+
109
+ def to_pickle(self) -> bytes:
110
+ """Serialize state to pickle format."""
111
+ return pickle.dumps(self)
112
+
113
+ @classmethod
114
+ def from_pickle(cls, pickle_data: bytes) -> "SessionState":
115
+ """Deserialize state from pickle format."""
116
+ return pickle.loads(pickle_data)
117
+
118
+ def get_hash(self) -> str:
119
+ """Get a hash of the current state for validation."""
120
+ # Create a copy without timestamps for consistent hashing
121
+ data = asdict(self)
122
+ # Remove timestamp fields that shouldn't affect hash
123
+ data.pop("created_at", None)
124
+ data.pop("last_modified", None)
125
+
126
+ # Sort for consistent ordering
127
+ state_str = json.dumps(data, sort_keys=True, default=str)
128
+ return hashlib.sha256(state_str.encode()).hexdigest()
129
+
130
+
131
+ @dataclass
132
+ class SwitchContext:
133
+ """Context for a persona switch operation."""
134
+
135
+ from_persona: Optional[AgentPersona]
136
+ to_persona: AgentPersona
137
+ session_state: SessionState
138
+ validation_checks: List[str] = field(default_factory=list)
139
+ transition_hooks: List[Callable] = field(default_factory=list)
140
+ rollback_state: Optional[SessionState] = None
141
+ error_message: Optional[str] = None
142
+ switch_reason: str = ""
143
+ timestamp: datetime = field(default_factory=datetime.now)
144
+
145
+
146
+ class AgentSwitcher:
147
+ """
148
+ Manages persona switching with state preservation and validation.
149
+
150
+ This class handles the complex process of switching between agent personas
151
+ while preserving session state and ensuring clean transitions.
152
+ """
153
+
154
+ def __init__(
155
+ self,
156
+ persona_manager: PersonaManager,
157
+ agent_config: Optional[AgentConfig] = None,
158
+ status_manager: Optional[AgentStatusManager] = None,
159
+ state_storage_path: Optional[Path] = None,
160
+ ):
161
+ """
162
+ Initialize the AgentSwitcher.
163
+
164
+ Args:
165
+ persona_manager: PersonaManager instance for managing personas
166
+ agent_config: Agent configuration instance
167
+ status_manager: Status manager for tracking agent state
168
+ state_storage_path: Path for persistent state storage
169
+ """
170
+ self.persona_manager = persona_manager
171
+ self.agent_config = agent_config
172
+ self.status_manager = status_manager or get_status_manager()
173
+ self.state_storage_path = (
174
+ state_storage_path or Path.home() / ".omnimancer" / "session_states"
175
+ )
176
+
177
+ # Ensure storage directory exists
178
+ self.state_storage_path.mkdir(parents=True, exist_ok=True)
179
+
180
+ # Current state
181
+ self.current_state = SwitchState.IDLE
182
+ self.current_session_state: Optional[SessionState] = None
183
+ self.switch_history: List[SwitchContext] = []
184
+
185
+ # Thread safety
186
+ self._lock = Lock()
187
+
188
+ # Hooks and validators
189
+ self._pre_switch_hooks: List[Callable] = []
190
+ self._post_switch_hooks: List[Callable] = []
191
+ self._validation_hooks: List[Callable] = []
192
+
193
+ # Register default hooks
194
+ self._register_default_hooks()
195
+
196
+ def _register_default_hooks(self) -> None:
197
+ """Register default transition and validation hooks."""
198
+ # Default pre-switch hooks
199
+ self._pre_switch_hooks.append(self._save_current_state)
200
+
201
+ # Default post-switch hooks
202
+ self._post_switch_hooks.append(self._restore_state)
203
+ self._post_switch_hooks.append(self._update_status_manager)
204
+
205
+ # Default validation hooks
206
+ self._validation_hooks.append(self._validate_persona_available)
207
+ self._validation_hooks.append(self._validate_no_active_operations)
208
+ self._validation_hooks.append(self._validate_state_integrity)
209
+
210
+ def switch_persona(
211
+ self, target_persona_id: str, reason: str = "", force: bool = False
212
+ ) -> Tuple[bool, str]:
213
+ """
214
+ Switch to a target persona with state preservation.
215
+
216
+ Args:
217
+ target_persona_id: ID of the persona to switch to
218
+ reason: Reason for the switch (for logging)
219
+ force: Force switch even if validation fails
220
+
221
+ Returns:
222
+ Tuple of (success, message)
223
+ """
224
+ with self._lock:
225
+ try:
226
+ # Set state to preparing
227
+ self.current_state = SwitchState.PREPARING
228
+
229
+ # Get target persona
230
+ target_persona = self.persona_manager.get_persona(target_persona_id)
231
+ if not target_persona:
232
+ raise SwitchValidationError(
233
+ f"Persona not found: {target_persona_id}"
234
+ )
235
+
236
+ # Get current persona
237
+ current_persona = self.persona_manager.active_persona
238
+
239
+ # Create switch context
240
+ context = SwitchContext(
241
+ from_persona=current_persona,
242
+ to_persona=target_persona,
243
+ session_state=self._capture_current_state(),
244
+ switch_reason=reason,
245
+ )
246
+
247
+ # Validation phase
248
+ if not force:
249
+ self.current_state = SwitchState.VALIDATING
250
+ validation_result = self._validate_switch(context)
251
+ if not validation_result[0]:
252
+ self.current_state = SwitchState.ERROR
253
+ context.error_message = validation_result[1]
254
+ self.switch_history.append(context)
255
+ return validation_result
256
+
257
+ # Pre-switch phase
258
+ self.current_state = SwitchState.SWITCHING
259
+ for hook in self._pre_switch_hooks:
260
+ try:
261
+ hook(context)
262
+ except Exception as e:
263
+ logger.error(f"Pre-switch hook failed: {e}")
264
+ if not force:
265
+ self._rollback(context)
266
+ return (False, f"Pre-switch hook failed: {e}")
267
+
268
+ # Perform the switch
269
+ if current_persona:
270
+ self.persona_manager.deactivate_persona()
271
+
272
+ success = self.persona_manager.activate_persona(target_persona_id)
273
+ if not success:
274
+ self._rollback(context)
275
+ return (
276
+ False,
277
+ f"Failed to activate persona: {target_persona_id}",
278
+ )
279
+
280
+ # Post-switch phase
281
+ for hook in self._post_switch_hooks:
282
+ try:
283
+ hook(context)
284
+ except Exception as e:
285
+ logger.error(f"Post-switch hook failed: {e}")
286
+ # Post-switch failures are logged but don't rollback
287
+
288
+ # Complete
289
+ self.current_state = SwitchState.COMPLETE
290
+ self.switch_history.append(context)
291
+
292
+ logger.info(f"Successfully switched to persona: {target_persona_id}")
293
+ return (True, f"Switched to {target_persona.name}")
294
+
295
+ except Exception as e:
296
+ logger.error(f"Persona switch failed: {e}")
297
+ self.current_state = SwitchState.ERROR
298
+ return (False, str(e))
299
+ finally:
300
+ # Reset to idle after a short delay
301
+ self.current_state = SwitchState.IDLE
302
+
303
+ def _capture_current_state(self) -> SessionState:
304
+ """Capture the current session state."""
305
+ if self.current_session_state:
306
+ return self.current_session_state
307
+
308
+ # Create new session state
309
+ state = SessionState()
310
+
311
+ # Capture from active persona if available
312
+ active_persona = self.persona_manager.active_persona
313
+ if active_persona:
314
+ state.persona_data = {
315
+ "persona_id": active_persona.id,
316
+ "session_data": active_persona._session_data.copy(),
317
+ }
318
+
319
+ # TODO: Capture conversation history from the main engine
320
+ # This would need integration with the main conversation loop
321
+
322
+ return state
323
+
324
+ def _validate_switch(self, context: SwitchContext) -> Tuple[bool, str]:
325
+ """
326
+ Validate that a switch can be performed.
327
+
328
+ Args:
329
+ context: Switch context
330
+
331
+ Returns:
332
+ Tuple of (valid, message)
333
+ """
334
+ for validator in self._validation_hooks:
335
+ try:
336
+ result = validator(context)
337
+ if not result[0]:
338
+ context.validation_checks.append(f"Failed: {validator.__name__}")
339
+ return result
340
+ context.validation_checks.append(f"Passed: {validator.__name__}")
341
+ except Exception as e:
342
+ logger.error(f"Validation hook error: {e}")
343
+ return (False, f"Validation error: {e}")
344
+
345
+ return (True, "All validations passed")
346
+
347
+ def _validate_persona_available(self, context: SwitchContext) -> Tuple[bool, str]:
348
+ """Validate that target persona is available."""
349
+ if context.to_persona.status not in [
350
+ PersonaStatus.AVAILABLE,
351
+ PersonaStatus.ACTIVE,
352
+ ]:
353
+ return (
354
+ False,
355
+ f"Persona {context.to_persona.id} is not available (status: {context.to_persona.status})",
356
+ )
357
+ return (True, "Persona available")
358
+
359
+ def _validate_no_active_operations(
360
+ self, context: SwitchContext
361
+ ) -> Tuple[bool, str]:
362
+ """Validate no active operations are in progress."""
363
+ if context.session_state.active_operations:
364
+ return (
365
+ False,
366
+ f"Cannot switch: {len(context.session_state.active_operations)} operations in progress",
367
+ )
368
+ return (True, "No active operations")
369
+
370
+ def _validate_state_integrity(self, context: SwitchContext) -> Tuple[bool, str]:
371
+ """Validate state integrity."""
372
+ try:
373
+ # Test serialization/deserialization
374
+ json_str = context.session_state.to_json()
375
+ restored = SessionState.from_json(json_str)
376
+
377
+ # Verify hash matches
378
+ if context.session_state.get_hash() != restored.get_hash():
379
+ return (False, "State integrity check failed")
380
+
381
+ return (True, "State integrity verified")
382
+ except Exception as e:
383
+ return (False, f"State validation error: {e}")
384
+
385
+ def _save_current_state(self, context: SwitchContext) -> None:
386
+ """Save current state before switch."""
387
+ # Create rollback state
388
+ context.rollback_state = context.session_state
389
+
390
+ # Persist to disk if we have a from_persona
391
+ if context.from_persona:
392
+ state_file = (
393
+ self.state_storage_path / f"{context.from_persona.id}_state.json"
394
+ )
395
+ try:
396
+ with open(state_file, "w") as f:
397
+ f.write(context.session_state.to_json())
398
+ logger.debug(f"Saved state for persona: {context.from_persona.id}")
399
+ except Exception as e:
400
+ logger.error(f"Failed to save state: {e}")
401
+
402
+ def _restore_state(self, context: SwitchContext) -> None:
403
+ """Restore state after switch."""
404
+ # Check if we have saved state for the target persona
405
+ state_file = self.state_storage_path / f"{context.to_persona.id}_state.json"
406
+
407
+ if state_file.exists():
408
+ try:
409
+ with open(state_file, "r") as f:
410
+ saved_state = SessionState.from_json(f.read())
411
+
412
+ # Merge with current session state
413
+ context.session_state.persona_data = saved_state.persona_data
414
+ context.session_state.user_preferences.update(
415
+ saved_state.user_preferences
416
+ )
417
+
418
+ # Restore to persona
419
+ if "session_data" in saved_state.persona_data:
420
+ for key, value in saved_state.persona_data["session_data"].items():
421
+ context.to_persona.set_session_data(key, value)
422
+
423
+ logger.debug(f"Restored state for persona: {context.to_persona.id}")
424
+ except Exception as e:
425
+ logger.error(f"Failed to restore state: {e}")
426
+
427
+ # Update current session state
428
+ self.current_session_state = context.session_state
429
+
430
+ def _update_status_manager(self, context: SwitchContext) -> None:
431
+ """Update status manager after switch."""
432
+ if self.status_manager:
433
+ try:
434
+ import asyncio
435
+
436
+ # Try to get running loop
437
+ try:
438
+ loop = asyncio.get_running_loop()
439
+
440
+ # Update old persona status
441
+ if context.from_persona:
442
+ loop.create_task(
443
+ self.status_manager.set_agent_status(
444
+ agent_id=f"persona_{context.from_persona.id}",
445
+ status=AgentStatus.DISABLED,
446
+ )
447
+ )
448
+
449
+ # Update new persona status
450
+ loop.create_task(
451
+ self.status_manager.set_agent_status(
452
+ agent_id=f"persona_{context.to_persona.id}",
453
+ status=AgentStatus.ENABLED,
454
+ metadata={
455
+ "switch_reason": context.switch_reason,
456
+ "from_persona": (
457
+ context.from_persona.id
458
+ if context.from_persona
459
+ else None
460
+ ),
461
+ },
462
+ )
463
+ )
464
+ except RuntimeError:
465
+ # No event loop running, skip async status updates
466
+ logger.debug("No event loop available for status update")
467
+
468
+ except Exception as e:
469
+ # Catch all exceptions to prevent status update failures from affecting the switch
470
+ logger.debug(f"Status manager update failed: {e}")
471
+
472
+ def _rollback(self, context: SwitchContext) -> None:
473
+ """Rollback a failed switch."""
474
+ self.current_state = SwitchState.ROLLBACK
475
+
476
+ try:
477
+ # Restore original persona if we had one
478
+ if context.from_persona:
479
+ self.persona_manager.activate_persona(context.from_persona.id)
480
+
481
+ # Restore original state
482
+ if context.rollback_state:
483
+ self.current_session_state = context.rollback_state
484
+
485
+ logger.info("Successfully rolled back persona switch")
486
+ except Exception as e:
487
+ logger.error(f"Rollback failed: {e}")
488
+ finally:
489
+ self.current_state = SwitchState.IDLE
490
+
491
+ def add_pre_switch_hook(self, hook: Callable) -> None:
492
+ """Add a pre-switch hook."""
493
+ self._pre_switch_hooks.append(hook)
494
+
495
+ def add_post_switch_hook(self, hook: Callable) -> None:
496
+ """Add a post-switch hook."""
497
+ self._post_switch_hooks.append(hook)
498
+
499
+ def add_validation_hook(self, hook: Callable) -> None:
500
+ """Add a validation hook."""
501
+ self._validation_hooks.append(hook)
502
+
503
+ def get_switch_history(self) -> List[SwitchContext]:
504
+ """Get the switch history."""
505
+ return self.switch_history.copy()
506
+
507
+ def get_current_state(self) -> SwitchState:
508
+ """Get the current switch state."""
509
+ return self.current_state
510
+
511
+ def can_switch(self, target_persona_id: str) -> Tuple[bool, str]:
512
+ """
513
+ Check if a switch to target persona is possible.
514
+
515
+ Args:
516
+ target_persona_id: ID of target persona
517
+
518
+ Returns:
519
+ Tuple of (can_switch, reason)
520
+ """
521
+ # Check if persona exists
522
+ target_persona = self.persona_manager.get_persona(target_persona_id)
523
+ if not target_persona:
524
+ return (False, f"Persona not found: {target_persona_id}")
525
+
526
+ # Check if already active
527
+ if self.persona_manager.active_persona == target_persona:
528
+ return (False, "Persona already active")
529
+
530
+ # Check if switch is in progress
531
+ if self.current_state != SwitchState.IDLE:
532
+ return (
533
+ False,
534
+ f"Switch in progress (state: {self.current_state.value})",
535
+ )
536
+
537
+ # Create mock context for validation
538
+ mock_context = SwitchContext(
539
+ from_persona=self.persona_manager.active_persona,
540
+ to_persona=target_persona,
541
+ session_state=self._capture_current_state(),
542
+ )
543
+
544
+ # Run validations
545
+ return self._validate_switch(mock_context)
546
+
547
+ def handle_active_operation_switch(
548
+ self, target_persona_id: str, operation_handler: Callable
549
+ ) -> Tuple[bool, str]:
550
+ """
551
+ Handle switching during an active operation.
552
+
553
+ This is an edge case handler that attempts to gracefully handle
554
+ switches when operations are in progress.
555
+
556
+ Args:
557
+ target_persona_id: Target persona ID
558
+ operation_handler: Callable to handle active operations
559
+
560
+ Returns:
561
+ Tuple of (success, message)
562
+ """
563
+ try:
564
+ # Capture current state (within lock)
565
+ with self._lock:
566
+ current_state = self._capture_current_state()
567
+
568
+ if current_state.active_operations:
569
+ logger.warning(
570
+ f"Attempting switch with {len(current_state.active_operations)} active operations"
571
+ )
572
+
573
+ # Call the operation handler
574
+ handled = operation_handler(current_state.active_operations)
575
+ if not handled:
576
+ return (False, "Could not handle active operations")
577
+
578
+ # Clear active operations after handling
579
+ current_state.active_operations.clear()
580
+
581
+ # Proceed with normal switch (outside of lock to avoid deadlock)
582
+ return self.switch_persona(
583
+ target_persona_id,
584
+ reason="Switch with operation handling",
585
+ force=False,
586
+ )
587
+
588
+ except Exception as e:
589
+ logger.error(f"Failed to handle active operation switch: {e}")
590
+ return (False, str(e))
591
+
592
+ def export_session_state(self, format: str = "json") -> Optional[str]:
593
+ """
594
+ Export current session state.
595
+
596
+ Args:
597
+ format: Export format ('json' or 'pickle')
598
+
599
+ Returns:
600
+ Exported state string or None
601
+ """
602
+ if not self.current_session_state:
603
+ self.current_session_state = self._capture_current_state()
604
+
605
+ try:
606
+ if format == "json":
607
+ return self.current_session_state.to_json()
608
+ elif format == "pickle":
609
+ import base64
610
+
611
+ return base64.b64encode(self.current_session_state.to_pickle()).decode()
612
+ else:
613
+ logger.error(f"Unknown export format: {format}")
614
+ return None
615
+ except Exception as e:
616
+ logger.error(f"Failed to export session state: {e}")
617
+ return None
618
+
619
+ def import_session_state(self, state_data: str, format: str = "json") -> bool:
620
+ """
621
+ Import session state.
622
+
623
+ Args:
624
+ state_data: State data string
625
+ format: Import format ('json' or 'pickle')
626
+
627
+ Returns:
628
+ Success status
629
+ """
630
+ try:
631
+ if format == "json":
632
+ self.current_session_state = SessionState.from_json(state_data)
633
+ elif format == "pickle":
634
+ import base64
635
+
636
+ pickle_data = base64.b64decode(state_data.encode())
637
+ self.current_session_state = SessionState.from_pickle(pickle_data)
638
+ else:
639
+ logger.error(f"Unknown import format: {format}")
640
+ return False
641
+
642
+ return True
643
+ except Exception as e:
644
+ logger.error(f"Failed to import session state: {e}")
645
+ return False
646
+
647
+
648
+ # Global instance for easy access
649
+ _global_agent_switcher: Optional[AgentSwitcher] = None
650
+
651
+
652
+ def get_agent_switcher(
653
+ persona_manager: Optional[PersonaManager] = None,
654
+ ) -> AgentSwitcher:
655
+ """Get the global agent switcher instance."""
656
+ global _global_agent_switcher
657
+ if _global_agent_switcher is None:
658
+ from .persona import get_persona_manager
659
+
660
+ pm = persona_manager or get_persona_manager()
661
+ _global_agent_switcher = AgentSwitcher(pm)
662
+ return _global_agent_switcher
663
+
664
+
665
+ def set_agent_switcher(switcher: AgentSwitcher) -> None:
666
+ """Set the global agent switcher instance."""
667
+ global _global_agent_switcher
668
+ _global_agent_switcher = switcher