claude-mpm 4.1.3__py3-none-any.whl → 4.1.4__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
- claude_mpm/agents/MEMORY.md +21 -49
- claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
- claude_mpm/agents/templates/api_qa.json +36 -116
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
- claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
- claude_mpm/agents/templates/code_analyzer.json +18 -36
- claude_mpm/agents/templates/data_engineer.json +43 -14
- claude_mpm/agents/templates/documentation.json +55 -74
- claude_mpm/agents/templates/engineer.json +56 -61
- claude_mpm/agents/templates/imagemagick.json +7 -2
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +36 -4
- claude_mpm/agents/templates/project_organizer.json +23 -71
- claude_mpm/agents/templates/qa.json +34 -2
- claude_mpm/agents/templates/refactoring_engineer.json +9 -5
- claude_mpm/agents/templates/research.json +36 -4
- claude_mpm/agents/templates/security.json +29 -2
- claude_mpm/agents/templates/ticketing.json +3 -3
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +28 -2
- claude_mpm/agents/templates/web_qa.json +38 -151
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/commands/agent_manager.py +221 -1
- claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
- claude_mpm/core/framework_loader.py +91 -0
- claude_mpm/core/log_manager.py +49 -1
- claude_mpm/services/memory/router.py +116 -10
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +43 -42
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
|
@@ -57,6 +57,7 @@ class AgentManagerCommand(AgentCommand):
|
|
|
57
57
|
"show": self._show_agent,
|
|
58
58
|
"test": self._test_agent,
|
|
59
59
|
"templates": self._list_templates,
|
|
60
|
+
"reset": self._reset_agents,
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
command = args.agent_manager_command
|
|
@@ -330,6 +331,218 @@ class AgentManagerCommand(AgentCommand):
|
|
|
330
331
|
output += f" {template['description']}\n"
|
|
331
332
|
return CommandResult.success_result(output)
|
|
332
333
|
|
|
334
|
+
def _reset_agents(self, args) -> CommandResult:
|
|
335
|
+
"""Reset by removing claude-mpm authored agents from project and user directories.
|
|
336
|
+
|
|
337
|
+
This command removes any agents with "author: claude-mpm" in their frontmatter,
|
|
338
|
+
preserving user-created agents. This is useful for clean reinstalls or when
|
|
339
|
+
wanting to get fresh versions of system agents.
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
# Determine which directories to clean
|
|
343
|
+
clean_project = not getattr(args, "user_only", False)
|
|
344
|
+
clean_user = not getattr(args, "project_only", False)
|
|
345
|
+
dry_run = getattr(args, "dry_run", False)
|
|
346
|
+
force = getattr(args, "force", False)
|
|
347
|
+
output_format = getattr(args, "format", "text")
|
|
348
|
+
|
|
349
|
+
# Track results
|
|
350
|
+
results = {
|
|
351
|
+
"project": {"checked": False, "removed": [], "preserved": []},
|
|
352
|
+
"user": {"checked": False, "removed": [], "preserved": []},
|
|
353
|
+
"dry_run": dry_run,
|
|
354
|
+
"total_removed": 0,
|
|
355
|
+
"total_preserved": 0,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Check project directory - always scan first to see what's there
|
|
359
|
+
if clean_project:
|
|
360
|
+
project_dir = Path.cwd() / ".claude" / "agents"
|
|
361
|
+
if project_dir.exists():
|
|
362
|
+
results["project"]["checked"] = True
|
|
363
|
+
# Always scan with dry_run=True first to see what's there
|
|
364
|
+
self._scan_and_clean_directory(
|
|
365
|
+
project_dir, results["project"], dry_run=True
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Check user directory - always scan first to see what's there
|
|
369
|
+
if clean_user:
|
|
370
|
+
user_dir = Path.home() / ".claude" / "agents"
|
|
371
|
+
if user_dir.exists():
|
|
372
|
+
results["user"]["checked"] = True
|
|
373
|
+
# Always scan with dry_run=True first to see what's there
|
|
374
|
+
self._scan_and_clean_directory(
|
|
375
|
+
user_dir, results["user"], dry_run=True
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Calculate totals
|
|
379
|
+
results["total_removed"] = len(results["project"]["removed"]) + len(
|
|
380
|
+
results["user"]["removed"]
|
|
381
|
+
)
|
|
382
|
+
results["total_preserved"] = len(results["project"]["preserved"]) + len(
|
|
383
|
+
results["user"]["preserved"]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Handle output based on format
|
|
387
|
+
if output_format == "json":
|
|
388
|
+
return CommandResult.success_result("Reset completed", data=results)
|
|
389
|
+
|
|
390
|
+
# Generate text output
|
|
391
|
+
output = self._format_reset_results(results, dry_run, force)
|
|
392
|
+
|
|
393
|
+
# If not dry-run, perform actual removal
|
|
394
|
+
if not dry_run and results["total_removed"] > 0:
|
|
395
|
+
# If force mode, remove immediately; otherwise get confirmation
|
|
396
|
+
if not force:
|
|
397
|
+
# Get confirmation first
|
|
398
|
+
print(output)
|
|
399
|
+
print("\n⚠️ This will permanently remove the agents listed above.")
|
|
400
|
+
|
|
401
|
+
# Ensure stdout is flushed before reading input
|
|
402
|
+
sys.stdout.flush()
|
|
403
|
+
|
|
404
|
+
# Get confirmation
|
|
405
|
+
try:
|
|
406
|
+
response = input("Continue? [y/N]: ").strip().lower()
|
|
407
|
+
if response not in ["y", "yes"]:
|
|
408
|
+
return CommandResult.success_result(
|
|
409
|
+
"Reset cancelled by user"
|
|
410
|
+
)
|
|
411
|
+
except (KeyboardInterrupt, EOFError):
|
|
412
|
+
return CommandResult.success_result("\nReset cancelled")
|
|
413
|
+
|
|
414
|
+
# Perform actual removal using the list we already have
|
|
415
|
+
if clean_project and results["project"]["removed"]:
|
|
416
|
+
project_dir = Path.cwd() / ".claude" / "agents"
|
|
417
|
+
for agent in results["project"]["removed"]:
|
|
418
|
+
agent_file = project_dir / agent
|
|
419
|
+
try:
|
|
420
|
+
if agent_file.exists():
|
|
421
|
+
agent_file.unlink()
|
|
422
|
+
self.logger.info(
|
|
423
|
+
f"Removed claude-mpm agent: {agent_file}"
|
|
424
|
+
)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.logger.warning(f"Could not remove {agent_file}: {e}")
|
|
427
|
+
|
|
428
|
+
if clean_user and results["user"]["removed"]:
|
|
429
|
+
user_dir = Path.home() / ".claude" / "agents"
|
|
430
|
+
for agent in results["user"]["removed"]:
|
|
431
|
+
agent_file = user_dir / agent
|
|
432
|
+
try:
|
|
433
|
+
if agent_file.exists():
|
|
434
|
+
agent_file.unlink()
|
|
435
|
+
self.logger.info(
|
|
436
|
+
f"Removed claude-mpm agent: {agent_file}"
|
|
437
|
+
)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
self.logger.warning(f"Could not remove {agent_file}: {e}")
|
|
440
|
+
|
|
441
|
+
# Update output to show actual removal
|
|
442
|
+
output = self._format_reset_results(results, dry_run=False, force=force)
|
|
443
|
+
|
|
444
|
+
return CommandResult.success_result(output)
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
self.logger.error(f"Failed to reset agents: {e}", exc_info=True)
|
|
448
|
+
return CommandResult.error_result(f"Failed to reset agents: {e}")
|
|
449
|
+
|
|
450
|
+
def _scan_and_clean_directory(
|
|
451
|
+
self, directory: Path, results: Dict[str, Any], dry_run: bool
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Scan a directory for claude-mpm authored agents and optionally remove them.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
directory: Directory to scan
|
|
457
|
+
results: Results dictionary to update
|
|
458
|
+
dry_run: If True, only scan without removing
|
|
459
|
+
"""
|
|
460
|
+
for agent_file in directory.glob("*.md"):
|
|
461
|
+
try:
|
|
462
|
+
content = agent_file.read_text()
|
|
463
|
+
# Check if this is a claude-mpm authored agent
|
|
464
|
+
if "author: claude-mpm" in content.lower():
|
|
465
|
+
results["removed"].append(agent_file.name)
|
|
466
|
+
if not dry_run:
|
|
467
|
+
agent_file.unlink()
|
|
468
|
+
self.logger.info(f"Removed claude-mpm agent: {agent_file}")
|
|
469
|
+
else:
|
|
470
|
+
results["preserved"].append(agent_file.name)
|
|
471
|
+
self.logger.debug(f"Preserved user agent: {agent_file}")
|
|
472
|
+
except Exception as e:
|
|
473
|
+
self.logger.warning(f"Could not process {agent_file}: {e}")
|
|
474
|
+
|
|
475
|
+
def _format_reset_results(
|
|
476
|
+
self, results: Dict[str, Any], dry_run: bool, force: bool
|
|
477
|
+
) -> str:
|
|
478
|
+
"""Format reset results for display.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
results: Results dictionary
|
|
482
|
+
dry_run: Whether this was a dry run
|
|
483
|
+
force: Whether force mode was used
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Formatted output string
|
|
487
|
+
"""
|
|
488
|
+
if dry_run:
|
|
489
|
+
output = "🔍 DRY RUN - No changes will be made\n"
|
|
490
|
+
output += "=" * 50 + "\n\n"
|
|
491
|
+
else:
|
|
492
|
+
output = "🧹 Agent Reset Complete\n"
|
|
493
|
+
output += "=" * 50 + "\n\n"
|
|
494
|
+
|
|
495
|
+
# Show project results
|
|
496
|
+
if results["project"]["checked"]:
|
|
497
|
+
output += "📁 Project Level (.claude/agents):\n"
|
|
498
|
+
if results["project"]["removed"]:
|
|
499
|
+
action = "Would remove" if dry_run else "Removed"
|
|
500
|
+
output += f" {action} {len(results['project']['removed'])} claude-mpm agent(s):\n"
|
|
501
|
+
for agent in results["project"]["removed"][:5]:
|
|
502
|
+
output += f" • {agent}\n"
|
|
503
|
+
if len(results["project"]["removed"]) > 5:
|
|
504
|
+
output += (
|
|
505
|
+
f" ... and {len(results['project']['removed']) - 5} more\n"
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
output += " No claude-mpm agents found\n"
|
|
509
|
+
|
|
510
|
+
if results["project"]["preserved"]:
|
|
511
|
+
output += f" Preserved {len(results['project']['preserved'])} user-created agent(s)\n"
|
|
512
|
+
output += "\n"
|
|
513
|
+
|
|
514
|
+
# Show user results
|
|
515
|
+
if results["user"]["checked"]:
|
|
516
|
+
output += "📁 User Level (~/.claude/agents):\n"
|
|
517
|
+
if results["user"]["removed"]:
|
|
518
|
+
action = "Would remove" if dry_run else "Removed"
|
|
519
|
+
output += f" {action} {len(results['user']['removed'])} claude-mpm agent(s):\n"
|
|
520
|
+
for agent in results["user"]["removed"][:5]:
|
|
521
|
+
output += f" • {agent}\n"
|
|
522
|
+
if len(results["user"]["removed"]) > 5:
|
|
523
|
+
output += (
|
|
524
|
+
f" ... and {len(results['user']['removed']) - 5} more\n"
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
output += " No claude-mpm agents found\n"
|
|
528
|
+
|
|
529
|
+
if results["user"]["preserved"]:
|
|
530
|
+
output += f" Preserved {len(results['user']['preserved'])} user-created agent(s)\n"
|
|
531
|
+
output += "\n"
|
|
532
|
+
|
|
533
|
+
# Show summary
|
|
534
|
+
output += "📊 Summary:\n"
|
|
535
|
+
if dry_run:
|
|
536
|
+
output += f" • Would remove: {results['total_removed']} agent(s)\n"
|
|
537
|
+
else:
|
|
538
|
+
output += f" • Removed: {results['total_removed']} agent(s)\n"
|
|
539
|
+
output += f" • Preserved: {results['total_preserved']} user agent(s)\n"
|
|
540
|
+
|
|
541
|
+
if dry_run and results["total_removed"] > 0:
|
|
542
|
+
output += "\n💡 Run with --force to execute this cleanup immediately"
|
|
543
|
+
|
|
544
|
+
return output
|
|
545
|
+
|
|
333
546
|
def _interactive_create(self) -> CommandResult:
|
|
334
547
|
"""Interactive agent creation wizard."""
|
|
335
548
|
print("\n=== Agent Creation Wizard ===\n")
|
|
@@ -493,6 +706,7 @@ Commands:
|
|
|
493
706
|
show Display detailed agent information
|
|
494
707
|
test Validate agent configuration
|
|
495
708
|
templates List available agent templates
|
|
709
|
+
reset Remove claude-mpm authored agents for clean install
|
|
496
710
|
|
|
497
711
|
Examples:
|
|
498
712
|
claude-mpm agent-manager list
|
|
@@ -500,6 +714,8 @@ Examples:
|
|
|
500
714
|
claude-mpm agent-manager variant --base research --id research-v2
|
|
501
715
|
claude-mpm agent-manager deploy --agent-id my-agent --tier user
|
|
502
716
|
claude-mpm agent-manager customize-pm --level project
|
|
717
|
+
claude-mpm agent-manager reset --dry-run
|
|
718
|
+
claude-mpm agent-manager reset --force --project-only
|
|
503
719
|
|
|
504
720
|
Note: PM customization writes to .claude-mpm/INSTRUCTIONS.md, not CLAUDE.md
|
|
505
721
|
"""
|
|
@@ -520,7 +736,11 @@ def manage_agent_manager(args) -> int:
|
|
|
520
736
|
result = command.run(args)
|
|
521
737
|
|
|
522
738
|
if result.success:
|
|
523
|
-
|
|
739
|
+
# Handle JSON output format
|
|
740
|
+
output_format = getattr(args, "format", "text")
|
|
741
|
+
if output_format == "json" and result.data is not None:
|
|
742
|
+
print(json.dumps(result.data, indent=2))
|
|
743
|
+
elif result.message:
|
|
524
744
|
print(result.message)
|
|
525
745
|
return 0
|
|
526
746
|
if result.message:
|
|
@@ -32,6 +32,9 @@ Examples:
|
|
|
32
32
|
claude-mpm agent-manager show --id engineer # Show agent details
|
|
33
33
|
claude-mpm agent-manager test --id my-agent # Test agent configuration
|
|
34
34
|
claude-mpm agent-manager templates # List available templates
|
|
35
|
+
claude-mpm agent-manager reset --dry-run # Preview agent cleanup
|
|
36
|
+
claude-mpm agent-manager reset --force # Remove all claude-mpm agents
|
|
37
|
+
claude-mpm agent-manager reset --project-only # Clean only project agents
|
|
35
38
|
""",
|
|
36
39
|
)
|
|
37
40
|
|
|
@@ -197,3 +200,34 @@ Examples:
|
|
|
197
200
|
default="text",
|
|
198
201
|
help="Output format (default: text)",
|
|
199
202
|
)
|
|
203
|
+
|
|
204
|
+
# Reset command
|
|
205
|
+
reset_parser = agent_subparsers.add_parser(
|
|
206
|
+
"reset", help="Remove claude-mpm authored agents for clean install"
|
|
207
|
+
)
|
|
208
|
+
reset_parser.add_argument(
|
|
209
|
+
"--force",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Execute cleanup immediately without confirmation",
|
|
212
|
+
)
|
|
213
|
+
reset_parser.add_argument(
|
|
214
|
+
"--dry-run",
|
|
215
|
+
action="store_true",
|
|
216
|
+
help="Preview what would be removed without making changes",
|
|
217
|
+
)
|
|
218
|
+
reset_parser.add_argument(
|
|
219
|
+
"--project-only",
|
|
220
|
+
action="store_true",
|
|
221
|
+
help="Only clean project-level agents (.claude/agents)",
|
|
222
|
+
)
|
|
223
|
+
reset_parser.add_argument(
|
|
224
|
+
"--user-only",
|
|
225
|
+
action="store_true",
|
|
226
|
+
help="Only clean user-level agents (~/.claude/agents)",
|
|
227
|
+
)
|
|
228
|
+
reset_parser.add_argument(
|
|
229
|
+
"--format",
|
|
230
|
+
choices=["text", "json"],
|
|
231
|
+
default="text",
|
|
232
|
+
help="Output format (default: text)",
|
|
233
|
+
)
|
|
@@ -1283,6 +1283,14 @@ Extract tickets from these patterns:
|
|
|
1283
1283
|
if agent.get("model") and agent["model"] != "opus":
|
|
1284
1284
|
section += f"- **Model**: {agent['model']}\n"
|
|
1285
1285
|
|
|
1286
|
+
# Add memory routing information if available
|
|
1287
|
+
if agent.get("memory_routing"):
|
|
1288
|
+
memory_routing = agent["memory_routing"]
|
|
1289
|
+
if memory_routing.get("description"):
|
|
1290
|
+
section += (
|
|
1291
|
+
f"- **Memory Routing**: {memory_routing['description']}\n"
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1286
1294
|
# Add simple Context-Aware Agent Selection
|
|
1287
1295
|
section += "\n## Context-Aware Agent Selection\n\n"
|
|
1288
1296
|
section += (
|
|
@@ -1384,6 +1392,14 @@ Extract tickets from these patterns:
|
|
|
1384
1392
|
if routing_data:
|
|
1385
1393
|
agent_data["routing"] = routing_data
|
|
1386
1394
|
|
|
1395
|
+
# Try to load memory routing metadata from JSON template if not in YAML frontmatter
|
|
1396
|
+
if "memory_routing" not in agent_data:
|
|
1397
|
+
memory_routing_data = self._load_memory_routing_from_template(
|
|
1398
|
+
agent_file.stem
|
|
1399
|
+
)
|
|
1400
|
+
if memory_routing_data:
|
|
1401
|
+
agent_data["memory_routing"] = memory_routing_data
|
|
1402
|
+
|
|
1387
1403
|
# Cache the parsed metadata
|
|
1388
1404
|
self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
|
|
1389
1405
|
|
|
@@ -1393,6 +1409,81 @@ Extract tickets from these patterns:
|
|
|
1393
1409
|
self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
|
|
1394
1410
|
return None
|
|
1395
1411
|
|
|
1412
|
+
def _load_memory_routing_from_template(
|
|
1413
|
+
self, agent_name: str
|
|
1414
|
+
) -> Optional[Dict[str, Any]]:
|
|
1415
|
+
"""Load memory routing metadata from agent JSON template.
|
|
1416
|
+
|
|
1417
|
+
Args:
|
|
1418
|
+
agent_name: Name of the agent (stem of the file)
|
|
1419
|
+
|
|
1420
|
+
Returns:
|
|
1421
|
+
Dictionary with memory routing metadata or None if not found
|
|
1422
|
+
"""
|
|
1423
|
+
try:
|
|
1424
|
+
import json
|
|
1425
|
+
|
|
1426
|
+
# Check if we have a framework path
|
|
1427
|
+
if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
|
|
1428
|
+
# For packaged installations, try to load from package resources
|
|
1429
|
+
if files:
|
|
1430
|
+
try:
|
|
1431
|
+
templates_package = files("claude_mpm.agents.templates")
|
|
1432
|
+
template_file = templates_package / f"{agent_name}.json"
|
|
1433
|
+
|
|
1434
|
+
if template_file.is_file():
|
|
1435
|
+
template_content = template_file.read_text()
|
|
1436
|
+
template_data = json.loads(template_content)
|
|
1437
|
+
return template_data.get("memory_routing")
|
|
1438
|
+
except Exception as e:
|
|
1439
|
+
self.logger.debug(
|
|
1440
|
+
f"Could not load memory routing from packaged template for {agent_name}: {e}"
|
|
1441
|
+
)
|
|
1442
|
+
return None
|
|
1443
|
+
|
|
1444
|
+
# For development mode, load from filesystem
|
|
1445
|
+
templates_dir = (
|
|
1446
|
+
self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
|
|
1447
|
+
)
|
|
1448
|
+
template_file = templates_dir / f"{agent_name}.json"
|
|
1449
|
+
|
|
1450
|
+
if template_file.exists():
|
|
1451
|
+
with open(template_file) as f:
|
|
1452
|
+
template_data = json.load(f)
|
|
1453
|
+
return template_data.get("memory_routing")
|
|
1454
|
+
|
|
1455
|
+
# Also check for variations in naming (underscore vs dash)
|
|
1456
|
+
# Handle common naming variations between deployed .md files and .json templates
|
|
1457
|
+
# Remove duplicates by using a set
|
|
1458
|
+
alternative_names = list(
|
|
1459
|
+
{
|
|
1460
|
+
agent_name.replace("-", "_"), # api-qa -> api_qa
|
|
1461
|
+
agent_name.replace("_", "-"), # api_qa -> api-qa
|
|
1462
|
+
agent_name.replace("-", ""), # api-qa -> apiqa
|
|
1463
|
+
agent_name.replace("_", ""), # api_qa -> apiqa
|
|
1464
|
+
agent_name.replace("-agent", ""), # research-agent -> research
|
|
1465
|
+
agent_name.replace("_agent", ""), # research_agent -> research
|
|
1466
|
+
agent_name + "_agent", # research -> research_agent
|
|
1467
|
+
agent_name + "-agent", # research -> research-agent
|
|
1468
|
+
}
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
for alt_name in alternative_names:
|
|
1472
|
+
if alt_name != agent_name: # Skip the original name we already tried
|
|
1473
|
+
alt_file = templates_dir / f"{alt_name}.json"
|
|
1474
|
+
if alt_file.exists():
|
|
1475
|
+
with open(alt_file) as f:
|
|
1476
|
+
template_data = json.load(f)
|
|
1477
|
+
return template_data.get("memory_routing")
|
|
1478
|
+
|
|
1479
|
+
return None
|
|
1480
|
+
|
|
1481
|
+
except Exception as e:
|
|
1482
|
+
self.logger.debug(
|
|
1483
|
+
f"Could not load memory routing from template for {agent_name}: {e}"
|
|
1484
|
+
)
|
|
1485
|
+
return None
|
|
1486
|
+
|
|
1396
1487
|
def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
|
|
1397
1488
|
"""Load routing metadata from agent JSON template.
|
|
1398
1489
|
|
claude_mpm/core/log_manager.py
CHANGED
|
@@ -179,6 +179,11 @@ class LogManager:
|
|
|
179
179
|
# Add to cache
|
|
180
180
|
self._dir_cache[log_type] = log_dir
|
|
181
181
|
|
|
182
|
+
# One-time migration for MPM logs from old location to new subdirectory
|
|
183
|
+
if log_type == "mpm" and not hasattr(self, "_mpm_logs_migrated"):
|
|
184
|
+
await self._migrate_mpm_logs()
|
|
185
|
+
self._mpm_logs_migrated = True
|
|
186
|
+
|
|
182
187
|
# Schedule cleanup for old logs
|
|
183
188
|
await self.cleanup_old_logs(
|
|
184
189
|
log_dir,
|
|
@@ -206,7 +211,7 @@ class LogManager:
|
|
|
206
211
|
# Map log types to directory names
|
|
207
212
|
dir_mapping = {
|
|
208
213
|
"startup": "startup",
|
|
209
|
-
"mpm": "", #
|
|
214
|
+
"mpm": "mpm", # MPM logs in dedicated subdirectory
|
|
210
215
|
"prompts": "prompts",
|
|
211
216
|
"sessions": "sessions",
|
|
212
217
|
"agents": "agents",
|
|
@@ -343,6 +348,49 @@ class LogManager:
|
|
|
343
348
|
|
|
344
349
|
return deleted_count
|
|
345
350
|
|
|
351
|
+
async def _migrate_mpm_logs(self):
|
|
352
|
+
"""
|
|
353
|
+
One-time migration to move existing MPM logs to new subdirectory.
|
|
354
|
+
|
|
355
|
+
Moves mpm_*.log files from .claude-mpm/logs/ to .claude-mpm/logs/mpm/
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
old_location = self.base_log_dir
|
|
359
|
+
new_location = self.base_log_dir / "mpm"
|
|
360
|
+
|
|
361
|
+
# Only proceed if old location exists and has MPM logs
|
|
362
|
+
if not old_location.exists():
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Find all MPM log files in the old location
|
|
366
|
+
mpm_logs = list(old_location.glob("mpm_*.log"))
|
|
367
|
+
|
|
368
|
+
if not mpm_logs:
|
|
369
|
+
return # No logs to migrate
|
|
370
|
+
|
|
371
|
+
# Ensure new directory exists
|
|
372
|
+
new_location.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
|
|
374
|
+
migrated_count = 0
|
|
375
|
+
for log_file in mpm_logs:
|
|
376
|
+
try:
|
|
377
|
+
# Move file to new location
|
|
378
|
+
new_path = new_location / log_file.name
|
|
379
|
+
if not new_path.exists(): # Don't overwrite existing files
|
|
380
|
+
log_file.rename(new_path)
|
|
381
|
+
migrated_count += 1
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.debug(f"Could not migrate {log_file}: {e}")
|
|
384
|
+
|
|
385
|
+
if migrated_count > 0:
|
|
386
|
+
logger.info(
|
|
387
|
+
f"Migrated {migrated_count} MPM log files to {new_location}"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
# Migration is best-effort, don't fail if something goes wrong
|
|
392
|
+
logger.debug(f"MPM log migration skipped: {e}")
|
|
393
|
+
|
|
346
394
|
async def log_prompt(
|
|
347
395
|
self, prompt_type: str, content: str, metadata: Optional[Dict[str, Any]] = None
|
|
348
396
|
) -> Optional[Path]:
|
|
@@ -25,6 +25,7 @@ from datetime import datetime
|
|
|
25
25
|
from typing import Any, Dict, List, Optional, Tuple
|
|
26
26
|
|
|
27
27
|
from claude_mpm.core.config import Config
|
|
28
|
+
from claude_mpm.core.framework_loader import FrameworkLoader
|
|
28
29
|
from claude_mpm.core.mixins import LoggerMixin
|
|
29
30
|
|
|
30
31
|
|
|
@@ -467,6 +468,84 @@ class MemoryRouter(LoggerMixin):
|
|
|
467
468
|
"""
|
|
468
469
|
super().__init__()
|
|
469
470
|
self.config = config or Config()
|
|
471
|
+
self._dynamic_patterns_loaded = False
|
|
472
|
+
self._dynamic_patterns = {}
|
|
473
|
+
|
|
474
|
+
def _load_dynamic_patterns(self) -> None:
|
|
475
|
+
"""Load memory routing patterns dynamically from agent templates.
|
|
476
|
+
|
|
477
|
+
WHY: Allows agents to define their own memory routing patterns
|
|
478
|
+
in their template files, making the system more flexible and
|
|
479
|
+
maintainable.
|
|
480
|
+
"""
|
|
481
|
+
if self._dynamic_patterns_loaded:
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
# Initialize framework loader to access agent templates
|
|
486
|
+
framework_loader = FrameworkLoader()
|
|
487
|
+
|
|
488
|
+
# Try to load patterns from deployed agents
|
|
489
|
+
from pathlib import Path
|
|
490
|
+
|
|
491
|
+
# Check both project and user agent directories
|
|
492
|
+
agent_dirs = [
|
|
493
|
+
Path(".claude/agents"), # Project agents
|
|
494
|
+
Path.home() / ".claude-mpm/agents", # User agents
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
for agent_dir in agent_dirs:
|
|
498
|
+
if not agent_dir.exists():
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
# Look for deployed agent files
|
|
502
|
+
for agent_file in agent_dir.glob("*.md"):
|
|
503
|
+
agent_name = agent_file.stem
|
|
504
|
+
|
|
505
|
+
# Try to load memory routing from template
|
|
506
|
+
memory_routing = (
|
|
507
|
+
framework_loader._load_memory_routing_from_template(agent_name)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if memory_routing:
|
|
511
|
+
# Convert agent name to pattern key format
|
|
512
|
+
# e.g., "research-agent" -> "research"
|
|
513
|
+
pattern_key = (
|
|
514
|
+
agent_name.replace("-agent", "")
|
|
515
|
+
.replace("_agent", "")
|
|
516
|
+
.replace("-", "_")
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Build pattern structure from memory routing
|
|
520
|
+
pattern_data = {
|
|
521
|
+
"keywords": memory_routing.get("keywords", []),
|
|
522
|
+
"sections": memory_routing.get("categories", []),
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Merge with existing patterns or add new
|
|
526
|
+
if pattern_key in self.AGENT_PATTERNS:
|
|
527
|
+
# Merge keywords, keeping unique values
|
|
528
|
+
existing_keywords = set(
|
|
529
|
+
self.AGENT_PATTERNS[pattern_key]["keywords"]
|
|
530
|
+
)
|
|
531
|
+
new_keywords = set(memory_routing.get("keywords", []))
|
|
532
|
+
pattern_data["keywords"] = list(
|
|
533
|
+
existing_keywords | new_keywords
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
self._dynamic_patterns[pattern_key] = pattern_data
|
|
537
|
+
self.logger.debug(
|
|
538
|
+
f"Loaded dynamic memory routing for {pattern_key}"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
self._dynamic_patterns_loaded = True
|
|
542
|
+
self.logger.info(
|
|
543
|
+
f"Loaded memory routing patterns for {len(self._dynamic_patterns)} agents"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
self.logger.warning(f"Could not load dynamic memory routing patterns: {e}")
|
|
548
|
+
self._dynamic_patterns_loaded = True # Don't retry
|
|
470
549
|
|
|
471
550
|
def get_supported_agents(self) -> List[str]:
|
|
472
551
|
"""Get list of supported agent types.
|
|
@@ -477,7 +556,12 @@ class MemoryRouter(LoggerMixin):
|
|
|
477
556
|
Returns:
|
|
478
557
|
List of supported agent type names
|
|
479
558
|
"""
|
|
480
|
-
|
|
559
|
+
self._load_dynamic_patterns()
|
|
560
|
+
|
|
561
|
+
# Combine static and dynamic patterns
|
|
562
|
+
all_agents = set(self.AGENT_PATTERNS.keys())
|
|
563
|
+
all_agents.update(self._dynamic_patterns.keys())
|
|
564
|
+
return list(all_agents)
|
|
481
565
|
|
|
482
566
|
def is_agent_supported(self, agent_type: str) -> bool:
|
|
483
567
|
"""Check if an agent type is supported by the memory router.
|
|
@@ -491,7 +575,8 @@ class MemoryRouter(LoggerMixin):
|
|
|
491
575
|
Returns:
|
|
492
576
|
True if agent type is supported, False otherwise
|
|
493
577
|
"""
|
|
494
|
-
|
|
578
|
+
self._load_dynamic_patterns()
|
|
579
|
+
return agent_type in self.AGENT_PATTERNS or agent_type in self._dynamic_patterns
|
|
495
580
|
|
|
496
581
|
def analyze_and_route(
|
|
497
582
|
self, content: str, context: Optional[Dict] = None
|
|
@@ -605,21 +690,30 @@ class MemoryRouter(LoggerMixin):
|
|
|
605
690
|
Returns:
|
|
606
691
|
Dict containing routing patterns and statistics
|
|
607
692
|
"""
|
|
693
|
+
self._load_dynamic_patterns()
|
|
694
|
+
|
|
695
|
+
# Combine static and dynamic patterns
|
|
696
|
+
all_patterns = dict(self.AGENT_PATTERNS)
|
|
697
|
+
all_patterns.update(self._dynamic_patterns)
|
|
698
|
+
|
|
608
699
|
return {
|
|
609
|
-
"agents": list(
|
|
700
|
+
"agents": list(all_patterns.keys()),
|
|
610
701
|
"default_agent": self.DEFAULT_AGENT,
|
|
702
|
+
"static_agents": list(self.AGENT_PATTERNS.keys()),
|
|
703
|
+
"dynamic_agents": list(self._dynamic_patterns.keys()),
|
|
611
704
|
"patterns": {
|
|
612
705
|
agent: {
|
|
613
706
|
"keyword_count": len(patterns["keywords"]),
|
|
614
707
|
"section_count": len(patterns["sections"]),
|
|
615
708
|
"keywords": patterns["keywords"][:10], # Show first 10
|
|
616
709
|
"sections": patterns["sections"],
|
|
710
|
+
"source": (
|
|
711
|
+
"dynamic" if agent in self._dynamic_patterns else "static"
|
|
712
|
+
),
|
|
617
713
|
}
|
|
618
|
-
for agent, patterns in
|
|
714
|
+
for agent, patterns in all_patterns.items()
|
|
619
715
|
},
|
|
620
|
-
"total_keywords": sum(
|
|
621
|
-
len(p["keywords"]) for p in self.AGENT_PATTERNS.values()
|
|
622
|
-
),
|
|
716
|
+
"total_keywords": sum(len(p["keywords"]) for p in all_patterns.values()),
|
|
623
717
|
}
|
|
624
718
|
|
|
625
719
|
def _normalize_content(self, content: str) -> str:
|
|
@@ -663,9 +757,14 @@ class MemoryRouter(LoggerMixin):
|
|
|
663
757
|
Returns:
|
|
664
758
|
Dict mapping agent names to relevance scores
|
|
665
759
|
"""
|
|
760
|
+
self._load_dynamic_patterns()
|
|
666
761
|
scores = {}
|
|
667
762
|
|
|
668
|
-
|
|
763
|
+
# Combine static and dynamic patterns
|
|
764
|
+
all_patterns = dict(self.AGENT_PATTERNS)
|
|
765
|
+
all_patterns.update(self._dynamic_patterns)
|
|
766
|
+
|
|
767
|
+
for agent, patterns in all_patterns.items():
|
|
669
768
|
score = 0.0
|
|
670
769
|
matched_keywords = []
|
|
671
770
|
|
|
@@ -773,10 +872,17 @@ class MemoryRouter(LoggerMixin):
|
|
|
773
872
|
Returns:
|
|
774
873
|
Section name for memory storage
|
|
775
874
|
"""
|
|
776
|
-
|
|
875
|
+
self._load_dynamic_patterns()
|
|
876
|
+
|
|
877
|
+
# Check both static and dynamic patterns
|
|
878
|
+
if agent in self.AGENT_PATTERNS:
|
|
879
|
+
sections = self.AGENT_PATTERNS[agent]["sections"]
|
|
880
|
+
elif agent in self._dynamic_patterns:
|
|
881
|
+
sections = self._dynamic_patterns[agent]["sections"]
|
|
882
|
+
else:
|
|
777
883
|
return "Recent Learnings"
|
|
778
884
|
|
|
779
|
-
sections =
|
|
885
|
+
sections = sections if sections else []
|
|
780
886
|
|
|
781
887
|
# Simple heuristics for section selection
|
|
782
888
|
if "mistake" in content or "error" in content or "avoid" in content:
|