claude-mpm 4.0.31__py3-none-any.whl → 4.0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +33 -25
- claude_mpm/agents/INSTRUCTIONS.md +14 -10
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +63 -26
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/core/framework_loader.py +272 -113
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +85 -6
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
- claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
- claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
- claude_mpm/services/agents/memory/content_manager.py +182 -232
- claude_mpm/services/agents/memory/template_generator.py +4 -40
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/event_bus.py +334 -0
- claude_mpm/services/event_bus/relay.py +301 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +78 -20
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Event Bus implementation using pyee.
|
|
2
|
+
|
|
3
|
+
WHY pyee over alternatives:
|
|
4
|
+
- AsyncIOEventEmitter supports both sync and async handlers
|
|
5
|
+
- Battle-tested library with minimal dependencies
|
|
6
|
+
- Simple EventEmitter pattern familiar to developers
|
|
7
|
+
- Thread-safe for multi-threaded environments
|
|
8
|
+
- Efficient event dispatch with minimal overhead
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
16
|
+
from pyee import AsyncIOEventEmitter
|
|
17
|
+
|
|
18
|
+
# Configure logger
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EventBus:
|
|
23
|
+
"""Singleton Event Bus for decoupled event handling.
|
|
24
|
+
|
|
25
|
+
WHY singleton pattern:
|
|
26
|
+
- Ensures single point of event coordination
|
|
27
|
+
- Prevents duplicate event processing
|
|
28
|
+
- Simplifies configuration and management
|
|
29
|
+
- Thread-safe initialization with proper locking
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_instance: Optional["EventBus"] = None
|
|
33
|
+
_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
def __new__(cls) -> "EventBus":
|
|
36
|
+
"""Ensure singleton instance creation."""
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
with cls._lock:
|
|
39
|
+
if cls._instance is None:
|
|
40
|
+
cls._instance = super().__new__(cls)
|
|
41
|
+
return cls._instance
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
"""Initialize the event bus once."""
|
|
45
|
+
# Only initialize once
|
|
46
|
+
if hasattr(self, "_initialized"):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
self._initialized = True
|
|
50
|
+
self._emitter = AsyncIOEventEmitter()
|
|
51
|
+
self._enabled = True
|
|
52
|
+
self._event_filters: Set[str] = set()
|
|
53
|
+
self._stats = {
|
|
54
|
+
"events_published": 0,
|
|
55
|
+
"events_filtered": 0,
|
|
56
|
+
"events_failed": 0,
|
|
57
|
+
"last_event_time": None
|
|
58
|
+
}
|
|
59
|
+
self._debug = False
|
|
60
|
+
|
|
61
|
+
# Event history for debugging (limited size)
|
|
62
|
+
self._event_history: List[Dict[str, Any]] = []
|
|
63
|
+
self._max_history_size = 100
|
|
64
|
+
|
|
65
|
+
logger.info("EventBus initialized")
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_instance(cls) -> "EventBus":
|
|
69
|
+
"""Get the singleton EventBus instance.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
EventBus: The singleton instance
|
|
73
|
+
"""
|
|
74
|
+
return cls()
|
|
75
|
+
|
|
76
|
+
def enable(self) -> None:
|
|
77
|
+
"""Enable event bus processing."""
|
|
78
|
+
self._enabled = True
|
|
79
|
+
logger.info("EventBus enabled")
|
|
80
|
+
|
|
81
|
+
def disable(self) -> None:
|
|
82
|
+
"""Disable event bus processing (for testing or maintenance)."""
|
|
83
|
+
self._enabled = False
|
|
84
|
+
logger.info("EventBus disabled")
|
|
85
|
+
|
|
86
|
+
def set_debug(self, debug: bool) -> None:
|
|
87
|
+
"""Enable or disable debug logging.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
debug: Whether to enable debug logging
|
|
91
|
+
"""
|
|
92
|
+
self._debug = debug
|
|
93
|
+
if debug:
|
|
94
|
+
logger.setLevel(logging.DEBUG)
|
|
95
|
+
else:
|
|
96
|
+
logger.setLevel(logging.INFO)
|
|
97
|
+
|
|
98
|
+
def add_filter(self, pattern: str) -> None:
|
|
99
|
+
"""Add an event filter pattern.
|
|
100
|
+
|
|
101
|
+
Only events matching filter patterns will be processed.
|
|
102
|
+
Use wildcards: 'hook.*' matches all hook events.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
pattern: Event name pattern to allow
|
|
106
|
+
"""
|
|
107
|
+
self._event_filters.add(pattern)
|
|
108
|
+
logger.debug(f"Added event filter: {pattern}")
|
|
109
|
+
|
|
110
|
+
def remove_filter(self, pattern: str) -> None:
|
|
111
|
+
"""Remove an event filter pattern.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
pattern: Event name pattern to remove
|
|
115
|
+
"""
|
|
116
|
+
self._event_filters.discard(pattern)
|
|
117
|
+
logger.debug(f"Removed event filter: {pattern}")
|
|
118
|
+
|
|
119
|
+
def clear_filters(self) -> None:
|
|
120
|
+
"""Clear all event filters (allow all events)."""
|
|
121
|
+
self._event_filters.clear()
|
|
122
|
+
logger.debug("Cleared all event filters")
|
|
123
|
+
|
|
124
|
+
def _should_process_event(self, event_type: str) -> bool:
|
|
125
|
+
"""Check if an event should be processed based on filters.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
event_type: The event type to check
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
bool: True if event should be processed
|
|
132
|
+
"""
|
|
133
|
+
# If no filters, process all events
|
|
134
|
+
if not self._event_filters:
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
# Check if event matches any filter
|
|
138
|
+
for filter_pattern in self._event_filters:
|
|
139
|
+
if filter_pattern.endswith("*"):
|
|
140
|
+
# Wildcard pattern
|
|
141
|
+
prefix = filter_pattern[:-1]
|
|
142
|
+
if event_type.startswith(prefix):
|
|
143
|
+
return True
|
|
144
|
+
elif event_type == filter_pattern:
|
|
145
|
+
# Exact match
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def publish(self, event_type: str, data: Any) -> bool:
|
|
151
|
+
"""Publish an event synchronously (for use from sync contexts like hooks).
|
|
152
|
+
|
|
153
|
+
This method is thread-safe and can be called from any thread.
|
|
154
|
+
Events are dispatched asynchronously to handlers.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
event_type: The event type (e.g., 'hook.pre_tool')
|
|
158
|
+
data: The event data
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
bool: True if event was published, False if filtered or disabled
|
|
162
|
+
"""
|
|
163
|
+
if not self._enabled:
|
|
164
|
+
if self._debug:
|
|
165
|
+
logger.debug(f"EventBus disabled, dropping event: {event_type}")
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
# Check filters
|
|
169
|
+
if not self._should_process_event(event_type):
|
|
170
|
+
self._stats["events_filtered"] += 1
|
|
171
|
+
if self._debug:
|
|
172
|
+
logger.debug(f"Event filtered out: {event_type}")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# Record event in history
|
|
177
|
+
self._record_event(event_type, data)
|
|
178
|
+
|
|
179
|
+
# Emit event (pyee handles thread safety)
|
|
180
|
+
self._emitter.emit(event_type, data)
|
|
181
|
+
|
|
182
|
+
# Update stats
|
|
183
|
+
self._stats["events_published"] += 1
|
|
184
|
+
self._stats["last_event_time"] = datetime.now().isoformat()
|
|
185
|
+
|
|
186
|
+
if self._debug:
|
|
187
|
+
logger.debug(f"Published event: {event_type}")
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self._stats["events_failed"] += 1
|
|
193
|
+
logger.error(f"Failed to publish event {event_type}: {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
async def publish_async(self, event_type: str, data: Any) -> bool:
|
|
197
|
+
"""Publish an event from an async context.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
event_type: The event type
|
|
201
|
+
data: The event data
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
bool: True if event was published
|
|
205
|
+
"""
|
|
206
|
+
# Just delegate to sync publish (pyee handles both)
|
|
207
|
+
return self.publish(event_type, data)
|
|
208
|
+
|
|
209
|
+
def on(self, event_type: str, handler: Callable) -> None:
|
|
210
|
+
"""Register an event handler.
|
|
211
|
+
|
|
212
|
+
The handler can be sync or async. For async handlers,
|
|
213
|
+
they will be scheduled on the event loop.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
event_type: The event type to listen for (supports wildcards)
|
|
217
|
+
handler: The handler function
|
|
218
|
+
"""
|
|
219
|
+
if event_type.endswith("*"):
|
|
220
|
+
# Register for wildcard pattern
|
|
221
|
+
prefix = event_type[:-1]
|
|
222
|
+
|
|
223
|
+
# Create a wrapper that checks event names
|
|
224
|
+
def wildcard_wrapper(actual_event_type: str):
|
|
225
|
+
def wrapper(data):
|
|
226
|
+
if actual_event_type.startswith(prefix):
|
|
227
|
+
if asyncio.iscoroutinefunction(handler):
|
|
228
|
+
return handler(actual_event_type, data)
|
|
229
|
+
else:
|
|
230
|
+
handler(actual_event_type, data)
|
|
231
|
+
return wrapper
|
|
232
|
+
|
|
233
|
+
# Register for all possible events (we'll filter in the wrapper)
|
|
234
|
+
# For now, register common prefixes
|
|
235
|
+
for common_event in ["hook", "socketio", "system", "agent"]:
|
|
236
|
+
if common_event.startswith(prefix) or prefix.startswith(common_event):
|
|
237
|
+
self._emitter.on(f"{common_event}.*", wildcard_wrapper(f"{common_event}.*"))
|
|
238
|
+
else:
|
|
239
|
+
# Regular event registration
|
|
240
|
+
self._emitter.on(event_type, handler)
|
|
241
|
+
|
|
242
|
+
logger.debug(f"Registered handler for: {event_type}")
|
|
243
|
+
|
|
244
|
+
def once(self, event_type: str, handler: Callable) -> None:
|
|
245
|
+
"""Register a one-time event handler.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
event_type: The event type to listen for
|
|
249
|
+
handler: The handler function
|
|
250
|
+
"""
|
|
251
|
+
self._emitter.once(event_type, handler)
|
|
252
|
+
logger.debug(f"Registered one-time handler for: {event_type}")
|
|
253
|
+
|
|
254
|
+
def remove_listener(self, event_type: str, handler: Callable) -> None:
|
|
255
|
+
"""Remove an event handler.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
event_type: The event type
|
|
259
|
+
handler: The handler to remove
|
|
260
|
+
"""
|
|
261
|
+
self._emitter.remove_listener(event_type, handler)
|
|
262
|
+
logger.debug(f"Removed handler for: {event_type}")
|
|
263
|
+
|
|
264
|
+
def remove_all_listeners(self, event_type: Optional[str] = None) -> None:
|
|
265
|
+
"""Remove all listeners for an event type, or all listeners.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
event_type: Optional event type. If None, removes all listeners.
|
|
269
|
+
"""
|
|
270
|
+
if event_type:
|
|
271
|
+
self._emitter.remove_all_listeners(event_type)
|
|
272
|
+
logger.debug(f"Removed all handlers for: {event_type}")
|
|
273
|
+
else:
|
|
274
|
+
self._emitter.remove_all_listeners()
|
|
275
|
+
logger.debug("Removed all event handlers")
|
|
276
|
+
|
|
277
|
+
def _record_event(self, event_type: str, data: Any) -> None:
|
|
278
|
+
"""Record event in history for debugging.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
event_type: The event type
|
|
282
|
+
data: The event data
|
|
283
|
+
"""
|
|
284
|
+
event_record = {
|
|
285
|
+
"timestamp": datetime.now().isoformat(),
|
|
286
|
+
"type": event_type,
|
|
287
|
+
"data": data
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
self._event_history.append(event_record)
|
|
291
|
+
|
|
292
|
+
# Trim history if too large
|
|
293
|
+
if len(self._event_history) > self._max_history_size:
|
|
294
|
+
self._event_history = self._event_history[-self._max_history_size:]
|
|
295
|
+
|
|
296
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
297
|
+
"""Get event bus statistics.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
dict: Statistics about event processing
|
|
301
|
+
"""
|
|
302
|
+
return {
|
|
303
|
+
**self._stats,
|
|
304
|
+
"enabled": self._enabled,
|
|
305
|
+
"filters_active": len(self._event_filters) > 0,
|
|
306
|
+
"filter_count": len(self._event_filters),
|
|
307
|
+
"history_size": len(self._event_history)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
def get_recent_events(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
311
|
+
"""Get recent events from history.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
limit: Maximum number of events to return
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
list: Recent events
|
|
318
|
+
"""
|
|
319
|
+
return self._event_history[-limit:]
|
|
320
|
+
|
|
321
|
+
def clear_history(self) -> None:
|
|
322
|
+
"""Clear the event history."""
|
|
323
|
+
self._event_history.clear()
|
|
324
|
+
logger.debug("Cleared event history")
|
|
325
|
+
|
|
326
|
+
def reset_stats(self) -> None:
|
|
327
|
+
"""Reset event statistics."""
|
|
328
|
+
self._stats = {
|
|
329
|
+
"events_published": 0,
|
|
330
|
+
"events_filtered": 0,
|
|
331
|
+
"events_failed": 0,
|
|
332
|
+
"last_event_time": None
|
|
333
|
+
}
|
|
334
|
+
logger.debug("Reset event statistics")
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Socket.IO Relay - Consumes events from EventBus and relays to Socket.IO.
|
|
2
|
+
|
|
3
|
+
WHY separate relay component:
|
|
4
|
+
- Single point of Socket.IO connection management
|
|
5
|
+
- Isolates Socket.IO failures from event producers
|
|
6
|
+
- Enables graceful degradation when Socket.IO unavailable
|
|
7
|
+
- Simplifies testing by mocking just the relay
|
|
8
|
+
- Supports batching and retry logic in one place
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
# Socket.IO imports
|
|
19
|
+
try:
|
|
20
|
+
import socketio
|
|
21
|
+
SOCKETIO_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
SOCKETIO_AVAILABLE = False
|
|
24
|
+
socketio = None
|
|
25
|
+
|
|
26
|
+
from .event_bus import EventBus
|
|
27
|
+
|
|
28
|
+
# Configure logger
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SocketIORelay:
|
|
33
|
+
"""Relay events from EventBus to Socket.IO clients.
|
|
34
|
+
|
|
35
|
+
WHY relay pattern:
|
|
36
|
+
- Decouples event production from Socket.IO emission
|
|
37
|
+
- Handles connection failures without affecting producers
|
|
38
|
+
- Provides single point for Socket.IO configuration
|
|
39
|
+
- Enables event batching and optimization
|
|
40
|
+
- Simplifies debugging with centralized logging
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, port: Optional[int] = None):
|
|
44
|
+
"""Initialize the Socket.IO relay.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
port: Socket.IO server port (defaults to env var or 8765)
|
|
48
|
+
"""
|
|
49
|
+
self.port = port or int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
|
|
50
|
+
self.event_bus = EventBus.get_instance()
|
|
51
|
+
self.client: Optional[Any] = None
|
|
52
|
+
self.connected = False
|
|
53
|
+
self.enabled = True
|
|
54
|
+
self.debug = os.environ.get("CLAUDE_MPM_RELAY_DEBUG", "false").lower() == "true"
|
|
55
|
+
|
|
56
|
+
# Connection retry settings
|
|
57
|
+
self.max_retries = 3
|
|
58
|
+
self.retry_delay = 0.5
|
|
59
|
+
self.last_connection_attempt = 0
|
|
60
|
+
self.connection_cooldown = 5.0 # Seconds between connection attempts
|
|
61
|
+
|
|
62
|
+
# Statistics
|
|
63
|
+
self.stats = {
|
|
64
|
+
"events_relayed": 0,
|
|
65
|
+
"events_failed": 0,
|
|
66
|
+
"connection_failures": 0,
|
|
67
|
+
"last_relay_time": None
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if not SOCKETIO_AVAILABLE:
|
|
71
|
+
logger.warning("Socket.IO not available, relay will be disabled")
|
|
72
|
+
self.enabled = False
|
|
73
|
+
|
|
74
|
+
def enable(self) -> None:
|
|
75
|
+
"""Enable the relay."""
|
|
76
|
+
if not SOCKETIO_AVAILABLE:
|
|
77
|
+
logger.warning("Cannot enable relay: Socket.IO not available")
|
|
78
|
+
return
|
|
79
|
+
self.enabled = True
|
|
80
|
+
logger.info("SocketIO relay enabled")
|
|
81
|
+
|
|
82
|
+
def disable(self) -> None:
|
|
83
|
+
"""Disable the relay."""
|
|
84
|
+
self.enabled = False
|
|
85
|
+
if self.client and self.connected:
|
|
86
|
+
try:
|
|
87
|
+
self.client.disconnect()
|
|
88
|
+
except:
|
|
89
|
+
pass
|
|
90
|
+
logger.info("SocketIO relay disabled")
|
|
91
|
+
|
|
92
|
+
def _create_client(self) -> bool:
|
|
93
|
+
"""Create and connect Socket.IO client.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
bool: True if connection successful
|
|
97
|
+
"""
|
|
98
|
+
if not SOCKETIO_AVAILABLE or not self.enabled:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
# Check connection cooldown
|
|
102
|
+
current_time = time.time()
|
|
103
|
+
if current_time - self.last_connection_attempt < self.connection_cooldown:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
self.last_connection_attempt = current_time
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Create new client
|
|
110
|
+
self.client = socketio.Client(
|
|
111
|
+
reconnection=True,
|
|
112
|
+
reconnection_attempts=3,
|
|
113
|
+
reconnection_delay=1,
|
|
114
|
+
logger=False,
|
|
115
|
+
engineio_logger=False
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Connect to server
|
|
119
|
+
self.client.connect(
|
|
120
|
+
f"http://localhost:{self.port}",
|
|
121
|
+
wait=True,
|
|
122
|
+
wait_timeout=2.0,
|
|
123
|
+
transports=['websocket', 'polling']
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.connected = True
|
|
127
|
+
logger.info(f"SocketIO relay connected to port {self.port}")
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self.stats["connection_failures"] += 1
|
|
132
|
+
if self.debug:
|
|
133
|
+
logger.debug(f"Failed to connect to Socket.IO server: {e}")
|
|
134
|
+
self.connected = False
|
|
135
|
+
self.client = None
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def _ensure_connection(self) -> bool:
|
|
139
|
+
"""Ensure Socket.IO client is connected.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if connected or reconnected
|
|
143
|
+
"""
|
|
144
|
+
if not self.enabled:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Check existing connection
|
|
148
|
+
if self.client and self.connected:
|
|
149
|
+
try:
|
|
150
|
+
# Verify connection is still alive
|
|
151
|
+
if self.client.connected:
|
|
152
|
+
return True
|
|
153
|
+
except:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# Need to create or reconnect
|
|
157
|
+
return self._create_client()
|
|
158
|
+
|
|
159
|
+
async def relay_event(self, event_type: str, data: Any) -> bool:
|
|
160
|
+
"""Relay an event to Socket.IO.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
event_type: The event type
|
|
164
|
+
data: The event data
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
bool: True if successfully relayed
|
|
168
|
+
"""
|
|
169
|
+
if not self.enabled:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Ensure we have a connection
|
|
173
|
+
if not self._ensure_connection():
|
|
174
|
+
self.stats["events_failed"] += 1
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Emit to Socket.IO
|
|
179
|
+
self.client.emit("claude_event", {
|
|
180
|
+
"event": "claude_event",
|
|
181
|
+
"type": event_type.split(".")[0] if "." in event_type else event_type,
|
|
182
|
+
"subtype": event_type.split(".", 1)[1] if "." in event_type else "generic",
|
|
183
|
+
"timestamp": data.get("timestamp", datetime.now().isoformat()),
|
|
184
|
+
"data": data,
|
|
185
|
+
"source": "event_bus"
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
# Update statistics
|
|
189
|
+
self.stats["events_relayed"] += 1
|
|
190
|
+
self.stats["last_relay_time"] = datetime.now().isoformat()
|
|
191
|
+
|
|
192
|
+
if self.debug:
|
|
193
|
+
logger.debug(f"Relayed event to Socket.IO: {event_type}")
|
|
194
|
+
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.stats["events_failed"] += 1
|
|
199
|
+
if self.debug:
|
|
200
|
+
logger.debug(f"Failed to relay event {event_type}: {e}")
|
|
201
|
+
# Mark connection as failed for retry
|
|
202
|
+
self.connected = False
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def start(self) -> None:
|
|
206
|
+
"""Start the relay by subscribing to EventBus events.
|
|
207
|
+
|
|
208
|
+
This sets up listeners for all hook events and relays them
|
|
209
|
+
to Socket.IO clients.
|
|
210
|
+
"""
|
|
211
|
+
if not self.enabled:
|
|
212
|
+
logger.warning("Cannot start relay: disabled or Socket.IO not available")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Define async handler for events
|
|
216
|
+
async def handle_hook_event(event_type: str, data: Any):
|
|
217
|
+
"""Handle events from the event bus."""
|
|
218
|
+
# Only relay hook events by default
|
|
219
|
+
if event_type.startswith("hook."):
|
|
220
|
+
await self.relay_event(event_type, data)
|
|
221
|
+
|
|
222
|
+
# Subscribe to all hook events
|
|
223
|
+
self.event_bus.on("hook.*", handle_hook_event)
|
|
224
|
+
|
|
225
|
+
# Also subscribe to specific high-priority events
|
|
226
|
+
for event in ["hook.pre_tool", "hook.post_tool", "hook.subagent_stop",
|
|
227
|
+
"hook.user_prompt", "hook.assistant_response"]:
|
|
228
|
+
self.event_bus.on(event, lambda data, evt=event:
|
|
229
|
+
asyncio.create_task(self.relay_event(evt, data)))
|
|
230
|
+
|
|
231
|
+
logger.info("SocketIO relay started and subscribed to events")
|
|
232
|
+
|
|
233
|
+
def stop(self) -> None:
|
|
234
|
+
"""Stop the relay and clean up resources."""
|
|
235
|
+
# Disconnect Socket.IO client
|
|
236
|
+
if self.client and self.connected:
|
|
237
|
+
try:
|
|
238
|
+
self.client.disconnect()
|
|
239
|
+
except:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Could remove event bus listeners here if needed
|
|
243
|
+
# For now, let them be cleaned up naturally
|
|
244
|
+
|
|
245
|
+
self.connected = False
|
|
246
|
+
self.client = None
|
|
247
|
+
logger.info("SocketIO relay stopped")
|
|
248
|
+
|
|
249
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
250
|
+
"""Get relay statistics.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
dict: Statistics about relay operation
|
|
254
|
+
"""
|
|
255
|
+
return {
|
|
256
|
+
**self.stats,
|
|
257
|
+
"enabled": self.enabled,
|
|
258
|
+
"connected": self.connected,
|
|
259
|
+
"port": self.port
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Global relay instance
|
|
264
|
+
_relay_instance: Optional[SocketIORelay] = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_relay(port: Optional[int] = None) -> SocketIORelay:
|
|
268
|
+
"""Get or create the global SocketIO relay instance.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
port: Optional port number
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
SocketIORelay: The relay instance
|
|
275
|
+
"""
|
|
276
|
+
global _relay_instance
|
|
277
|
+
if _relay_instance is None:
|
|
278
|
+
_relay_instance = SocketIORelay(port)
|
|
279
|
+
return _relay_instance
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def start_relay(port: Optional[int] = None) -> SocketIORelay:
|
|
283
|
+
"""Start the global SocketIO relay.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
port: Optional port number
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
SocketIORelay: The started relay instance
|
|
290
|
+
"""
|
|
291
|
+
relay = get_relay(port)
|
|
292
|
+
relay.start()
|
|
293
|
+
return relay
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def stop_relay() -> None:
|
|
297
|
+
"""Stop the global SocketIO relay."""
|
|
298
|
+
global _relay_instance
|
|
299
|
+
if _relay_instance:
|
|
300
|
+
_relay_instance.stop()
|
|
301
|
+
_relay_instance = None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Bus System for Claude MPM
|
|
3
|
+
===============================
|
|
4
|
+
|
|
5
|
+
A decoupled event system that separates event producers from consumers,
|
|
6
|
+
providing reliable, testable, and maintainable event handling.
|
|
7
|
+
|
|
8
|
+
Key Components:
|
|
9
|
+
- EventBus: Core pub/sub system
|
|
10
|
+
- Event: Standard event format
|
|
11
|
+
- IEventProducer: Interface for event producers
|
|
12
|
+
- IEventConsumer: Interface for event consumers
|
|
13
|
+
- Various consumer implementations
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .core import Event, EventBus, EventMetadata, EventPriority
|
|
17
|
+
from .interfaces import IEventConsumer, IEventProducer, ConsumerConfig
|
|
18
|
+
from .consumers import (
|
|
19
|
+
SocketIOConsumer,
|
|
20
|
+
LoggingConsumer,
|
|
21
|
+
MetricsConsumer,
|
|
22
|
+
DeadLetterConsumer,
|
|
23
|
+
)
|
|
24
|
+
from .producers import HookEventProducer, SystemEventProducer
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Core
|
|
28
|
+
"Event",
|
|
29
|
+
"EventBus",
|
|
30
|
+
"EventMetadata",
|
|
31
|
+
"EventPriority",
|
|
32
|
+
# Interfaces
|
|
33
|
+
"IEventConsumer",
|
|
34
|
+
"IEventProducer",
|
|
35
|
+
"ConsumerConfig",
|
|
36
|
+
# Consumers
|
|
37
|
+
"SocketIOConsumer",
|
|
38
|
+
"LoggingConsumer",
|
|
39
|
+
"MetricsConsumer",
|
|
40
|
+
"DeadLetterConsumer",
|
|
41
|
+
# Producers
|
|
42
|
+
"HookEventProducer",
|
|
43
|
+
"SystemEventProducer",
|
|
44
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Bus Consumers
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
Various consumer implementations for processing events from the event bus.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .dead_letter import DeadLetterConsumer
|
|
9
|
+
from .logging import LoggingConsumer
|
|
10
|
+
from .metrics import MetricsConsumer
|
|
11
|
+
from .socketio import SocketIOConsumer
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SocketIOConsumer",
|
|
15
|
+
"LoggingConsumer",
|
|
16
|
+
"MetricsConsumer",
|
|
17
|
+
"DeadLetterConsumer",
|
|
18
|
+
]
|