devloop 0.2.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.
- devloop/__init__.py +3 -0
- devloop/agents/__init__.py +33 -0
- devloop/agents/agent_health_monitor.py +105 -0
- devloop/agents/ci_monitor.py +237 -0
- devloop/agents/code_rabbit.py +248 -0
- devloop/agents/doc_lifecycle.py +374 -0
- devloop/agents/echo.py +24 -0
- devloop/agents/file_logger.py +46 -0
- devloop/agents/formatter.py +511 -0
- devloop/agents/git_commit_assistant.py +421 -0
- devloop/agents/linter.py +399 -0
- devloop/agents/performance_profiler.py +284 -0
- devloop/agents/security_scanner.py +322 -0
- devloop/agents/snyk.py +292 -0
- devloop/agents/test_runner.py +484 -0
- devloop/agents/type_checker.py +242 -0
- devloop/cli/__init__.py +1 -0
- devloop/cli/commands/__init__.py +1 -0
- devloop/cli/commands/custom_agents.py +144 -0
- devloop/cli/commands/feedback.py +161 -0
- devloop/cli/commands/summary.py +50 -0
- devloop/cli/main.py +430 -0
- devloop/cli/main_v1.py +144 -0
- devloop/collectors/__init__.py +17 -0
- devloop/collectors/base.py +55 -0
- devloop/collectors/filesystem.py +126 -0
- devloop/collectors/git.py +171 -0
- devloop/collectors/manager.py +159 -0
- devloop/collectors/process.py +221 -0
- devloop/collectors/system.py +195 -0
- devloop/core/__init__.py +21 -0
- devloop/core/agent.py +206 -0
- devloop/core/agent_template.py +498 -0
- devloop/core/amp_integration.py +166 -0
- devloop/core/auto_fix.py +224 -0
- devloop/core/config.py +272 -0
- devloop/core/context.py +0 -0
- devloop/core/context_store.py +530 -0
- devloop/core/contextual_feedback.py +311 -0
- devloop/core/custom_agent.py +439 -0
- devloop/core/debug_trace.py +289 -0
- devloop/core/event.py +105 -0
- devloop/core/event_store.py +316 -0
- devloop/core/feedback.py +311 -0
- devloop/core/learning.py +351 -0
- devloop/core/manager.py +219 -0
- devloop/core/performance.py +433 -0
- devloop/core/proactive_feedback.py +302 -0
- devloop/core/summary_formatter.py +159 -0
- devloop/core/summary_generator.py +275 -0
- devloop-0.2.0.dist-info/METADATA +705 -0
- devloop-0.2.0.dist-info/RECORD +55 -0
- devloop-0.2.0.dist-info/WHEEL +4 -0
- devloop-0.2.0.dist-info/entry_points.txt +3 -0
- devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""System event collector using psutil for resource monitoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import psutil
|
|
11
|
+
|
|
12
|
+
HAS_PSUTIL = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
HAS_PSUTIL = False
|
|
15
|
+
psutil = None
|
|
16
|
+
|
|
17
|
+
from devloop.collectors.base import BaseCollector
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SystemCollector(BaseCollector):
|
|
21
|
+
"""Collects system-related events like resource usage and idle time."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
event_bus: Any, # EventBus type (avoiding circular import)
|
|
26
|
+
config: Optional[Dict[str, Any]] = None,
|
|
27
|
+
):
|
|
28
|
+
super().__init__("system", event_bus, config)
|
|
29
|
+
|
|
30
|
+
if not HAS_PSUTIL:
|
|
31
|
+
self.logger.warning("psutil not available - system monitoring disabled")
|
|
32
|
+
self._psutil_available = False
|
|
33
|
+
else:
|
|
34
|
+
self._psutil_available = True
|
|
35
|
+
|
|
36
|
+
# Configuration
|
|
37
|
+
self.check_interval = self.config.get("check_interval", 30) # seconds
|
|
38
|
+
self.cpu_threshold = self.config.get("cpu_threshold", 80) # percent
|
|
39
|
+
self.memory_threshold = self.config.get("memory_threshold", 85) # percent
|
|
40
|
+
self.idle_threshold = self.config.get("idle_threshold", 300) # seconds
|
|
41
|
+
|
|
42
|
+
# State tracking
|
|
43
|
+
self._last_cpu_percent = 0
|
|
44
|
+
self._last_memory_percent = 0
|
|
45
|
+
self._last_idle_time = time.time()
|
|
46
|
+
self._is_idle = False
|
|
47
|
+
self._monitoring_task: Optional[asyncio.Task] = None
|
|
48
|
+
|
|
49
|
+
def _get_system_stats(self) -> Dict[str, Any]:
|
|
50
|
+
"""Get current system statistics."""
|
|
51
|
+
if not self._psutil_available:
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
return {
|
|
56
|
+
"cpu_percent": psutil.cpu_percent(interval=1),
|
|
57
|
+
"memory_percent": psutil.virtual_memory().percent,
|
|
58
|
+
"memory_used": psutil.virtual_memory().used,
|
|
59
|
+
"memory_total": psutil.virtual_memory().total,
|
|
60
|
+
"disk_usage": psutil.disk_usage("/").percent,
|
|
61
|
+
"load_average": (
|
|
62
|
+
psutil.getloadavg() if hasattr(psutil, "getloadavg") else None
|
|
63
|
+
),
|
|
64
|
+
"timestamp": time.time(),
|
|
65
|
+
}
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.error(f"Error getting system stats: {e}")
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
async def _check_system_resources(self) -> None:
|
|
71
|
+
"""Check system resources and emit events if thresholds are exceeded."""
|
|
72
|
+
if not self._psutil_available:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
stats = self._get_system_stats()
|
|
76
|
+
if not stats:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Check CPU usage
|
|
80
|
+
cpu_percent = stats["cpu_percent"]
|
|
81
|
+
if (
|
|
82
|
+
cpu_percent > self.cpu_threshold
|
|
83
|
+
and self._last_cpu_percent <= self.cpu_threshold
|
|
84
|
+
):
|
|
85
|
+
await self._emit_event(
|
|
86
|
+
"system:high_cpu",
|
|
87
|
+
{
|
|
88
|
+
"cpu_percent": cpu_percent,
|
|
89
|
+
"threshold": self.cpu_threshold,
|
|
90
|
+
"timestamp": stats["timestamp"],
|
|
91
|
+
},
|
|
92
|
+
"high",
|
|
93
|
+
"system",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self._last_cpu_percent = cpu_percent
|
|
97
|
+
|
|
98
|
+
# Check memory usage
|
|
99
|
+
memory_percent = stats["memory_percent"]
|
|
100
|
+
if (
|
|
101
|
+
memory_percent > self.memory_threshold
|
|
102
|
+
and self._last_memory_percent <= self.memory_threshold
|
|
103
|
+
):
|
|
104
|
+
await self._emit_event(
|
|
105
|
+
"system:low_memory",
|
|
106
|
+
{
|
|
107
|
+
"memory_percent": memory_percent,
|
|
108
|
+
"memory_used": stats["memory_used"],
|
|
109
|
+
"memory_total": stats["memory_total"],
|
|
110
|
+
"threshold": self.memory_threshold,
|
|
111
|
+
"timestamp": stats["timestamp"],
|
|
112
|
+
},
|
|
113
|
+
"critical",
|
|
114
|
+
"system",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self._last_memory_percent = memory_percent
|
|
118
|
+
|
|
119
|
+
# Check idle time (simplified - in a real implementation you'd use more sophisticated idle detection)
|
|
120
|
+
current_time = time.time()
|
|
121
|
+
# For demo purposes, consider system idle if no significant CPU usage for a period
|
|
122
|
+
if cpu_percent < 5: # Very low CPU usage
|
|
123
|
+
if not self._is_idle:
|
|
124
|
+
idle_duration = current_time - self._last_idle_time
|
|
125
|
+
if idle_duration > self.idle_threshold:
|
|
126
|
+
await self._emit_event(
|
|
127
|
+
"system:idle",
|
|
128
|
+
{"idle_duration": idle_duration, "timestamp": current_time},
|
|
129
|
+
"normal",
|
|
130
|
+
"system",
|
|
131
|
+
)
|
|
132
|
+
self._is_idle = True
|
|
133
|
+
else:
|
|
134
|
+
if self._is_idle:
|
|
135
|
+
await self._emit_event(
|
|
136
|
+
"system:active", {"timestamp": current_time}, "normal", "system"
|
|
137
|
+
)
|
|
138
|
+
self._is_idle = False
|
|
139
|
+
self._last_idle_time = current_time
|
|
140
|
+
|
|
141
|
+
async def _monitor_system(self) -> None:
|
|
142
|
+
"""Main monitoring loop."""
|
|
143
|
+
self.logger.info(
|
|
144
|
+
f"Starting system monitoring (interval: {self.check_interval}s)"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
while self.is_running:
|
|
148
|
+
try:
|
|
149
|
+
await self._check_system_resources()
|
|
150
|
+
await asyncio.sleep(self.check_interval)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
self.logger.error(f"Error in system monitoring loop: {e}")
|
|
153
|
+
await asyncio.sleep(self.check_interval)
|
|
154
|
+
|
|
155
|
+
async def start(self) -> None:
|
|
156
|
+
"""Start the system collector."""
|
|
157
|
+
if self.is_running:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if not self._psutil_available:
|
|
161
|
+
self.logger.error("Cannot start system collector - psutil not available")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
self._set_running(True)
|
|
165
|
+
|
|
166
|
+
# Start monitoring task
|
|
167
|
+
self._monitoring_task = asyncio.create_task(self._monitor_system())
|
|
168
|
+
|
|
169
|
+
# Emit initial system status
|
|
170
|
+
stats = self._get_system_stats()
|
|
171
|
+
if stats:
|
|
172
|
+
await self._emit_event("system:status", stats, "normal", "system")
|
|
173
|
+
|
|
174
|
+
self.logger.info("System collector started")
|
|
175
|
+
|
|
176
|
+
async def stop(self) -> None:
|
|
177
|
+
"""Stop the system collector."""
|
|
178
|
+
if not self.is_running:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
self._set_running(False)
|
|
182
|
+
|
|
183
|
+
# Cancel monitoring task
|
|
184
|
+
if self._monitoring_task:
|
|
185
|
+
self._monitoring_task.cancel()
|
|
186
|
+
try:
|
|
187
|
+
await self._monitoring_task
|
|
188
|
+
except asyncio.CancelledError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
self.logger.info("System collector stopped")
|
|
192
|
+
|
|
193
|
+
async def emit_system_event(self, event_type: str, payload: Dict[str, Any]) -> None:
|
|
194
|
+
"""Manually emit a system event (for testing or external triggers)."""
|
|
195
|
+
await self._emit_event(f"system:{event_type}", payload, "normal", "system")
|
devloop/core/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Core framework components."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent, AgentResult
|
|
4
|
+
from .config import Config, ConfigWrapper
|
|
5
|
+
from .context_store import context_store
|
|
6
|
+
from .event import Event, EventBus, Priority
|
|
7
|
+
from .event_store import event_store
|
|
8
|
+
from .manager import AgentManager
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Agent",
|
|
12
|
+
"AgentResult",
|
|
13
|
+
"Config",
|
|
14
|
+
"ConfigWrapper",
|
|
15
|
+
"context_store",
|
|
16
|
+
"Event",
|
|
17
|
+
"EventBus",
|
|
18
|
+
"event_store",
|
|
19
|
+
"Priority",
|
|
20
|
+
"AgentManager",
|
|
21
|
+
]
|
devloop/core/agent.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Base agent class with performance monitoring and feedback support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .event import Event, EventBus
|
|
13
|
+
from .feedback import FeedbackAPI
|
|
14
|
+
from .performance import PerformanceMonitor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AgentResult:
|
|
19
|
+
"""Agent execution result."""
|
|
20
|
+
|
|
21
|
+
agent_name: str
|
|
22
|
+
success: bool
|
|
23
|
+
duration: float
|
|
24
|
+
message: str = ""
|
|
25
|
+
data: Dict[str, Any] | None = None
|
|
26
|
+
error: str | None = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
"""Validate AgentResult parameters."""
|
|
30
|
+
# Validate agent_name
|
|
31
|
+
if not isinstance(self.agent_name, str):
|
|
32
|
+
raise TypeError(
|
|
33
|
+
f"agent_name must be a string, got {type(self.agent_name).__name__}"
|
|
34
|
+
)
|
|
35
|
+
if not self.agent_name:
|
|
36
|
+
raise ValueError("agent_name cannot be empty")
|
|
37
|
+
|
|
38
|
+
# Validate success
|
|
39
|
+
if not isinstance(self.success, bool):
|
|
40
|
+
raise TypeError(
|
|
41
|
+
f"success must be a boolean, got {type(self.success).__name__}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Validate duration
|
|
45
|
+
if not isinstance(self.duration, (int, float)):
|
|
46
|
+
raise TypeError(
|
|
47
|
+
f"duration must be a number, got {type(self.duration).__name__}. "
|
|
48
|
+
"Did you forget to include duration parameter in AgentResult creation?"
|
|
49
|
+
)
|
|
50
|
+
if self.duration < 0:
|
|
51
|
+
raise ValueError(f"duration must be non-negative, got {self.duration}")
|
|
52
|
+
|
|
53
|
+
# Validate message
|
|
54
|
+
if not isinstance(self.message, str):
|
|
55
|
+
raise TypeError(
|
|
56
|
+
f"message must be a string, got {type(self.message).__name__}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Validate data
|
|
60
|
+
if self.data is not None and not isinstance(self.data, dict):
|
|
61
|
+
raise TypeError(
|
|
62
|
+
f"data must be a dict or None, got {type(self.data).__name__}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Validate error
|
|
66
|
+
if self.error is not None and not isinstance(self.error, str):
|
|
67
|
+
raise TypeError(
|
|
68
|
+
f"error must be a string or None, got {type(self.error).__name__}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Agent(ABC):
|
|
73
|
+
"""Base agent class with performance monitoring and feedback."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
name: str,
|
|
78
|
+
triggers: List[str],
|
|
79
|
+
event_bus: EventBus,
|
|
80
|
+
feedback_api: Optional[FeedbackAPI] = None,
|
|
81
|
+
performance_monitor: Optional[PerformanceMonitor] = None,
|
|
82
|
+
):
|
|
83
|
+
self.name = name
|
|
84
|
+
self.triggers = triggers
|
|
85
|
+
self.event_bus = event_bus
|
|
86
|
+
self.feedback_api = feedback_api
|
|
87
|
+
self.performance_monitor = performance_monitor
|
|
88
|
+
self.enabled = True
|
|
89
|
+
self.logger = logging.getLogger(f"agent.{name}")
|
|
90
|
+
self._running = False
|
|
91
|
+
self._event_queue: asyncio.Queue[Event] = asyncio.Queue()
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
95
|
+
"""Handle an event. Must be implemented by subclasses."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
async def start(self) -> None:
|
|
99
|
+
"""Start the agent."""
|
|
100
|
+
if self._running:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self._running = True
|
|
104
|
+
|
|
105
|
+
# Subscribe to configured triggers
|
|
106
|
+
for trigger in self.triggers:
|
|
107
|
+
await self.event_bus.subscribe(trigger, self._event_queue)
|
|
108
|
+
|
|
109
|
+
# Start event processing loop
|
|
110
|
+
asyncio.create_task(self._process_events())
|
|
111
|
+
self.logger.info(f"Agent {self.name} started, listening to {self.triggers}")
|
|
112
|
+
|
|
113
|
+
async def stop(self) -> None:
|
|
114
|
+
"""Stop the agent."""
|
|
115
|
+
if not self._running:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self._running = False
|
|
119
|
+
|
|
120
|
+
# Unsubscribe from events
|
|
121
|
+
for trigger in self.triggers:
|
|
122
|
+
await self.event_bus.unsubscribe(trigger, self._event_queue)
|
|
123
|
+
|
|
124
|
+
self.logger.info(f"Agent {self.name} stopped")
|
|
125
|
+
|
|
126
|
+
async def _process_events(self) -> None:
|
|
127
|
+
"""Process events from the queue with performance monitoring."""
|
|
128
|
+
while self._running:
|
|
129
|
+
try:
|
|
130
|
+
# Wait for event with timeout to allow checking _running
|
|
131
|
+
event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
|
|
132
|
+
except asyncio.TimeoutError:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if not self.enabled:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Execute handler with performance monitoring
|
|
139
|
+
try:
|
|
140
|
+
operation_name = f"agent.{self.name}.handle"
|
|
141
|
+
|
|
142
|
+
if self.performance_monitor:
|
|
143
|
+
async with self.performance_monitor.monitor_operation(
|
|
144
|
+
operation_name,
|
|
145
|
+
metadata={"event_type": event.type, "agent_name": self.name},
|
|
146
|
+
) as metrics:
|
|
147
|
+
result = await self.handle(event)
|
|
148
|
+
metrics.complete(result.success, result.error)
|
|
149
|
+
|
|
150
|
+
# Update result duration from metrics
|
|
151
|
+
if metrics.duration:
|
|
152
|
+
result.duration = metrics.duration
|
|
153
|
+
else:
|
|
154
|
+
start_time = time.time()
|
|
155
|
+
result = await self.handle(event)
|
|
156
|
+
result.duration = time.time() - start_time
|
|
157
|
+
|
|
158
|
+
# Update performance store if available
|
|
159
|
+
if self.feedback_api:
|
|
160
|
+
await self.feedback_api.feedback_store.update_performance(
|
|
161
|
+
self.name, result.success, result.duration
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Publish result
|
|
165
|
+
await self._publish_result(result)
|
|
166
|
+
|
|
167
|
+
# Log result
|
|
168
|
+
status = "✓" if result.success else "✗"
|
|
169
|
+
self.logger.info(
|
|
170
|
+
f"{status} {self.name}: {result.message} ({result.duration:.2f}s)"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self.logger.error(f"Error in {self.name}: {e}", exc_info=True)
|
|
175
|
+
|
|
176
|
+
error_result = AgentResult(
|
|
177
|
+
agent_name=self.name,
|
|
178
|
+
success=False,
|
|
179
|
+
duration=0.1, # Default duration for errors
|
|
180
|
+
error=str(e),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Update performance store for failed operations
|
|
184
|
+
if self.feedback_api:
|
|
185
|
+
await self.feedback_api.feedback_store.update_performance(
|
|
186
|
+
self.name, False, error_result.duration
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
await self._publish_result(error_result)
|
|
190
|
+
|
|
191
|
+
async def _publish_result(self, result: AgentResult) -> None:
|
|
192
|
+
"""Publish agent result as an event."""
|
|
193
|
+
await self.event_bus.emit(
|
|
194
|
+
Event(
|
|
195
|
+
type=f"agent:{self.name}:completed",
|
|
196
|
+
payload={
|
|
197
|
+
"agent_name": result.agent_name,
|
|
198
|
+
"success": result.success,
|
|
199
|
+
"duration": result.duration,
|
|
200
|
+
"message": result.message,
|
|
201
|
+
"data": result.data,
|
|
202
|
+
"error": result.error,
|
|
203
|
+
},
|
|
204
|
+
source=self.name,
|
|
205
|
+
)
|
|
206
|
+
)
|