devsquad 3.6.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 (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ EnhancedWorker Output Slicing (P1-3)
5
+
6
+ Adds incremental output capability to EnhancedWorker:
7
+ - Splits large outputs into configurable slices
8
+ - Writes intermediate slices to scratchpad for real-time monitoring
9
+ - Provides progress tracking during long-running tasks
10
+
11
+ Spec reference: SPEC_V35_Agent_Skills_Quality_Framework.md Section 7.3
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class OutputSlice:
24
+ """A single slice of output with metadata."""
25
+ slice_number: int
26
+ total_slices: int
27
+ content: str
28
+ line_start: int
29
+ line_end: int
30
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return {
34
+ "slice_number": self.slice_number,
35
+ "total_slices": self.total_slices,
36
+ "line_range": f"{self.line_start}-{self.line_end}",
37
+ "content_length": len(self.content),
38
+ "timestamp": self.timestamp,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class SlicedOutput:
44
+ """Complete sliced output result."""
45
+ original_output: str
46
+ slices: List[OutputSlice] = field(default_factory=list)
47
+ total_lines: int = 0
48
+ total_slices: int = 0
49
+ was_sliced: bool = False
50
+
51
+ def get_full_output(self) -> str:
52
+ if not self.was_sliced:
53
+ return self.original_output
54
+ return "\n".join(s.content for s in self.slices)
55
+
56
+ def get_slice(self, index: int) -> Optional[OutputSlice]:
57
+ if 0 <= index < len(self.slices):
58
+ return self.slices[index]
59
+ return None
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ return {
63
+ "total_lines": self.total_lines,
64
+ "total_slices": self.total_slices,
65
+ "was_sliced": self.was_sliced,
66
+ "slices": [s.to_dict() for s in self.slices],
67
+ }
68
+
69
+
70
+ class OutputSlicer:
71
+ """
72
+ Splits large outputs into manageable slices.
73
+
74
+ Usage:
75
+ slicer = OutputSlicer(max_slice_lines=100)
76
+ result = slicer.slice_output(large_text)
77
+ print(f"Total: {result.total_slices} slices")
78
+ for s in result.slices:
79
+ print(s.content)
80
+ """
81
+
82
+ DEFAULT_MAX_SLICE_LINES = 100
83
+ SLICE_HEADER_TEMPLATE = "\n--- Slice {current}/{total} (lines {start}-{end}) ---\n"
84
+
85
+ def __init__(
86
+ self,
87
+ max_slice_lines: int = DEFAULT_MAX_SLICE_LINES,
88
+ include_headers: bool = True,
89
+ write_to_scratchpad: bool = False,
90
+ scratchpad=None,
91
+ ):
92
+ """
93
+ Initialize output slicer.
94
+
95
+ Args:
96
+ max_slice_lines: Maximum lines per slice (default: 100)
97
+ include_headers: Add slice headers to each slice
98
+ write_to_scratchpad: Write intermediate slices to scratchpad
99
+ scratchpad: Scratchpad instance for writing slices
100
+ """
101
+ self.max_slice_lines = max(max_slice_lines, 10) # Minimum 10 lines
102
+ self.include_headers = include_headers
103
+ self.write_to_scratchpad = write_to_scratchpad
104
+ self._scratchpad = scratchpad
105
+
106
+ def slice_output(
107
+ self,
108
+ output: str,
109
+ task_id: Optional[str] = None,
110
+ role_id: Optional[str] = None,
111
+ ) -> SlicedOutput:
112
+ """
113
+ Split output into slices if it exceeds max_slice_lines.
114
+
115
+ Args:
116
+ output: The full output text to potentially slice
117
+ task_id: Optional task identifier for scratchpad keys
118
+ role_id: Optional role identifier for scratchpad keys
119
+
120
+ Returns:
121
+ SlicedOutput with all metadata
122
+ """
123
+ if not output or not output.strip():
124
+ return SlicedOutput(
125
+ original_output=output,
126
+ total_lines=0,
127
+ total_slices=0,
128
+ was_sliced=False,
129
+ )
130
+
131
+ lines = output.split('\n')
132
+ total_lines = len(lines)
133
+
134
+ if total_lines <= self.max_slice_lines:
135
+ return SlicedOutput(
136
+ original_output=output,
137
+ total_lines=total_lines,
138
+ total_slices=1,
139
+ was_sliced=False,
140
+ )
141
+
142
+ # Calculate number of slices needed
143
+ total_slices = (total_lines + self.max_slice_lines - 1) // self.max_slice_lines
144
+
145
+ slices = []
146
+ for i in range(0, total_lines, self.max_slice_lines):
147
+ slice_num = i // self.max_slice_lines + 1
148
+ line_start = i + 1
149
+ line_end = min(i + self.max_slice_lines, total_lines)
150
+ slice_lines = lines[i:i + self.max_slice_lines]
151
+
152
+ if self.include_headers:
153
+ header = self.SLICE_HEADER_TEMPLATE.format(
154
+ current=slice_num,
155
+ total=total_slices,
156
+ start=line_start,
157
+ end=line_end,
158
+ )
159
+ content = header + '\n'.join(slice_lines)
160
+ else:
161
+ content = '\n'.join(slice_lines)
162
+
163
+ output_slice = OutputSlice(
164
+ slice_number=slice_num,
165
+ total_slices=total_slices,
166
+ content=content,
167
+ line_start=line_start,
168
+ line_end=line_end,
169
+ )
170
+ slices.append(output_slice)
171
+
172
+ # Write to scratchpad if enabled
173
+ if self.write_to_scratchpad and self._scratchpad is not None:
174
+ try:
175
+ key = f"{role_id or 'unknown'}/slice_{slice_num}"
176
+ if task_id:
177
+ key = f"{task_id}/{key}"
178
+ self._scratchpad.write(key, content)
179
+ except Exception as e:
180
+ logger.debug("Failed to write slice %d to scratchpad: %s", slice_num, e)
181
+
182
+ return SlicedOutput(
183
+ original_output=output,
184
+ slices=slices,
185
+ total_lines=total_lines,
186
+ total_slices=total_slices,
187
+ was_sliced=True,
188
+ )
189
+
190
+ def batch_slice_outputs(
191
+ self,
192
+ outputs: List[str],
193
+ task_ids: Optional[List[str]] = None,
194
+ ) -> List[SlicedOutput]:
195
+ """
196
+ Slice multiple outputs at once.
197
+
198
+ Args:
199
+ outputs: List of output strings to slice
200
+ task_ids: Optional list of task IDs (must match outputs length)
201
+
202
+ Returns:
203
+ List of SlicedOutput results
204
+ """
205
+ results = []
206
+ for i, output in enumerate(outputs):
207
+ tid = task_ids[i] if task_ids and i < len(task_ids) else None
208
+ result = self.slice_output(output, task_id=tid)
209
+ results.append(result)
210
+ return results
211
+
212
+
213
+ def create_default_slicer() -> OutputSlicer:
214
+ """Create slicer with default settings (100 lines per slice)."""
215
+ return OutputSlicer()
216
+
217
+
218
+ def create_compact_slicer() -> OutputSlicer:
219
+ """Create compact slicer for small context windows (50 lines)."""
220
+ return OutputSlicer(max_slice_lines=50)
221
+
222
+
223
+ def create_large_slicer() -> OutputSlicer:
224
+ """Create large slicer for detailed outputs (200 lines)."""
225
+ return OutputSlicer(max_slice_lines=200)
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Performance Monitoring Module
5
+
6
+ Tracks and analyzes system performance metrics:
7
+ - Response time monitoring
8
+ - Resource usage tracking
9
+ - Performance bottleneck detection
10
+ - Real-time metrics dashboard
11
+
12
+ Usage:
13
+ from scripts.collaboration.performance_monitor import monitor_performance
14
+
15
+ @monitor_performance(name="llm_call")
16
+ def call_llm(prompt: str):
17
+ return response
18
+
19
+ from scripts.collaboration.performance_monitor import get_monitor
20
+ stats = get_monitor().get_stats()
21
+ """
22
+
23
+ import time
24
+ import logging
25
+ from typing import Callable, Dict, Any, List, Optional
26
+ from functools import wraps
27
+ from dataclasses import dataclass, field
28
+ from datetime import datetime
29
+ from collections import defaultdict, deque
30
+
31
+ try:
32
+ import psutil
33
+ _PSUTIL_AVAILABLE = True
34
+ except ImportError:
35
+ _PSUTIL_AVAILABLE = False
36
+
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @dataclass
42
+ class PerformanceMetric:
43
+ """Performance metric data structure."""
44
+ name: str
45
+ start_time: float
46
+ end_time: float
47
+ duration: float
48
+ cpu_percent: float
49
+ memory_mb: float
50
+ success: bool
51
+ error: Optional[str] = None
52
+
53
+ def to_dict(self) -> Dict[str, Any]:
54
+ return {
55
+ "name": self.name,
56
+ "duration_ms": self.duration * 1000,
57
+ "cpu_percent": self.cpu_percent,
58
+ "memory_mb": self.memory_mb,
59
+ "success": self.success,
60
+ "error": self.error,
61
+ "timestamp": datetime.fromtimestamp(self.start_time).isoformat()
62
+ }
63
+
64
+
65
+ @dataclass
66
+ class FunctionStats:
67
+ """Function execution statistics."""
68
+ name: str
69
+ call_count: int = 0
70
+ success_count: int = 0
71
+ failure_count: int = 0
72
+ total_duration: float = 0.0
73
+ min_duration: float = float('inf')
74
+ max_duration: float = 0.0
75
+ recent_metrics: deque = field(default_factory=lambda: deque(maxlen=100))
76
+
77
+ def add_metric(self, metric: PerformanceMetric):
78
+ """Add a performance metric."""
79
+ self.call_count += 1
80
+ if metric.success:
81
+ self.success_count += 1
82
+ else:
83
+ self.failure_count += 1
84
+
85
+ self.total_duration += metric.duration
86
+ self.min_duration = min(self.min_duration, metric.duration)
87
+ self.max_duration = max(self.max_duration, metric.duration)
88
+ self.recent_metrics.append(metric)
89
+
90
+ @property
91
+ def avg_duration(self) -> float:
92
+ """Average execution duration."""
93
+ return self.total_duration / self.call_count if self.call_count > 0 else 0.0
94
+
95
+ @property
96
+ def success_rate(self) -> float:
97
+ """Success rate."""
98
+ return self.success_count / self.call_count if self.call_count > 0 else 0.0
99
+
100
+ @property
101
+ def p95_duration(self) -> float:
102
+ """P95 response time."""
103
+ if not self.recent_metrics:
104
+ return 0.0
105
+ durations = sorted([m.duration for m in self.recent_metrics])
106
+ idx = int(len(durations) * 0.95)
107
+ return durations[idx] if idx < len(durations) else durations[-1]
108
+
109
+ @property
110
+ def p99_duration(self) -> float:
111
+ """P99 response time."""
112
+ if not self.recent_metrics:
113
+ return 0.0
114
+ durations = sorted([m.duration for m in self.recent_metrics])
115
+ idx = int(len(durations) * 0.99)
116
+ return durations[idx] if idx < len(durations) else durations[-1]
117
+
118
+
119
+ def _get_cpu_percent() -> float:
120
+ """Safely get CPU percent, returns 0.0 if psutil unavailable."""
121
+ if not _PSUTIL_AVAILABLE:
122
+ return 0.0
123
+ try:
124
+ return psutil.Process().cpu_percent()
125
+ except Exception:
126
+ return 0.0
127
+
128
+
129
+ def _get_memory_mb() -> float:
130
+ """Safely get memory usage in MB, returns 0.0 if psutil unavailable."""
131
+ if not _PSUTIL_AVAILABLE:
132
+ return 0.0
133
+ try:
134
+ return psutil.Process().memory_info().rss / 1024 / 1024
135
+ except Exception:
136
+ return 0.0
137
+
138
+
139
+ class PerformanceMonitor:
140
+ """
141
+ Performance Monitor
142
+
143
+ Features:
144
+ - Automatic function execution time tracking
145
+ - CPU and memory usage monitoring
146
+ - P95/P99 response time calculation
147
+ - Performance bottleneck detection
148
+ - Performance report generation
149
+ """
150
+
151
+ def __init__(self, max_history: int = 1000):
152
+ """
153
+ Initialize performance monitor.
154
+
155
+ Args:
156
+ max_history: Maximum number of historical records to retain
157
+ """
158
+ self.function_stats: Dict[str, FunctionStats] = defaultdict(
159
+ lambda: FunctionStats(name="")
160
+ )
161
+ self.all_metrics: deque = deque(maxlen=max_history)
162
+ self.start_time = time.time()
163
+
164
+ def record_metric(self, metric: PerformanceMetric):
165
+ """Record a performance metric."""
166
+ self.all_metrics.append(metric)
167
+
168
+ if metric.name not in self.function_stats:
169
+ self.function_stats[metric.name] = FunctionStats(name=metric.name)
170
+
171
+ self.function_stats[metric.name].add_metric(metric)
172
+
173
+ def monitor(self, name: str):
174
+ """
175
+ Decorator: monitor function performance.
176
+
177
+ Args:
178
+ name: Function name identifier
179
+ """
180
+ def decorator(func: Callable) -> Callable:
181
+ @wraps(func)
182
+ def wrapper(*args, **kwargs):
183
+ start_time = time.time()
184
+ start_cpu = _get_cpu_percent()
185
+ start_memory = _get_memory_mb()
186
+
187
+ success = True
188
+ error = None
189
+
190
+ try:
191
+ result = func(*args, **kwargs)
192
+ return result
193
+ except Exception as e:
194
+ success = False
195
+ error = str(e)
196
+ raise
197
+ finally:
198
+ end_time = time.time()
199
+ end_cpu = _get_cpu_percent()
200
+ end_memory = _get_memory_mb()
201
+
202
+ metric = PerformanceMetric(
203
+ name=name,
204
+ start_time=start_time,
205
+ end_time=end_time,
206
+ duration=end_time - start_time,
207
+ cpu_percent=(start_cpu + end_cpu) / 2,
208
+ memory_mb=(start_memory + end_memory) / 2,
209
+ success=success,
210
+ error=error
211
+ )
212
+
213
+ self.record_metric(metric)
214
+
215
+ return wrapper
216
+ return decorator
217
+
218
+ def get_stats(self, function_name: Optional[str] = None) -> Dict[str, Any]:
219
+ """
220
+ Get statistics (compatible with MonitorProvider Protocol).
221
+
222
+ Args:
223
+ function_name: If specified, return only that function's stats
224
+
225
+ Returns:
226
+ Statistics dictionary
227
+ """
228
+ if function_name:
229
+ if function_name not in self.function_stats:
230
+ return {}
231
+
232
+ stats = self.function_stats[function_name]
233
+ return {
234
+ "name": stats.name,
235
+ "call_count": stats.call_count,
236
+ "success_count": stats.success_count,
237
+ "failure_count": stats.failure_count,
238
+ "success_rate": f"{stats.success_rate * 100:.1f}%",
239
+ "avg_duration_ms": stats.avg_duration * 1000,
240
+ "min_duration_ms": stats.min_duration * 1000,
241
+ "max_duration_ms": stats.max_duration * 1000,
242
+ "p95_duration_ms": stats.p95_duration * 1000,
243
+ "p99_duration_ms": stats.p99_duration * 1000,
244
+ }
245
+
246
+ llm_calls = [s for name, s in self.function_stats.items() if name.startswith("llm_call:")]
247
+ agent_executions = [s for name, s in self.function_stats.items() if name.startswith("agent:")]
248
+
249
+ total_llm_calls = sum(s.call_count for s in llm_calls)
250
+ total_agent_executions = sum(s.call_count for s in agent_executions)
251
+
252
+ avg_llm_duration = (
253
+ sum(s.total_duration for s in llm_calls) / total_llm_calls
254
+ if total_llm_calls > 0 else 0.0
255
+ )
256
+
257
+ avg_agent_duration = (
258
+ sum(s.total_duration for s in agent_executions) / total_agent_executions
259
+ if total_agent_executions > 0 else 0.0
260
+ )
261
+
262
+ return {
263
+ "total_llm_calls": total_llm_calls,
264
+ "total_agent_executions": total_agent_executions,
265
+ "avg_llm_duration": avg_llm_duration,
266
+ "avg_agent_duration": avg_agent_duration,
267
+ "total_tokens": 0,
268
+ "uptime_seconds": time.time() - self.start_time,
269
+ "total_metrics": len(self.all_metrics),
270
+ "functions": {
271
+ name: {
272
+ "call_count": stats.call_count,
273
+ "success_rate": f"{stats.success_rate * 100:.1f}%",
274
+ "avg_duration_ms": stats.avg_duration * 1000,
275
+ "p95_duration_ms": stats.p95_duration * 1000,
276
+ }
277
+ for name, stats in self.function_stats.items()
278
+ }
279
+ }
280
+
281
+ def get_slowest_functions(self, limit: int = 10) -> List[Dict[str, Any]]:
282
+ """Get the slowest functions by average duration."""
283
+ sorted_funcs = sorted(
284
+ self.function_stats.values(),
285
+ key=lambda s: s.avg_duration,
286
+ reverse=True
287
+ )[:limit]
288
+
289
+ return [
290
+ {
291
+ "name": stats.name,
292
+ "avg_duration_ms": stats.avg_duration * 1000,
293
+ "call_count": stats.call_count,
294
+ "p95_duration_ms": stats.p95_duration * 1000,
295
+ }
296
+ for stats in sorted_funcs
297
+ ]
298
+
299
+ def get_bottlenecks(self, threshold_ms: float = 1000) -> List[Dict[str, Any]]:
300
+ """
301
+ Detect performance bottlenecks.
302
+
303
+ Args:
304
+ threshold_ms: Threshold in milliseconds; functions above this are bottlenecks
305
+
306
+ Returns:
307
+ List of bottleneck entries
308
+ """
309
+ bottlenecks = []
310
+
311
+ for name, stats in self.function_stats.items():
312
+ if stats.avg_duration * 1000 > threshold_ms:
313
+ bottlenecks.append({
314
+ "name": name,
315
+ "avg_duration_ms": stats.avg_duration * 1000,
316
+ "p95_duration_ms": stats.p95_duration * 1000,
317
+ "call_count": stats.call_count,
318
+ "severity": "high" if stats.avg_duration * 1000 > threshold_ms * 2 else "medium"
319
+ })
320
+
321
+ return sorted(bottlenecks, key=lambda x: x["avg_duration_ms"], reverse=True)
322
+
323
+ def get_recent_errors(self, limit: int = 10) -> List[Dict[str, Any]]:
324
+ """Get recent error entries."""
325
+ errors = [m for m in self.all_metrics if not m.success]
326
+ return [m.to_dict() for m in list(errors)[-limit:]]
327
+
328
+ def record_llm_call(
329
+ self,
330
+ backend: str,
331
+ model: str,
332
+ duration: float,
333
+ token_count: int,
334
+ success: bool,
335
+ metadata: Optional[Dict[str, Any]] = None
336
+ ) -> None:
337
+ """Record an LLM API call (implements MonitorProvider Protocol)."""
338
+ metric_name = f"llm_call:{backend}:{model}"
339
+
340
+ metric = PerformanceMetric(
341
+ name=metric_name,
342
+ start_time=time.time() - duration,
343
+ end_time=time.time(),
344
+ duration=duration,
345
+ cpu_percent=_get_cpu_percent(),
346
+ memory_mb=_get_memory_mb(),
347
+ success=success,
348
+ error=metadata.get("error") if metadata else None
349
+ )
350
+
351
+ self.record_metric(metric)
352
+
353
+ def record_agent_execution(
354
+ self,
355
+ agent_role: str,
356
+ task: str,
357
+ duration: float,
358
+ success: bool,
359
+ metadata: Optional[Dict[str, Any]] = None
360
+ ) -> None:
361
+ """Record an agent execution (implements MonitorProvider Protocol)."""
362
+ metric_name = f"agent:{agent_role}"
363
+
364
+ metric = PerformanceMetric(
365
+ name=metric_name,
366
+ start_time=time.time() - duration,
367
+ end_time=time.time(),
368
+ duration=duration,
369
+ cpu_percent=_get_cpu_percent(),
370
+ memory_mb=_get_memory_mb(),
371
+ success=success,
372
+ error=metadata.get("error") if metadata else None
373
+ )
374
+
375
+ self.record_metric(metric)
376
+
377
+ def generate_report(self, output_path: str) -> None:
378
+ """Generate performance report to file (implements MonitorProvider Protocol)."""
379
+ try:
380
+ report = self.export_report()
381
+ with open(output_path, 'w', encoding='utf-8') as f:
382
+ f.write(report)
383
+ logger.info("Performance report generated: %s", output_path)
384
+ except Exception as e:
385
+ logger.error("Failed to generate report: %s", e)
386
+ raise IOError(f"Failed to generate report: {e}")
387
+
388
+ def is_available(self) -> bool:
389
+ """Check if monitoring is available (implements MonitorProvider Protocol)."""
390
+ return True
391
+
392
+ def export_report(self) -> str:
393
+ """Export performance report as Markdown string."""
394
+ stats = self.get_stats()
395
+ slowest = self.get_slowest_functions(5)
396
+ bottlenecks = self.get_bottlenecks()
397
+ errors = self.get_recent_errors(5)
398
+
399
+ report = f"""# Performance Monitoring Report
400
+
401
+ **Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
402
+ **Uptime**: {stats['uptime_seconds']:.0f} seconds
403
+ **Total Metrics**: {stats['total_metrics']}
404
+
405
+ ## Overall Statistics
406
+
407
+ | Function | Calls | Success Rate | Avg Duration | P95 Duration |
408
+ |----------|-------|--------------|--------------|--------------|
409
+ """
410
+
411
+ for name, func_stats in stats['functions'].items():
412
+ report += f"| {name} | {func_stats['call_count']} | {func_stats['success_rate']} | {func_stats['avg_duration_ms']:.1f}ms | {func_stats['p95_duration_ms']:.1f}ms |\n"
413
+
414
+ if slowest:
415
+ report += "\n## Slowest Functions\n\n"
416
+ for i, func in enumerate(slowest, 1):
417
+ report += f"{i}. **{func['name']}**: {func['avg_duration_ms']:.1f}ms avg ({func['call_count']} calls)\n"
418
+
419
+ if bottlenecks:
420
+ report += "\n## Performance Bottlenecks\n\n"
421
+ for bottleneck in bottlenecks:
422
+ report += f"- **{bottleneck['name']}** ({bottleneck['severity']}): {bottleneck['avg_duration_ms']:.1f}ms avg\n"
423
+
424
+ if errors:
425
+ report += "\n## Recent Errors\n\n"
426
+ for error in errors:
427
+ report += f"- **{error['name']}** at {error['timestamp']}: {error['error']}\n"
428
+
429
+ return report
430
+
431
+
432
+ _monitor_instance: Optional[PerformanceMonitor] = None
433
+
434
+
435
+ def get_monitor() -> PerformanceMonitor:
436
+ """Get or create global monitor instance."""
437
+ global _monitor_instance
438
+ if _monitor_instance is None:
439
+ _monitor_instance = PerformanceMonitor()
440
+ return _monitor_instance
441
+
442
+
443
+ def monitor_performance(name: str):
444
+ """
445
+ Decorator: monitor function performance.
446
+
447
+ Args:
448
+ name: Function name identifier
449
+
450
+ Example:
451
+ @monitor_performance("llm_call")
452
+ def call_llm(prompt: str):
453
+ return response
454
+ """
455
+ monitor = get_monitor()
456
+ return monitor.monitor(name)
457
+
458
+
459
+ def reset_monitor():
460
+ """Reset global monitor instance (mainly for testing)."""
461
+ global _monitor_instance
462
+ _monitor_instance = None