claude-mpm 3.4.27__py3-none-any.whl → 3.5.1__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 (123) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +182 -299
  3. claude_mpm/agents/agent_loader.py +283 -57
  4. claude_mpm/agents/agent_loader_integration.py +6 -9
  5. claude_mpm/agents/base_agent.json +2 -1
  6. claude_mpm/agents/base_agent_loader.py +1 -1
  7. claude_mpm/cli/__init__.py +5 -7
  8. claude_mpm/cli/commands/__init__.py +0 -2
  9. claude_mpm/cli/commands/agents.py +1 -1
  10. claude_mpm/cli/commands/memory.py +1 -1
  11. claude_mpm/cli/commands/run.py +12 -0
  12. claude_mpm/cli/parser.py +0 -13
  13. claude_mpm/cli/utils.py +1 -1
  14. claude_mpm/config/__init__.py +44 -2
  15. claude_mpm/config/agent_config.py +348 -0
  16. claude_mpm/config/paths.py +322 -0
  17. claude_mpm/constants.py +0 -1
  18. claude_mpm/core/__init__.py +2 -5
  19. claude_mpm/core/agent_registry.py +63 -17
  20. claude_mpm/core/claude_runner.py +354 -43
  21. claude_mpm/core/config.py +7 -1
  22. claude_mpm/core/config_aliases.py +4 -3
  23. claude_mpm/core/config_paths.py +151 -0
  24. claude_mpm/core/factories.py +4 -50
  25. claude_mpm/core/logger.py +11 -13
  26. claude_mpm/core/service_registry.py +2 -2
  27. claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
  28. claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
  30. claude_mpm/hooks/memory_integration_hook.py +1 -1
  31. claude_mpm/init.py +37 -6
  32. claude_mpm/scripts/socketio_daemon.py +6 -2
  33. claude_mpm/services/__init__.py +71 -3
  34. claude_mpm/services/agents/__init__.py +85 -0
  35. claude_mpm/services/agents/deployment/__init__.py +21 -0
  36. claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
  37. claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
  38. claude_mpm/services/agents/loading/__init__.py +11 -0
  39. claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
  40. claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
  41. claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
  42. claude_mpm/services/agents/management/__init__.py +9 -0
  43. claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
  44. claude_mpm/services/agents/memory/__init__.py +21 -0
  45. claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
  46. claude_mpm/services/agents/registry/__init__.py +29 -0
  47. claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
  48. claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
  49. claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
  50. claude_mpm/services/async_session_logger.py +584 -0
  51. claude_mpm/services/claude_session_logger.py +299 -0
  52. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  53. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
  54. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
  56. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
  57. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
  58. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
  59. claude_mpm/services/framework_claude_md_generator.py +4 -2
  60. claude_mpm/services/memory/__init__.py +17 -0
  61. claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
  62. claude_mpm/services/memory/cache/__init__.py +14 -0
  63. claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
  64. claude_mpm/services/memory/cache/simple_cache.py +317 -0
  65. claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
  66. claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
  67. claude_mpm/services/optimized_hook_service.py +542 -0
  68. claude_mpm/services/project_registry.py +14 -8
  69. claude_mpm/services/response_tracker.py +237 -0
  70. claude_mpm/services/ticketing_service_original.py +4 -2
  71. claude_mpm/services/version_control/branch_strategy.py +3 -1
  72. claude_mpm/utils/paths.py +12 -10
  73. claude_mpm/utils/session_logging.py +114 -0
  74. claude_mpm/validation/agent_validator.py +2 -1
  75. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/METADATA +28 -20
  76. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/RECORD +83 -106
  77. claude_mpm/cli/commands/ui.py +0 -57
  78. claude_mpm/core/simple_runner.py +0 -1046
  79. claude_mpm/hooks/builtin/__init__.py +0 -1
  80. claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
  81. claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
  82. claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
  83. claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
  84. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
  85. claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
  86. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
  87. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
  88. claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
  89. claude_mpm/orchestration/__init__.py +0 -6
  90. claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
  91. claude_mpm/orchestration/archive/factory.py +0 -215
  92. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
  93. claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
  94. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
  95. claude_mpm/orchestration/archive/orchestrator.py +0 -501
  96. claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
  97. claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
  98. claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
  99. claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
  100. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
  101. claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
  102. claude_mpm/schemas/workflow_validator.py +0 -411
  103. claude_mpm/services/parent_directory_manager/__init__.py +0 -577
  104. claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
  105. claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
  106. claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
  107. claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
  108. claude_mpm/services/parent_directory_manager/operations.py +0 -186
  109. claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
  110. claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
  111. claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
  112. claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
  113. claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
  114. claude_mpm/ui/__init__.py +0 -1
  115. claude_mpm/ui/rich_terminal_ui.py +0 -295
  116. claude_mpm/ui/terminal_ui.py +0 -328
  117. /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
  118. /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
  119. /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
  120. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/WHEEL +0 -0
  121. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/entry_points.txt +0 -0
  122. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/licenses/LICENSE +0 -0
  123. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/top_level.txt +0 -0
@@ -1,181 +0,0 @@
1
- """Hook to log workflow information at the start of execution."""
2
-
3
- import json
4
- from typing import Dict, Any, List, Optional
5
- from datetime import datetime
6
-
7
- from claude_mpm.hooks.base_hook import SubmitHook, HookContext, HookResult, HookType
8
- from claude_mpm.core.logger import get_logger
9
-
10
- logger = get_logger(__name__)
11
-
12
-
13
- class WorkflowStartHook(SubmitHook):
14
- """Hook that logs workflow information including steps list at workflow start."""
15
-
16
- def __init__(self):
17
- super().__init__(name="workflow_start_logger", priority=5)
18
-
19
- def execute(self, context: HookContext) -> HookResult:
20
- """Log workflow information when a workflow starts."""
21
- try:
22
- # Extract prompt and workflow data from context
23
- prompt = context.data.get('prompt', '')
24
- workflow_data = context.data.get('workflow', {})
25
-
26
- # Check if this is a workflow start (either by explicit workflow data or detected pattern)
27
- if workflow_data or self._is_workflow_prompt(prompt):
28
- # Extract workflow information
29
- workflow_name = workflow_data.get('name', 'Unnamed Workflow')
30
- workflow_steps = workflow_data.get('steps', [])
31
-
32
- # If no explicit steps provided, try to parse from prompt
33
- if not workflow_steps and prompt:
34
- workflow_steps = self._extract_steps_from_prompt(prompt)
35
-
36
- # Log workflow start information
37
- logger.info("="*60)
38
- logger.info("WORKFLOW START")
39
- logger.info("="*60)
40
- logger.info(f"Workflow: {workflow_name}")
41
- logger.info(f"Started at: {context.timestamp.isoformat()}")
42
- logger.info(f"Session ID: {context.session_id or 'N/A'}")
43
- logger.info(f"User ID: {context.user_id or 'N/A'}")
44
-
45
- if workflow_steps:
46
- logger.info(f"\nWorkflow Steps ({len(workflow_steps)} total):")
47
- for i, step in enumerate(workflow_steps, 1):
48
- if isinstance(step, dict):
49
- step_name = step.get('name', step.get('description', 'Unnamed step'))
50
- step_type = step.get('type', 'task')
51
- logger.info(f" {i}. [{step_type}] {step_name}")
52
- else:
53
- # Handle simple string steps
54
- logger.info(f" {i}. {step}")
55
- else:
56
- logger.info("\nNo explicit workflow steps defined")
57
-
58
- # Log additional metadata if present
59
- metadata = workflow_data.get('metadata', {})
60
- if metadata:
61
- logger.info(f"\nWorkflow Metadata:")
62
- for key, value in metadata.items():
63
- logger.info(f" {key}: {value}")
64
-
65
- logger.info("="*60)
66
-
67
- # Add workflow info to result metadata for downstream hooks
68
- return HookResult(
69
- success=True,
70
- modified=False,
71
- metadata={
72
- 'workflow_logged': True,
73
- 'workflow_name': workflow_name,
74
- 'step_count': len(workflow_steps),
75
- 'has_explicit_workflow': bool(workflow_data)
76
- }
77
- )
78
-
79
- # Not a workflow start, pass through
80
- return HookResult(success=True, modified=False)
81
-
82
- except Exception as e:
83
- logger.error(f"Workflow start logging failed: {e}")
84
- # Don't block execution on logging errors
85
- return HookResult(
86
- success=True,
87
- modified=False,
88
- error=str(e)
89
- )
90
-
91
- def _is_workflow_prompt(self, prompt: str) -> bool:
92
- """Detect if a prompt indicates a workflow start."""
93
- if not prompt:
94
- return False
95
-
96
- prompt_lower = prompt.lower()
97
- workflow_indicators = [
98
- 'workflow', 'steps:', 'step 1', 'first,', 'then,', 'finally,',
99
- 'process:', 'procedure:', 'sequence:', 'plan:'
100
- ]
101
-
102
- return any(indicator in prompt_lower for indicator in workflow_indicators)
103
-
104
- def _extract_steps_from_prompt(self, prompt: str) -> List[str]:
105
- """Try to extract workflow steps from a prompt text."""
106
- steps = []
107
-
108
- # Look for numbered steps (1. 2. 3. or 1) 2) 3))
109
- import re
110
- numbered_pattern = re.compile(r'^\s*\d+[\)\.]\s*(.+)$', re.MULTILINE)
111
- matches = numbered_pattern.findall(prompt)
112
- if matches:
113
- steps.extend(matches)
114
- return steps
115
-
116
- # Look for bullet points
117
- bullet_pattern = re.compile(r'^\s*[-*•]\s*(.+)$', re.MULTILINE)
118
- matches = bullet_pattern.findall(prompt)
119
- if matches:
120
- steps.extend(matches)
121
- return steps
122
-
123
- # Look for sequential keywords
124
- sequential_pattern = re.compile(
125
- r'(?:first|then|next|after that|finally|lastly),?\s*(.+?)(?=(?:first|then|next|after that|finally|lastly|$))',
126
- re.IGNORECASE | re.DOTALL
127
- )
128
- matches = sequential_pattern.findall(prompt)
129
- if matches:
130
- steps.extend([m.strip() for m in matches if m.strip()])
131
-
132
- return steps
133
-
134
- def validate(self, context: HookContext) -> bool:
135
- """Validate if hook should run for given context."""
136
- if not super().validate(context):
137
- return False
138
-
139
- # This hook runs for all submit contexts to check for workflows
140
- return context.hook_type == HookType.SUBMIT
141
-
142
-
143
- class WorkflowStepLogger(SubmitHook):
144
- """Alternative hook that logs individual workflow step execution."""
145
-
146
- def __init__(self):
147
- super().__init__(name="workflow_step_logger", priority=6)
148
-
149
- def execute(self, context: HookContext) -> HookResult:
150
- """Log individual workflow step execution."""
151
- try:
152
- # Check if this is a workflow step execution
153
- step_data = context.data.get('workflow_step', {})
154
- if not step_data:
155
- return HookResult(success=True, modified=False)
156
-
157
- # Extract step information
158
- step_number = step_data.get('number', 0)
159
- step_name = step_data.get('name', 'Unnamed step')
160
- step_type = step_data.get('type', 'task')
161
- workflow_name = step_data.get('workflow_name', 'Unknown workflow')
162
-
163
- # Log step execution
164
- logger.info(f"\n→ Executing Step {step_number}: {step_name}")
165
- logger.info(f" Type: {step_type}")
166
- logger.info(f" Workflow: {workflow_name}")
167
- logger.info(f" Started: {context.timestamp.isoformat()}")
168
-
169
- return HookResult(
170
- success=True,
171
- modified=False,
172
- metadata={
173
- 'step_logged': True,
174
- 'step_number': step_number,
175
- 'step_name': step_name
176
- }
177
- )
178
-
179
- except Exception as e:
180
- logger.error(f"Workflow step logging failed: {e}")
181
- return HookResult(success=True, modified=False, error=str(e))
@@ -1,6 +0,0 @@
1
- """Orchestration components for Claude MPM (legacy)."""
2
-
3
- # Most orchestration components have been simplified and moved to core.claude_runner
4
- # This module is kept for backwards compatibility
5
-
6
- __all__ = []
@@ -1,195 +0,0 @@
1
- """Direct orchestrator that runs Claude with minimal intervention."""
2
-
3
- import os
4
- from pathlib import Path
5
- from typing import Optional
6
- from datetime import datetime
7
- import logging
8
- import tempfile
9
-
10
- try:
11
- from ..core.logger import get_logger, setup_logging
12
- from ..utils.subprocess_runner import SubprocessRunner
13
- # TicketExtractor removed from project
14
- from ..core.framework_loader import FrameworkLoader
15
- from .agent_delegator import AgentDelegator
16
- except ImportError:
17
- from core.logger import get_logger, setup_logging
18
- from utils.subprocess_runner import SubprocessRunner
19
- # TicketExtractor removed from project
20
- from core.framework_loader import FrameworkLoader
21
- from orchestration.agent_delegator import AgentDelegator
22
-
23
-
24
- class DirectOrchestrator:
25
- """Orchestrator that runs Claude directly with framework injection via file."""
26
-
27
- def __init__(
28
- self,
29
- framework_path: Optional[Path] = None,
30
- agents_dir: Optional[Path] = None,
31
- log_level: str = "OFF",
32
- log_dir: Optional[Path] = None,
33
- ):
34
- """Initialize the orchestrator."""
35
- self.log_level = log_level
36
- self.log_dir = log_dir or (Path.home() / ".claude-mpm" / "logs")
37
-
38
- # Set up logging
39
- if log_level != "OFF":
40
- self.logger = setup_logging(level=log_level, log_dir=log_dir)
41
- self.logger.info(f"Initializing Direct Orchestrator (log_level={log_level})")
42
- else:
43
- # Minimal logger
44
- self.logger = get_logger("direct_orchestrator")
45
- self.logger.setLevel(logging.WARNING)
46
-
47
- # Components
48
- self.framework_loader = FrameworkLoader(framework_path, agents_dir)
49
- # TicketExtractor removed from project
50
- self.agent_delegator = AgentDelegator(self.framework_loader.agent_registry)
51
-
52
- # State
53
- self.session_start = datetime.now()
54
- # Ticket creation removed from project
55
-
56
- # Initialize subprocess runner
57
- self.subprocess_runner = SubprocessRunner(logger=self.logger)
58
-
59
- def run_interactive(self):
60
- """Run an interactive session by launching Claude directly."""
61
- print("Claude MPM Interactive Session")
62
- print("Framework will be injected on first interaction")
63
- print("-" * 50)
64
-
65
- # Get framework instructions
66
- framework = self.framework_loader.get_framework_instructions()
67
-
68
- # Save framework to a temporary file
69
- with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
70
- f.write(framework)
71
- f.write("\n\nNOTE: This is the claude-mpm framework. Please acknowledge you've received these instructions and then we can begin our session.\n")
72
- framework_file = f.name
73
-
74
- try:
75
- # Log framework injection
76
- if self.log_level != "OFF":
77
- self.logger.info(f"Framework saved to temporary file: {framework_file}")
78
-
79
- # Also save to prompts directory
80
- prompt_path = Path.home() / ".claude-mpm" / "prompts"
81
- prompt_path.mkdir(parents=True, exist_ok=True)
82
- prompt_file = prompt_path / f"prompt_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
83
- prompt_file.write_text(framework)
84
- self.logger.info(f"Framework also saved to: {prompt_file}")
85
-
86
- # Read the framework content
87
- with open(framework_file, 'r') as f:
88
- framework_content = f.read()
89
-
90
- # Generate a unique session ID
91
- import uuid
92
- session_id = str(uuid.uuid4())
93
-
94
- # Build command to start Claude with print mode and session ID
95
- cmd = [
96
- "claude",
97
- "--model", "opus",
98
- "--dangerously-skip-permissions",
99
- "--session-id", session_id,
100
- "--print", # Print mode
101
- framework_content
102
- ]
103
-
104
- self.logger.info(f"Starting Claude with framework injection (session: {session_id})")
105
-
106
- # Run Claude with framework
107
- print("\nInjecting framework instructions...")
108
- result = self.subprocess_runner.run(cmd)
109
-
110
- if result.success:
111
- print("\nFramework injected. Claude's response:")
112
- print("-" * 50)
113
- print(result.stdout)
114
- print("-" * 50)
115
-
116
- # Debug: show stderr if logging is enabled
117
- if self.log_level != "OFF" and result.stderr:
118
- self.logger.debug(f"Claude stderr: {result.stderr}")
119
-
120
- # Check if we can find the conversation file
121
- # Parse stderr for conversation file location
122
- conversation_file = None
123
- if result.stderr:
124
- import re
125
- # Look for patterns like "Conversation saved to: /path/to/file"
126
- match = re.search(r'(?:conversation saved to|saved to)[:\s]+([^\s]+)', result.stderr, re.I)
127
- if match:
128
- conversation_file = match.group(1).strip()
129
- self.logger.info(f"Found conversation file: {conversation_file}")
130
-
131
- # Now start interactive Claude session with same session ID
132
- print("\nStarting interactive session...")
133
- interactive_cmd = [
134
- "claude",
135
- "--model", "opus",
136
- "--dangerously-skip-permissions"
137
- ]
138
-
139
- # Try to continue the conversation
140
- if conversation_file and Path(conversation_file).exists():
141
- interactive_cmd.extend(["--continue", conversation_file])
142
- print(f"Continuing conversation from: {conversation_file}")
143
- elif session_id:
144
- interactive_cmd.extend(["--session-id", session_id])
145
- print(f"Using session ID: {session_id}")
146
-
147
- # Run Claude interactively
148
- self.subprocess_runner.run(interactive_cmd)
149
- else:
150
- print(f"Error injecting framework: {result.stderr}")
151
-
152
- self.logger.info(f"Claude exited with code: {result.returncode}")
153
-
154
- finally:
155
- # Clean up temporary file
156
- try:
157
- os.unlink(framework_file)
158
- except:
159
- pass
160
-
161
- # Ticket creation removed from project
162
-
163
- # _create_tickets method removed - TicketExtractor functionality removed from project
164
-
165
- def run_non_interactive(self, user_input: str):
166
- """Run a non-interactive session using print mode."""
167
- try:
168
- # Prepare message with framework
169
- framework = self.framework_loader.get_framework_instructions()
170
- full_message = framework + "\n\nUser: " + user_input
171
-
172
- # Build command
173
- cmd = [
174
- "claude",
175
- "--model", "opus",
176
- "--dangerously-skip-permissions",
177
- "--print", # Print mode
178
- full_message
179
- ]
180
-
181
- # Run Claude
182
- result = self.subprocess_runner.run(cmd)
183
-
184
- if result.success:
185
- print(result.stdout)
186
-
187
- # Ticket extraction removed from project
188
- else:
189
- print(f"Error: {result.stderr}")
190
-
191
- # Ticket creation removed from project
192
-
193
- except Exception as e:
194
- print(f"Error: {e}")
195
- self.logger.error(f"Non-interactive error: {e}")
@@ -1,215 +0,0 @@
1
- """Orchestrator factory for creating orchestrators based on mode and configuration."""
2
-
3
- import logging
4
- from pathlib import Path
5
- from typing import Optional, Dict, Type, Any
6
- from enum import Enum
7
-
8
- from ..core.logger import get_logger
9
- from .orchestrator import MPMOrchestrator
10
- from .system_prompt_orchestrator import SystemPromptOrchestrator
11
- from .subprocess_orchestrator import SubprocessOrchestrator
12
- from .interactive_subprocess_orchestrator import InteractiveSubprocessOrchestrator
13
-
14
-
15
- class OrchestratorMode(Enum):
16
- """Available orchestrator modes."""
17
- SYSTEM_PROMPT = "system_prompt"
18
- SUBPROCESS = "subprocess"
19
- INTERACTIVE_SUBPROCESS = "interactive_subprocess"
20
- DIRECT = "direct"
21
- PTY = "pty"
22
- WRAPPER = "wrapper"
23
- SIMPLE = "simple"
24
-
25
-
26
- class OrchestratorFactory:
27
- """Factory for creating orchestrators based on mode and configuration.
28
-
29
- This factory simplifies orchestrator selection and reduces complexity
30
- in the run_session function by centralizing the logic for choosing
31
- and instantiating the appropriate orchestrator.
32
- """
33
-
34
- # Registry of available orchestrators
35
- _registry: Dict[OrchestratorMode, Type[MPMOrchestrator]] = {
36
- OrchestratorMode.SYSTEM_PROMPT: SystemPromptOrchestrator,
37
- OrchestratorMode.SUBPROCESS: SubprocessOrchestrator,
38
- OrchestratorMode.INTERACTIVE_SUBPROCESS: InteractiveSubprocessOrchestrator,
39
- }
40
-
41
- def __init__(self):
42
- """Initialize the orchestrator factory."""
43
- self.logger = get_logger(self.__class__.__name__)
44
- self._discover_orchestrators()
45
-
46
- def _discover_orchestrators(self):
47
- """Discover and register additional orchestrators.
48
-
49
- This method enables automatic discovery of new orchestrator types
50
- without modifying the factory code.
51
- """
52
- # Try to import optional orchestrators
53
- try:
54
- from .direct_orchestrator import DirectOrchestrator
55
- self._registry[OrchestratorMode.DIRECT] = DirectOrchestrator
56
- self.logger.debug("Registered DirectOrchestrator")
57
- except ImportError:
58
- self.logger.debug("DirectOrchestrator not available")
59
-
60
- try:
61
- from .pty_orchestrator import PTYOrchestrator
62
- self._registry[OrchestratorMode.PTY] = PTYOrchestrator
63
- self.logger.debug("Registered PTYOrchestrator")
64
- except ImportError:
65
- self.logger.debug("PTYOrchestrator not available")
66
-
67
- try:
68
- from .wrapper_orchestrator import WrapperOrchestrator
69
- self._registry[OrchestratorMode.WRAPPER] = WrapperOrchestrator
70
- self.logger.debug("Registered WrapperOrchestrator")
71
- except ImportError:
72
- self.logger.debug("WrapperOrchestrator not available")
73
-
74
- try:
75
- from .simple_orchestrator import SimpleOrchestrator
76
- self._registry[OrchestratorMode.SIMPLE] = SimpleOrchestrator
77
- self.logger.debug("Registered SimpleOrchestrator")
78
- except ImportError:
79
- self.logger.debug("SimpleOrchestrator not available")
80
-
81
- def create_orchestrator(
82
- self,
83
- mode: Optional[str] = None,
84
- config: Optional[Dict[str, Any]] = None
85
- ) -> MPMOrchestrator:
86
- """Create an orchestrator based on mode and configuration.
87
-
88
- Args:
89
- mode: Orchestrator mode (string or OrchestratorMode enum)
90
- config: Configuration dictionary containing:
91
- - framework_path: Path to framework directory
92
- - agents_dir: Custom agents directory
93
- - log_level: Logging level (OFF, INFO, DEBUG)
94
- - log_dir: Custom log directory
95
- - hook_manager: Hook service manager instance
96
- - enable_todo_hijacking: Enable TODO hijacking (subprocess mode)
97
- - subprocess: Use subprocess orchestration
98
- - interactive_subprocess: Use interactive subprocess
99
- - Any other orchestrator-specific parameters
100
-
101
- Returns:
102
- Configured orchestrator instance
103
-
104
- Raises:
105
- ValueError: If mode is invalid or orchestrator creation fails
106
- """
107
- if config is None:
108
- config = {}
109
-
110
- # Determine orchestrator mode
111
- orchestrator_mode = self._determine_mode(mode, config)
112
-
113
- # Validate mode
114
- if orchestrator_mode not in self._registry:
115
- available = ", ".join(m.value for m in self._registry.keys())
116
- raise ValueError(
117
- f"Invalid orchestrator mode: {orchestrator_mode.value}. "
118
- f"Available modes: {available}"
119
- )
120
-
121
- # Get orchestrator class
122
- orchestrator_class = self._registry[orchestrator_mode]
123
-
124
- # Extract common parameters
125
- common_params = {
126
- "framework_path": config.get("framework_path"),
127
- "agents_dir": config.get("agents_dir"),
128
- "log_level": config.get("log_level", "OFF"),
129
- "log_dir": config.get("log_dir"),
130
- }
131
-
132
- # Add hook manager if available
133
- if "hook_manager" in config:
134
- common_params["hook_manager"] = config["hook_manager"]
135
-
136
- # Add mode-specific parameters
137
- if orchestrator_mode == OrchestratorMode.SUBPROCESS:
138
- common_params["enable_todo_hijacking"] = config.get("enable_todo_hijacking", False)
139
-
140
- try:
141
- # Create orchestrator instance
142
- orchestrator = orchestrator_class(**common_params)
143
-
144
- # Configure additional settings
145
- if "no_tickets" in config and config["no_tickets"]:
146
- orchestrator.ticket_creation_enabled = False
147
- self.logger.info("Ticket creation disabled for orchestrator")
148
-
149
- self.logger.info(
150
- f"Created {orchestrator_class.__name__} "
151
- f"(mode: {orchestrator_mode.value})"
152
- )
153
-
154
- return orchestrator
155
-
156
- except Exception as e:
157
- self.logger.error(f"Failed to create orchestrator: {e}")
158
- raise ValueError(f"Failed to create orchestrator: {e}") from e
159
-
160
- def _determine_mode(
161
- self,
162
- mode: Optional[str],
163
- config: Dict[str, Any]
164
- ) -> OrchestratorMode:
165
- """Determine orchestrator mode from explicit mode or config flags.
166
-
167
- Args:
168
- mode: Explicit mode string
169
- config: Configuration dictionary
170
-
171
- Returns:
172
- OrchestratorMode enum value
173
- """
174
- # Always use subprocess orchestrator for simplicity
175
- # This provides consistent behavior in both interactive and non-interactive modes
176
- return OrchestratorMode.SUBPROCESS
177
-
178
- def list_available_modes(self) -> Dict[str, Dict[str, Any]]:
179
- """List all available orchestrator modes with metadata.
180
-
181
- Returns:
182
- Dictionary mapping mode names to metadata
183
- """
184
- modes = {}
185
- for mode, orchestrator_class in self._registry.items():
186
- modes[mode.value] = {
187
- "class": orchestrator_class.__name__,
188
- "module": orchestrator_class.__module__,
189
- "description": orchestrator_class.__doc__.strip() if orchestrator_class.__doc__ else "No description",
190
- }
191
- return modes
192
-
193
- def register_orchestrator(
194
- self,
195
- mode: OrchestratorMode,
196
- orchestrator_class: Type[MPMOrchestrator]
197
- ):
198
- """Register a custom orchestrator.
199
-
200
- Args:
201
- mode: Orchestrator mode
202
- orchestrator_class: Orchestrator class (must inherit from MPMOrchestrator)
203
-
204
- Raises:
205
- ValueError: If orchestrator class is invalid
206
- """
207
- if not issubclass(orchestrator_class, MPMOrchestrator):
208
- raise ValueError(
209
- f"{orchestrator_class.__name__} must inherit from MPMOrchestrator"
210
- )
211
-
212
- self._registry[mode] = orchestrator_class
213
- self.logger.info(
214
- f"Registered {orchestrator_class.__name__} for mode {mode.value}"
215
- )