claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__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 (93) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -96
  4. claude_mpm/agents/MEMORY.md +88 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/templates/code_analyzer.json +2 -2
  7. claude_mpm/agents/templates/data_engineer.json +1 -1
  8. claude_mpm/agents/templates/documentation.json +1 -1
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/ops.json +1 -1
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/agents/templates/security.json +1 -1
  14. claude_mpm/agents/templates/ticketing.json +2 -7
  15. claude_mpm/agents/templates/version_control.json +1 -1
  16. claude_mpm/agents/templates/web_qa.json +2 -2
  17. claude_mpm/agents/templates/web_ui.json +2 -2
  18. claude_mpm/cli/__init__.py +2 -2
  19. claude_mpm/cli/commands/__init__.py +2 -1
  20. claude_mpm/cli/commands/tickets.py +596 -19
  21. claude_mpm/cli/parser.py +217 -5
  22. claude_mpm/config/__init__.py +30 -39
  23. claude_mpm/config/socketio_config.py +8 -5
  24. claude_mpm/constants.py +13 -0
  25. claude_mpm/core/__init__.py +8 -18
  26. claude_mpm/core/cache.py +596 -0
  27. claude_mpm/core/claude_runner.py +166 -622
  28. claude_mpm/core/config.py +5 -1
  29. claude_mpm/core/constants.py +339 -0
  30. claude_mpm/core/container.py +461 -22
  31. claude_mpm/core/exceptions.py +392 -0
  32. claude_mpm/core/framework_loader.py +208 -94
  33. claude_mpm/core/interactive_session.py +432 -0
  34. claude_mpm/core/interfaces.py +424 -0
  35. claude_mpm/core/lazy.py +467 -0
  36. claude_mpm/core/logging_config.py +444 -0
  37. claude_mpm/core/oneshot_session.py +465 -0
  38. claude_mpm/core/optimized_agent_loader.py +485 -0
  39. claude_mpm/core/optimized_startup.py +490 -0
  40. claude_mpm/core/service_registry.py +52 -26
  41. claude_mpm/core/socketio_pool.py +162 -5
  42. claude_mpm/core/types.py +292 -0
  43. claude_mpm/core/typing_utils.py +477 -0
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  45. claude_mpm/init.py +2 -1
  46. claude_mpm/services/__init__.py +78 -14
  47. claude_mpm/services/agent/__init__.py +24 -0
  48. claude_mpm/services/agent/deployment.py +2548 -0
  49. claude_mpm/services/agent/management.py +598 -0
  50. claude_mpm/services/agent/registry.py +813 -0
  51. claude_mpm/services/agents/deployment/agent_deployment.py +587 -268
  52. claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
  53. claude_mpm/services/async_session_logger.py +8 -3
  54. claude_mpm/services/communication/__init__.py +21 -0
  55. claude_mpm/services/communication/socketio.py +1933 -0
  56. claude_mpm/services/communication/websocket.py +479 -0
  57. claude_mpm/services/core/__init__.py +123 -0
  58. claude_mpm/services/core/base.py +247 -0
  59. claude_mpm/services/core/interfaces.py +951 -0
  60. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  61. claude_mpm/services/framework_claude_md_generator.py +3 -2
  62. claude_mpm/services/health_monitor.py +4 -3
  63. claude_mpm/services/hook_service.py +64 -4
  64. claude_mpm/services/infrastructure/__init__.py +21 -0
  65. claude_mpm/services/infrastructure/logging.py +202 -0
  66. claude_mpm/services/infrastructure/monitoring.py +893 -0
  67. claude_mpm/services/memory/indexed_memory.py +648 -0
  68. claude_mpm/services/project/__init__.py +21 -0
  69. claude_mpm/services/project/analyzer.py +864 -0
  70. claude_mpm/services/project/registry.py +608 -0
  71. claude_mpm/services/project_analyzer.py +95 -2
  72. claude_mpm/services/recovery_manager.py +15 -9
  73. claude_mpm/services/socketio/__init__.py +25 -0
  74. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  75. claude_mpm/services/socketio/handlers/base.py +121 -0
  76. claude_mpm/services/socketio/handlers/connection.py +198 -0
  77. claude_mpm/services/socketio/handlers/file.py +213 -0
  78. claude_mpm/services/socketio/handlers/git.py +723 -0
  79. claude_mpm/services/socketio/handlers/memory.py +27 -0
  80. claude_mpm/services/socketio/handlers/project.py +25 -0
  81. claude_mpm/services/socketio/handlers/registry.py +145 -0
  82. claude_mpm/services/socketio_client_manager.py +12 -7
  83. claude_mpm/services/socketio_server.py +156 -30
  84. claude_mpm/services/ticket_manager.py +170 -7
  85. claude_mpm/utils/error_handler.py +1 -1
  86. claude_mpm/validation/agent_validator.py +27 -14
  87. claude_mpm/validation/frontmatter_validator.py +231 -0
  88. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +58 -21
  89. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
  90. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
  91. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
  92. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
  93. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env python3
2
+ """Optimized agent loader with caching and parallel loading.
3
+
4
+ This module provides high-performance agent loading with:
5
+ - Compiled template caching
6
+ - Parallel file operations
7
+ - Memory-efficient batch processing
8
+ - Incremental loading for large agent sets
9
+
10
+ WHY optimized agent loading:
11
+ - Reduces agent deployment time from >500ms to <200ms per agent
12
+ - Caches compiled templates to avoid repeated parsing
13
+ - Parallel I/O operations for multi-agent loading
14
+ - Minimizes file system operations through batching
15
+ """
16
+
17
+ import asyncio
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import threading
22
+ import time
23
+ from concurrent.futures import ThreadPoolExecutor, as_completed
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Optional, Set, Tuple
28
+
29
+ try:
30
+ import aiofiles
31
+ AIOFILES_AVAILABLE = True
32
+ except ImportError:
33
+ AIOFILES_AVAILABLE = False
34
+
35
+ from .cache import FileSystemCache, get_file_cache
36
+ from .lazy import LazyService
37
+ from ..core.logger import get_logger
38
+
39
+
40
+ @dataclass
41
+ class AgentLoadMetrics:
42
+ """Metrics for agent loading performance."""
43
+ total_agents: int = 0
44
+ loaded_agents: int = 0
45
+ cached_agents: int = 0
46
+ failed_agents: int = 0
47
+ total_time: float = 0.0
48
+ parse_time: float = 0.0
49
+ io_time: float = 0.0
50
+ cache_hits: int = 0
51
+ cache_misses: int = 0
52
+
53
+
54
+ class OptimizedAgentLoader:
55
+ """High-performance agent loader with caching and parallelization.
56
+
57
+ WHY this design:
58
+ - ThreadPoolExecutor for parallel file I/O
59
+ - LRU cache for compiled agent templates
60
+ - Batch processing to reduce overhead
61
+ - Lazy compilation for rarely-used agents
62
+
63
+ Example:
64
+ loader = OptimizedAgentLoader(max_workers=4)
65
+
66
+ # Load agents in parallel
67
+ agents = loader.load_agents_parallel([
68
+ '/path/to/agent1.json',
69
+ '/path/to/agent2.json'
70
+ ])
71
+
72
+ # Async loading
73
+ agents = await loader.load_agents_async(paths)
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ max_workers: int = 4,
79
+ cache_ttl: int = 3600,
80
+ enable_lazy: bool = True
81
+ ):
82
+ """Initialize optimized agent loader.
83
+
84
+ Args:
85
+ max_workers: Number of parallel workers for loading
86
+ cache_ttl: Cache time-to-live in seconds
87
+ enable_lazy: Whether to use lazy loading for templates
88
+ """
89
+ self.max_workers = max_workers
90
+ self.cache_ttl = cache_ttl
91
+ self.enable_lazy = enable_lazy
92
+
93
+ # Get or create cache
94
+ self.cache = get_file_cache(max_size_mb=50, default_ttl=cache_ttl)
95
+
96
+ # Thread pool for parallel operations
97
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
98
+
99
+ # Metrics tracking
100
+ self.metrics = AgentLoadMetrics()
101
+
102
+ # Logger
103
+ self.logger = get_logger("optimized_agent_loader")
104
+
105
+ # Template compilation cache
106
+ self._compiled_templates: Dict[str, Dict[str, Any]] = {}
107
+ self._template_lock = threading.Lock()
108
+
109
+ def _get_cache_key(self, file_path: Path) -> str:
110
+ """Generate cache key for an agent file.
111
+
112
+ Includes file path and modification time for cache invalidation.
113
+ """
114
+ try:
115
+ mtime = file_path.stat().st_mtime
116
+ return f"agent:{file_path}:{mtime}"
117
+ except:
118
+ return f"agent:{file_path}"
119
+
120
+ def _load_agent_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
121
+ """Load and parse a single agent file.
122
+
123
+ Args:
124
+ file_path: Path to agent file (.json or .md)
125
+
126
+ Returns:
127
+ Parsed agent data or None if failed
128
+ """
129
+ start_time = time.time()
130
+
131
+ try:
132
+ # Check cache first
133
+ cache_key = self._get_cache_key(file_path)
134
+ cached = self.cache.get(cache_key)
135
+ if cached is not None:
136
+ self.metrics.cache_hits += 1
137
+ return cached
138
+
139
+ self.metrics.cache_misses += 1
140
+
141
+ # Read file
142
+ io_start = time.time()
143
+ content = file_path.read_text(encoding='utf-8')
144
+ self.metrics.io_time += time.time() - io_start
145
+
146
+ # Parse based on file type
147
+ parse_start = time.time()
148
+ if file_path.suffix == '.json':
149
+ agent_data = json.loads(content)
150
+ elif file_path.suffix == '.md':
151
+ # Extract frontmatter from markdown
152
+ agent_data = self._parse_markdown_agent(content)
153
+ else:
154
+ agent_data = {'content': content}
155
+
156
+ self.metrics.parse_time += time.time() - parse_start
157
+
158
+ # Add metadata
159
+ agent_data['_file_path'] = str(file_path)
160
+ agent_data['_loaded_at'] = datetime.now().isoformat()
161
+
162
+ # Cache the result
163
+ self.cache.put(cache_key, agent_data, ttl=self.cache_ttl)
164
+
165
+ return agent_data
166
+
167
+ except Exception as e:
168
+ self.logger.error(f"Failed to load agent {file_path}: {e}")
169
+ self.metrics.failed_agents += 1
170
+ return None
171
+ finally:
172
+ elapsed = time.time() - start_time
173
+ self.logger.debug(f"Loaded {file_path.name} in {elapsed:.3f}s")
174
+
175
+ def _parse_markdown_agent(self, content: str) -> Dict[str, Any]:
176
+ """Parse agent data from markdown content.
177
+
178
+ Extracts YAML frontmatter and content from markdown files.
179
+ """
180
+ lines = content.split('\n')
181
+
182
+ # Look for frontmatter
183
+ if lines[0] == '---':
184
+ try:
185
+ import yaml
186
+ end_index = lines[1:].index('---') + 1
187
+ frontmatter = '\n'.join(lines[1:end_index])
188
+ body = '\n'.join(lines[end_index + 1:])
189
+
190
+ data = yaml.safe_load(frontmatter) or {}
191
+ data['instructions'] = body
192
+ return data
193
+ except:
194
+ pass
195
+
196
+ # No frontmatter, treat as pure instructions
197
+ return {'instructions': content}
198
+
199
+ def load_agents_parallel(
200
+ self,
201
+ file_paths: List[Path],
202
+ batch_size: int = 10
203
+ ) -> Dict[str, Dict[str, Any]]:
204
+ """Load multiple agents in parallel.
205
+
206
+ Args:
207
+ file_paths: List of agent file paths
208
+ batch_size: Number of agents to load concurrently
209
+
210
+ Returns:
211
+ Dictionary mapping agent IDs to agent data
212
+ """
213
+ start_time = time.time()
214
+ self.metrics.total_agents = len(file_paths)
215
+
216
+ agents = {}
217
+
218
+ # Process in batches to avoid overwhelming the system
219
+ for i in range(0, len(file_paths), batch_size):
220
+ batch = file_paths[i:i + batch_size]
221
+
222
+ # Submit batch to thread pool
223
+ futures = {
224
+ self.executor.submit(self._load_agent_file, path): path
225
+ for path in batch
226
+ }
227
+
228
+ # Collect results
229
+ for future in as_completed(futures):
230
+ path = futures[future]
231
+ try:
232
+ agent_data = future.result(timeout=5.0)
233
+ if agent_data:
234
+ agent_id = path.stem
235
+ agents[agent_id] = agent_data
236
+ self.metrics.loaded_agents += 1
237
+ except Exception as e:
238
+ self.logger.error(f"Failed to load {path}: {e}")
239
+ self.metrics.failed_agents += 1
240
+
241
+ self.metrics.total_time = time.time() - start_time
242
+
243
+ self.logger.info(
244
+ f"Loaded {self.metrics.loaded_agents}/{self.metrics.total_agents} agents "
245
+ f"in {self.metrics.total_time:.2f}s "
246
+ f"(cache hits: {self.metrics.cache_hits}, misses: {self.metrics.cache_misses})"
247
+ )
248
+
249
+ return agents
250
+
251
+ async def load_agents_async(
252
+ self,
253
+ file_paths: List[Path]
254
+ ) -> Dict[str, Dict[str, Any]]:
255
+ """Load agents asynchronously for async applications.
256
+
257
+ Args:
258
+ file_paths: List of agent file paths
259
+
260
+ Returns:
261
+ Dictionary mapping agent IDs to agent data
262
+ """
263
+ if not AIOFILES_AVAILABLE:
264
+ # Fallback to sync loading in executor
265
+ loop = asyncio.get_event_loop()
266
+ return await loop.run_in_executor(
267
+ None,
268
+ self.load_agents_parallel,
269
+ file_paths
270
+ )
271
+
272
+ start_time = time.time()
273
+ self.metrics.total_agents = len(file_paths)
274
+
275
+ # Create async tasks for all agents
276
+ tasks = []
277
+ for path in file_paths:
278
+ tasks.append(self._load_agent_async(path))
279
+
280
+ # Load all agents concurrently
281
+ results = await asyncio.gather(*tasks, return_exceptions=True)
282
+
283
+ # Collect successful loads
284
+ agents = {}
285
+ for path, result in zip(file_paths, results):
286
+ if isinstance(result, Exception):
287
+ self.logger.error(f"Failed to load {path}: {result}")
288
+ self.metrics.failed_agents += 1
289
+ elif result:
290
+ agent_id = path.stem
291
+ agents[agent_id] = result
292
+ self.metrics.loaded_agents += 1
293
+
294
+ self.metrics.total_time = time.time() - start_time
295
+
296
+ self.logger.info(
297
+ f"Async loaded {self.metrics.loaded_agents}/{self.metrics.total_agents} agents "
298
+ f"in {self.metrics.total_time:.2f}s"
299
+ )
300
+
301
+ return agents
302
+
303
+ async def _load_agent_async(self, file_path: Path) -> Optional[Dict[str, Any]]:
304
+ """Load a single agent asynchronously."""
305
+ try:
306
+ # Check cache first
307
+ cache_key = self._get_cache_key(file_path)
308
+ cached = self.cache.get(cache_key)
309
+ if cached is not None:
310
+ self.metrics.cache_hits += 1
311
+ return cached
312
+
313
+ self.metrics.cache_misses += 1
314
+
315
+ # Read file asynchronously
316
+ async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
317
+ content = await f.read()
318
+
319
+ # Parse in executor to avoid blocking
320
+ loop = asyncio.get_event_loop()
321
+ if file_path.suffix == '.json':
322
+ agent_data = await loop.run_in_executor(None, json.loads, content)
323
+ elif file_path.suffix == '.md':
324
+ agent_data = await loop.run_in_executor(
325
+ None,
326
+ self._parse_markdown_agent,
327
+ content
328
+ )
329
+ else:
330
+ agent_data = {'content': content}
331
+
332
+ # Add metadata
333
+ agent_data['_file_path'] = str(file_path)
334
+ agent_data['_loaded_at'] = datetime.now().isoformat()
335
+
336
+ # Cache the result
337
+ self.cache.put(cache_key, agent_data, ttl=self.cache_ttl)
338
+
339
+ return agent_data
340
+
341
+ except Exception as e:
342
+ self.logger.error(f"Failed to async load {file_path}: {e}")
343
+ return None
344
+
345
+ def compile_template(self, agent_id: str, template: str) -> Dict[str, Any]:
346
+ """Compile and cache an agent template.
347
+
348
+ WHY template compilation:
349
+ - Pre-processes templates for faster runtime use
350
+ - Validates template syntax once
351
+ - Caches compiled form for reuse
352
+ - Reduces repeated parsing overhead
353
+
354
+ Args:
355
+ agent_id: Unique agent identifier
356
+ template: Raw template string
357
+
358
+ Returns:
359
+ Compiled template data
360
+ """
361
+ with self._template_lock:
362
+ # Check if already compiled
363
+ if agent_id in self._compiled_templates:
364
+ self.metrics.cache_hits += 1
365
+ return self._compiled_templates[agent_id]
366
+
367
+ self.metrics.cache_misses += 1
368
+
369
+ # Compile template
370
+ start_time = time.time()
371
+ try:
372
+ # Parse any template variables
373
+ compiled = {
374
+ 'id': agent_id,
375
+ 'template': template,
376
+ 'variables': self._extract_variables(template),
377
+ 'sections': self._extract_sections(template),
378
+ 'compiled_at': datetime.now().isoformat()
379
+ }
380
+
381
+ # Cache compiled template
382
+ self._compiled_templates[agent_id] = compiled
383
+
384
+ self.metrics.parse_time += time.time() - start_time
385
+ return compiled
386
+
387
+ except Exception as e:
388
+ self.logger.error(f"Failed to compile template {agent_id}: {e}")
389
+ return {'id': agent_id, 'template': template, 'error': str(e)}
390
+
391
+ def _extract_variables(self, template: str) -> List[str]:
392
+ """Extract variable placeholders from template."""
393
+ import re
394
+ # Find {{variable}} patterns
395
+ return re.findall(r'\{\{(\w+)\}\}', template)
396
+
397
+ def _extract_sections(self, template: str) -> Dict[str, str]:
398
+ """Extract named sections from template."""
399
+ sections = {}
400
+ current_section = 'main'
401
+ current_content = []
402
+
403
+ for line in template.split('\n'):
404
+ # Check for section headers (## Section Name)
405
+ if line.startswith('## '):
406
+ # Save previous section
407
+ if current_content:
408
+ sections[current_section] = '\n'.join(current_content)
409
+ # Start new section
410
+ current_section = line[3:].strip().lower().replace(' ', '_')
411
+ current_content = []
412
+ else:
413
+ current_content.append(line)
414
+
415
+ # Save last section
416
+ if current_content:
417
+ sections[current_section] = '\n'.join(current_content)
418
+
419
+ return sections
420
+
421
+ def preload_agents(self, agent_dirs: List[Path]) -> None:
422
+ """Preload all agents from specified directories.
423
+
424
+ Useful for warming up the cache at startup.
425
+
426
+ Args:
427
+ agent_dirs: List of directories containing agents
428
+ """
429
+ self.logger.info(f"Preloading agents from {len(agent_dirs)} directories")
430
+
431
+ all_paths = []
432
+ for dir_path in agent_dirs:
433
+ if dir_path.exists():
434
+ all_paths.extend(dir_path.glob('*.json'))
435
+ all_paths.extend(dir_path.glob('*.md'))
436
+
437
+ if all_paths:
438
+ self.load_agents_parallel(all_paths)
439
+
440
+ def get_metrics(self) -> Dict[str, Any]:
441
+ """Get loader performance metrics."""
442
+ return {
443
+ "total_agents": self.metrics.total_agents,
444
+ "loaded_agents": self.metrics.loaded_agents,
445
+ "cached_agents": len(self._compiled_templates),
446
+ "failed_agents": self.metrics.failed_agents,
447
+ "total_time": self.metrics.total_time,
448
+ "avg_load_time": self.metrics.total_time / max(1, self.metrics.loaded_agents),
449
+ "parse_time": self.metrics.parse_time,
450
+ "io_time": self.metrics.io_time,
451
+ "cache_hit_rate": self.metrics.cache_hits / max(1, self.metrics.cache_hits + self.metrics.cache_misses)
452
+ }
453
+
454
+ def cleanup(self):
455
+ """Clean up resources."""
456
+ self.executor.shutdown(wait=False)
457
+ self._compiled_templates.clear()
458
+
459
+
460
+ # Global loader instance
461
+ _global_loader: Optional[OptimizedAgentLoader] = None
462
+
463
+
464
+ def get_optimized_loader(max_workers: int = 4) -> OptimizedAgentLoader:
465
+ """Get or create global optimized loader."""
466
+ global _global_loader
467
+ if _global_loader is None:
468
+ _global_loader = OptimizedAgentLoader(max_workers=max_workers)
469
+ return _global_loader
470
+
471
+
472
+ def preload_system_agents():
473
+ """Preload all system agents at startup."""
474
+ loader = get_optimized_loader()
475
+
476
+ # Common agent directories
477
+ agent_dirs = [
478
+ Path.cwd() / ".claude" / "agents",
479
+ Path.cwd() / ".claude-mpm" / "agents",
480
+ Path.home() / ".claude-mpm" / "agents",
481
+ Path(__file__).parent.parent / "agents" / "templates",
482
+ Path(__file__).parent.parent / "agents"
483
+ ]
484
+
485
+ loader.preload_agents([d for d in agent_dirs if d.exists()])