claude-mpm 4.2.51__py3-none-any.whl → 4.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 (no spaces!)
161
- tools_str = ",".join(tools) if isinstance(tools, list) else str(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
- agent_specific_instructions = (
335
- template_data.get("instructions")
336
- or base_agent_data.get("content")
337
- or base_agent_data.get("instructions")
338
- or "# Agent Instructions\n\nThis agent provides specialized assistance."
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
- tools = merged_config.get("tools", ["Read", "Write", "Edit"])
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()
@@ -280,7 +280,6 @@ class LocalAgentTemplateManager:
280
280
  parent_agent=parent_agent,
281
281
  )
282
282
 
283
-
284
283
  def save_local_template(
285
284
  self, template: LocalAgentTemplate, tier: Optional[str] = None
286
285
  ) -> Path:
@@ -575,9 +575,7 @@ class DaemonManager:
575
575
  stdout=log_file,
576
576
  stderr=subprocess.STDOUT if self.log_file else subprocess.DEVNULL,
577
577
  start_new_session=True, # Create new process group
578
- close_fds=(
579
- not self.log_file
580
- ), # Keep log file open if redirecting
578
+ close_fds=(not self.log_file), # Keep log file open if redirecting
581
579
  env=env, # Pass modified environment
582
580
  )
583
581
 
@@ -186,7 +186,11 @@ class AsyncEventEmitter:
186
186
  return False
187
187
 
188
188
  async def _emit_http(
189
- self, namespace: str, event: str, data: Dict[str, Any], endpoint: Optional[str] = None
189
+ self,
190
+ namespace: str,
191
+ event: str,
192
+ data: Dict[str, Any],
193
+ endpoint: Optional[str] = None,
190
194
  ) -> bool:
191
195
  """Emit event via HTTP with connection pooling."""
192
196
  if not self._http_session:
@@ -428,7 +428,6 @@ class HookHandler:
428
428
  "original_event": data, # Keep original for debugging
429
429
  }
430
430
 
431
-
432
431
  def _process_hook_event(self, data: Dict) -> Dict:
433
432
  """Process and normalize hook event data.
434
433
 
@@ -447,7 +446,6 @@ class HookHandler:
447
446
  "processed_at": asyncio.get_event_loop().time(),
448
447
  }
449
448
 
450
-
451
449
  def _update_session_tracking(self, session_id: str, event: Dict):
452
450
  """Update session tracking with new event.
453
451
 
@@ -1756,9 +1756,7 @@ class CodeTreeAnalyzer:
1756
1756
  return node.name not in important_magic
1757
1757
 
1758
1758
  # Filter very generic getters/setters only if they're trivial
1759
- if (name_lower.startswith(("get_", "set_"))) and len(
1760
- node.name
1761
- ) <= 8:
1759
+ if (name_lower.startswith(("get_", "set_"))) and len(node.name) <= 8:
1762
1760
  return True
1763
1761
 
1764
1762
  # Don't filter single underscore functions - they're often important