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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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}")
|