claude-mpm 0.3.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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agent Registry Service - Consolidated Module
|
|
4
|
+
===========================================
|
|
5
|
+
|
|
6
|
+
Provides fully synchronous agent discovery and management system with caching,
|
|
7
|
+
validation, and hierarchical organization support.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Two-tier hierarchy discovery (user → system)
|
|
11
|
+
- Synchronous directory scanning
|
|
12
|
+
- Agent metadata collection and caching
|
|
13
|
+
- Agent type detection and classification
|
|
14
|
+
- SharedPromptCache integration
|
|
15
|
+
- Agent validation and error handling
|
|
16
|
+
|
|
17
|
+
This is a consolidated version combining all functionality from the previous
|
|
18
|
+
multi-file implementation for better maintainability.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
import hashlib
|
|
25
|
+
import logging
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Dict, List, Optional, Set, Tuple, Any, Union
|
|
28
|
+
from dataclasses import dataclass, field, asdict
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from enum import Enum
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# Constants and Types
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
CORE_AGENT_TYPES = {
|
|
40
|
+
'engineer', 'architect', 'qa', 'security', 'documentation',
|
|
41
|
+
'ops', 'data', 'research', 'version_control'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
SPECIALIZED_AGENT_TYPES = {
|
|
45
|
+
'pm_orchestrator', 'frontend', 'backend', 'devops', 'ml',
|
|
46
|
+
'database', 'api', 'mobile', 'cloud', 'testing'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ALL_AGENT_TYPES = CORE_AGENT_TYPES | SPECIALIZED_AGENT_TYPES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AgentTier(Enum):
|
|
53
|
+
"""Agent hierarchy tiers."""
|
|
54
|
+
USER = "user"
|
|
55
|
+
SYSTEM = "system"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AgentType(Enum):
|
|
59
|
+
"""Agent classification types."""
|
|
60
|
+
CORE = "core"
|
|
61
|
+
SPECIALIZED = "specialized"
|
|
62
|
+
CUSTOM = "custom"
|
|
63
|
+
UNKNOWN = "unknown"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Data Models
|
|
68
|
+
# ============================================================================
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class AgentMetadata:
|
|
72
|
+
"""Complete metadata for discovered agent."""
|
|
73
|
+
name: str
|
|
74
|
+
path: str
|
|
75
|
+
tier: AgentTier
|
|
76
|
+
agent_type: AgentType
|
|
77
|
+
description: str = ""
|
|
78
|
+
version: str = "0.0.0"
|
|
79
|
+
dependencies: List[str] = field(default_factory=list)
|
|
80
|
+
capabilities: List[str] = field(default_factory=list)
|
|
81
|
+
created_at: float = field(default_factory=time.time)
|
|
82
|
+
last_modified: float = field(default_factory=time.time)
|
|
83
|
+
file_size: int = 0
|
|
84
|
+
checksum: str = ""
|
|
85
|
+
is_valid: bool = True
|
|
86
|
+
validation_errors: List[str] = field(default_factory=list)
|
|
87
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
"""Convert to dictionary for serialization."""
|
|
91
|
+
data = asdict(self)
|
|
92
|
+
data['tier'] = self.tier.value
|
|
93
|
+
data['agent_type'] = self.agent_type.value
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'AgentMetadata':
|
|
98
|
+
"""Create from dictionary."""
|
|
99
|
+
data['tier'] = AgentTier(data['tier'])
|
|
100
|
+
data['agent_type'] = AgentType(data['agent_type'])
|
|
101
|
+
return cls(**data)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Main Registry Class
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
class AgentRegistry:
|
|
109
|
+
"""
|
|
110
|
+
Core Agent Registry - Fully synchronous agent discovery and management system.
|
|
111
|
+
|
|
112
|
+
This consolidated version combines all functionality from the previous
|
|
113
|
+
multi-file implementation into a single, maintainable module.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, cache_service=None, model_selector=None):
|
|
117
|
+
"""Initialize AgentRegistry with optional cache service and model selector."""
|
|
118
|
+
self.cache_service = cache_service
|
|
119
|
+
self.model_selector = model_selector
|
|
120
|
+
|
|
121
|
+
# Registry storage
|
|
122
|
+
self.registry: Dict[str, AgentMetadata] = {}
|
|
123
|
+
self.discovery_paths: List[Path] = []
|
|
124
|
+
|
|
125
|
+
# Cache configuration
|
|
126
|
+
self.cache_enabled = cache_service is not None
|
|
127
|
+
self.cache_ttl = 3600 # 1 hour
|
|
128
|
+
self.cache_prefix = "agent_registry"
|
|
129
|
+
|
|
130
|
+
# Discovery configuration
|
|
131
|
+
self.file_extensions = {'.md', '.json', '.yaml', '.yml'}
|
|
132
|
+
self.ignore_patterns = {'__pycache__', '.git', 'node_modules', '.pytest_cache'}
|
|
133
|
+
|
|
134
|
+
# Statistics
|
|
135
|
+
self.discovery_stats = {
|
|
136
|
+
'last_discovery': None,
|
|
137
|
+
'total_discovered': 0,
|
|
138
|
+
'cache_hits': 0,
|
|
139
|
+
'cache_misses': 0,
|
|
140
|
+
'discovery_duration': 0.0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Setup discovery paths
|
|
144
|
+
self._setup_discovery_paths()
|
|
145
|
+
|
|
146
|
+
logger.info(f"AgentRegistry initialized with cache={'enabled' if self.cache_enabled else 'disabled'}")
|
|
147
|
+
|
|
148
|
+
def _setup_discovery_paths(self) -> None:
|
|
149
|
+
"""Setup standard discovery paths for agent files."""
|
|
150
|
+
# User-level agents
|
|
151
|
+
user_path = Path.home() / '.claude-pm' / 'agents'
|
|
152
|
+
if user_path.exists():
|
|
153
|
+
self.discovery_paths.append(user_path)
|
|
154
|
+
|
|
155
|
+
# System-level agents - multiple possible locations
|
|
156
|
+
system_paths = [
|
|
157
|
+
Path(__file__).parent.parent / 'agents' / 'templates',
|
|
158
|
+
Path(__file__).parent.parent / 'framework' / 'agent-roles',
|
|
159
|
+
Path('/opt/claude-pm/agents'),
|
|
160
|
+
Path('/usr/local/claude-pm/agents')
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
for path in system_paths:
|
|
164
|
+
if path.exists():
|
|
165
|
+
self.discovery_paths.append(path)
|
|
166
|
+
|
|
167
|
+
logger.debug(f"Discovery paths configured: {[str(p) for p in self.discovery_paths]}")
|
|
168
|
+
|
|
169
|
+
# ========================================================================
|
|
170
|
+
# Discovery Methods
|
|
171
|
+
# ========================================================================
|
|
172
|
+
|
|
173
|
+
def discover_agents(self, force_refresh: bool = False) -> Dict[str, AgentMetadata]:
|
|
174
|
+
"""
|
|
175
|
+
Discover all available agents across configured paths.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
force_refresh: Force re-discovery even if cache is valid
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary of agent name to metadata
|
|
182
|
+
"""
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
|
|
185
|
+
# Try cache first
|
|
186
|
+
if not force_refresh and self.cache_enabled:
|
|
187
|
+
cached = self._get_cached_registry()
|
|
188
|
+
if cached:
|
|
189
|
+
self.registry = cached
|
|
190
|
+
self.discovery_stats['cache_hits'] += 1
|
|
191
|
+
logger.debug("Using cached agent registry")
|
|
192
|
+
return self.registry
|
|
193
|
+
|
|
194
|
+
self.discovery_stats['cache_misses'] += 1
|
|
195
|
+
|
|
196
|
+
# Clear existing registry
|
|
197
|
+
self.registry.clear()
|
|
198
|
+
|
|
199
|
+
# Discover agents from all paths
|
|
200
|
+
for discovery_path in self.discovery_paths:
|
|
201
|
+
tier = self._determine_tier(discovery_path)
|
|
202
|
+
self._discover_path(discovery_path, tier)
|
|
203
|
+
|
|
204
|
+
# Handle tier precedence
|
|
205
|
+
self._apply_tier_precedence()
|
|
206
|
+
|
|
207
|
+
# Cache the results
|
|
208
|
+
if self.cache_enabled:
|
|
209
|
+
self._cache_registry()
|
|
210
|
+
|
|
211
|
+
# Update statistics
|
|
212
|
+
self.discovery_stats['last_discovery'] = time.time()
|
|
213
|
+
self.discovery_stats['total_discovered'] = len(self.registry)
|
|
214
|
+
self.discovery_stats['discovery_duration'] = time.time() - start_time
|
|
215
|
+
|
|
216
|
+
logger.info(f"Discovered {len(self.registry)} agents in {self.discovery_stats['discovery_duration']:.2f}s")
|
|
217
|
+
|
|
218
|
+
return self.registry
|
|
219
|
+
|
|
220
|
+
def _discover_path(self, path: Path, tier: AgentTier) -> None:
|
|
221
|
+
"""Discover agents in a specific path."""
|
|
222
|
+
if not path.exists():
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
for file_path in path.rglob('*'):
|
|
226
|
+
# Skip directories and ignored patterns
|
|
227
|
+
if file_path.is_dir():
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if any(pattern in str(file_path) for pattern in self.ignore_patterns):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Check file extension
|
|
234
|
+
if file_path.suffix not in self.file_extensions:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Extract agent name
|
|
238
|
+
agent_name = self._extract_agent_name(file_path)
|
|
239
|
+
if not agent_name:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
# Create metadata
|
|
243
|
+
metadata = self._create_agent_metadata(file_path, agent_name, tier)
|
|
244
|
+
|
|
245
|
+
# Validate agent
|
|
246
|
+
if self._validate_agent(metadata):
|
|
247
|
+
# Check tier precedence
|
|
248
|
+
if agent_name in self.registry:
|
|
249
|
+
existing = self.registry[agent_name]
|
|
250
|
+
if self._has_tier_precedence(metadata.tier, existing.tier):
|
|
251
|
+
self.registry[agent_name] = metadata
|
|
252
|
+
logger.debug(f"Replaced {agent_name} with higher precedence version from {tier.value}")
|
|
253
|
+
else:
|
|
254
|
+
self.registry[agent_name] = metadata
|
|
255
|
+
|
|
256
|
+
def _extract_agent_name(self, file_path: Path) -> Optional[str]:
|
|
257
|
+
"""Extract agent name from file path."""
|
|
258
|
+
name = file_path.stem
|
|
259
|
+
|
|
260
|
+
# Remove common suffixes
|
|
261
|
+
suffixes_to_remove = ['_agent', '-agent', '.agent']
|
|
262
|
+
for suffix in suffixes_to_remove:
|
|
263
|
+
if name.endswith(suffix):
|
|
264
|
+
name = name[:-len(suffix)]
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
# Skip empty or invalid names
|
|
268
|
+
if not name or name.startswith('.'):
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
return name
|
|
272
|
+
|
|
273
|
+
def _create_agent_metadata(self, file_path: Path, agent_name: str, tier: AgentTier) -> AgentMetadata:
|
|
274
|
+
"""Create agent metadata from file."""
|
|
275
|
+
# Get file stats
|
|
276
|
+
stat = file_path.stat()
|
|
277
|
+
|
|
278
|
+
# Calculate checksum
|
|
279
|
+
checksum = ""
|
|
280
|
+
try:
|
|
281
|
+
with open(file_path, 'rb') as f:
|
|
282
|
+
checksum = hashlib.md5(f.read()).hexdigest()
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.warning(f"Failed to calculate checksum for {file_path}: {e}")
|
|
285
|
+
|
|
286
|
+
# Determine agent type
|
|
287
|
+
agent_type = self._classify_agent(agent_name)
|
|
288
|
+
|
|
289
|
+
# Extract description and metadata from file
|
|
290
|
+
description = ""
|
|
291
|
+
version = "0.0.0"
|
|
292
|
+
capabilities = []
|
|
293
|
+
metadata = {}
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
content = file_path.read_text()
|
|
297
|
+
|
|
298
|
+
# Try to parse as JSON/YAML for structured data
|
|
299
|
+
if file_path.suffix in ['.json', '.yaml', '.yml']:
|
|
300
|
+
try:
|
|
301
|
+
if file_path.suffix == '.json':
|
|
302
|
+
data = json.loads(content)
|
|
303
|
+
else:
|
|
304
|
+
import yaml
|
|
305
|
+
data = yaml.safe_load(content)
|
|
306
|
+
|
|
307
|
+
description = data.get('description', '')
|
|
308
|
+
version = data.get('version', '0.0.0')
|
|
309
|
+
capabilities = data.get('capabilities', [])
|
|
310
|
+
metadata = data.get('metadata', {})
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# Extract from markdown files
|
|
315
|
+
elif file_path.suffix == '.md':
|
|
316
|
+
lines = content.split('\n')
|
|
317
|
+
for i, line in enumerate(lines[:20]): # Check first 20 lines
|
|
318
|
+
if line.strip().startswith('#') and i == 0:
|
|
319
|
+
description = line.strip('#').strip()
|
|
320
|
+
elif line.startswith('Version:'):
|
|
321
|
+
version = line.split(':', 1)[1].strip()
|
|
322
|
+
elif line.startswith('Description:'):
|
|
323
|
+
description = line.split(':', 1)[1].strip()
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.warning(f"Failed to parse {file_path}: {e}")
|
|
327
|
+
|
|
328
|
+
return AgentMetadata(
|
|
329
|
+
name=agent_name,
|
|
330
|
+
path=str(file_path),
|
|
331
|
+
tier=tier,
|
|
332
|
+
agent_type=agent_type,
|
|
333
|
+
description=description,
|
|
334
|
+
version=version,
|
|
335
|
+
capabilities=capabilities,
|
|
336
|
+
created_at=stat.st_ctime,
|
|
337
|
+
last_modified=stat.st_mtime,
|
|
338
|
+
file_size=stat.st_size,
|
|
339
|
+
checksum=checksum,
|
|
340
|
+
metadata=metadata
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _classify_agent(self, agent_name: str) -> AgentType:
|
|
344
|
+
"""Classify agent based on name."""
|
|
345
|
+
name_lower = agent_name.lower()
|
|
346
|
+
|
|
347
|
+
# Remove common suffixes for classification
|
|
348
|
+
for suffix in ['_agent', '-agent', '.agent']:
|
|
349
|
+
if name_lower.endswith(suffix):
|
|
350
|
+
name_lower = name_lower[:-len(suffix)]
|
|
351
|
+
|
|
352
|
+
if name_lower in CORE_AGENT_TYPES:
|
|
353
|
+
return AgentType.CORE
|
|
354
|
+
elif name_lower in SPECIALIZED_AGENT_TYPES:
|
|
355
|
+
return AgentType.SPECIALIZED
|
|
356
|
+
elif any(core in name_lower for core in CORE_AGENT_TYPES):
|
|
357
|
+
return AgentType.CORE
|
|
358
|
+
elif any(spec in name_lower for spec in SPECIALIZED_AGENT_TYPES):
|
|
359
|
+
return AgentType.SPECIALIZED
|
|
360
|
+
else:
|
|
361
|
+
return AgentType.CUSTOM
|
|
362
|
+
|
|
363
|
+
def _determine_tier(self, path: Path) -> AgentTier:
|
|
364
|
+
"""Determine tier based on path location."""
|
|
365
|
+
path_str = str(path).lower()
|
|
366
|
+
|
|
367
|
+
if '.claude-pm' in path_str or str(Path.home()) in str(path):
|
|
368
|
+
return AgentTier.USER
|
|
369
|
+
else:
|
|
370
|
+
return AgentTier.SYSTEM
|
|
371
|
+
|
|
372
|
+
def _has_tier_precedence(self, tier1: AgentTier, tier2: AgentTier) -> bool:
|
|
373
|
+
"""Check if tier1 has precedence over tier2."""
|
|
374
|
+
precedence = {
|
|
375
|
+
AgentTier.USER: 2,
|
|
376
|
+
AgentTier.SYSTEM: 1
|
|
377
|
+
}
|
|
378
|
+
return precedence.get(tier1, 0) > precedence.get(tier2, 0)
|
|
379
|
+
|
|
380
|
+
def _apply_tier_precedence(self) -> None:
|
|
381
|
+
"""Apply tier precedence rules to discovered agents."""
|
|
382
|
+
# Group agents by name
|
|
383
|
+
agents_by_name: Dict[str, List[AgentMetadata]] = {}
|
|
384
|
+
|
|
385
|
+
for agent in self.registry.values():
|
|
386
|
+
if agent.name not in agents_by_name:
|
|
387
|
+
agents_by_name[agent.name] = []
|
|
388
|
+
agents_by_name[agent.name].append(agent)
|
|
389
|
+
|
|
390
|
+
# Apply precedence
|
|
391
|
+
self.registry.clear()
|
|
392
|
+
for agent_name, agents in agents_by_name.items():
|
|
393
|
+
if len(agents) == 1:
|
|
394
|
+
self.registry[agent_name] = agents[0]
|
|
395
|
+
else:
|
|
396
|
+
# Sort by tier precedence
|
|
397
|
+
agents.sort(key=lambda a: {AgentTier.USER: 2, AgentTier.SYSTEM: 1}.get(a.tier, 0), reverse=True)
|
|
398
|
+
self.registry[agent_name] = agents[0]
|
|
399
|
+
|
|
400
|
+
if len(agents) > 1:
|
|
401
|
+
logger.debug(f"Applied tier precedence for {agent_name}: using {agents[0].tier.value} version")
|
|
402
|
+
|
|
403
|
+
# ========================================================================
|
|
404
|
+
# Validation Methods
|
|
405
|
+
# ========================================================================
|
|
406
|
+
|
|
407
|
+
def _validate_agent(self, metadata: AgentMetadata) -> bool:
|
|
408
|
+
"""Validate agent metadata and file."""
|
|
409
|
+
errors = []
|
|
410
|
+
|
|
411
|
+
# Check file exists
|
|
412
|
+
if not Path(metadata.path).exists():
|
|
413
|
+
errors.append("Agent file does not exist")
|
|
414
|
+
|
|
415
|
+
# Check name validity
|
|
416
|
+
if not metadata.name or metadata.name.startswith('.'):
|
|
417
|
+
errors.append("Invalid agent name")
|
|
418
|
+
|
|
419
|
+
# Check for required fields based on file type
|
|
420
|
+
if metadata.path.endswith('.json'):
|
|
421
|
+
try:
|
|
422
|
+
with open(metadata.path) as f:
|
|
423
|
+
data = json.load(f)
|
|
424
|
+
if 'name' not in data:
|
|
425
|
+
errors.append("Missing 'name' field in JSON")
|
|
426
|
+
if 'role' not in data:
|
|
427
|
+
errors.append("Missing 'role' field in JSON")
|
|
428
|
+
except Exception as e:
|
|
429
|
+
errors.append(f"Invalid JSON: {e}")
|
|
430
|
+
|
|
431
|
+
# Update metadata
|
|
432
|
+
metadata.is_valid = len(errors) == 0
|
|
433
|
+
metadata.validation_errors = errors
|
|
434
|
+
|
|
435
|
+
return metadata.is_valid
|
|
436
|
+
|
|
437
|
+
# ========================================================================
|
|
438
|
+
# Cache Methods
|
|
439
|
+
# ========================================================================
|
|
440
|
+
|
|
441
|
+
def _get_cached_registry(self) -> Optional[Dict[str, AgentMetadata]]:
|
|
442
|
+
"""Get registry from cache if available."""
|
|
443
|
+
if not self.cache_service:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
cache_key = f"{self.cache_prefix}_registry"
|
|
448
|
+
cached_data = self.cache_service.get(cache_key)
|
|
449
|
+
|
|
450
|
+
if cached_data:
|
|
451
|
+
# Deserialize metadata
|
|
452
|
+
registry = {}
|
|
453
|
+
for name, data in cached_data.items():
|
|
454
|
+
registry[name] = AgentMetadata.from_dict(data)
|
|
455
|
+
return registry
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
logger.warning(f"Failed to get cached registry: {e}")
|
|
459
|
+
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
def _cache_registry(self) -> None:
|
|
463
|
+
"""Cache the current registry."""
|
|
464
|
+
if not self.cache_service:
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
cache_key = f"{self.cache_prefix}_registry"
|
|
469
|
+
# Serialize metadata
|
|
470
|
+
cache_data = {
|
|
471
|
+
name: metadata.to_dict()
|
|
472
|
+
for name, metadata in self.registry.items()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
self.cache_service.set(cache_key, cache_data, ttl=self.cache_ttl)
|
|
476
|
+
logger.debug("Cached agent registry")
|
|
477
|
+
|
|
478
|
+
except Exception as e:
|
|
479
|
+
logger.warning(f"Failed to cache registry: {e}")
|
|
480
|
+
|
|
481
|
+
def invalidate_cache(self) -> None:
|
|
482
|
+
"""Invalidate the registry cache."""
|
|
483
|
+
if self.cache_service:
|
|
484
|
+
try:
|
|
485
|
+
cache_key = f"{self.cache_prefix}_registry"
|
|
486
|
+
self.cache_service.delete(cache_key)
|
|
487
|
+
logger.debug("Invalidated registry cache")
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.warning(f"Failed to invalidate cache: {e}")
|
|
490
|
+
|
|
491
|
+
# ========================================================================
|
|
492
|
+
# Query Methods
|
|
493
|
+
# ========================================================================
|
|
494
|
+
|
|
495
|
+
def get_agent(self, name: str) -> Optional[AgentMetadata]:
|
|
496
|
+
"""Get metadata for a specific agent."""
|
|
497
|
+
# Ensure registry is populated
|
|
498
|
+
if not self.registry:
|
|
499
|
+
self.discover_agents()
|
|
500
|
+
|
|
501
|
+
return self.registry.get(name)
|
|
502
|
+
|
|
503
|
+
def list_agents(self, tier: Optional[AgentTier] = None,
|
|
504
|
+
agent_type: Optional[AgentType] = None) -> List[AgentMetadata]:
|
|
505
|
+
"""List agents with optional filtering."""
|
|
506
|
+
# Ensure registry is populated
|
|
507
|
+
if not self.registry:
|
|
508
|
+
self.discover_agents()
|
|
509
|
+
|
|
510
|
+
agents = list(self.registry.values())
|
|
511
|
+
|
|
512
|
+
# Apply filters
|
|
513
|
+
if tier:
|
|
514
|
+
agents = [a for a in agents if a.tier == tier]
|
|
515
|
+
|
|
516
|
+
if agent_type:
|
|
517
|
+
agents = [a for a in agents if a.agent_type == agent_type]
|
|
518
|
+
|
|
519
|
+
return agents
|
|
520
|
+
|
|
521
|
+
def get_agent_names(self) -> List[str]:
|
|
522
|
+
"""Get list of all agent names."""
|
|
523
|
+
if not self.registry:
|
|
524
|
+
self.discover_agents()
|
|
525
|
+
|
|
526
|
+
return sorted(self.registry.keys())
|
|
527
|
+
|
|
528
|
+
def get_core_agents(self) -> List[AgentMetadata]:
|
|
529
|
+
"""Get all core framework agents."""
|
|
530
|
+
return self.list_agents(agent_type=AgentType.CORE)
|
|
531
|
+
|
|
532
|
+
def get_specialized_agents(self) -> List[AgentMetadata]:
|
|
533
|
+
"""Get all specialized agents."""
|
|
534
|
+
return self.list_agents(agent_type=AgentType.SPECIALIZED)
|
|
535
|
+
|
|
536
|
+
def get_custom_agents(self) -> List[AgentMetadata]:
|
|
537
|
+
"""Get all custom user-defined agents."""
|
|
538
|
+
return self.list_agents(agent_type=AgentType.CUSTOM)
|
|
539
|
+
|
|
540
|
+
def search_agents(self, query: str) -> List[AgentMetadata]:
|
|
541
|
+
"""Search agents by name or description."""
|
|
542
|
+
if not self.registry:
|
|
543
|
+
self.discover_agents()
|
|
544
|
+
|
|
545
|
+
query_lower = query.lower()
|
|
546
|
+
results = []
|
|
547
|
+
|
|
548
|
+
for agent in self.registry.values():
|
|
549
|
+
if (query_lower in agent.name.lower() or
|
|
550
|
+
query_lower in agent.description.lower()):
|
|
551
|
+
results.append(agent)
|
|
552
|
+
|
|
553
|
+
return results
|
|
554
|
+
|
|
555
|
+
# ========================================================================
|
|
556
|
+
# Statistics and Monitoring
|
|
557
|
+
# ========================================================================
|
|
558
|
+
|
|
559
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
560
|
+
"""Get comprehensive registry statistics."""
|
|
561
|
+
if not self.registry:
|
|
562
|
+
self.discover_agents()
|
|
563
|
+
|
|
564
|
+
stats = {
|
|
565
|
+
'total_agents': len(self.registry),
|
|
566
|
+
'discovery_stats': self.discovery_stats.copy(),
|
|
567
|
+
'agents_by_tier': {},
|
|
568
|
+
'agents_by_type': {},
|
|
569
|
+
'validation_stats': {
|
|
570
|
+
'valid': 0,
|
|
571
|
+
'invalid': 0,
|
|
572
|
+
'errors': []
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# Count by tier
|
|
577
|
+
for agent in self.registry.values():
|
|
578
|
+
tier = agent.tier.value
|
|
579
|
+
stats['agents_by_tier'][tier] = stats['agents_by_tier'].get(tier, 0) + 1
|
|
580
|
+
|
|
581
|
+
# Count by type
|
|
582
|
+
for agent in self.registry.values():
|
|
583
|
+
agent_type = agent.agent_type.value
|
|
584
|
+
stats['agents_by_type'][agent_type] = stats['agents_by_type'].get(agent_type, 0) + 1
|
|
585
|
+
|
|
586
|
+
# Validation stats
|
|
587
|
+
for agent in self.registry.values():
|
|
588
|
+
if agent.is_valid:
|
|
589
|
+
stats['validation_stats']['valid'] += 1
|
|
590
|
+
else:
|
|
591
|
+
stats['validation_stats']['invalid'] += 1
|
|
592
|
+
stats['validation_stats']['errors'].extend(agent.validation_errors)
|
|
593
|
+
|
|
594
|
+
return stats
|
|
595
|
+
|
|
596
|
+
def validate_all_agents(self) -> Dict[str, List[str]]:
|
|
597
|
+
"""Validate all discovered agents and return errors."""
|
|
598
|
+
if not self.registry:
|
|
599
|
+
self.discover_agents()
|
|
600
|
+
|
|
601
|
+
errors = {}
|
|
602
|
+
|
|
603
|
+
for agent_name, metadata in self.registry.items():
|
|
604
|
+
# Re-validate
|
|
605
|
+
self._validate_agent(metadata)
|
|
606
|
+
|
|
607
|
+
if not metadata.is_valid:
|
|
608
|
+
errors[agent_name] = metadata.validation_errors
|
|
609
|
+
|
|
610
|
+
return errors
|
|
611
|
+
|
|
612
|
+
# ========================================================================
|
|
613
|
+
# Utility Methods
|
|
614
|
+
# ========================================================================
|
|
615
|
+
|
|
616
|
+
def add_discovery_path(self, path: Union[str, Path]) -> None:
|
|
617
|
+
"""Add a new path for agent discovery."""
|
|
618
|
+
path = Path(path)
|
|
619
|
+
if path.exists() and path not in self.discovery_paths:
|
|
620
|
+
self.discovery_paths.append(path)
|
|
621
|
+
logger.info(f"Added discovery path: {path}")
|
|
622
|
+
# Invalidate cache since paths changed
|
|
623
|
+
self.invalidate_cache()
|
|
624
|
+
|
|
625
|
+
def remove_discovery_path(self, path: Union[str, Path]) -> None:
|
|
626
|
+
"""Remove a path from agent discovery."""
|
|
627
|
+
path = Path(path)
|
|
628
|
+
if path in self.discovery_paths:
|
|
629
|
+
self.discovery_paths.remove(path)
|
|
630
|
+
logger.info(f"Removed discovery path: {path}")
|
|
631
|
+
# Invalidate cache since paths changed
|
|
632
|
+
self.invalidate_cache()
|
|
633
|
+
|
|
634
|
+
def export_registry(self, output_path: Union[str, Path]) -> None:
|
|
635
|
+
"""Export registry to JSON file."""
|
|
636
|
+
if not self.registry:
|
|
637
|
+
self.discover_agents()
|
|
638
|
+
|
|
639
|
+
output_path = Path(output_path)
|
|
640
|
+
|
|
641
|
+
# Serialize registry
|
|
642
|
+
export_data = {
|
|
643
|
+
'metadata': {
|
|
644
|
+
'exported_at': time.time(),
|
|
645
|
+
'total_agents': len(self.registry),
|
|
646
|
+
'discovery_paths': [str(p) for p in self.discovery_paths]
|
|
647
|
+
},
|
|
648
|
+
'agents': {
|
|
649
|
+
name: metadata.to_dict()
|
|
650
|
+
for name, metadata in self.registry.items()
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
with open(output_path, 'w') as f:
|
|
655
|
+
json.dump(export_data, f, indent=2)
|
|
656
|
+
|
|
657
|
+
logger.info(f"Exported registry to {output_path}")
|
|
658
|
+
|
|
659
|
+
def import_registry(self, input_path: Union[str, Path]) -> None:
|
|
660
|
+
"""Import registry from JSON file."""
|
|
661
|
+
input_path = Path(input_path)
|
|
662
|
+
|
|
663
|
+
with open(input_path, 'r') as f:
|
|
664
|
+
data = json.load(f)
|
|
665
|
+
|
|
666
|
+
# Clear current registry
|
|
667
|
+
self.registry.clear()
|
|
668
|
+
|
|
669
|
+
# Import agents
|
|
670
|
+
for name, agent_data in data.get('agents', {}).items():
|
|
671
|
+
self.registry[name] = AgentMetadata.from_dict(agent_data)
|
|
672
|
+
|
|
673
|
+
# Cache imported registry
|
|
674
|
+
if self.cache_enabled:
|
|
675
|
+
self._cache_registry()
|
|
676
|
+
|
|
677
|
+
logger.info(f"Imported {len(self.registry)} agents from {input_path}")
|