claude-mpm 0.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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Core mixins for Claude MPM.
|
|
2
|
+
|
|
3
|
+
This module provides reusable mixins that add common functionality
|
|
4
|
+
to classes throughout the framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoggerMixin:
|
|
12
|
+
"""Mixin that provides automatic logger initialization.
|
|
13
|
+
|
|
14
|
+
This mixin eliminates duplicate logger initialization code by automatically
|
|
15
|
+
creating a logger instance based on the class name or a custom name.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
class MyService(LoggerMixin):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
# Logger is automatically available as self.logger
|
|
21
|
+
self.logger.info("Service initialized")
|
|
22
|
+
|
|
23
|
+
class CustomLogger(LoggerMixin):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
# Use custom logger name
|
|
26
|
+
self._logger_name = "my.custom.logger"
|
|
27
|
+
self.logger.info("Using custom logger")
|
|
28
|
+
|
|
29
|
+
The logger is lazily initialized on first access, using either:
|
|
30
|
+
1. A custom name set via self._logger_name
|
|
31
|
+
2. The module and class name (e.g., 'claude_mpm.services.MyService')
|
|
32
|
+
3. The module name if class name is not available
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
logger: The logging.Logger instance for this class
|
|
36
|
+
_logger_name: Optional custom logger name to use instead of class name
|
|
37
|
+
_logger_instance: Cached logger instance (internal use)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_logger_instance: Optional[logging.Logger] = None
|
|
41
|
+
_logger_name: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def logger(self) -> logging.Logger:
|
|
45
|
+
"""Get or create the logger instance.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
logging.Logger: The logger instance for this class
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> class MyService(LoggerMixin):
|
|
52
|
+
... def process(self):
|
|
53
|
+
... self.logger.info("Processing...")
|
|
54
|
+
>>>
|
|
55
|
+
>>> service = MyService()
|
|
56
|
+
>>> service.process() # Logs with 'claude_mpm.module.MyService'
|
|
57
|
+
|
|
58
|
+
>>> class CustomService(LoggerMixin):
|
|
59
|
+
... def __init__(self):
|
|
60
|
+
... self._logger_name = "custom.service"
|
|
61
|
+
... self.logger.debug("Initialized")
|
|
62
|
+
>>>
|
|
63
|
+
>>> custom = CustomService() # Logs with 'custom.service'
|
|
64
|
+
"""
|
|
65
|
+
if self._logger_instance is None:
|
|
66
|
+
if self._logger_name:
|
|
67
|
+
# Use custom logger name if provided
|
|
68
|
+
logger_name = self._logger_name
|
|
69
|
+
else:
|
|
70
|
+
# Build logger name from module and class
|
|
71
|
+
module = self.__class__.__module__
|
|
72
|
+
class_name = self.__class__.__name__
|
|
73
|
+
|
|
74
|
+
if module and module != "__main__":
|
|
75
|
+
logger_name = f"{module}.{class_name}"
|
|
76
|
+
else:
|
|
77
|
+
# Fallback to just class name if no proper module
|
|
78
|
+
logger_name = class_name
|
|
79
|
+
|
|
80
|
+
self._logger_instance = logging.getLogger(logger_name)
|
|
81
|
+
|
|
82
|
+
return self._logger_instance
|
|
83
|
+
|
|
84
|
+
def set_logger(self, logger: Optional[logging.Logger] = None,
|
|
85
|
+
name: Optional[str] = None) -> None:
|
|
86
|
+
"""Set a custom logger instance or name.
|
|
87
|
+
|
|
88
|
+
This method allows overriding the automatic logger creation with either
|
|
89
|
+
a pre-configured logger instance or a custom logger name.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
logger: A pre-configured Logger instance to use
|
|
93
|
+
name: A custom name for creating a new logger
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If both logger and name are provided
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> service = MyService()
|
|
100
|
+
>>> service.set_logger(name="my.custom.logger")
|
|
101
|
+
>>> service.logger.info("Using custom logger")
|
|
102
|
+
|
|
103
|
+
>>> import logging
|
|
104
|
+
>>> custom_logger = logging.getLogger("preconfigured")
|
|
105
|
+
>>> service.set_logger(logger=custom_logger)
|
|
106
|
+
"""
|
|
107
|
+
if logger and name:
|
|
108
|
+
raise ValueError("Cannot specify both logger instance and name")
|
|
109
|
+
|
|
110
|
+
if logger:
|
|
111
|
+
self._logger_instance = logger
|
|
112
|
+
self._logger_name = logger.name
|
|
113
|
+
elif name:
|
|
114
|
+
self._logger_name = name
|
|
115
|
+
self._logger_instance = None # Reset to force recreation
|
|
116
|
+
else:
|
|
117
|
+
# Reset to defaults
|
|
118
|
+
self._logger_name = None
|
|
119
|
+
self._logger_instance = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Example usage patterns for reference
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
# Example 1: Basic usage with automatic logger naming
|
|
125
|
+
class ExampleService(LoggerMixin):
|
|
126
|
+
def do_work(self):
|
|
127
|
+
self.logger.info("Doing work...")
|
|
128
|
+
self.logger.debug("Debug details")
|
|
129
|
+
|
|
130
|
+
# Example 2: Custom logger name
|
|
131
|
+
class CustomService(LoggerMixin):
|
|
132
|
+
def __init__(self):
|
|
133
|
+
self._logger_name = "custom.service"
|
|
134
|
+
self.logger.info("Service initialized with custom logger")
|
|
135
|
+
|
|
136
|
+
# Example 3: Setting logger after initialization
|
|
137
|
+
class ConfigurableService(LoggerMixin):
|
|
138
|
+
def configure(self, logger_name: str):
|
|
139
|
+
self.set_logger(name=logger_name)
|
|
140
|
+
self.logger.info(f"Reconfigured with logger: {logger_name}")
|
|
141
|
+
|
|
142
|
+
# Example 4: Multiple inheritance friendly
|
|
143
|
+
class BaseService:
|
|
144
|
+
def __init__(self, config=None):
|
|
145
|
+
self.config = config
|
|
146
|
+
|
|
147
|
+
class AdvancedService(BaseService, LoggerMixin):
|
|
148
|
+
def __init__(self, config=None):
|
|
149
|
+
super().__init__(config)
|
|
150
|
+
self.logger.info("Advanced service ready")
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service Registry for Claude MPM.
|
|
3
|
+
|
|
4
|
+
Provides centralized service registration and discovery, working with
|
|
5
|
+
the DI container to manage application services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
11
|
+
|
|
12
|
+
from .container import DIContainer, ServiceLifetime, get_container
|
|
13
|
+
from .base_service import BaseService
|
|
14
|
+
from .logger import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ServiceRegistry:
|
|
20
|
+
"""
|
|
21
|
+
Central registry for all application services.
|
|
22
|
+
|
|
23
|
+
Manages service registration, configuration, and lifecycle.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, container: Optional[DIContainer] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize service registry.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
container: DI container to use (uses global if not provided)
|
|
32
|
+
"""
|
|
33
|
+
self.container = container or get_container()
|
|
34
|
+
self._services: Dict[str, Type[BaseService]] = {}
|
|
35
|
+
self._initialized = False
|
|
36
|
+
|
|
37
|
+
def register_core_services(self) -> None:
|
|
38
|
+
"""Register all core framework services."""
|
|
39
|
+
from ..services.shared_prompt_cache import SharedPromptCache
|
|
40
|
+
from ..services.json_rpc_hook_manager import JSONRPCHookManager
|
|
41
|
+
from ..services.ticket_manager import TicketManager
|
|
42
|
+
from ..services.agent_deployment import AgentDeploymentService
|
|
43
|
+
from .session_manager import SessionManager
|
|
44
|
+
from .agent_session_manager import AgentSessionManager
|
|
45
|
+
from .config import Config
|
|
46
|
+
|
|
47
|
+
# Register configuration as singleton
|
|
48
|
+
config = Config()
|
|
49
|
+
self.container.register_singleton(Config, instance=config)
|
|
50
|
+
|
|
51
|
+
# Register core services
|
|
52
|
+
self.register_service(
|
|
53
|
+
"session_manager",
|
|
54
|
+
SessionManager,
|
|
55
|
+
lifetime=ServiceLifetime.SINGLETON
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.register_service(
|
|
59
|
+
"agent_session_manager",
|
|
60
|
+
AgentSessionManager,
|
|
61
|
+
lifetime=ServiceLifetime.SINGLETON,
|
|
62
|
+
dependencies={
|
|
63
|
+
'session_dir': lambda c: c.resolve(Config).get('session_dir')
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Register shared cache as singleton (it's already a singleton internally)
|
|
68
|
+
self.register_service(
|
|
69
|
+
"prompt_cache",
|
|
70
|
+
SharedPromptCache,
|
|
71
|
+
lifetime=ServiceLifetime.SINGLETON,
|
|
72
|
+
factory=lambda c: SharedPromptCache.get_instance()
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Register hook manager
|
|
76
|
+
self.register_service(
|
|
77
|
+
"hook_manager",
|
|
78
|
+
JSONRPCHookManager,
|
|
79
|
+
lifetime=ServiceLifetime.SINGLETON
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Register ticket manager
|
|
83
|
+
self.register_service(
|
|
84
|
+
"ticket_manager",
|
|
85
|
+
TicketManager,
|
|
86
|
+
lifetime=ServiceLifetime.TRANSIENT
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Register agent deployment service
|
|
90
|
+
self.register_service(
|
|
91
|
+
"agent_deployment",
|
|
92
|
+
AgentDeploymentService,
|
|
93
|
+
lifetime=ServiceLifetime.TRANSIENT
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
logger.info("Core services registered")
|
|
97
|
+
|
|
98
|
+
def register_service(
|
|
99
|
+
self,
|
|
100
|
+
name: str,
|
|
101
|
+
service_class: Type[BaseService],
|
|
102
|
+
lifetime: ServiceLifetime = ServiceLifetime.SINGLETON,
|
|
103
|
+
factory: Optional[Any] = None,
|
|
104
|
+
dependencies: Optional[Dict[str, Any]] = None,
|
|
105
|
+
config: Optional[Dict[str, Any]] = None
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Register a service with the registry.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: Service name for lookup
|
|
112
|
+
service_class: Service class (must inherit from BaseService)
|
|
113
|
+
lifetime: Service lifetime management
|
|
114
|
+
factory: Optional factory function
|
|
115
|
+
dependencies: Optional dependency mapping
|
|
116
|
+
config: Optional service-specific configuration
|
|
117
|
+
"""
|
|
118
|
+
# Store service metadata
|
|
119
|
+
self._services[name] = service_class
|
|
120
|
+
|
|
121
|
+
# Create factory wrapper if config provided
|
|
122
|
+
if config and not factory:
|
|
123
|
+
factory = lambda c: service_class(name=name, config=config, container=c)
|
|
124
|
+
elif not factory:
|
|
125
|
+
# Default factory with container injection
|
|
126
|
+
factory = lambda c: service_class(name=name, container=c)
|
|
127
|
+
|
|
128
|
+
# Register with DI container
|
|
129
|
+
self.container.register(
|
|
130
|
+
service_class,
|
|
131
|
+
factory=factory,
|
|
132
|
+
lifetime=lifetime,
|
|
133
|
+
dependencies=dependencies
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
logger.debug(f"Registered service: {name} ({service_class.__name__})")
|
|
137
|
+
|
|
138
|
+
def get_service(self, service_type: Union[str, Type[BaseService]]) -> BaseService:
|
|
139
|
+
"""
|
|
140
|
+
Get a service instance.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
service_type: Service name or class
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Service instance
|
|
147
|
+
"""
|
|
148
|
+
if isinstance(service_type, str):
|
|
149
|
+
# Look up by name
|
|
150
|
+
if service_type not in self._services:
|
|
151
|
+
raise KeyError(f"Service '{service_type}' not registered")
|
|
152
|
+
service_class = self._services[service_type]
|
|
153
|
+
else:
|
|
154
|
+
service_class = service_type
|
|
155
|
+
|
|
156
|
+
return self.container.resolve(service_class)
|
|
157
|
+
|
|
158
|
+
def get_service_optional(
|
|
159
|
+
self,
|
|
160
|
+
service_type: Union[str, Type[BaseService]],
|
|
161
|
+
default: Optional[BaseService] = None
|
|
162
|
+
) -> Optional[BaseService]:
|
|
163
|
+
"""Get a service if available, otherwise return default."""
|
|
164
|
+
try:
|
|
165
|
+
return self.get_service(service_type)
|
|
166
|
+
except (KeyError, Exception):
|
|
167
|
+
return default
|
|
168
|
+
|
|
169
|
+
def start_all_services(self) -> None:
|
|
170
|
+
"""Start all registered singleton services."""
|
|
171
|
+
import asyncio
|
|
172
|
+
|
|
173
|
+
async def _start_all():
|
|
174
|
+
for name, service_class in self._services.items():
|
|
175
|
+
try:
|
|
176
|
+
# Only start singleton services
|
|
177
|
+
registration = self.container.get_all_registrations().get(service_class)
|
|
178
|
+
if registration and registration.lifetime == ServiceLifetime.SINGLETON:
|
|
179
|
+
service = self.get_service(service_class)
|
|
180
|
+
if hasattr(service, 'start'):
|
|
181
|
+
await service.start()
|
|
182
|
+
logger.info(f"Started service: {name}")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Failed to start service {name}: {e}")
|
|
185
|
+
|
|
186
|
+
asyncio.run(_start_all())
|
|
187
|
+
self._initialized = True
|
|
188
|
+
|
|
189
|
+
def stop_all_services(self) -> None:
|
|
190
|
+
"""Stop all running singleton services."""
|
|
191
|
+
import asyncio
|
|
192
|
+
|
|
193
|
+
async def _stop_all():
|
|
194
|
+
for name, service_class in reversed(list(self._services.items())):
|
|
195
|
+
try:
|
|
196
|
+
# Only stop singleton services
|
|
197
|
+
registration = self.container.get_all_registrations().get(service_class)
|
|
198
|
+
if registration and registration.lifetime == ServiceLifetime.SINGLETON:
|
|
199
|
+
if service_class in self.container._singletons:
|
|
200
|
+
service = self.container._singletons[service_class]
|
|
201
|
+
if hasattr(service, 'stop') and service.running:
|
|
202
|
+
await service.stop()
|
|
203
|
+
logger.info(f"Stopped service: {name}")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Failed to stop service {name}: {e}")
|
|
206
|
+
|
|
207
|
+
asyncio.run(_stop_all())
|
|
208
|
+
|
|
209
|
+
def get_service_health(self) -> Dict[str, Dict[str, Any]]:
|
|
210
|
+
"""Get health status of all services."""
|
|
211
|
+
import asyncio
|
|
212
|
+
|
|
213
|
+
async def _get_health():
|
|
214
|
+
health_status = {}
|
|
215
|
+
|
|
216
|
+
for name, service_class in self._services.items():
|
|
217
|
+
try:
|
|
218
|
+
if service_class in self.container._singletons:
|
|
219
|
+
service = self.container._singletons[service_class]
|
|
220
|
+
if hasattr(service, 'health_check'):
|
|
221
|
+
health = await service.health_check()
|
|
222
|
+
health_status[name] = {
|
|
223
|
+
'status': health.status,
|
|
224
|
+
'message': health.message,
|
|
225
|
+
'metrics': health.metrics
|
|
226
|
+
}
|
|
227
|
+
except Exception as e:
|
|
228
|
+
health_status[name] = {
|
|
229
|
+
'status': 'error',
|
|
230
|
+
'message': str(e)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return health_status
|
|
234
|
+
|
|
235
|
+
return asyncio.run(_get_health())
|
|
236
|
+
|
|
237
|
+
def list_services(self) -> List[Dict[str, Any]]:
|
|
238
|
+
"""List all registered services with their metadata."""
|
|
239
|
+
services = []
|
|
240
|
+
|
|
241
|
+
for name, service_class in self._services.items():
|
|
242
|
+
registration = self.container.get_all_registrations().get(service_class)
|
|
243
|
+
|
|
244
|
+
service_info = {
|
|
245
|
+
'name': name,
|
|
246
|
+
'class': service_class.__name__,
|
|
247
|
+
'module': service_class.__module__,
|
|
248
|
+
'lifetime': registration.lifetime.value if registration else 'unknown',
|
|
249
|
+
'is_singleton': registration.lifetime == ServiceLifetime.SINGLETON if registration else False,
|
|
250
|
+
'is_running': False
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Check if singleton is running
|
|
254
|
+
if service_class in self.container._singletons:
|
|
255
|
+
service = self.container._singletons[service_class]
|
|
256
|
+
if hasattr(service, 'running'):
|
|
257
|
+
service_info['is_running'] = service.running
|
|
258
|
+
|
|
259
|
+
services.append(service_info)
|
|
260
|
+
|
|
261
|
+
return services
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Global registry instance
|
|
265
|
+
_global_registry: Optional[ServiceRegistry] = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_service_registry() -> ServiceRegistry:
|
|
269
|
+
"""Get the global service registry instance."""
|
|
270
|
+
global _global_registry
|
|
271
|
+
if _global_registry is None:
|
|
272
|
+
_global_registry = ServiceRegistry()
|
|
273
|
+
_global_registry.register_core_services()
|
|
274
|
+
return _global_registry
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def initialize_services(config: Optional[Dict[str, Any]] = None) -> ServiceRegistry:
|
|
278
|
+
"""
|
|
279
|
+
Initialize all application services.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
config: Optional configuration overrides
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Initialized service registry
|
|
286
|
+
"""
|
|
287
|
+
registry = get_service_registry()
|
|
288
|
+
|
|
289
|
+
if config:
|
|
290
|
+
# Apply configuration overrides
|
|
291
|
+
config_service = registry.container.resolve(Config)
|
|
292
|
+
for key, value in config.items():
|
|
293
|
+
config_service.set(key, value)
|
|
294
|
+
|
|
295
|
+
# Start all services
|
|
296
|
+
if not registry._initialized:
|
|
297
|
+
registry.start_all_services()
|
|
298
|
+
|
|
299
|
+
return registry
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Session ID management for Claude subprocess optimization."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..core.logger import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SessionManager:
|
|
15
|
+
"""Manages session IDs for Claude subprocess reuse."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, session_dir: Optional[Path] = None):
|
|
18
|
+
"""Initialize session manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
session_dir: Directory to store session metadata
|
|
22
|
+
"""
|
|
23
|
+
self.session_dir = session_dir or Path.home() / ".claude-mpm" / "sessions"
|
|
24
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
self.active_sessions: Dict[str, Dict[str, Any]] = {}
|
|
26
|
+
self._load_sessions()
|
|
27
|
+
|
|
28
|
+
def create_session(self, context: str = "default") -> str:
|
|
29
|
+
"""Create a new session ID.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
context: Context identifier (e.g., "pm_orchestration", "agent_delegation")
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
UUID session ID
|
|
36
|
+
"""
|
|
37
|
+
session_id = str(uuid.uuid4())
|
|
38
|
+
|
|
39
|
+
self.active_sessions[session_id] = {
|
|
40
|
+
"id": session_id,
|
|
41
|
+
"context": context,
|
|
42
|
+
"created_at": datetime.now().isoformat(),
|
|
43
|
+
"last_used": datetime.now().isoformat(),
|
|
44
|
+
"use_count": 0,
|
|
45
|
+
"agents_run": []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
self._save_sessions()
|
|
49
|
+
logger.info(f"Created session {session_id} for context: {context}")
|
|
50
|
+
|
|
51
|
+
return session_id
|
|
52
|
+
|
|
53
|
+
def get_or_create_session(self, context: str = "default", max_age_minutes: int = 30) -> str:
|
|
54
|
+
"""Get existing session or create new one.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
context: Context identifier
|
|
58
|
+
max_age_minutes: Maximum age of session to reuse
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Session ID
|
|
62
|
+
"""
|
|
63
|
+
# Look for existing session in context
|
|
64
|
+
now = datetime.now()
|
|
65
|
+
max_age = timedelta(minutes=max_age_minutes)
|
|
66
|
+
|
|
67
|
+
for session_id, session_data in self.active_sessions.items():
|
|
68
|
+
if session_data["context"] == context:
|
|
69
|
+
last_used = datetime.fromisoformat(session_data["last_used"])
|
|
70
|
+
if now - last_used < max_age:
|
|
71
|
+
# Reuse this session
|
|
72
|
+
session_data["last_used"] = now.isoformat()
|
|
73
|
+
session_data["use_count"] += 1
|
|
74
|
+
self._save_sessions()
|
|
75
|
+
logger.info(f"Reusing session {session_id} for context: {context}")
|
|
76
|
+
return session_id
|
|
77
|
+
|
|
78
|
+
# No valid session found, create new one
|
|
79
|
+
return self.create_session(context)
|
|
80
|
+
|
|
81
|
+
def record_agent_use(self, session_id: str, agent: str, task: str):
|
|
82
|
+
"""Record that an agent used this session.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
session_id: Session ID
|
|
86
|
+
agent: Agent name
|
|
87
|
+
task: Task description
|
|
88
|
+
"""
|
|
89
|
+
if session_id in self.active_sessions:
|
|
90
|
+
self.active_sessions[session_id]["agents_run"].append({
|
|
91
|
+
"agent": agent,
|
|
92
|
+
"task": task[:100], # Truncate long tasks
|
|
93
|
+
"timestamp": datetime.now().isoformat()
|
|
94
|
+
})
|
|
95
|
+
self.active_sessions[session_id]["last_used"] = datetime.now().isoformat()
|
|
96
|
+
self._save_sessions()
|
|
97
|
+
|
|
98
|
+
def cleanup_old_sessions(self, max_age_hours: int = 24):
|
|
99
|
+
"""Remove sessions older than max_age_hours.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
max_age_hours: Maximum age in hours
|
|
103
|
+
"""
|
|
104
|
+
now = datetime.now()
|
|
105
|
+
max_age = timedelta(hours=max_age_hours)
|
|
106
|
+
|
|
107
|
+
expired = []
|
|
108
|
+
for session_id, session_data in self.active_sessions.items():
|
|
109
|
+
created = datetime.fromisoformat(session_data["created_at"])
|
|
110
|
+
if now - created > max_age:
|
|
111
|
+
expired.append(session_id)
|
|
112
|
+
|
|
113
|
+
for session_id in expired:
|
|
114
|
+
del self.active_sessions[session_id]
|
|
115
|
+
logger.info(f"Cleaned up expired session: {session_id}")
|
|
116
|
+
|
|
117
|
+
if expired:
|
|
118
|
+
self._save_sessions()
|
|
119
|
+
|
|
120
|
+
def _save_sessions(self):
|
|
121
|
+
"""Save sessions to disk."""
|
|
122
|
+
session_file = self.session_dir / "active_sessions.json"
|
|
123
|
+
try:
|
|
124
|
+
with open(session_file, 'w') as f:
|
|
125
|
+
json.dump(self.active_sessions, f, indent=2)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Failed to save sessions: {e}")
|
|
128
|
+
|
|
129
|
+
def _load_sessions(self):
|
|
130
|
+
"""Load sessions from disk."""
|
|
131
|
+
session_file = self.session_dir / "active_sessions.json"
|
|
132
|
+
if session_file.exists():
|
|
133
|
+
try:
|
|
134
|
+
with open(session_file, 'r') as f:
|
|
135
|
+
self.active_sessions = json.load(f)
|
|
136
|
+
|
|
137
|
+
# Clean up old sessions on load
|
|
138
|
+
self.cleanup_old_sessions()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Failed to load sessions: {e}")
|
|
141
|
+
self.active_sessions = {}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class OrchestrationSession:
|
|
145
|
+
"""Context manager for orchestration sessions."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, session_manager: SessionManager, context: str = "orchestration"):
|
|
148
|
+
"""Initialize orchestration session.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
session_manager: SessionManager instance
|
|
152
|
+
context: Session context
|
|
153
|
+
"""
|
|
154
|
+
self.session_manager = session_manager
|
|
155
|
+
self.context = context
|
|
156
|
+
self.session_id: Optional[str] = None
|
|
157
|
+
|
|
158
|
+
def __enter__(self) -> str:
|
|
159
|
+
"""Enter session context, return session ID."""
|
|
160
|
+
self.session_id = self.session_manager.get_or_create_session(self.context)
|
|
161
|
+
return self.session_id
|
|
162
|
+
|
|
163
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
164
|
+
"""Exit session context."""
|
|
165
|
+
# Could add cleanup here if needed
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Example usage in subprocess orchestrator:
|
|
170
|
+
"""
|
|
171
|
+
session_manager = SessionManager()
|
|
172
|
+
|
|
173
|
+
# In run_non_interactive:
|
|
174
|
+
with OrchestrationSession(session_manager, "pm_delegation") as session_id:
|
|
175
|
+
# Run PM with session
|
|
176
|
+
stdout, stderr, returncode = self.launcher.launch_oneshot(
|
|
177
|
+
message=pm_prompt,
|
|
178
|
+
session_id=session_id, # First call creates context
|
|
179
|
+
timeout=30
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Run agents with same session (reuses context)
|
|
183
|
+
for agent, task in delegations:
|
|
184
|
+
stdout, stderr, returncode = self.launcher.launch_oneshot(
|
|
185
|
+
message=agent_prompt,
|
|
186
|
+
session_id=session_id, # Reuses PM context!
|
|
187
|
+
timeout=60
|
|
188
|
+
)
|
|
189
|
+
session_manager.record_agent_use(session_id, agent, task)
|
|
190
|
+
"""
|