claude-mpm 3.1.3__py3-none-any.whl → 3.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.
Files changed (80) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +149 -17
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/launch_socketio_dashboard.py +261 -0
  43. claude_mpm/scripts/manage_version.py +479 -0
  44. claude_mpm/scripts/socketio_daemon.py +181 -0
  45. claude_mpm/scripts/socketio_server_manager.py +428 -0
  46. claude_mpm/services/__init__.py +5 -0
  47. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  48. claude_mpm/services/agent_memory_manager.py +684 -0
  49. claude_mpm/services/agent_modification_tracker.py +98 -17
  50. claude_mpm/services/agent_persistence_service.py +33 -13
  51. claude_mpm/services/agent_registry.py +82 -43
  52. claude_mpm/services/hook_service.py +362 -0
  53. claude_mpm/services/socketio_client_manager.py +474 -0
  54. claude_mpm/services/socketio_server.py +922 -0
  55. claude_mpm/services/standalone_socketio_server.py +631 -0
  56. claude_mpm/services/ticket_manager.py +4 -5
  57. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  58. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  59. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  60. claude_mpm/services/websocket_server.py +376 -0
  61. claude_mpm/utils/dependency_manager.py +211 -0
  62. claude_mpm/utils/import_migration_example.py +80 -0
  63. claude_mpm/utils/path_operations.py +0 -20
  64. claude_mpm/web/open_dashboard.py +34 -0
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/METADATA +20 -9
  66. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/RECORD +71 -50
  67. claude_mpm-3.3.0.dist-info/entry_points.txt +7 -0
  68. claude_mpm/cli_old.py +0 -728
  69. claude_mpm/models/common.py +0 -41
  70. claude_mpm/models/lifecycle.py +0 -97
  71. claude_mpm/models/modification.py +0 -126
  72. claude_mpm/models/persistence.py +0 -57
  73. claude_mpm/models/registry.py +0 -91
  74. claude_mpm/security/__init__.py +0 -8
  75. claude_mpm/security/bash_validator.py +0 -393
  76. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  77. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/WHEEL +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/licenses/LICENSE +0 -0
  80. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/top_level.txt +0 -0
@@ -1,39 +1,16 @@
1
1
  """
2
- Ticket Manager with Dependency Injection
3
- ========================================
2
+ Enhanced Ticket Manager with Dependency Injection support.
4
3
 
5
- Modern implementation of the ticket manager service with full dependency injection support.
6
- This is the recommended implementation for new development requiring ticket management.
7
-
8
- Key Features:
9
- - Constructor-based dependency injection
10
- - Interface-based design for testability
11
- - Easy mocking for unit tests
12
- - Follows SOLID principles
13
-
14
- Usage:
15
- # With dependency injection container
16
- ticket_manager = container.get(TicketManagerDependencyInjection)
17
-
18
- # Manual instantiation with dependencies
19
- ticket_manager = TicketManagerDependencyInjection(
20
- config=config_service,
21
- task_adapter=mock_adapter # For testing
22
- )
23
-
24
- This implementation supersedes the legacy_ticketing_service.py.
4
+ This version demonstrates proper constructor injection and testability.
25
5
  """
26
6
 
27
7
  from pathlib import Path
28
8
  from typing import Optional, Dict, Any, List
29
9
  from datetime import datetime
30
10
 
31
- from claude_mpm.utils.imports import safe_import
32
-
33
- # Import dependencies using safe_import pattern
34
- InjectableService = safe_import('claude_mpm.core.injectable_service', None, ['InjectableService'])
35
- Config = safe_import('claude_mpm.core.config', None, ['Config'])
36
- get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
11
+ from ..core.injectable_service import InjectableService
12
+ from ..core.config import Config
13
+ from ..core.logger import get_logger
37
14
 
38
15
 
39
16
  class ITaskManagerAdapter:
@@ -63,17 +40,10 @@ class AITrackdownAdapter(ITaskManagerAdapter):
63
40
 
64
41
  def _init_task_manager(self):
65
42
  """Initialize ai-trackdown-pytools TaskManager."""
66
- # Import using safe_import pattern
67
- TaskManager = safe_import('ai_trackdown_pytools.core.task', None, ['TaskManager'])
68
- trackdown_imports = safe_import('ai_trackdown_pytools', None, ['Config', 'Project'])
69
-
70
- if not TaskManager or not trackdown_imports:
71
- self.logger.error("ai-trackdown-pytools not installed")
72
- return None
73
-
74
- TrackdownConfig, Project = trackdown_imports
75
-
76
43
  try:
44
+ from ai_trackdown_pytools.core.task import TaskManager
45
+ from ai_trackdown_pytools import Config as TrackdownConfig, Project
46
+
77
47
  # Ensure tickets directory exists
78
48
  tickets_dir = self.project_path / "tickets"
79
49
  if not tickets_dir.exists():
@@ -97,6 +67,9 @@ class AITrackdownAdapter(ITaskManagerAdapter):
97
67
  # Initialize task manager directly
98
68
  return TaskManager(self.project_path)
99
69
 
70
+ except ImportError:
71
+ self.logger.error("ai-trackdown-pytools not installed")
72
+ return None
100
73
  except Exception as e:
101
74
  self.logger.error(f"Failed to initialize TaskManager: {e}")
102
75
  return None
@@ -120,7 +93,7 @@ class AITrackdownAdapter(ITaskManagerAdapter):
120
93
  return self._task_manager.load_task(task_id)
121
94
 
122
95
 
123
- class TicketManagerDependencyInjection(InjectableService):
96
+ class TicketManagerDI(InjectableService):
124
97
  """
125
98
  Enhanced Ticket Manager with Dependency Injection.
126
99
 
@@ -1,13 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Legacy Ticketing Service (DEPRECATED)
4
- =====================================
3
+ Ticketing Service
4
+ =================
5
5
 
6
- DEPRECATED: This is the original ticketing service implementation.
7
- For new development, use TicketManager or TicketManagerDependencyInjection instead.
8
-
9
- This service is kept for backward compatibility and reference purposes.
10
- It wraps ai-trackdown-pytools for simplified ticket management.
6
+ Core service that wraps ai-trackdown-pytools for simplified ticket management.
7
+ Provides a clean interface for PM orchestration and agent ticket operations.
11
8
 
12
9
  Key Features:
13
10
  - Singleton pattern for consistent ticket management
@@ -47,19 +44,15 @@ from datetime import datetime
47
44
  from pathlib import Path
48
45
  from typing import Any, Dict, List, Optional, Union
49
46
 
50
- from claude_mpm.utils.imports import safe_import
51
-
52
- # Import ai-trackdown-pytools using safe_import pattern
53
- trackdown_imports = safe_import('ai_trackdown_pytools', None, ['Task', 'Project'])
54
- TaskManager = safe_import('ai_trackdown_pytools.core.task', None, ['TaskManager'])
55
-
56
- if trackdown_imports and TaskManager:
57
- Task, Project = trackdown_imports
47
+ # Import ai-trackdown-pytools
48
+ try:
49
+ from ai_trackdown_pytools import Task, Project
50
+ from ai_trackdown_pytools.core.task import TaskManager
58
51
  AI_TRACKDOWN_AVAILABLE = True
59
52
  # Map to expected names
60
53
  Ticket = Task
61
54
  TicketManager = TaskManager
62
- else:
55
+ except ImportError:
63
56
  AI_TRACKDOWN_AVAILABLE = False
64
57
  # Define fallback classes for type hints
65
58
  class TicketStatus:
@@ -46,10 +46,7 @@ from dataclasses import dataclass, field
46
46
  from enum import Enum
47
47
  import logging
48
48
 
49
- from claude_mpm.utils.imports import safe_import
50
-
51
- # Import dependencies using safe_import pattern
52
- ConfigurationManager = safe_import('claude_mpm.utils.config_manager', None, ['ConfigurationManager'])
49
+ from ...utils.config_manager import ConfigurationManager
53
50
 
54
51
 
55
52
  class VersionBumpType(Enum):
@@ -370,12 +367,14 @@ class SemanticVersionManager:
370
367
 
371
368
  def _parse_pyproject_toml_version(self, file_path: Path) -> Optional[str]:
372
369
  """Parse version from pyproject.toml."""
373
- # Try to import tomllib (Python 3.11+) or tomli as fallback
374
- tomllib = safe_import('tomllib', 'tomli')
375
-
376
- if not tomllib:
377
- # Fallback to simple regex parsing
378
- return self._parse_toml_version_regex(file_path)
370
+ try:
371
+ import tomllib
372
+ except ImportError:
373
+ try:
374
+ import tomli as tomllib
375
+ except ImportError:
376
+ # Fallback to simple regex parsing
377
+ return self._parse_toml_version_regex(file_path)
379
378
 
380
379
  try:
381
380
  with open(file_path, "rb") as f:
@@ -0,0 +1,376 @@
1
+ """WebSocket server for real-time monitoring of Claude MPM sessions."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ from datetime import datetime
11
+ from typing import Set, Dict, Any, Optional, List
12
+ from collections import deque
13
+
14
+ try:
15
+ import websockets
16
+ from websockets.server import WebSocketServerProtocol
17
+ WEBSOCKETS_AVAILABLE = True
18
+ except ImportError:
19
+ WEBSOCKETS_AVAILABLE = False
20
+ websockets = None
21
+ WebSocketServerProtocol = None
22
+
23
+ from ..core.logger import get_logger
24
+
25
+
26
+ class WebSocketServer:
27
+ """WebSocket server for broadcasting Claude MPM events."""
28
+
29
+ def __init__(self, host: str = "localhost", port: int = 8765):
30
+ self.host = host
31
+ self.port = port
32
+ self.logger = get_logger("websocket_server")
33
+ self.clients: Set[WebSocketServerProtocol] = set() if WEBSOCKETS_AVAILABLE else set()
34
+ self.event_history: deque = deque(maxlen=1000) # Keep last 1000 events
35
+ self.server = None
36
+ self.loop = None
37
+ self.thread = None
38
+ self.running = False
39
+
40
+ # Session state
41
+ self.session_id = None
42
+ self.session_start = None
43
+ self.claude_status = "stopped"
44
+ self.claude_pid = None
45
+
46
+ if not WEBSOCKETS_AVAILABLE:
47
+ self.logger.warning("WebSocket support not available. Install 'websockets' package to enable.")
48
+
49
+ def start(self):
50
+ """Start the WebSocket server in a background thread."""
51
+ if not WEBSOCKETS_AVAILABLE:
52
+ self.logger.debug("WebSocket server skipped - websockets package not installed")
53
+ return
54
+
55
+ if self.running:
56
+ return
57
+
58
+ self.running = True
59
+ self.thread = threading.Thread(target=self._run_server, daemon=True)
60
+ self.thread.start()
61
+ self.logger.info(f"WebSocket server starting on ws://{self.host}:{self.port}")
62
+
63
+ def stop(self):
64
+ """Stop the WebSocket server."""
65
+ self.running = False
66
+ if self.loop:
67
+ asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop)
68
+ if self.thread:
69
+ self.thread.join(timeout=5)
70
+ self.logger.info("WebSocket server stopped")
71
+
72
+ def _run_server(self):
73
+ """Run the server event loop."""
74
+ self.loop = asyncio.new_event_loop()
75
+ asyncio.set_event_loop(self.loop)
76
+
77
+ try:
78
+ self.loop.run_until_complete(self._serve())
79
+ except Exception as e:
80
+ self.logger.error(f"WebSocket server error: {e}")
81
+ finally:
82
+ self.loop.close()
83
+
84
+ async def _serve(self):
85
+ """Start the WebSocket server."""
86
+ async with websockets.serve(self._handle_client, self.host, self.port):
87
+ self.logger.info(f"WebSocket server listening on ws://{self.host}:{self.port}")
88
+ while self.running:
89
+ await asyncio.sleep(0.1)
90
+
91
+ async def _shutdown(self):
92
+ """Shutdown the server."""
93
+ # Close all client connections
94
+ if self.clients:
95
+ await asyncio.gather(
96
+ *[client.close() for client in self.clients],
97
+ return_exceptions=True
98
+ )
99
+
100
+ async def _handle_client(self, websocket: WebSocketServerProtocol, path: str):
101
+ """Handle a new client connection."""
102
+ self.clients.add(websocket)
103
+ client_addr = websocket.remote_address
104
+ self.logger.info(f"Client connected from {client_addr}")
105
+
106
+ try:
107
+ # Send current status
108
+ await self._send_current_status(websocket)
109
+
110
+ # Handle client messages
111
+ async for message in websocket:
112
+ try:
113
+ data = json.loads(message)
114
+ await self._handle_command(websocket, data)
115
+ except json.JSONDecodeError:
116
+ await websocket.send(json.dumps({
117
+ "type": "error",
118
+ "data": {"message": "Invalid JSON"}
119
+ }))
120
+
121
+ except websockets.exceptions.ConnectionClosed:
122
+ pass
123
+ finally:
124
+ self.clients.remove(websocket)
125
+ self.logger.info(f"Client disconnected from {client_addr}")
126
+
127
+ async def _send_current_status(self, websocket: WebSocketServerProtocol):
128
+ """Send current system status to a client."""
129
+ status = {
130
+ "type": "system.status",
131
+ "timestamp": datetime.utcnow().isoformat() + "Z",
132
+ "data": {
133
+ "session_id": self.session_id,
134
+ "session_start": self.session_start,
135
+ "claude_status": self.claude_status,
136
+ "claude_pid": self.claude_pid,
137
+ "connected_clients": len(self.clients),
138
+ "websocket_port": self.port,
139
+ "instance_info": {
140
+ "port": self.port,
141
+ "host": self.host,
142
+ "working_dir": os.getcwd() if self.session_id else None
143
+ }
144
+ }
145
+ }
146
+ await websocket.send(json.dumps(status))
147
+
148
+ async def _handle_command(self, websocket: WebSocketServerProtocol, data: Dict[str, Any]):
149
+ """Handle commands from clients."""
150
+ command = data.get("command")
151
+
152
+ if command == "get_status":
153
+ await self._send_current_status(websocket)
154
+
155
+ elif command == "get_history":
156
+ # Send recent events
157
+ params = data.get("params", {})
158
+ event_types = params.get("event_types", [])
159
+ limit = min(params.get("limit", 100), len(self.event_history))
160
+
161
+ history = []
162
+ for event in reversed(self.event_history):
163
+ if not event_types or event["type"] in event_types:
164
+ history.append(event)
165
+ if len(history) >= limit:
166
+ break
167
+
168
+ await websocket.send(json.dumps({
169
+ "type": "history",
170
+ "data": {"events": list(reversed(history))}
171
+ }))
172
+
173
+ elif command == "subscribe":
174
+ # For now, all clients get all events
175
+ await websocket.send(json.dumps({
176
+ "type": "subscribed",
177
+ "data": {"channels": data.get("channels", ["*"])}
178
+ }))
179
+
180
+ def broadcast_event(self, event_type: str, data: Dict[str, Any]):
181
+ """Broadcast an event to all connected clients."""
182
+ if not WEBSOCKETS_AVAILABLE:
183
+ return
184
+
185
+ event = {
186
+ "type": event_type,
187
+ "timestamp": datetime.utcnow().isoformat() + "Z",
188
+ "data": data
189
+ }
190
+
191
+ # Store in history
192
+ self.event_history.append(event)
193
+
194
+ # Broadcast to clients
195
+ if self.clients and self.loop:
196
+ asyncio.run_coroutine_threadsafe(
197
+ self._broadcast(json.dumps(event)),
198
+ self.loop
199
+ )
200
+
201
+ async def _broadcast(self, message: str):
202
+ """Send a message to all connected clients."""
203
+ if self.clients:
204
+ # Send to all clients concurrently
205
+ await asyncio.gather(
206
+ *[client.send(message) for client in self.clients],
207
+ return_exceptions=True
208
+ )
209
+
210
+ # Convenience methods for common events
211
+
212
+ def _get_git_branch(self, working_dir: str) -> str:
213
+ """Get the current git branch for the working directory."""
214
+ try:
215
+ result = subprocess.run(
216
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
217
+ cwd=working_dir,
218
+ capture_output=True,
219
+ text=True,
220
+ timeout=2
221
+ )
222
+ if result.returncode == 0:
223
+ return result.stdout.strip()
224
+ except Exception:
225
+ pass
226
+ return "not a git repo"
227
+
228
+ def session_started(self, session_id: str, launch_method: str, working_dir: str):
229
+ """Notify that a session has started."""
230
+ self.session_id = session_id
231
+ self.session_start = datetime.utcnow().isoformat() + "Z"
232
+
233
+ # Get git branch if in a git repo
234
+ git_branch = self._get_git_branch(working_dir)
235
+
236
+ self.broadcast_event("session.start", {
237
+ "session_id": session_id,
238
+ "start_time": self.session_start,
239
+ "launch_method": launch_method,
240
+ "working_directory": working_dir,
241
+ "git_branch": git_branch,
242
+ "websocket_port": self.port,
243
+ "instance_info": {
244
+ "port": self.port,
245
+ "host": self.host,
246
+ "working_dir": working_dir
247
+ }
248
+ })
249
+
250
+ def session_ended(self):
251
+ """Notify that a session has ended."""
252
+ if self.session_id:
253
+ duration = None
254
+ if self.session_start:
255
+ start = datetime.fromisoformat(self.session_start.replace("Z", "+00:00"))
256
+ duration = (datetime.utcnow() - start.replace(tzinfo=None)).total_seconds()
257
+
258
+ self.broadcast_event("session.end", {
259
+ "session_id": self.session_id,
260
+ "end_time": datetime.utcnow().isoformat() + "Z",
261
+ "duration_seconds": duration
262
+ })
263
+
264
+ self.session_id = None
265
+ self.session_start = None
266
+
267
+ def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
268
+ """Notify Claude status change."""
269
+ self.claude_status = status
270
+ self.claude_pid = pid
271
+ self.broadcast_event("claude.status", {
272
+ "status": status,
273
+ "pid": pid,
274
+ "message": message
275
+ })
276
+
277
+ def claude_output(self, content: str, stream: str = "stdout"):
278
+ """Broadcast Claude output."""
279
+ self.broadcast_event("claude.output", {
280
+ "content": content,
281
+ "stream": stream
282
+ })
283
+
284
+ def agent_delegated(self, agent: str, task: str, status: str = "started"):
285
+ """Notify agent delegation."""
286
+ self.broadcast_event("agent.delegation", {
287
+ "agent": agent,
288
+ "task": task,
289
+ "status": status,
290
+ "timestamp": datetime.utcnow().isoformat() + "Z"
291
+ })
292
+
293
+ def todo_updated(self, todos: List[Dict[str, Any]]):
294
+ """Notify todo list update."""
295
+ stats = {
296
+ "total": len(todos),
297
+ "completed": sum(1 for t in todos if t.get("status") == "completed"),
298
+ "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
299
+ "pending": sum(1 for t in todos if t.get("status") == "pending")
300
+ }
301
+
302
+ self.broadcast_event("todo.update", {
303
+ "todos": todos,
304
+ "stats": stats
305
+ })
306
+
307
+ def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
308
+ """Notify ticket creation."""
309
+ self.broadcast_event("ticket.created", {
310
+ "id": ticket_id,
311
+ "title": title,
312
+ "priority": priority,
313
+ "created_at": datetime.utcnow().isoformat() + "Z"
314
+ })
315
+
316
+ def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
317
+ """Notify when agent memory is loaded from file."""
318
+ self.broadcast_event("memory:loaded", {
319
+ "agent_id": agent_id,
320
+ "memory_size": memory_size,
321
+ "sections_count": sections_count,
322
+ "timestamp": datetime.utcnow().isoformat() + "Z"
323
+ })
324
+
325
+ def memory_created(self, agent_id: str, template_type: str):
326
+ """Notify when new agent memory is created from template."""
327
+ self.broadcast_event("memory:created", {
328
+ "agent_id": agent_id,
329
+ "template_type": template_type,
330
+ "timestamp": datetime.utcnow().isoformat() + "Z"
331
+ })
332
+
333
+ def memory_updated(self, agent_id: str, learning_type: str, content: str, section: str):
334
+ """Notify when learning is added to agent memory."""
335
+ self.broadcast_event("memory:updated", {
336
+ "agent_id": agent_id,
337
+ "learning_type": learning_type,
338
+ "content": content,
339
+ "section": section,
340
+ "timestamp": datetime.utcnow().isoformat() + "Z"
341
+ })
342
+
343
+ def memory_injected(self, agent_id: str, context_size: int):
344
+ """Notify when agent memory is injected into context."""
345
+ self.broadcast_event("memory:injected", {
346
+ "agent_id": agent_id,
347
+ "context_size": context_size,
348
+ "timestamp": datetime.utcnow().isoformat() + "Z"
349
+ })
350
+
351
+
352
+ # Global instance for easy access
353
+ _websocket_server: Optional[WebSocketServer] = None
354
+
355
+
356
+ def get_websocket_server() -> WebSocketServer:
357
+ """Get or create the global WebSocket server instance."""
358
+ global _websocket_server
359
+ if _websocket_server is None:
360
+ _websocket_server = WebSocketServer()
361
+ return _websocket_server
362
+
363
+
364
+ def start_websocket_server():
365
+ """Start the global WebSocket server."""
366
+ server = get_websocket_server()
367
+ server.start()
368
+ return server
369
+
370
+
371
+ def stop_websocket_server():
372
+ """Stop the global WebSocket server."""
373
+ global _websocket_server
374
+ if _websocket_server:
375
+ _websocket_server.stop()
376
+ _websocket_server = None