claude-mpm 1.1.0__py3-none-any.whl → 2.0.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 (29) hide show
  1. claude_mpm/_version.py +3 -2
  2. claude_mpm/agents/INSTRUCTIONS.md +117 -312
  3. claude_mpm/agents/agent_loader.py +184 -278
  4. claude_mpm/agents/base_agent.json +1 -1
  5. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +46 -0
  6. claude_mpm/agents/templates/{engineer_agent.json → backup/engineer_agent_20250726_234551.json} +1 -1
  7. claude_mpm/agents/templates/data_engineer.json +107 -0
  8. claude_mpm/agents/templates/documentation.json +106 -0
  9. claude_mpm/agents/templates/engineer.json +110 -0
  10. claude_mpm/agents/templates/ops.json +106 -0
  11. claude_mpm/agents/templates/qa.json +106 -0
  12. claude_mpm/agents/templates/research.json +107 -0
  13. claude_mpm/agents/templates/security.json +105 -0
  14. claude_mpm/agents/templates/version_control.json +103 -0
  15. claude_mpm/schemas/agent_schema.json +328 -0
  16. claude_mpm/validation/agent_validator.py +252 -125
  17. {claude_mpm-1.1.0.dist-info → claude_mpm-2.0.0.dist-info}/METADATA +100 -26
  18. {claude_mpm-1.1.0.dist-info → claude_mpm-2.0.0.dist-info}/RECORD +27 -19
  19. claude_mpm/agents/templates/data_engineer_agent.json +0 -46
  20. claude_mpm/agents/templates/update-optimized-specialized-agents.json +0 -374
  21. /claude_mpm/agents/templates/{documentation_agent.json → backup/documentation_agent_20250726_234551.json} +0 -0
  22. /claude_mpm/agents/templates/{ops_agent.json → backup/ops_agent_20250726_234551.json} +0 -0
  23. /claude_mpm/agents/templates/{qa_agent.json → backup/qa_agent_20250726_234551.json} +0 -0
  24. /claude_mpm/agents/templates/{research_agent.json → backup/research_agent_20250726_234551.json} +0 -0
  25. /claude_mpm/agents/templates/{security_agent.json → backup/security_agent_20250726_234551.json} +0 -0
  26. /claude_mpm/agents/templates/{version_control_agent.json → backup/version_control_agent_20250726_234551.json} +0 -0
  27. {claude_mpm-1.1.0.dist-info → claude_mpm-2.0.0.dist-info}/WHEEL +0 -0
  28. {claude_mpm-1.1.0.dist-info → claude_mpm-2.0.0.dist-info}/entry_points.txt +0 -0
  29. {claude_mpm-1.1.0.dist-info → claude_mpm-2.0.0.dist-info}/top_level.txt +0 -0
@@ -3,19 +3,15 @@
3
3
  Unified Agent Loader System
4
4
  ==========================
5
5
 
6
- Provides unified loading of agent prompts from JSON template files.
7
- Integrates with SharedPromptCache for performance optimization.
6
+ Provides unified loading of agent prompts from JSON template files using
7
+ the new standardized schema format.
8
8
 
9
9
  Key Features:
10
10
  - Loads agent prompts from src/claude_mpm/agents/templates/*.json files
11
11
  - Handles base_agent.md prepending
12
12
  - Provides backward-compatible get_*_agent_prompt() functions
13
13
  - Uses SharedPromptCache for performance
14
- - Special handling for ticketing agent's dynamic CLI help
15
-
16
- For advanced agent management features (CRUD, versioning, section updates), use:
17
- from claude_pm.agents.agent_loader_integration import get_enhanced_loader
18
- from claude_pm.services.agent_management_service import AgentManager
14
+ - Validates agents against schema before loading
19
15
 
20
16
  Usage:
21
17
  from claude_pm.agents.agent_loader import get_documentation_agent_prompt
@@ -24,14 +20,15 @@ Usage:
24
20
  prompt = get_documentation_agent_prompt()
25
21
  """
26
22
 
23
+ import json
27
24
  import logging
28
25
  import os
29
26
  from pathlib import Path
30
- from typing import Optional, Dict, Any, Tuple, Union
27
+ from typing import Optional, Dict, Any, Tuple, Union, List
31
28
 
32
29
  from ..services.shared_prompt_cache import SharedPromptCache
33
30
  from .base_agent_loader import prepend_base_instructions
34
- # from ..services.task_complexity_analyzer import TaskComplexityAnalyzer, ComplexityLevel, ModelType
31
+ from ..validation.agent_validator import AgentValidator, ValidationResult
35
32
  from ..utils.paths import PathResolver
36
33
 
37
34
  # Temporary placeholders for missing module
@@ -51,7 +48,6 @@ logger = logging.getLogger(__name__)
51
48
 
52
49
  def _get_agent_templates_dir() -> Path:
53
50
  """Get the agent templates directory."""
54
- # Agent templates are now in the agents/templates directory
55
51
  return Path(__file__).parent / "templates"
56
52
 
57
53
 
@@ -59,20 +55,7 @@ def _get_agent_templates_dir() -> Path:
59
55
  AGENT_TEMPLATES_DIR = _get_agent_templates_dir()
60
56
 
61
57
  # Cache prefix for agent prompts
62
- AGENT_CACHE_PREFIX = "agent_prompt:"
63
-
64
- # Agent name mappings (agent name -> JSON file name)
65
- AGENT_MAPPINGS = {
66
- "documentation": "documentation_agent.json",
67
- "version_control": "version_control_agent.json",
68
- "qa": "qa_agent.json",
69
- "research": "research_agent.json",
70
- "ops": "ops_agent.json",
71
- "security": "security_agent.json",
72
- "engineer": "engineer_agent.json",
73
- "data_engineer": "data_engineer_agent.json"
74
- # Note: pm, orchestrator, and pm_orchestrator agents are handled separately
75
- }
58
+ AGENT_CACHE_PREFIX = "agent_prompt:v2:"
76
59
 
77
60
  # Model configuration thresholds
78
61
  MODEL_THRESHOLDS = {
@@ -81,99 +64,138 @@ MODEL_THRESHOLDS = {
81
64
  ModelType.OPUS: {"min_complexity": 71, "max_complexity": 100}
82
65
  }
83
66
 
84
- # Default model for each agent type (fallback when dynamic selection is disabled)
85
- DEFAULT_AGENT_MODELS = {
86
- 'orchestrator': 'claude-4-opus',
87
- 'pm': 'claude-4-opus',
88
- 'pm_orchestrator': 'claude-4-opus',
89
- 'engineer': 'claude-4-opus',
90
- 'architecture': 'claude-4-opus',
91
- 'documentation': 'claude-sonnet-4-20250514',
92
- 'version_control': 'claude-sonnet-4-20250514',
93
- 'qa': 'claude-sonnet-4-20250514',
94
- 'research': 'claude-sonnet-4-20250514',
95
- 'ops': 'claude-sonnet-4-20250514',
96
- 'security': 'claude-sonnet-4-20250514',
97
- 'data_engineer': 'claude-sonnet-4-20250514'
98
- }
99
-
100
- # Model name mappings for Claude API
67
+ # Model name mappings for Claude API (updated for new schema)
101
68
  MODEL_NAME_MAPPINGS = {
102
69
  ModelType.HAIKU: "claude-3-haiku-20240307",
103
70
  ModelType.SONNET: "claude-sonnet-4-20250514",
104
- ModelType.OPUS: "claude-4-opus"
71
+ ModelType.OPUS: "claude-opus-4-20250514"
105
72
  }
106
73
 
107
74
 
108
- def load_agent_prompt_from_md(agent_name: str, force_reload: bool = False) -> Optional[str]:
109
- """
110
- Load agent prompt from JSON template file.
75
+ class AgentLoader:
76
+ """Loads and manages agent templates with schema validation."""
111
77
 
112
- Args:
113
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
114
- force_reload: Force reload from file, bypassing cache
78
+ def __init__(self):
79
+ """Initialize the agent loader."""
80
+ self.validator = AgentValidator()
81
+ self.cache = SharedPromptCache.get_instance()
82
+ self._agent_registry: Dict[str, Dict[str, Any]] = {}
83
+ self._load_agents()
84
+
85
+ def _load_agents(self) -> None:
86
+ """Load all valid agents from the templates directory."""
87
+ logger.info(f"Loading agents from {AGENT_TEMPLATES_DIR}")
115
88
 
116
- Returns:
117
- str: Agent prompt content from JSON template, or None if not found
118
- """
119
- try:
120
- # Get cache instance
121
- cache = SharedPromptCache.get_instance()
122
- cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:json"
89
+ for json_file in AGENT_TEMPLATES_DIR.glob("*.json"):
90
+ if json_file.name == "agent_schema.json":
91
+ continue
92
+
93
+ try:
94
+ with open(json_file, 'r') as f:
95
+ agent_data = json.load(f)
96
+
97
+ # Validate against schema
98
+ validation_result = self.validator.validate_agent(agent_data)
99
+
100
+ if validation_result.is_valid:
101
+ agent_id = agent_data.get("id")
102
+ if agent_id:
103
+ self._agent_registry[agent_id] = agent_data
104
+ logger.debug(f"Loaded agent: {agent_id}")
105
+ else:
106
+ logger.warning(f"Invalid agent in {json_file.name}: {validation_result.errors}")
107
+
108
+ except Exception as e:
109
+ logger.error(f"Failed to load {json_file.name}: {e}")
110
+
111
+ def get_agent(self, agent_id: str) -> Optional[Dict[str, Any]]:
112
+ """Get agent data by ID."""
113
+ return self._agent_registry.get(agent_id)
114
+
115
+ def list_agents(self) -> List[Dict[str, Any]]:
116
+ """List all available agents."""
117
+ agents = []
118
+ for agent_id, agent_data in self._agent_registry.items():
119
+ agents.append({
120
+ "id": agent_id,
121
+ "name": agent_data.get("metadata", {}).get("name", agent_id),
122
+ "description": agent_data.get("metadata", {}).get("description", ""),
123
+ "category": agent_data.get("metadata", {}).get("category", ""),
124
+ "model": agent_data.get("capabilities", {}).get("model", ""),
125
+ "resource_tier": agent_data.get("capabilities", {}).get("resource_tier", "")
126
+ })
127
+ return sorted(agents, key=lambda x: x["id"])
128
+
129
+ def get_agent_prompt(self, agent_id: str, force_reload: bool = False) -> Optional[str]:
130
+ """Get agent instructions by ID."""
131
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_id}"
123
132
 
124
- # Check cache first (unless force reload)
133
+ # Check cache first
125
134
  if not force_reload:
126
- cached_content = cache.get(cache_key)
135
+ cached_content = self.cache.get(cache_key)
127
136
  if cached_content is not None:
128
- logger.debug(f"Agent prompt for '{agent_name}' loaded from cache")
137
+ logger.debug(f"Agent prompt for '{agent_id}' loaded from cache")
129
138
  return str(cached_content)
130
139
 
131
- # Get JSON file path
132
- json_filename = AGENT_MAPPINGS.get(agent_name)
133
- if not json_filename:
134
- logger.warning(f"No JSON file mapping found for agent: {agent_name}")
135
- return None
136
-
137
- json_path = AGENT_TEMPLATES_DIR / json_filename
138
-
139
- # Check if file exists
140
- if not json_path.exists():
141
- logger.warning(f"Agent JSON file not found: {json_path}")
140
+ # Get agent data
141
+ agent_data = self.get_agent(agent_id)
142
+ if not agent_data:
143
+ logger.warning(f"Agent not found: {agent_id}")
142
144
  return None
143
-
144
- logger.debug(f"Loading agent prompt from: {json_path}")
145
-
146
- # Load and parse JSON
147
- import json
148
- with open(json_path, 'r', encoding='utf-8') as f:
149
- data = json.load(f)
150
-
151
- # Extract prompt content from JSON
152
- # Check multiple possible locations for instructions/content
153
- # Following the same pattern as AgentDeploymentService
154
- content = (
155
- data.get('instructions') or
156
- data.get('narrative_fields', {}).get('instructions') or
157
- data.get('content') or
158
- ''
159
- )
160
145
 
161
- if not content:
162
- logger.warning(f"No content/instructions found in JSON template: {json_path}")
163
- logger.debug(f"Checked fields: instructions, narrative_fields.instructions, content")
146
+ # Extract instructions
147
+ instructions = agent_data.get("instructions", "")
148
+ if not instructions:
149
+ logger.warning(f"No instructions found for agent: {agent_id}")
164
150
  return None
165
151
 
166
152
  # Cache the content with 1 hour TTL
167
- cache.set(cache_key, content, ttl=3600)
168
- logger.debug(f"Agent prompt for '{agent_name}' cached successfully")
153
+ self.cache.set(cache_key, instructions, ttl=3600)
154
+ logger.debug(f"Agent prompt for '{agent_id}' cached successfully")
169
155
 
170
- return content
156
+ return instructions
157
+
158
+ def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]:
159
+ """Get agent metadata including capabilities and configuration."""
160
+ agent_data = self.get_agent(agent_id)
161
+ if not agent_data:
162
+ return None
171
163
 
172
- except Exception as e:
173
- logger.error(f"Error loading agent prompt from JSON for '{agent_name}': {e}")
174
- return None
164
+ return {
165
+ "id": agent_id,
166
+ "version": agent_data.get("version", "1.0.0"),
167
+ "metadata": agent_data.get("metadata", {}),
168
+ "capabilities": agent_data.get("capabilities", {}),
169
+ "knowledge": agent_data.get("knowledge", {}),
170
+ "interactions": agent_data.get("interactions", {})
171
+ }
172
+
173
+
174
+ # Global loader instance
175
+ _loader: Optional[AgentLoader] = None
175
176
 
176
177
 
178
+ def _get_loader() -> AgentLoader:
179
+ """Get or create the global agent loader instance."""
180
+ global _loader
181
+ if _loader is None:
182
+ _loader = AgentLoader()
183
+ return _loader
184
+
185
+
186
+ def load_agent_prompt_from_md(agent_name: str, force_reload: bool = False) -> Optional[str]:
187
+ """
188
+ Load agent prompt from new schema JSON template.
189
+
190
+ Args:
191
+ agent_name: Agent name (matches agent ID in new schema)
192
+ force_reload: Force reload from file, bypassing cache
193
+
194
+ Returns:
195
+ str: Agent instructions from JSON template, or None if not found
196
+ """
197
+ loader = _get_loader()
198
+ return loader.get_agent_prompt(agent_name, force_reload)
177
199
 
178
200
 
179
201
  def _analyze_task_complexity(task_description: str, context_size: int = 0, **kwargs: Any) -> Dict[str, Any]:
@@ -210,13 +232,19 @@ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, A
210
232
  Returns:
211
233
  Tuple of (selected_model, model_config)
212
234
  """
235
+ loader = _get_loader()
236
+ agent_data = loader.get_agent(agent_name)
237
+
238
+ if not agent_data:
239
+ # Fallback for unknown agents
240
+ return "claude-sonnet-4-20250514", {"selection_method": "default"}
241
+
242
+ # Get model from agent capabilities
243
+ default_model = agent_data.get("capabilities", {}).get("model", "claude-sonnet-4-20250514")
244
+
213
245
  # Check if dynamic model selection is enabled
214
246
  enable_dynamic_selection = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
215
247
 
216
- # Debug logging
217
- logger.debug(f"Environment ENABLE_DYNAMIC_MODEL_SELECTION: {os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION')}")
218
- logger.debug(f"Enable dynamic selection: {enable_dynamic_selection}")
219
-
220
248
  # Check for per-agent override in environment
221
249
  agent_override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
222
250
  agent_override = os.getenv(agent_override_key, '').lower()
@@ -226,44 +254,24 @@ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, A
226
254
  elif agent_override == 'false':
227
255
  enable_dynamic_selection = False
228
256
 
229
- # Log model selection decision
230
- logger.info(f"Model selection for {agent_name}: dynamic={enable_dynamic_selection}, "
231
- f"complexity_available={complexity_analysis is not None}")
232
-
233
257
  # Dynamic model selection based on complexity
234
258
  if enable_dynamic_selection and complexity_analysis:
235
259
  recommended_model = complexity_analysis.get('recommended_model', ModelType.SONNET)
236
- selected_model = MODEL_NAME_MAPPINGS.get(recommended_model, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'))
260
+ selected_model = MODEL_NAME_MAPPINGS.get(recommended_model, default_model)
237
261
 
238
262
  model_config = {
239
263
  "selection_method": "dynamic_complexity_based",
240
264
  "complexity_score": complexity_analysis.get('complexity_score', 50),
241
265
  "complexity_level": complexity_analysis.get('complexity_level', ComplexityLevel.MEDIUM).value,
242
266
  "optimal_prompt_size": complexity_analysis.get('optimal_prompt_size', (700, 1000)),
243
- "scoring_breakdown": complexity_analysis.get('scoring_breakdown', {}),
244
- "analysis_details": complexity_analysis.get('analysis_details', {})
267
+ "default_model": default_model
245
268
  }
246
-
247
- # Log metrics
248
- logger.info(f"Dynamic model selection for {agent_name}: "
249
- f"model={selected_model}, "
250
- f"complexity_score={model_config['complexity_score']}, "
251
- f"complexity_level={model_config['complexity_level']}")
252
-
253
- # Track model selection metrics
254
- log_model_selection(
255
- agent_name=agent_name,
256
- selected_model=selected_model,
257
- complexity_score=model_config['complexity_score'],
258
- selection_method=model_config['selection_method']
259
- )
260
-
261
269
  else:
262
- # Use default model mapping
263
- selected_model = DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
270
+ selected_model = default_model
264
271
  model_config = {
265
- "selection_method": "default_mapping",
266
- "reason": "dynamic_selection_disabled" if not enable_dynamic_selection else "no_complexity_analysis"
272
+ "selection_method": "agent_default",
273
+ "reason": "dynamic_selection_disabled" if not enable_dynamic_selection else "no_complexity_analysis",
274
+ "default_model": default_model
267
275
  }
268
276
 
269
277
  return selected_model, model_config
@@ -274,24 +282,23 @@ def get_agent_prompt(agent_name: str, force_reload: bool = False, return_model_i
274
282
  Get agent prompt from JSON template with optional dynamic model selection.
275
283
 
276
284
  Args:
277
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
285
+ agent_name: Agent name (agent ID in new schema)
278
286
  force_reload: Force reload from source, bypassing cache
279
287
  return_model_info: If True, returns tuple (prompt, model, config)
280
288
  **kwargs: Additional arguments including:
281
289
  - task_description: Description of the task for complexity analysis
282
290
  - context_size: Size of context for complexity analysis
283
291
  - enable_complexity_analysis: Override for complexity analysis
284
- - Additional complexity factors (file_count, integration_points, etc.)
285
292
 
286
293
  Returns:
287
294
  str or tuple: Complete agent prompt with base instructions prepended,
288
295
  or tuple of (prompt, selected_model, model_config) if return_model_info=True
289
296
  """
290
- # Load from JSON template
297
+ # Load from new schema JSON template
291
298
  prompt = load_agent_prompt_from_md(agent_name, force_reload)
292
299
 
293
300
  if prompt is None:
294
- raise ValueError(f"No agent prompt JSON template found for: {agent_name}")
301
+ raise ValueError(f"No agent found with ID: {agent_name}")
295
302
 
296
303
  # Analyze task complexity if task description is provided
297
304
  complexity_analysis = None
@@ -299,38 +306,15 @@ def get_agent_prompt(agent_name: str, force_reload: bool = False, return_model_i
299
306
  enable_analysis = kwargs.get('enable_complexity_analysis', True)
300
307
 
301
308
  if task_description and enable_analysis:
302
- # Remove already specified parameters from kwargs to avoid duplicates
303
- analysis_kwargs = {k: v for k, v in kwargs.items()
304
- if k not in ['task_description', 'context_size']}
305
309
  complexity_analysis = _analyze_task_complexity(
306
310
  task_description=task_description,
307
311
  context_size=kwargs.get('context_size', 0),
308
- **analysis_kwargs
312
+ **{k: v for k, v in kwargs.items() if k not in ['task_description', 'context_size']}
309
313
  )
310
314
 
311
- # Get model configuration (always happens, even without complexity analysis)
315
+ # Get model configuration
312
316
  selected_model, model_config = _get_model_config(agent_name, complexity_analysis)
313
317
 
314
- # Always store model selection info in kwargs for potential use by callers
315
- kwargs['_selected_model'] = selected_model
316
- kwargs['_model_config'] = model_config
317
-
318
- # Handle dynamic template formatting if needed
319
- if "{dynamic_help}" in prompt:
320
- try:
321
- # Import CLI helper module to get dynamic help
322
- from ..orchestration.ai_trackdown_tools import CLIHelpFormatter
323
-
324
- # Create a CLI helper instance
325
- cli_helper = CLIHelpFormatter()
326
- help_content, _ = cli_helper.get_cli_help()
327
- dynamic_help = cli_helper.format_help_for_prompt(help_content)
328
- prompt = prompt.format(dynamic_help=dynamic_help)
329
- except Exception as e:
330
- logger.warning(f"Could not format dynamic help for ticketing agent: {e}")
331
- # Remove the placeholder if we can't fill it
332
- prompt = prompt.replace("{dynamic_help}", "")
333
-
334
318
  # Add model selection metadata to prompt if dynamic selection is enabled
335
319
  if selected_model and model_config.get('selection_method') == 'dynamic_complexity_based':
336
320
  model_metadata = f"\n<!-- Model Selection: {selected_model} (Complexity: {model_config.get('complexity_level', 'UNKNOWN')}) -->\n"
@@ -355,7 +339,6 @@ def get_documentation_agent_prompt() -> str:
355
339
  return prompt
356
340
 
357
341
 
358
-
359
342
  def get_version_control_agent_prompt() -> str:
360
343
  """Get the complete Version Control Agent prompt with base instructions."""
361
344
  prompt = get_agent_prompt("version_control", return_model_info=False)
@@ -410,43 +393,54 @@ def get_agent_prompt_with_model_info(agent_name: str, force_reload: bool = False
410
393
  Get agent prompt with model selection information.
411
394
 
412
395
  Args:
413
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
396
+ agent_name: Agent name (agent ID)
414
397
  force_reload: Force reload from source, bypassing cache
415
398
  **kwargs: Additional arguments for prompt generation and model selection
416
399
 
417
400
  Returns:
418
401
  Tuple of (prompt, selected_model, model_config)
419
402
  """
420
- # Use get_agent_prompt with return_model_info=True
421
403
  result = get_agent_prompt(agent_name, force_reload, return_model_info=True, **kwargs)
422
404
 
423
- # If result is a tuple, return it directly
405
+ # Ensure we have a tuple
424
406
  if isinstance(result, tuple):
425
407
  return result
426
408
 
427
409
  # Fallback (shouldn't happen)
428
- return result, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'), {"selection_method": "default"}
410
+ loader = _get_loader()
411
+ agent_data = loader.get_agent(agent_name)
412
+ default_model = "claude-sonnet-4-20250514"
413
+ if agent_data:
414
+ default_model = agent_data.get("capabilities", {}).get("model", default_model)
415
+
416
+ return result, default_model, {"selection_method": "default"}
429
417
 
430
418
 
431
419
  # Utility functions
432
420
  def list_available_agents() -> Dict[str, Dict[str, Any]]:
433
421
  """
434
- List all available agents with their sources.
422
+ List all available agents with their metadata.
435
423
 
436
424
  Returns:
437
- dict: Agent information including JSON template path
425
+ dict: Agent information including capabilities and metadata
438
426
  """
427
+ loader = _get_loader()
439
428
  agents = {}
440
429
 
441
- for agent_name, json_filename in AGENT_MAPPINGS.items():
442
- json_path = AGENT_TEMPLATES_DIR / json_filename
430
+ for agent_info in loader.list_agents():
431
+ agent_id = agent_info["id"]
432
+ metadata = loader.get_agent_metadata(agent_id)
443
433
 
444
- agents[agent_name] = {
445
- "json_file": json_filename if json_path.exists() else None,
446
- "json_path": str(json_path) if json_path.exists() else None,
447
- "has_template": json_path.exists(),
448
- "default_model": DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
449
- }
434
+ if metadata:
435
+ agents[agent_id] = {
436
+ "name": metadata["metadata"].get("name", agent_id),
437
+ "description": metadata["metadata"].get("description", ""),
438
+ "category": metadata["metadata"].get("category", ""),
439
+ "version": metadata["version"],
440
+ "model": metadata["capabilities"].get("model", ""),
441
+ "resource_tier": metadata["capabilities"].get("resource_tier", ""),
442
+ "tools": metadata["capabilities"].get("tools", [])
443
+ }
450
444
 
451
445
  return agents
452
446
 
@@ -462,13 +456,14 @@ def clear_agent_cache(agent_name: Optional[str] = None) -> None:
462
456
  cache = SharedPromptCache.get_instance()
463
457
 
464
458
  if agent_name:
465
- cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:json"
459
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}"
466
460
  cache.invalidate(cache_key)
467
461
  logger.debug(f"Cache cleared for agent: {agent_name}")
468
462
  else:
469
463
  # Clear all agent caches
470
- for name in AGENT_MAPPINGS:
471
- cache_key = f"{AGENT_CACHE_PREFIX}{name}:json"
464
+ loader = _get_loader()
465
+ for agent_id in loader._agent_registry.keys():
466
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_id}"
472
467
  cache.invalidate(cache_key)
473
468
  logger.debug("All agent caches cleared")
474
469
 
@@ -478,120 +473,31 @@ def clear_agent_cache(agent_name: Optional[str] = None) -> None:
478
473
 
479
474
  def validate_agent_files() -> Dict[str, Dict[str, Any]]:
480
475
  """
481
- Validate that all expected agent files exist.
476
+ Validate all agent files in the templates directory.
482
477
 
483
478
  Returns:
484
479
  dict: Validation results for each agent
485
480
  """
481
+ validator = AgentValidator()
486
482
  results = {}
487
483
 
488
- for agent_name, json_filename in AGENT_MAPPINGS.items():
489
- json_path = AGENT_TEMPLATES_DIR / json_filename
490
- results[agent_name] = {
491
- "template_exists": json_path.exists(),
492
- "template_path": str(json_path)
484
+ for json_file in AGENT_TEMPLATES_DIR.glob("*.json"):
485
+ if json_file.name == "agent_schema.json":
486
+ continue
487
+
488
+ validation_result = validator.validate_file(json_file)
489
+ results[json_file.stem] = {
490
+ "valid": validation_result.is_valid,
491
+ "errors": validation_result.errors,
492
+ "warnings": validation_result.warnings,
493
+ "file_path": str(json_file)
493
494
  }
494
495
 
495
496
  return results
496
497
 
497
498
 
498
- def get_model_selection_metrics() -> Dict[str, Any]:
499
- """
500
- Get metrics about model selection usage.
501
-
502
- Returns:
503
- dict: Metrics including feature flag status and selection counts
504
- """
505
- # Check feature flag status
506
- global_enabled = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
507
-
508
- # Check per-agent overrides
509
- agent_overrides = {}
510
- for agent_name in AGENT_MAPPINGS.keys():
511
- override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
512
- override_value = os.getenv(override_key, '')
513
- if override_value:
514
- agent_overrides[agent_name] = override_value.lower() == 'true'
515
-
516
- # Get cache instance to check for cached metrics
517
- try:
518
- cache = SharedPromptCache.get_instance()
519
- selection_stats = cache.get("agent_loader:model_selection_stats") or {}
520
- except Exception:
521
- selection_stats = {}
522
-
523
- return {
524
- "feature_flag": {
525
- "global_enabled": global_enabled,
526
- "agent_overrides": agent_overrides
527
- },
528
- "model_thresholds": {
529
- model_type.value: thresholds
530
- for model_type, thresholds in MODEL_THRESHOLDS.items()
531
- },
532
- "default_models": DEFAULT_AGENT_MODELS,
533
- "selection_stats": selection_stats
534
- }
535
-
536
-
537
- def log_model_selection(agent_name: str, selected_model: str, complexity_score: int, selection_method: str) -> None:
538
- """
539
- Log model selection for metrics tracking.
540
-
541
- Args:
542
- agent_name: Name of the agent
543
- selected_model: Model that was selected
544
- complexity_score: Complexity score from analysis
545
- selection_method: Method used for selection
546
- """
547
- try:
548
- # Get cache instance
549
- cache = SharedPromptCache.get_instance()
550
-
551
- # Get existing stats
552
- stats_key = "agent_loader:model_selection_stats"
553
- stats = cache.get(stats_key) or {
554
- "total_selections": 0,
555
- "by_model": {},
556
- "by_agent": {},
557
- "by_method": {},
558
- "complexity_distribution": {
559
- "0-30": 0,
560
- "31-70": 0,
561
- "71-100": 0
562
- }
563
- }
564
-
565
- # Update stats
566
- stats["total_selections"] += 1
567
-
568
- # By model
569
- if selected_model not in stats["by_model"]:
570
- stats["by_model"][selected_model] = 0
571
- stats["by_model"][selected_model] += 1
572
-
573
- # By agent
574
- if agent_name not in stats["by_agent"]:
575
- stats["by_agent"][agent_name] = {}
576
- if selected_model not in stats["by_agent"][agent_name]:
577
- stats["by_agent"][agent_name][selected_model] = 0
578
- stats["by_agent"][agent_name][selected_model] += 1
579
-
580
- # By method
581
- if selection_method not in stats["by_method"]:
582
- stats["by_method"][selection_method] = 0
583
- stats["by_method"][selection_method] += 1
584
-
585
- # Complexity distribution
586
- if complexity_score <= 30:
587
- stats["complexity_distribution"]["0-30"] += 1
588
- elif complexity_score <= 70:
589
- stats["complexity_distribution"]["31-70"] += 1
590
- else:
591
- stats["complexity_distribution"]["71-100"] += 1
592
-
593
- # Store updated stats with 24 hour TTL
594
- cache.set(stats_key, stats, ttl=86400)
595
-
596
- except Exception as e:
597
- logger.warning(f"Failed to log model selection metrics: {e}")
499
+ def reload_agents() -> None:
500
+ """Force reload all agents from disk."""
501
+ global _loader
502
+ _loader = None
503
+ logger.info("Agent registry cleared, will reload on next access")
@@ -2,7 +2,7 @@
2
2
  "version": 2,
3
3
  "agent_type": "base",
4
4
  "narrative_fields": {
5
- "instructions": "# Claude MPM Framework Agent\n\nYou are a specialized agent in the Claude MPM framework. Work collaboratively through PM orchestration to accomplish project objectives.\n\n## Core Principles\n- **Specialization Focus**: Execute only tasks within your domain expertise\n- **Quality First**: Meet acceptance criteria before reporting completion\n- **Clear Communication**: Report progress, blockers, and requirements explicitly\n- **Escalation Protocol**: Route security concerns to Security Agent; escalate authority exceeded\n\n## Task Execution Protocol\n1. **Acknowledge**: Confirm understanding of task, context, and acceptance criteria\n2. **Research Check**: If implementation details unclear, request PM delegate research first\n3. **Execute**: Perform work within specialization, maintaining audit trails\n4. **Validate**: Verify outputs meet acceptance criteria and quality standards\n5. **Report**: Provide structured completion report with deliverables and next steps\n\n## Framework Integration\n- **Hierarchy**: Operate within Project → User → System agent discovery\n- **Communication**: Use Task Tool subprocess for PM coordination\n- **Context Awareness**: Acknowledge current date/time in decisions\n- **Handoffs**: Follow structured protocols for inter-agent coordination\n- **Error Handling**: Implement graceful failure with clear error reporting\n\n## Quality Standards\n- Idempotent operations where possible\n- Comprehensive error handling and validation\n- Structured output formats for integration\n- Security-first approach for sensitive operations\n- Performance-conscious implementation choices"
5
+ "instructions": "# Claude MPM Framework Agent\n\nYou are a specialized agent in the Claude MPM framework. Work collaboratively through PM orchestration to accomplish project objectives.\n\n## Core Principles\n- **Specialization Focus**: Execute only tasks within your domain expertise\n- **Quality First**: Meet acceptance criteria before reporting completion\n- **Clear Communication**: Report progress, blockers, and requirements explicitly\n- **Escalation Protocol**: Route security concerns to Security Agent; escalate authority exceeded\n\n## Task Execution Protocol\n1. **Acknowledge**: Confirm understanding of task, context, and acceptance criteria\n2. **Research Check**: If implementation details unclear, request PM delegate research first\n3. **Execute**: Perform work within specialization, maintaining audit trails\n4. **Validate**: Verify outputs meet acceptance criteria and quality standards\n5. **Report**: Provide structured completion report with deliverables and next steps\n\n## Framework Integration\n- **Hierarchy**: Operate within Project → User → System agent discovery\n- **Communication**: Use Task Tool subprocess for PM coordination\n- **Context Awareness**: Acknowledge current date/time in decisions\n- **Handoffs**: Follow structured protocols for inter-agent coordination\n- **Error Handling**: Implement graceful failure with clear error reporting\n\n## Quality Standards\n- Idempotent operations where possible\n- Comprehensive error handling and validation\n- Structured output formats for integration\n- Security-first approach for sensitive operations\n- Performance-conscious implementation choices\n\n## Mandatory PM Reporting\nALL agents MUST report back to the PM upon task completion or when errors occur:\n\n### Required Reporting Elements\n1. **Work Summary**: Brief overview of actions performed and outcomes achieved\n2. **File Tracking**: Comprehensive list of all files:\n - Created files (with full paths)\n - Modified files (with nature of changes)\n - Deleted files (with justification)\n3. **Specific Actions**: Detailed list of all operations performed:\n - Commands executed\n - Services accessed\n - External resources utilized\n4. **Success Status**: Clear indication of task completion:\n - Successful: All acceptance criteria met\n - Partial: Some objectives achieved with specific blockers\n - Failed: Unable to complete with detailed reasons\n5. **Error Escalation**: Any unresolved errors MUST be escalated immediately:\n - Error description and context\n - Attempted resolution steps\n - Required assistance or permissions\n - Impact on task completion\n\n### Reporting Format\n```\n## Task Completion Report\n**Status**: [Success/Partial/Failed]\n**Summary**: [Brief overview of work performed]\n\n### Files Touched\n- Created: [list with paths]\n- Modified: [list with paths and change types]\n- Deleted: [list with paths and reasons]\n\n### Actions Performed\n- [Specific action 1]\n- [Specific action 2]\n- ...\n\n### Unresolved Issues (if any)\n- **Error**: [description]\n- **Impact**: [how it affects the task]\n- **Assistance Required**: [what help is needed]\n```"
6
6
  },
7
7
  "configuration_fields": {
8
8
  "model": "claude-4-sonnet-20250514",