claude-mpm 4.2.51__py3-none-any.whl → 4.3.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_PM.md +77 -447
- claude_mpm/agents/OUTPUT_STYLE.md +0 -39
- claude_mpm/agents/PM_INSTRUCTIONS.md +145 -0
- claude_mpm/agents/WORKFLOW.md +74 -368
- claude_mpm/agents/templates/prompt-engineer.json +294 -0
- claude_mpm/agents/templates/vercel_ops_agent.json +153 -32
- claude_mpm/cli/commands/uninstall.py +0 -1
- claude_mpm/core/framework_loader.py +72 -24
- claude_mpm/core/log_manager.py +52 -0
- claude_mpm/core/logging_utils.py +30 -12
- claude_mpm/services/agents/deployment/agent_template_builder.py +260 -18
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +35 -16
- claude_mpm/services/agents/local_template_manager.py +0 -1
- claude_mpm/services/monitor/daemon_manager.py +1 -3
- claude_mpm/services/monitor/event_emitter.py +5 -1
- claude_mpm/services/monitor/handlers/hooks.py +0 -2
- claude_mpm/tools/code_tree_analyzer.py +1 -3
- claude_mpm/utils/log_cleanup.py +612 -0
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/METADATA +41 -28
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/RECORD +26 -23
- /claude_mpm/agents/{INSTRUCTIONS.md → INSTRUCTIONS_OLD_DEPRECATED.md} +0 -0
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/top_level.txt +0 -0
|
@@ -639,7 +639,14 @@ class FrameworkLoader:
|
|
|
639
639
|
self._load_packaged_framework_content(content)
|
|
640
640
|
else:
|
|
641
641
|
# Load from filesystem for development mode
|
|
642
|
-
#
|
|
642
|
+
# Try new consolidated PM_INSTRUCTIONS.md first, fall back to INSTRUCTIONS.md
|
|
643
|
+
pm_instructions_path = (
|
|
644
|
+
self.framework_path
|
|
645
|
+
/ "src"
|
|
646
|
+
/ "claude_mpm"
|
|
647
|
+
/ "agents"
|
|
648
|
+
/ "PM_INSTRUCTIONS.md"
|
|
649
|
+
)
|
|
643
650
|
framework_instructions_path = (
|
|
644
651
|
self.framework_path
|
|
645
652
|
/ "src"
|
|
@@ -647,12 +654,25 @@ class FrameworkLoader:
|
|
|
647
654
|
/ "agents"
|
|
648
655
|
/ "INSTRUCTIONS.md"
|
|
649
656
|
)
|
|
650
|
-
|
|
657
|
+
|
|
658
|
+
# Try loading new consolidated file first
|
|
659
|
+
if pm_instructions_path.exists():
|
|
651
660
|
loaded_content = self._try_load_file(
|
|
652
|
-
|
|
661
|
+
pm_instructions_path, "consolidated PM_INSTRUCTIONS.md"
|
|
653
662
|
)
|
|
654
663
|
if loaded_content:
|
|
655
664
|
content["framework_instructions"] = loaded_content
|
|
665
|
+
self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md")
|
|
666
|
+
# Fall back to legacy file for backward compatibility
|
|
667
|
+
elif framework_instructions_path.exists():
|
|
668
|
+
loaded_content = self._try_load_file(
|
|
669
|
+
framework_instructions_path, "framework INSTRUCTIONS.md (legacy)"
|
|
670
|
+
)
|
|
671
|
+
if loaded_content:
|
|
672
|
+
content["framework_instructions"] = loaded_content
|
|
673
|
+
self.logger.warning(
|
|
674
|
+
"Using legacy INSTRUCTIONS.md - consider migrating to PM_INSTRUCTIONS.md"
|
|
675
|
+
)
|
|
656
676
|
content["loaded"] = True
|
|
657
677
|
# Add framework version to content
|
|
658
678
|
if self.framework_version:
|
|
@@ -717,20 +737,33 @@ class FrameworkLoader:
|
|
|
717
737
|
return
|
|
718
738
|
|
|
719
739
|
try:
|
|
720
|
-
#
|
|
721
|
-
|
|
722
|
-
if
|
|
723
|
-
content["framework_instructions"] =
|
|
740
|
+
# Try new consolidated PM_INSTRUCTIONS.md first
|
|
741
|
+
pm_instructions_content = self._load_packaged_file("PM_INSTRUCTIONS.md")
|
|
742
|
+
if pm_instructions_content:
|
|
743
|
+
content["framework_instructions"] = pm_instructions_content
|
|
724
744
|
content["loaded"] = True
|
|
745
|
+
self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md from package")
|
|
725
746
|
# Extract and store version/timestamp metadata
|
|
726
747
|
self._extract_metadata_from_content(
|
|
727
|
-
|
|
748
|
+
pm_instructions_content, "PM_INSTRUCTIONS.md"
|
|
728
749
|
)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if
|
|
733
|
-
content["
|
|
750
|
+
else:
|
|
751
|
+
# Fall back to legacy INSTRUCTIONS.md
|
|
752
|
+
instructions_content = self._load_packaged_file("INSTRUCTIONS.md")
|
|
753
|
+
if instructions_content:
|
|
754
|
+
content["framework_instructions"] = instructions_content
|
|
755
|
+
content["loaded"] = True
|
|
756
|
+
self.logger.warning("Using legacy INSTRUCTIONS.md from package")
|
|
757
|
+
# Extract and store version/timestamp metadata
|
|
758
|
+
self._extract_metadata_from_content(
|
|
759
|
+
instructions_content, "INSTRUCTIONS.md"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if self.framework_version:
|
|
763
|
+
content["instructions_version"] = self.framework_version
|
|
764
|
+
content["version"] = self.framework_version
|
|
765
|
+
if self.framework_last_modified:
|
|
766
|
+
content["instructions_last_modified"] = self.framework_last_modified
|
|
734
767
|
|
|
735
768
|
# Load BASE_PM.md
|
|
736
769
|
base_pm_content = self._load_packaged_file("BASE_PM.md")
|
|
@@ -757,22 +790,37 @@ class FrameworkLoader:
|
|
|
757
790
|
) -> None:
|
|
758
791
|
"""Load framework content using importlib.resources fallback."""
|
|
759
792
|
try:
|
|
760
|
-
#
|
|
761
|
-
|
|
762
|
-
"
|
|
793
|
+
# Try new consolidated PM_INSTRUCTIONS.md first
|
|
794
|
+
pm_instructions_content = self._load_packaged_file_fallback(
|
|
795
|
+
"PM_INSTRUCTIONS.md", resources
|
|
763
796
|
)
|
|
764
|
-
if
|
|
765
|
-
content["framework_instructions"] =
|
|
797
|
+
if pm_instructions_content:
|
|
798
|
+
content["framework_instructions"] = pm_instructions_content
|
|
766
799
|
content["loaded"] = True
|
|
800
|
+
self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md via fallback")
|
|
767
801
|
# Extract and store version/timestamp metadata
|
|
768
802
|
self._extract_metadata_from_content(
|
|
769
|
-
|
|
803
|
+
pm_instructions_content, "PM_INSTRUCTIONS.md"
|
|
770
804
|
)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
805
|
+
else:
|
|
806
|
+
# Fall back to legacy INSTRUCTIONS.md
|
|
807
|
+
instructions_content = self._load_packaged_file_fallback(
|
|
808
|
+
"INSTRUCTIONS.md", resources
|
|
809
|
+
)
|
|
810
|
+
if instructions_content:
|
|
811
|
+
content["framework_instructions"] = instructions_content
|
|
812
|
+
content["loaded"] = True
|
|
813
|
+
self.logger.warning("Using legacy INSTRUCTIONS.md via fallback")
|
|
814
|
+
# Extract and store version/timestamp metadata
|
|
815
|
+
self._extract_metadata_from_content(
|
|
816
|
+
instructions_content, "INSTRUCTIONS.md"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
if self.framework_version:
|
|
820
|
+
content["instructions_version"] = self.framework_version
|
|
821
|
+
content["version"] = self.framework_version
|
|
822
|
+
if self.framework_last_modified:
|
|
823
|
+
content["instructions_last_modified"] = self.framework_last_modified
|
|
776
824
|
|
|
777
825
|
# Load BASE_PM.md
|
|
778
826
|
base_pm_content = self._load_packaged_file_fallback("BASE_PM.md", resources)
|
claude_mpm/core/log_manager.py
CHANGED
|
@@ -29,6 +29,12 @@ from ..core.constants import SystemLimits
|
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
32
|
+
# Import cleanup utility for automatic cleanup
|
|
33
|
+
try:
|
|
34
|
+
from ..utils.log_cleanup import run_cleanup_on_startup
|
|
35
|
+
except ImportError:
|
|
36
|
+
run_cleanup_on_startup = None
|
|
37
|
+
|
|
32
38
|
|
|
33
39
|
class LogManager:
|
|
34
40
|
"""
|
|
@@ -76,6 +82,9 @@ class LogManager:
|
|
|
76
82
|
# Start background threads
|
|
77
83
|
self._start_background_threads()
|
|
78
84
|
|
|
85
|
+
# Run automatic cleanup on startup if enabled
|
|
86
|
+
self._run_startup_cleanup()
|
|
87
|
+
|
|
79
88
|
def _setup_logging_config(self):
|
|
80
89
|
"""Load and setup logging configuration from config."""
|
|
81
90
|
logging_config = self.config.get("logging", {})
|
|
@@ -107,6 +116,49 @@ class LogManager:
|
|
|
107
116
|
if not self.base_log_dir.is_absolute():
|
|
108
117
|
self.base_log_dir = Path.cwd() / self.base_log_dir
|
|
109
118
|
|
|
119
|
+
def _run_startup_cleanup(self):
|
|
120
|
+
"""Run automatic log cleanup on startup if enabled."""
|
|
121
|
+
if run_cleanup_on_startup is None:
|
|
122
|
+
return # Cleanup utility not available
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Get cleanup configuration
|
|
126
|
+
cleanup_config = self.config.get("log_cleanup", {})
|
|
127
|
+
|
|
128
|
+
# Check if automatic cleanup is enabled (default: True)
|
|
129
|
+
if not cleanup_config.get("auto_cleanup_enabled", True):
|
|
130
|
+
logger.debug("Automatic log cleanup is disabled")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Convert hours to days for cleanup utility
|
|
134
|
+
cleanup_params = {
|
|
135
|
+
"auto_cleanup_enabled": True,
|
|
136
|
+
"session_retention_days": self.retention_hours.get("sessions", 168)
|
|
137
|
+
// 24,
|
|
138
|
+
"archive_retention_days": cleanup_config.get(
|
|
139
|
+
"archive_retention_days", 30
|
|
140
|
+
),
|
|
141
|
+
"log_retention_days": cleanup_config.get("log_retention_days", 14),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Run cleanup in background thread to avoid blocking startup
|
|
145
|
+
def cleanup_task():
|
|
146
|
+
try:
|
|
147
|
+
result = run_cleanup_on_startup(self.base_log_dir, cleanup_params)
|
|
148
|
+
if result:
|
|
149
|
+
logger.debug(
|
|
150
|
+
f"Startup cleanup completed: "
|
|
151
|
+
f"Removed {result.get('total_removed', 0)} items"
|
|
152
|
+
)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.debug(f"Startup cleanup failed: {e}")
|
|
155
|
+
|
|
156
|
+
cleanup_thread = Thread(target=cleanup_task, daemon=True)
|
|
157
|
+
cleanup_thread.start()
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.debug(f"Could not run startup cleanup: {e}")
|
|
161
|
+
|
|
110
162
|
def _start_background_threads(self):
|
|
111
163
|
"""Start background threads for async operations."""
|
|
112
164
|
with self._lock:
|
claude_mpm/core/logging_utils.py
CHANGED
|
@@ -42,8 +42,10 @@ class LoggingConfig:
|
|
|
42
42
|
ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
43
43
|
|
|
44
44
|
# File settings
|
|
45
|
-
MAX_BYTES =
|
|
45
|
+
MAX_BYTES = 5 * 1024 * 1024 # 5MB - lowered for better rotation testing
|
|
46
46
|
BACKUP_COUNT = 5
|
|
47
|
+
ROTATION_INTERVAL = "midnight" # Daily rotation at midnight
|
|
48
|
+
ROTATION_BACKUP_COUNT = 7 # Keep 7 days of daily logs
|
|
47
49
|
|
|
48
50
|
# Component-specific log names
|
|
49
51
|
COMPONENT_NAMES = {
|
|
@@ -129,30 +131,46 @@ class LoggerFactory:
|
|
|
129
131
|
log_format: Optional[str] = None,
|
|
130
132
|
date_format: Optional[str] = None,
|
|
131
133
|
) -> None:
|
|
132
|
-
"""Set up file logging
|
|
134
|
+
"""Set up file logging handlers with both size and time-based rotation."""
|
|
133
135
|
if not cls._log_dir:
|
|
134
136
|
return
|
|
135
137
|
|
|
136
138
|
# Ensure log directory exists
|
|
137
139
|
cls._log_dir.mkdir(parents=True, exist_ok=True)
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
formatter = logging.Formatter(
|
|
142
|
+
log_format or LoggingConfig.DETAILED_FORMAT,
|
|
143
|
+
date_format or LoggingConfig.DATE_FORMAT,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# 1. Size-based rotating file handler (for current active log)
|
|
140
147
|
log_file = cls._log_dir / "claude_mpm.log"
|
|
141
|
-
|
|
148
|
+
size_handler = logging.handlers.RotatingFileHandler(
|
|
142
149
|
log_file,
|
|
143
150
|
maxBytes=LoggingConfig.MAX_BYTES,
|
|
144
151
|
backupCount=LoggingConfig.BACKUP_COUNT,
|
|
145
152
|
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
size_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
|
|
154
|
+
size_handler.setFormatter(formatter)
|
|
155
|
+
logging.getLogger().addHandler(size_handler)
|
|
156
|
+
cls._handlers["file"] = size_handler
|
|
157
|
+
|
|
158
|
+
# 2. Time-based rotating file handler (daily rotation)
|
|
159
|
+
daily_log_file = cls._log_dir / "claude_mpm_daily.log"
|
|
160
|
+
time_handler = logging.handlers.TimedRotatingFileHandler(
|
|
161
|
+
daily_log_file,
|
|
162
|
+
when=LoggingConfig.ROTATION_INTERVAL,
|
|
163
|
+
interval=1,
|
|
164
|
+
backupCount=LoggingConfig.ROTATION_BACKUP_COUNT,
|
|
151
165
|
)
|
|
152
|
-
|
|
166
|
+
time_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
|
|
167
|
+
time_handler.setFormatter(formatter)
|
|
168
|
+
|
|
169
|
+
# Add suffix to rotated files (e.g., claude_mpm_daily.log.2024-09-18)
|
|
170
|
+
time_handler.suffix = "%Y-%m-%d"
|
|
153
171
|
|
|
154
|
-
logging.getLogger().addHandler(
|
|
155
|
-
cls._handlers["
|
|
172
|
+
logging.getLogger().addHandler(time_handler)
|
|
173
|
+
cls._handlers["file_daily"] = time_handler
|
|
156
174
|
|
|
157
175
|
@classmethod
|
|
158
176
|
def get_logger(
|
|
@@ -30,6 +30,52 @@ class AgentTemplateBuilder:
|
|
|
30
30
|
"""Initialize the template builder."""
|
|
31
31
|
self.logger = get_logger(__name__)
|
|
32
32
|
|
|
33
|
+
def normalize_tools_input(self, tools):
|
|
34
|
+
"""Normalize various tool input formats to a consistent list.
|
|
35
|
+
|
|
36
|
+
Handles multiple input formats:
|
|
37
|
+
- None/empty: Returns default tools
|
|
38
|
+
- String: Splits by comma and strips whitespace
|
|
39
|
+
- List: Ensures all items are strings and strips whitespace
|
|
40
|
+
- Dict: Takes enabled tools (where value is True)
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
tools: Tools input in various formats (str, list, dict, or None)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of tool names, normalized and cleaned
|
|
47
|
+
"""
|
|
48
|
+
default_tools = ["Read", "Write", "Edit", "Grep", "Glob", "Bash"]
|
|
49
|
+
|
|
50
|
+
# Handle None or empty
|
|
51
|
+
if not tools:
|
|
52
|
+
self.logger.debug("No tools provided, using defaults")
|
|
53
|
+
return default_tools
|
|
54
|
+
|
|
55
|
+
# Convert to list format
|
|
56
|
+
if isinstance(tools, str):
|
|
57
|
+
# Split by comma, strip whitespace
|
|
58
|
+
tool_list = [t.strip() for t in tools.split(",") if t.strip()]
|
|
59
|
+
self.logger.debug(f"Converted string tools '{tools}' to list: {tool_list}")
|
|
60
|
+
elif isinstance(tools, list):
|
|
61
|
+
# Ensure all items are strings and strip whitespace
|
|
62
|
+
tool_list = [str(t).strip() for t in tools if t and str(t).strip()]
|
|
63
|
+
self.logger.debug(f"Normalized list tools: {tool_list}")
|
|
64
|
+
elif isinstance(tools, dict):
|
|
65
|
+
# Handle dict format - take enabled tools
|
|
66
|
+
tool_list = [k for k, v in tools.items() if v]
|
|
67
|
+
self.logger.info(f"Converting dict tools format: {tools} -> {tool_list}")
|
|
68
|
+
else:
|
|
69
|
+
self.logger.warning(f"Unknown tools format: {type(tools)}, using defaults")
|
|
70
|
+
return default_tools
|
|
71
|
+
|
|
72
|
+
# Return processed list or defaults if empty
|
|
73
|
+
if not tool_list:
|
|
74
|
+
self.logger.debug("Tools list empty after processing, using defaults")
|
|
75
|
+
return default_tools
|
|
76
|
+
|
|
77
|
+
return tool_list
|
|
78
|
+
|
|
33
79
|
def _load_base_agent_instructions(self, agent_type: str) -> str:
|
|
34
80
|
"""Load BASE instructions for a specific agent type.
|
|
35
81
|
|
|
@@ -138,13 +184,39 @@ class AgentTemplateBuilder:
|
|
|
138
184
|
capabilities.get("tools") if isinstance(capabilities, dict) else None
|
|
139
185
|
)
|
|
140
186
|
|
|
141
|
-
tools
|
|
187
|
+
# Get raw tools from various possible locations
|
|
188
|
+
raw_tools = (
|
|
142
189
|
template_data.get("tools")
|
|
143
190
|
or capabilities_tools
|
|
144
191
|
or template_data.get("configuration_fields", {}).get("tools")
|
|
145
|
-
or ["Read", "Write", "Edit", "Grep", "Glob", "LS"] # Default fallback
|
|
146
192
|
)
|
|
147
193
|
|
|
194
|
+
# Normalize tools to a consistent list format
|
|
195
|
+
tools = self.normalize_tools_input(raw_tools)
|
|
196
|
+
|
|
197
|
+
# Log if we see non-standard tool names (info level, not warning)
|
|
198
|
+
standard_tools = {
|
|
199
|
+
"Read",
|
|
200
|
+
"Write",
|
|
201
|
+
"Edit",
|
|
202
|
+
"MultiEdit", # File operations
|
|
203
|
+
"Grep",
|
|
204
|
+
"Glob",
|
|
205
|
+
"LS", # Search and navigation
|
|
206
|
+
"Bash",
|
|
207
|
+
"BashOutput",
|
|
208
|
+
"KillShell", # Command execution
|
|
209
|
+
"TodoWrite",
|
|
210
|
+
"ExitPlanMode", # Task management
|
|
211
|
+
"WebSearch",
|
|
212
|
+
"WebFetch", # Web operations
|
|
213
|
+
"NotebookRead",
|
|
214
|
+
"NotebookEdit", # Jupyter notebook support
|
|
215
|
+
}
|
|
216
|
+
non_standard = [t for t in tools if t not in standard_tools]
|
|
217
|
+
if non_standard:
|
|
218
|
+
self.logger.info(f"Using non-standard tools: {non_standard}")
|
|
219
|
+
|
|
148
220
|
# Extract model from template with fallback
|
|
149
221
|
capabilities_model = (
|
|
150
222
|
capabilities.get("model") if isinstance(capabilities, dict) else None
|
|
@@ -157,15 +229,8 @@ class AgentTemplateBuilder:
|
|
|
157
229
|
or "sonnet" # Default fallback
|
|
158
230
|
)
|
|
159
231
|
|
|
160
|
-
# Convert tools list to comma-separated string (
|
|
161
|
-
tools_str = ",".join(tools)
|
|
162
|
-
|
|
163
|
-
# Validate tools format - CRITICAL: No spaces allowed!
|
|
164
|
-
if ", " in tools_str:
|
|
165
|
-
self.logger.error(f"Tools contain spaces: '{tools_str}'")
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"Tools must be comma-separated WITHOUT spaces: {tools_str}"
|
|
168
|
-
)
|
|
232
|
+
# Convert tools list to comma-separated string (without spaces for compatibility)
|
|
233
|
+
tools_str = ",".join(tools)
|
|
169
234
|
|
|
170
235
|
# Map model names to Claude Code format (as required)
|
|
171
236
|
model_map = {
|
|
@@ -331,12 +396,20 @@ class AgentTemplateBuilder:
|
|
|
331
396
|
base_instructions = self._load_base_agent_instructions(agent_type)
|
|
332
397
|
|
|
333
398
|
# Get agent instructions from template data (primary) or base agent data (fallback)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
399
|
+
raw_instructions = template_data.get("instructions")
|
|
400
|
+
|
|
401
|
+
# Handle dictionary instructions format
|
|
402
|
+
if isinstance(raw_instructions, dict):
|
|
403
|
+
agent_specific_instructions = self._convert_instructions_dict_to_markdown(
|
|
404
|
+
raw_instructions
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
agent_specific_instructions = (
|
|
408
|
+
raw_instructions
|
|
409
|
+
or base_agent_data.get("content")
|
|
410
|
+
or base_agent_data.get("instructions")
|
|
411
|
+
or "# Agent Instructions\n\nThis agent provides specialized assistance."
|
|
412
|
+
)
|
|
340
413
|
|
|
341
414
|
# Combine BASE instructions with agent-specific instructions
|
|
342
415
|
if base_instructions:
|
|
@@ -423,7 +496,8 @@ Only include memories that are:
|
|
|
423
496
|
)
|
|
424
497
|
|
|
425
498
|
# Get tools and model with fallbacks
|
|
426
|
-
|
|
499
|
+
raw_tools = merged_config.get("tools")
|
|
500
|
+
tools = self.normalize_tools_input(raw_tools)
|
|
427
501
|
model = merged_config.get("model", "sonnet")
|
|
428
502
|
|
|
429
503
|
# Format tools as YAML list
|
|
@@ -890,3 +964,171 @@ tools:
|
|
|
890
964
|
|
|
891
965
|
# Return as quoted string
|
|
892
966
|
return f'"{escaped}"'
|
|
967
|
+
|
|
968
|
+
def _convert_instructions_dict_to_markdown(self, instructions_dict: dict) -> str:
|
|
969
|
+
"""Convert complex instructions dictionary to markdown format.
|
|
970
|
+
|
|
971
|
+
Args:
|
|
972
|
+
instructions_dict: Dictionary containing structured instructions
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
Formatted markdown string representing the instructions
|
|
976
|
+
"""
|
|
977
|
+
if not instructions_dict:
|
|
978
|
+
return "# Agent Instructions\n\nThis agent provides specialized assistance."
|
|
979
|
+
|
|
980
|
+
markdown_parts = []
|
|
981
|
+
|
|
982
|
+
# Add primary role
|
|
983
|
+
if "primary_role" in instructions_dict:
|
|
984
|
+
markdown_parts.extend(["# Role", "", instructions_dict["primary_role"], ""])
|
|
985
|
+
|
|
986
|
+
# Add core identity
|
|
987
|
+
if "core_identity" in instructions_dict:
|
|
988
|
+
markdown_parts.extend(
|
|
989
|
+
["## Core Identity", "", instructions_dict["core_identity"], ""]
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
# Add responsibilities
|
|
993
|
+
if "responsibilities" in instructions_dict:
|
|
994
|
+
markdown_parts.extend(["## Responsibilities", ""])
|
|
995
|
+
|
|
996
|
+
responsibilities = instructions_dict["responsibilities"]
|
|
997
|
+
if isinstance(responsibilities, list):
|
|
998
|
+
for resp in responsibilities:
|
|
999
|
+
if isinstance(resp, dict):
|
|
1000
|
+
area = resp.get("area", "Unknown Area")
|
|
1001
|
+
tasks = resp.get("tasks", [])
|
|
1002
|
+
|
|
1003
|
+
markdown_parts.extend([f"### {area}", ""])
|
|
1004
|
+
|
|
1005
|
+
if isinstance(tasks, list):
|
|
1006
|
+
for task in tasks:
|
|
1007
|
+
markdown_parts.append(f"- {task}")
|
|
1008
|
+
|
|
1009
|
+
markdown_parts.append("")
|
|
1010
|
+
else:
|
|
1011
|
+
markdown_parts.append(f"- {resp}")
|
|
1012
|
+
|
|
1013
|
+
markdown_parts.append("")
|
|
1014
|
+
|
|
1015
|
+
# Add analytical framework
|
|
1016
|
+
if "analytical_framework" in instructions_dict:
|
|
1017
|
+
framework = instructions_dict["analytical_framework"]
|
|
1018
|
+
if isinstance(framework, dict):
|
|
1019
|
+
markdown_parts.extend(["## Analytical Framework", ""])
|
|
1020
|
+
|
|
1021
|
+
for framework_area, framework_data in framework.items():
|
|
1022
|
+
markdown_parts.extend(
|
|
1023
|
+
[f"### {framework_area.replace('_', ' ').title()}", ""]
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
if isinstance(framework_data, dict):
|
|
1027
|
+
for category, items in framework_data.items():
|
|
1028
|
+
markdown_parts.extend(
|
|
1029
|
+
[f"#### {category.replace('_', ' ').title()}", ""]
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
if isinstance(items, list):
|
|
1033
|
+
for item in items:
|
|
1034
|
+
markdown_parts.append(f"- {item}")
|
|
1035
|
+
elif isinstance(items, str):
|
|
1036
|
+
markdown_parts.append(items)
|
|
1037
|
+
|
|
1038
|
+
markdown_parts.append("")
|
|
1039
|
+
elif isinstance(framework_data, list):
|
|
1040
|
+
for item in framework_data:
|
|
1041
|
+
markdown_parts.append(f"- {item}")
|
|
1042
|
+
markdown_parts.append("")
|
|
1043
|
+
|
|
1044
|
+
# Add methodologies
|
|
1045
|
+
if "methodologies" in instructions_dict:
|
|
1046
|
+
methodologies = instructions_dict["methodologies"]
|
|
1047
|
+
if isinstance(methodologies, dict):
|
|
1048
|
+
markdown_parts.extend(["## Methodologies", ""])
|
|
1049
|
+
|
|
1050
|
+
for method_name, method_data in methodologies.items():
|
|
1051
|
+
markdown_parts.extend(
|
|
1052
|
+
[f"### {method_name.replace('_', ' ').title()}", ""]
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
if isinstance(method_data, dict):
|
|
1056
|
+
for key, value in method_data.items():
|
|
1057
|
+
if isinstance(value, list):
|
|
1058
|
+
markdown_parts.extend(
|
|
1059
|
+
[f"#### {key.replace('_', ' ').title()}", ""]
|
|
1060
|
+
)
|
|
1061
|
+
for item in value:
|
|
1062
|
+
markdown_parts.append(f"- {item}")
|
|
1063
|
+
markdown_parts.append("")
|
|
1064
|
+
elif isinstance(value, str):
|
|
1065
|
+
markdown_parts.extend(
|
|
1066
|
+
[
|
|
1067
|
+
f"**{key.replace('_', ' ').title()}**: {value}",
|
|
1068
|
+
"",
|
|
1069
|
+
]
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# Add quality standards
|
|
1073
|
+
if "quality_standards" in instructions_dict:
|
|
1074
|
+
standards = instructions_dict["quality_standards"]
|
|
1075
|
+
if isinstance(standards, dict):
|
|
1076
|
+
markdown_parts.extend(["## Quality Standards", ""])
|
|
1077
|
+
|
|
1078
|
+
for standard_area, standard_items in standards.items():
|
|
1079
|
+
markdown_parts.extend(
|
|
1080
|
+
[f"### {standard_area.replace('_', ' ').title()}", ""]
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
if isinstance(standard_items, list):
|
|
1084
|
+
for item in standard_items:
|
|
1085
|
+
markdown_parts.append(f"- {item}")
|
|
1086
|
+
elif isinstance(standard_items, str):
|
|
1087
|
+
markdown_parts.append(standard_items)
|
|
1088
|
+
|
|
1089
|
+
markdown_parts.append("")
|
|
1090
|
+
|
|
1091
|
+
# Add communication style
|
|
1092
|
+
if "communication_style" in instructions_dict:
|
|
1093
|
+
comm_style = instructions_dict["communication_style"]
|
|
1094
|
+
if isinstance(comm_style, dict):
|
|
1095
|
+
markdown_parts.extend(["## Communication Style", ""])
|
|
1096
|
+
|
|
1097
|
+
for style_area, style_items in comm_style.items():
|
|
1098
|
+
markdown_parts.extend(
|
|
1099
|
+
[f"### {style_area.replace('_', ' ').title()}", ""]
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
if isinstance(style_items, list):
|
|
1103
|
+
for item in style_items:
|
|
1104
|
+
markdown_parts.append(f"- {item}")
|
|
1105
|
+
elif isinstance(style_items, str):
|
|
1106
|
+
markdown_parts.append(style_items)
|
|
1107
|
+
|
|
1108
|
+
markdown_parts.append("")
|
|
1109
|
+
|
|
1110
|
+
# If no specific sections were found, convert as generic dict
|
|
1111
|
+
if not markdown_parts:
|
|
1112
|
+
markdown_parts = ["# Agent Instructions", ""]
|
|
1113
|
+
for key, value in instructions_dict.items():
|
|
1114
|
+
key_title = key.replace("_", " ").title()
|
|
1115
|
+
if isinstance(value, str):
|
|
1116
|
+
markdown_parts.extend([f"## {key_title}", "", value, ""])
|
|
1117
|
+
elif isinstance(value, list):
|
|
1118
|
+
markdown_parts.extend([f"## {key_title}", ""])
|
|
1119
|
+
for item in value:
|
|
1120
|
+
markdown_parts.append(f"- {item}")
|
|
1121
|
+
markdown_parts.append("")
|
|
1122
|
+
elif isinstance(value, dict):
|
|
1123
|
+
markdown_parts.extend([f"## {key_title}", ""])
|
|
1124
|
+
# Simple dict formatting
|
|
1125
|
+
for subkey, subvalue in value.items():
|
|
1126
|
+
if isinstance(subvalue, str):
|
|
1127
|
+
markdown_parts.extend(
|
|
1128
|
+
[
|
|
1129
|
+
f"**{subkey.replace('_', ' ').title()}**: {subvalue}",
|
|
1130
|
+
"",
|
|
1131
|
+
]
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
return "\n".join(markdown_parts).strip()
|
|
@@ -176,24 +176,43 @@ class MultiSourceAgentDeploymentService:
|
|
|
176
176
|
# Log if a higher priority source was overridden by version
|
|
177
177
|
for other_agent in agent_versions:
|
|
178
178
|
if other_agent != highest_version_agent:
|
|
179
|
-
|
|
179
|
+
# Parse both versions for comparison
|
|
180
|
+
other_version = self.version_manager.parse_version(
|
|
180
181
|
other_agent.get("version", "0.0.0")
|
|
181
182
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
183
|
+
highest_version = self.version_manager.parse_version(
|
|
184
|
+
highest_version_agent.get("version", "0.0.0")
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Compare the versions
|
|
188
|
+
version_comparison = self.version_manager.compare_versions(
|
|
189
|
+
other_version, highest_version
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Only warn if the other version is actually lower
|
|
193
|
+
if version_comparison < 0:
|
|
194
|
+
if (
|
|
195
|
+
other_agent["source"] == "project"
|
|
196
|
+
and highest_version_agent["source"] == "system"
|
|
197
|
+
):
|
|
198
|
+
self.logger.warning(
|
|
199
|
+
f"Project agent '{agent_name}' v{other_agent['version']} "
|
|
200
|
+
f"overridden by higher system version v{highest_version_agent['version']}"
|
|
201
|
+
)
|
|
202
|
+
elif other_agent["source"] == "user" and highest_version_agent[
|
|
203
|
+
"source"
|
|
204
|
+
] in ["system", "project"]:
|
|
205
|
+
self.logger.warning(
|
|
206
|
+
f"User agent '{agent_name}' v{other_agent['version']} "
|
|
207
|
+
f"overridden by higher {highest_version_agent['source']} version v{highest_version_agent['version']}"
|
|
208
|
+
)
|
|
209
|
+
elif version_comparison == 0:
|
|
210
|
+
# Log info when versions are equal but different sources
|
|
211
|
+
if other_agent["source"] != highest_version_agent["source"]:
|
|
212
|
+
self.logger.info(
|
|
213
|
+
f"Using {highest_version_agent['source']} source for '{agent_name}' "
|
|
214
|
+
f"(same version v{highest_version_agent['version']} as {other_agent['source']} source)"
|
|
215
|
+
)
|
|
197
216
|
|
|
198
217
|
return selected_agents
|
|
199
218
|
|