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,706 @@
1
+ """
2
+ Agent Mode Manager for Omnimancer.
3
+
4
+ This module provides the core agent mode functionality including state management,
5
+ operation queuing, and integration with the approval system.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from typing import Any, Callable, Dict, List, Optional
17
+
18
+ from .agent.approval_interface import ApprovalInterface
19
+ from .agent.approval_manager import EnhancedApprovalManager
20
+ from .agent_engine import Operation, OperationResult, OperationType
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AgentMode(Enum):
26
+ """Agent mode states."""
27
+
28
+ OFF = "off"
29
+ ON = "on"
30
+ PAUSED = "paused"
31
+
32
+
33
+ class AgentOperationStatus(Enum):
34
+ """Status of agent operations."""
35
+
36
+ QUEUED = "queued"
37
+ IN_PROGRESS = "in_progress"
38
+ COMPLETED = "completed"
39
+ FAILED = "failed"
40
+ CANCELLED = "cancelled"
41
+ REQUIRES_APPROVAL = "requires_approval"
42
+
43
+
44
+ @dataclass
45
+ class AgentOperation:
46
+ """Represents an operation in the agent queue."""
47
+
48
+ id: str
49
+ operation: Operation
50
+ status: AgentOperationStatus = AgentOperationStatus.QUEUED
51
+ created_at: datetime = field(default_factory=datetime.now)
52
+ started_at: Optional[datetime] = None
53
+ completed_at: Optional[datetime] = None
54
+ result: Optional[OperationResult] = None
55
+ error: Optional[str] = None
56
+ retry_count: int = 0
57
+ max_retries: int = 3
58
+ priority: int = 0 # Higher number = higher priority
59
+
60
+
61
+ @dataclass
62
+ class AgentModeSettings:
63
+ """Settings for agent mode."""
64
+
65
+ auto_approve_low_risk: bool = False
66
+ auto_approve_read_only: bool = True
67
+ max_concurrent_operations: int = 3
68
+ operation_timeout: int = 300 # seconds
69
+ retry_delay: int = 5 # seconds
70
+ enable_batch_approval: bool = True
71
+ persist_state: bool = True
72
+
73
+
74
+ class AgentModeManager:
75
+ """
76
+ Manages agent mode state and operations.
77
+
78
+ Provides functionality for:
79
+ - Agent mode state management (on/off/paused)
80
+ - Operation queuing and execution
81
+ - Integration with approval system
82
+ - Progress tracking and reporting
83
+ - Persistent state management
84
+ """
85
+
86
+ def __init__(self, config_manager, storage_path: str = "~/.omnimancer"):
87
+ """
88
+ Initialize the agent mode manager.
89
+
90
+ Args:
91
+ config_manager: Configuration manager instance
92
+ storage_path: Path for persistent storage
93
+ """
94
+ self.config_manager = config_manager
95
+ self.storage_path = Path(storage_path).expanduser()
96
+ self.state_file = self.storage_path / "agent_state.json"
97
+
98
+ # Initialize state
99
+ self.mode = AgentMode.ON
100
+ self.settings = AgentModeSettings()
101
+ self.operation_queue: List[AgentOperation] = []
102
+ self.active_operations: Dict[str, AgentOperation] = {}
103
+ self.completed_operations: List[AgentOperation] = []
104
+
105
+ # Initialize approval system
106
+ self.approval_manager = EnhancedApprovalManager()
107
+ self.approval_interface = ApprovalInterface(self.approval_manager)
108
+
109
+ # Event handlers
110
+ self.operation_callbacks: List[Callable[[AgentOperation], None]] = []
111
+ self.mode_change_callbacks: List[Callable[[AgentMode, AgentMode], None]] = []
112
+
113
+ # Execution control
114
+ self._execution_task: Optional[asyncio.Task] = None
115
+ self._shutdown_event = asyncio.Event()
116
+
117
+ # Load persistent state
118
+ self._load_state()
119
+
120
+ # Start execution loop if agent mode is enabled
121
+ if self.mode == AgentMode.ON:
122
+ # The execution loop will be started when the CLI initializes
123
+ logger.info("Agent mode will be started automatically")
124
+
125
+ async def enable_agent_mode(self, auto_approve: bool = False) -> bool:
126
+ """
127
+ Enable agent mode.
128
+
129
+ Args:
130
+ auto_approve: Whether to enable auto-approval for low-risk operations
131
+
132
+ Returns:
133
+ True if enabled successfully, False otherwise
134
+ """
135
+ try:
136
+ old_mode = self.mode
137
+ self.mode = AgentMode.ON
138
+
139
+ if auto_approve:
140
+ self.settings.auto_approve_low_risk = True
141
+ self.settings.auto_approve_read_only = True
142
+
143
+ # Start execution loop
144
+ if not self._execution_task or self._execution_task.done():
145
+ self._shutdown_event.clear()
146
+ self._execution_task = asyncio.create_task(self._execution_loop())
147
+
148
+ # Save state
149
+ self._save_state()
150
+
151
+ # Notify callbacks
152
+ self._notify_mode_change(old_mode, self.mode)
153
+
154
+ logger.info("Agent mode enabled")
155
+ return True
156
+
157
+ except Exception as e:
158
+ logger.error(f"Failed to enable agent mode: {e}")
159
+ self.mode = AgentMode.OFF
160
+ return False
161
+
162
+ async def disable_agent_mode(self, wait_for_completion: bool = True) -> bool:
163
+ """
164
+ Disable agent mode.
165
+
166
+ Args:
167
+ wait_for_completion: Whether to wait for active operations to complete
168
+
169
+ Returns:
170
+ True if disabled successfully, False otherwise
171
+ """
172
+ try:
173
+ old_mode = self.mode
174
+ self.mode = AgentMode.OFF
175
+
176
+ # Signal shutdown
177
+ self._shutdown_event.set()
178
+
179
+ if wait_for_completion and self.active_operations:
180
+ logger.info(
181
+ f"Waiting for {len(self.active_operations)} active operations to complete..."
182
+ )
183
+ # Wait with timeout
184
+ try:
185
+ await asyncio.wait_for(self._wait_for_operations(), timeout=30)
186
+ except asyncio.TimeoutError:
187
+ logger.warning(
188
+ "Timeout waiting for operations to complete, forcing shutdown"
189
+ )
190
+ self._cancel_active_operations()
191
+ else:
192
+ self._cancel_active_operations()
193
+
194
+ # Stop execution task
195
+ if self._execution_task and not self._execution_task.done():
196
+ self._execution_task.cancel()
197
+ try:
198
+ await self._execution_task
199
+ except asyncio.CancelledError:
200
+ pass
201
+
202
+ # Save state
203
+ self._save_state()
204
+
205
+ # Notify callbacks
206
+ self._notify_mode_change(old_mode, self.mode)
207
+
208
+ logger.info("Agent mode disabled")
209
+ return True
210
+
211
+ except Exception as e:
212
+ logger.error(f"Failed to disable agent mode: {e}")
213
+ return False
214
+
215
+ async def _start_execution_loop_when_ready(self):
216
+ """Start execution loop when event loop is ready."""
217
+ try:
218
+ # Wait a bit for initialization to complete
219
+ await asyncio.sleep(0.1)
220
+
221
+ if self.mode == AgentMode.ON and (
222
+ not self._execution_task or self._execution_task.done()
223
+ ):
224
+ self._shutdown_event.clear()
225
+ self._execution_task = asyncio.create_task(self._execution_loop())
226
+ logger.info("Agent execution loop started automatically")
227
+ except Exception as e:
228
+ logger.error(f"Failed to start agent execution loop: {e}")
229
+
230
+ def pause_agent_mode(self) -> bool:
231
+ """
232
+ Pause agent mode (stop processing new operations).
233
+
234
+ Returns:
235
+ True if paused successfully, False otherwise
236
+ """
237
+ if self.mode != AgentMode.ON:
238
+ return False
239
+
240
+ old_mode = self.mode
241
+ self.mode = AgentMode.PAUSED
242
+ self._save_state()
243
+ self._notify_mode_change(old_mode, self.mode)
244
+
245
+ logger.info("Agent mode paused")
246
+ return True
247
+
248
+ def resume_agent_mode(self) -> bool:
249
+ """
250
+ Resume agent mode from paused state.
251
+
252
+ Returns:
253
+ True if resumed successfully, False otherwise
254
+ """
255
+ if self.mode != AgentMode.PAUSED:
256
+ return False
257
+
258
+ old_mode = self.mode
259
+ self.mode = AgentMode.ON
260
+ self._save_state()
261
+ self._notify_mode_change(old_mode, self.mode)
262
+
263
+ logger.info("Agent mode resumed")
264
+ return True
265
+
266
+ def get_status(self) -> Dict[str, Any]:
267
+ """
268
+ Get current agent mode status.
269
+
270
+ Returns:
271
+ Dictionary containing status information
272
+ """
273
+ return {
274
+ "mode": self.mode.value,
275
+ "settings": {
276
+ "auto_approve_low_risk": self.settings.auto_approve_low_risk,
277
+ "auto_approve_read_only": self.settings.auto_approve_read_only,
278
+ "max_concurrent_operations": self.settings.max_concurrent_operations,
279
+ "operation_timeout": self.settings.operation_timeout,
280
+ "enable_batch_approval": self.settings.enable_batch_approval,
281
+ },
282
+ "operations": {
283
+ "queued": len(
284
+ [
285
+ op
286
+ for op in self.operation_queue
287
+ if op.status == AgentOperationStatus.QUEUED
288
+ ]
289
+ ),
290
+ "in_progress": len(self.active_operations),
291
+ "completed": len(
292
+ [
293
+ op
294
+ for op in self.completed_operations
295
+ if op.status == AgentOperationStatus.COMPLETED
296
+ ]
297
+ ),
298
+ "failed": len(
299
+ [
300
+ op
301
+ for op in self.completed_operations
302
+ if op.status == AgentOperationStatus.FAILED
303
+ ]
304
+ ),
305
+ "requires_approval": len(
306
+ [
307
+ op
308
+ for op in self.operation_queue
309
+ if op.status == AgentOperationStatus.REQUIRES_APPROVAL
310
+ ]
311
+ ),
312
+ },
313
+ "execution_task_active": self._execution_task is not None
314
+ and not self._execution_task.done(),
315
+ }
316
+
317
+ def queue_operation(self, operation: Operation, priority: int = 0) -> str:
318
+ """
319
+ Queue an operation for execution.
320
+
321
+ Args:
322
+ operation: Operation to queue
323
+ priority: Priority level (higher = more urgent)
324
+
325
+ Returns:
326
+ Operation ID
327
+ """
328
+ op_id = f"agent_op_{int(time.time() * 1000)}_{len(self.operation_queue)}"
329
+ agent_operation = AgentOperation(
330
+ id=op_id, operation=operation, priority=priority
331
+ )
332
+
333
+ # Insert based on priority
334
+ inserted = False
335
+ for i, existing_op in enumerate(self.operation_queue):
336
+ if priority > existing_op.priority:
337
+ self.operation_queue.insert(i, agent_operation)
338
+ inserted = True
339
+ break
340
+
341
+ if not inserted:
342
+ self.operation_queue.append(agent_operation)
343
+
344
+ logger.info(f"Queued operation {op_id}: {operation.description}")
345
+ self._save_state()
346
+
347
+ return op_id
348
+
349
+ def cancel_operation(self, operation_id: str) -> bool:
350
+ """
351
+ Cancel a queued or active operation.
352
+
353
+ Args:
354
+ operation_id: ID of operation to cancel
355
+
356
+ Returns:
357
+ True if cancelled successfully, False otherwise
358
+ """
359
+ # Check queued operations
360
+ for i, op in enumerate(self.operation_queue):
361
+ if op.id == operation_id:
362
+ op.status = AgentOperationStatus.CANCELLED
363
+ self.operation_queue.pop(i)
364
+ self.completed_operations.append(op)
365
+ logger.info(f"Cancelled queued operation {operation_id}")
366
+ self._save_state()
367
+ return True
368
+
369
+ # Check active operations
370
+ if operation_id in self.active_operations:
371
+ op = self.active_operations[operation_id]
372
+ op.status = AgentOperationStatus.CANCELLED
373
+ # Note: Actual cancellation of running operation depends on implementation
374
+ logger.info(f"Marked active operation {operation_id} for cancellation")
375
+ return True
376
+
377
+ return False
378
+
379
+ def get_operation_history(self, limit: int = 50) -> List[Dict[str, Any]]:
380
+ """
381
+ Get operation history.
382
+
383
+ Args:
384
+ limit: Maximum number of operations to return
385
+
386
+ Returns:
387
+ List of operation dictionaries
388
+ """
389
+ all_operations = (
390
+ self.operation_queue
391
+ + list(self.active_operations.values())
392
+ + self.completed_operations
393
+ )
394
+
395
+ # Sort by creation time, most recent first
396
+ all_operations.sort(key=lambda x: x.created_at, reverse=True)
397
+
398
+ history = []
399
+ for op in all_operations[:limit]:
400
+ duration = None
401
+ if op.started_at and op.completed_at:
402
+ duration = (op.completed_at - op.started_at).total_seconds()
403
+
404
+ history.append(
405
+ {
406
+ "id": op.id,
407
+ "type": (
408
+ op.operation.type.value if op.operation.type else "unknown"
409
+ ),
410
+ "description": op.operation.description,
411
+ "status": op.status.value,
412
+ "created_at": op.created_at.isoformat(),
413
+ "duration": duration,
414
+ "retry_count": op.retry_count,
415
+ "error": op.error,
416
+ }
417
+ )
418
+
419
+ return history
420
+
421
+ def add_operation_callback(self, callback: Callable[[AgentOperation], None]):
422
+ """Add callback for operation state changes."""
423
+ self.operation_callbacks.append(callback)
424
+
425
+ def add_mode_change_callback(
426
+ self, callback: Callable[[AgentMode, AgentMode], None]
427
+ ):
428
+ """Add callback for mode changes."""
429
+ self.mode_change_callbacks.append(callback)
430
+
431
+ def update_settings(self, **kwargs):
432
+ """Update agent mode settings."""
433
+ for key, value in kwargs.items():
434
+ if hasattr(self.settings, key):
435
+ setattr(self.settings, key, value)
436
+ self._save_state()
437
+
438
+ async def _execution_loop(self):
439
+ """Main execution loop for processing operations."""
440
+ logger.info("Agent execution loop started")
441
+
442
+ try:
443
+ while not self._shutdown_event.is_set():
444
+ if self.mode == AgentMode.ON:
445
+ await self._process_operations()
446
+
447
+ # Wait briefly before next iteration
448
+ try:
449
+ await asyncio.wait_for(self._shutdown_event.wait(), timeout=1.0)
450
+ break # Shutdown requested
451
+ except asyncio.TimeoutError:
452
+ continue # Normal timeout, continue loop
453
+
454
+ except asyncio.CancelledError:
455
+ logger.info("Agent execution loop cancelled")
456
+ raise
457
+ except Exception as e:
458
+ logger.error(f"Error in agent execution loop: {e}")
459
+ finally:
460
+ logger.info("Agent execution loop stopped")
461
+
462
+ async def _process_operations(self):
463
+ """Process queued operations."""
464
+ # Remove completed active operations
465
+ completed_ids = []
466
+ for op_id, op in self.active_operations.items():
467
+ if op.status in [
468
+ AgentOperationStatus.COMPLETED,
469
+ AgentOperationStatus.FAILED,
470
+ AgentOperationStatus.CANCELLED,
471
+ ]:
472
+ completed_ids.append(op_id)
473
+ self.completed_operations.append(op)
474
+
475
+ for op_id in completed_ids:
476
+ del self.active_operations[op_id]
477
+
478
+ # Start new operations if capacity allows
479
+ available_slots = self.settings.max_concurrent_operations - len(
480
+ self.active_operations
481
+ )
482
+
483
+ if available_slots > 0:
484
+ # Get operations ready for execution
485
+ ready_operations = [
486
+ op
487
+ for op in self.operation_queue
488
+ if op.status == AgentOperationStatus.QUEUED
489
+ ]
490
+
491
+ for op in ready_operations[:available_slots]:
492
+ try:
493
+ await self._start_operation(op)
494
+ except Exception as e:
495
+ logger.error(f"Failed to start operation {op.id}: {e}")
496
+ op.status = AgentOperationStatus.FAILED
497
+ op.error = str(e)
498
+
499
+ async def _start_operation(self, agent_operation: AgentOperation):
500
+ """Start executing an operation."""
501
+ operation = agent_operation.operation
502
+
503
+ # Check if approval is required
504
+ if operation.requires_approval:
505
+ # Handle approval
506
+ approval_granted = await self._handle_approval(agent_operation)
507
+ if not approval_granted:
508
+ agent_operation.status = AgentOperationStatus.CANCELLED
509
+ agent_operation.error = "Approval denied"
510
+ return
511
+
512
+ # Move to active operations
513
+ agent_operation.status = AgentOperationStatus.IN_PROGRESS
514
+ agent_operation.started_at = datetime.now()
515
+ self.operation_queue.remove(agent_operation)
516
+ self.active_operations[agent_operation.id] = agent_operation
517
+
518
+ # Notify callbacks
519
+ self._notify_operation_change(agent_operation)
520
+
521
+ logger.info(f"Starting operation {agent_operation.id}: {operation.description}")
522
+
523
+ # Execute operation asynchronously
524
+ asyncio.create_task(self._execute_operation(agent_operation))
525
+
526
+ async def _execute_operation(self, agent_operation: AgentOperation):
527
+ """Execute an operation."""
528
+ try:
529
+ # This would be replaced with actual operation execution
530
+ # For now, simulate execution
531
+ await asyncio.sleep(1)
532
+
533
+ # Simulate success
534
+ agent_operation.status = AgentOperationStatus.COMPLETED
535
+ agent_operation.completed_at = datetime.now()
536
+ agent_operation.result = OperationResult(
537
+ success=True,
538
+ data={"message": "Operation completed successfully"},
539
+ metadata={},
540
+ )
541
+
542
+ logger.info(f"Completed operation {agent_operation.id}")
543
+
544
+ except Exception as e:
545
+ agent_operation.status = AgentOperationStatus.FAILED
546
+ agent_operation.completed_at = datetime.now()
547
+ agent_operation.error = str(e)
548
+
549
+ logger.error(f"Operation {agent_operation.id} failed: {e}")
550
+
551
+ # Handle retry logic
552
+ if agent_operation.retry_count < agent_operation.max_retries:
553
+ agent_operation.retry_count += 1
554
+ agent_operation.status = AgentOperationStatus.QUEUED
555
+ agent_operation.started_at = None
556
+ agent_operation.completed_at = None
557
+
558
+ # Re-queue for retry
559
+ self.operation_queue.append(agent_operation)
560
+ del self.active_operations[agent_operation.id]
561
+
562
+ logger.info(
563
+ f"Queued operation {agent_operation.id} for retry ({agent_operation.retry_count}/{agent_operation.max_retries})"
564
+ )
565
+
566
+ finally:
567
+ # Notify callbacks
568
+ self._notify_operation_change(agent_operation)
569
+ self._save_state()
570
+
571
+ async def _handle_approval(self, agent_operation: AgentOperation) -> bool:
572
+ """Handle approval for an operation."""
573
+ operation = agent_operation.operation
574
+
575
+ # Check auto-approval rules
576
+ if self._check_auto_approval(operation):
577
+ logger.info(f"Auto-approved operation {agent_operation.id}")
578
+ return True
579
+
580
+ # Mark as requiring approval
581
+ agent_operation.status = AgentOperationStatus.REQUIRES_APPROVAL
582
+
583
+ # Use approval interface
584
+ approval_data = {
585
+ "operation": operation,
586
+ "preview": None, # Would generate preview based on operation
587
+ "approval_request": None, # Would create formal approval request
588
+ "risk_level": operation.data.get("risk_level", "medium"),
589
+ }
590
+
591
+ try:
592
+ approved = await self.approval_interface.handle_single_approval(
593
+ approval_data
594
+ )
595
+ return approved
596
+ except Exception as e:
597
+ logger.error(
598
+ f"Approval handling failed for operation {agent_operation.id}: {e}"
599
+ )
600
+ return False
601
+
602
+ def _check_auto_approval(self, operation: Operation) -> bool:
603
+ """Check if operation qualifies for auto-approval."""
604
+ if not operation.requires_approval:
605
+ return True
606
+
607
+ # Auto-approve read-only operations
608
+ if (
609
+ self.settings.auto_approve_read_only
610
+ and operation.type == OperationType.FILE_READ
611
+ ):
612
+ return True
613
+
614
+ # Auto-approve low-risk operations
615
+ if self.settings.auto_approve_low_risk:
616
+ risk_level = operation.data.get("risk_level", "medium")
617
+ if risk_level.lower() in ["low", "minimal"]:
618
+ return True
619
+
620
+ return False
621
+
622
+ async def _wait_for_operations(self):
623
+ """Wait for all active operations to complete."""
624
+ while self.active_operations:
625
+ await asyncio.sleep(0.5)
626
+
627
+ def _cancel_active_operations(self):
628
+ """Cancel all active operations."""
629
+ for op in self.active_operations.values():
630
+ op.status = AgentOperationStatus.CANCELLED
631
+ op.completed_at = datetime.now()
632
+ self.completed_operations.append(op)
633
+
634
+ self.active_operations.clear()
635
+
636
+ def _notify_operation_change(self, operation: AgentOperation):
637
+ """Notify callbacks of operation state change."""
638
+ for callback in self.operation_callbacks:
639
+ try:
640
+ callback(operation)
641
+ except Exception as e:
642
+ logger.error(f"Error in operation callback: {e}")
643
+
644
+ def _notify_mode_change(self, old_mode: AgentMode, new_mode: AgentMode):
645
+ """Notify callbacks of mode change."""
646
+ for callback in self.mode_change_callbacks:
647
+ try:
648
+ callback(old_mode, new_mode)
649
+ except Exception as e:
650
+ logger.error(f"Error in mode change callback: {e}")
651
+
652
+ def _save_state(self):
653
+ """Save persistent state to disk."""
654
+ if not self.settings.persist_state:
655
+ return
656
+
657
+ try:
658
+ self.storage_path.mkdir(parents=True, exist_ok=True)
659
+
660
+ state_data = {
661
+ "mode": self.mode.value,
662
+ "settings": {
663
+ "auto_approve_low_risk": self.settings.auto_approve_low_risk,
664
+ "auto_approve_read_only": self.settings.auto_approve_read_only,
665
+ "max_concurrent_operations": self.settings.max_concurrent_operations,
666
+ "operation_timeout": self.settings.operation_timeout,
667
+ "retry_delay": self.settings.retry_delay,
668
+ "enable_batch_approval": self.settings.enable_batch_approval,
669
+ "persist_state": self.settings.persist_state,
670
+ },
671
+ "operation_count": len(self.operation_queue)
672
+ + len(self.active_operations)
673
+ + len(self.completed_operations),
674
+ "last_updated": datetime.now().isoformat(),
675
+ }
676
+
677
+ with open(self.state_file, "w") as f:
678
+ json.dump(state_data, f, indent=2)
679
+
680
+ except Exception as e:
681
+ logger.error(f"Failed to save agent state: {e}")
682
+
683
+ def _load_state(self):
684
+ """Load persistent state from disk."""
685
+ try:
686
+ if not self.state_file.exists():
687
+ return
688
+
689
+ with open(self.state_file, "r") as f:
690
+ state_data = json.load(f)
691
+
692
+ # Restore mode - always enable agent mode by default
693
+ self.mode = AgentMode.ON
694
+ logger.info("Agent mode enabled by default")
695
+
696
+ # Restore settings
697
+ if "settings" in state_data:
698
+ settings_data = state_data["settings"]
699
+ for key, value in settings_data.items():
700
+ if hasattr(self.settings, key):
701
+ setattr(self.settings, key, value)
702
+
703
+ logger.info("Loaded agent state from disk")
704
+
705
+ except Exception as e:
706
+ logger.error(f"Failed to load agent state: {e}")