claude-mpm 3.1.3__py3-none-any.whl → 3.2.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 (79) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +81 -18
  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/manage_version.py +479 -0
  43. claude_mpm/scripts/socketio_daemon.py +181 -0
  44. claude_mpm/scripts/socketio_server_manager.py +428 -0
  45. claude_mpm/services/__init__.py +5 -0
  46. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  47. claude_mpm/services/agent_memory_manager.py +684 -0
  48. claude_mpm/services/agent_modification_tracker.py +98 -17
  49. claude_mpm/services/agent_persistence_service.py +33 -13
  50. claude_mpm/services/agent_registry.py +82 -43
  51. claude_mpm/services/hook_service.py +362 -0
  52. claude_mpm/services/socketio_client_manager.py +474 -0
  53. claude_mpm/services/socketio_server.py +698 -0
  54. claude_mpm/services/standalone_socketio_server.py +631 -0
  55. claude_mpm/services/ticket_manager.py +4 -5
  56. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  57. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  58. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  59. claude_mpm/services/websocket_server.py +376 -0
  60. claude_mpm/utils/dependency_manager.py +211 -0
  61. claude_mpm/utils/import_migration_example.py +80 -0
  62. claude_mpm/utils/path_operations.py +0 -20
  63. claude_mpm/web/open_dashboard.py +34 -0
  64. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -9
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +70 -50
  66. claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
  67. claude_mpm/cli_old.py +0 -728
  68. claude_mpm/models/common.py +0 -41
  69. claude_mpm/models/lifecycle.py +0 -97
  70. claude_mpm/models/modification.py +0 -126
  71. claude_mpm/models/persistence.py +0 -57
  72. claude_mpm/models/registry.py +0 -91
  73. claude_mpm/security/__init__.py +0 -8
  74. claude_mpm/security/bash_validator.py +0 -393
  75. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  76. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  77. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env python3
2
+ """Socket.IO Server Manager - Deployment-agnostic server management.
3
+
4
+ This script provides unified management for Socket.IO servers across different deployment scenarios:
5
+ - Local development
6
+ - PyPI installation
7
+ - Docker containers
8
+ - System service installation
9
+
10
+ Features:
11
+ - Start/stop/restart standalone servers
12
+ - Version compatibility checking
13
+ - Health monitoring and diagnostics
14
+ - Multi-instance management
15
+ - Automatic dependency installation
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Dict, List, Optional
27
+
28
+ try:
29
+ import requests
30
+ REQUESTS_AVAILABLE = True
31
+ except ImportError:
32
+ REQUESTS_AVAILABLE = False
33
+
34
+
35
+ class ServerManager:
36
+ """Manages Socket.IO server instances across different deployment modes."""
37
+
38
+ def __init__(self):
39
+ self.base_port = 8765
40
+ self.max_instances = 5
41
+ self.script_dir = Path(__file__).parent
42
+ self.project_root = self.script_dir.parent
43
+
44
+ def get_server_info(self, port: int) -> Optional[Dict]:
45
+ """Get server information from a running instance."""
46
+ if not REQUESTS_AVAILABLE:
47
+ return None
48
+
49
+ try:
50
+ response = requests.get(f"http://localhost:{port}/health", timeout=2.0)
51
+ if response.status_code == 200:
52
+ return response.json()
53
+ except Exception:
54
+ pass
55
+ return None
56
+
57
+ def list_running_servers(self) -> List[Dict]:
58
+ """List all running Socket.IO servers."""
59
+ running_servers = []
60
+
61
+ for port in range(self.base_port, self.base_port + self.max_instances):
62
+ server_info = self.get_server_info(port)
63
+ if server_info:
64
+ server_info['port'] = port
65
+ running_servers.append(server_info)
66
+
67
+ return running_servers
68
+
69
+ def find_available_port(self, start_port: int = None) -> int:
70
+ """Find the next available port for a new server."""
71
+ start_port = start_port or self.base_port
72
+
73
+ for port in range(start_port, start_port + self.max_instances):
74
+ if not self.get_server_info(port):
75
+ return port
76
+
77
+ raise RuntimeError(f"No available ports found in range {start_port}-{start_port + self.max_instances}")
78
+
79
+ def start_server(self, port: int = None, server_id: str = None,
80
+ host: str = "localhost") -> bool:
81
+ """Start a standalone Socket.IO server."""
82
+
83
+ # Find available port if not specified
84
+ if port is None:
85
+ try:
86
+ port = self.find_available_port()
87
+ except RuntimeError as e:
88
+ print(f"Error: {e}")
89
+ return False
90
+
91
+ # Check if server is already running on this port
92
+ if self.get_server_info(port):
93
+ print(f"Server already running on port {port}")
94
+ return False
95
+
96
+ # Try different ways to start the server based on deployment
97
+ success = False
98
+
99
+ # Method 1: Try installed claude-mpm package
100
+ try:
101
+ cmd = [
102
+ sys.executable, "-m", "claude_mpm.services.standalone_socketio_server",
103
+ "--host", host,
104
+ "--port", str(port)
105
+ ]
106
+ if server_id:
107
+ cmd.extend(["--server-id", server_id])
108
+
109
+ print(f"Starting server on {host}:{port} using installed package...")
110
+
111
+ # Start in background
112
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
113
+
114
+ # Wait for server to start
115
+ for _ in range(10): # Wait up to 10 seconds
116
+ time.sleep(1)
117
+ if self.get_server_info(port):
118
+ success = True
119
+ break
120
+
121
+ except Exception as e:
122
+ print(f"Failed to start via installed package: {e}")
123
+
124
+ # Method 2: Try local development mode
125
+ if not success:
126
+ try:
127
+ server_path = self.project_root / "src" / "claude_mpm" / "services" / "standalone_socketio_server.py"
128
+ if server_path.exists():
129
+ cmd = [
130
+ sys.executable, str(server_path),
131
+ "--host", host,
132
+ "--port", str(port)
133
+ ]
134
+ if server_id:
135
+ cmd.extend(["--server-id", server_id])
136
+
137
+ print(f"Starting server using local development mode...")
138
+
139
+ # Set PYTHONPATH for local development
140
+ env = os.environ.copy()
141
+ src_path = str(self.project_root / "src")
142
+ if "PYTHONPATH" in env:
143
+ env["PYTHONPATH"] = f"{src_path}:{env['PYTHONPATH']}"
144
+ else:
145
+ env["PYTHONPATH"] = src_path
146
+
147
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env)
148
+
149
+ # Wait for server to start
150
+ for _ in range(10):
151
+ time.sleep(1)
152
+ if self.get_server_info(port):
153
+ success = True
154
+ break
155
+
156
+ except Exception as e:
157
+ print(f"Failed to start in development mode: {e}")
158
+
159
+ if success:
160
+ print(f"✅ Server started successfully on {host}:{port}")
161
+ return True
162
+ else:
163
+ print(f"❌ Failed to start server on {host}:{port}")
164
+ return False
165
+
166
+ def stop_server(self, port: int = None, server_id: str = None) -> bool:
167
+ """Stop a running Socket.IO server."""
168
+
169
+ if port is None and server_id is None:
170
+ print("Must specify either port or server_id")
171
+ return False
172
+
173
+ # Find server by ID if port not specified
174
+ if port is None:
175
+ running_servers = self.list_running_servers()
176
+ for server in running_servers:
177
+ if server.get('server_id') == server_id:
178
+ port = server['port']
179
+ break
180
+
181
+ if port is None:
182
+ print(f"Server with ID '{server_id}' not found")
183
+ return False
184
+
185
+ # Get server info
186
+ server_info = self.get_server_info(port)
187
+ if not server_info:
188
+ print(f"No server running on port {port}")
189
+ return False
190
+
191
+ # Try to get PID and send termination signal
192
+ pid = server_info.get('pid')
193
+ if pid:
194
+ try:
195
+ os.kill(pid, signal.SIGTERM)
196
+ print(f"✅ Sent termination signal to server (PID: {pid})")
197
+
198
+ # Wait for server to stop
199
+ for _ in range(10):
200
+ time.sleep(1)
201
+ if not self.get_server_info(port):
202
+ print(f"✅ Server stopped successfully")
203
+ return True
204
+
205
+ # Force kill if still running
206
+ try:
207
+ os.kill(pid, signal.SIGKILL)
208
+ print(f"⚠️ Force killed server (PID: {pid})")
209
+ return True
210
+ except:
211
+ pass
212
+
213
+ except OSError as e:
214
+ print(f"Error stopping server: {e}")
215
+
216
+ print(f"❌ Failed to stop server on port {port}")
217
+ return False
218
+
219
+ def restart_server(self, port: int = None, server_id: str = None) -> bool:
220
+ """Restart a Socket.IO server."""
221
+
222
+ # Stop the server first
223
+ if self.stop_server(port, server_id):
224
+ time.sleep(2) # Give it time to fully stop
225
+
226
+ # Start it again
227
+ if port is None:
228
+ port = self.find_available_port()
229
+
230
+ return self.start_server(port)
231
+
232
+ return False
233
+
234
+ def status(self, verbose: bool = False) -> None:
235
+ """Show status of all Socket.IO servers."""
236
+ running_servers = self.list_running_servers()
237
+
238
+ if not running_servers:
239
+ print("No Socket.IO servers currently running")
240
+ return
241
+
242
+ print(f"Found {len(running_servers)} running server(s):")
243
+ print()
244
+
245
+ for server in running_servers:
246
+ port = server['port']
247
+ server_id = server.get('server_id', 'unknown')
248
+ version = server.get('server_version', 'unknown')
249
+ uptime = server.get('uptime_seconds', 0)
250
+ clients = server.get('clients_connected', 0)
251
+
252
+ print(f"🖥️ Server ID: {server_id}")
253
+ print(f" Port: {port}")
254
+ print(f" Version: {version}")
255
+ print(f" Uptime: {self._format_uptime(uptime)}")
256
+ print(f" Clients: {clients}")
257
+
258
+ if verbose:
259
+ print(f" PID: {server.get('pid', 'unknown')}")
260
+ print(f" Host: {server.get('host', 'unknown')}")
261
+
262
+ # Get additional stats
263
+ stats = self._get_server_stats(port)
264
+ if stats:
265
+ events_processed = stats.get('events', {}).get('total_processed', 0)
266
+ clients_served = stats.get('connections', {}).get('total_served', 0)
267
+ print(f" Events processed: {events_processed}")
268
+ print(f" Total clients served: {clients_served}")
269
+
270
+ print()
271
+
272
+ def health_check(self, port: int = None) -> bool:
273
+ """Perform health check on server(s)."""
274
+
275
+ if port:
276
+ # Check specific server
277
+ server_info = self.get_server_info(port)
278
+ if server_info:
279
+ status = server_info.get('status', 'unknown')
280
+ print(f"Server on port {port}: {status}")
281
+ return status == 'healthy'
282
+ else:
283
+ print(f"No server found on port {port}")
284
+ return False
285
+ else:
286
+ # Check all servers
287
+ running_servers = self.list_running_servers()
288
+ if not running_servers:
289
+ print("No servers running")
290
+ return False
291
+
292
+ all_healthy = True
293
+ for server in running_servers:
294
+ port = server['port']
295
+ status = server.get('status', 'unknown')
296
+ server_id = server.get('server_id', 'unknown')
297
+ print(f"Server {server_id} (port {port}): {status}")
298
+ if status != 'healthy':
299
+ all_healthy = False
300
+
301
+ return all_healthy
302
+
303
+ def install_dependencies(self) -> bool:
304
+ """Install required dependencies for Socket.IO server."""
305
+ dependencies = ['python-socketio>=5.11.0', 'aiohttp>=3.9.0', 'requests>=2.25.0']
306
+
307
+ print("Installing Socket.IO server dependencies...")
308
+
309
+ try:
310
+ cmd = [sys.executable, '-m', 'pip', 'install'] + dependencies
311
+ result = subprocess.run(cmd, capture_output=True, text=True)
312
+
313
+ if result.returncode == 0:
314
+ print("✅ Dependencies installed successfully")
315
+ return True
316
+ else:
317
+ print(f"❌ Failed to install dependencies: {result.stderr}")
318
+ return False
319
+
320
+ except Exception as e:
321
+ print(f"❌ Error installing dependencies: {e}")
322
+ return False
323
+
324
+ def _format_uptime(self, seconds: float) -> str:
325
+ """Format uptime in a human-readable way."""
326
+ if seconds < 60:
327
+ return f"{seconds:.1f}s"
328
+ elif seconds < 3600:
329
+ return f"{seconds/60:.1f}m"
330
+ else:
331
+ return f"{seconds/3600:.1f}h"
332
+
333
+ def _get_server_stats(self, port: int) -> Optional[Dict]:
334
+ """Get detailed server statistics."""
335
+ if not REQUESTS_AVAILABLE:
336
+ return None
337
+
338
+ try:
339
+ response = requests.get(f"http://localhost:{port}/stats", timeout=2.0)
340
+ if response.status_code == 200:
341
+ return response.json()
342
+ except Exception:
343
+ pass
344
+ return None
345
+
346
+
347
+ def main():
348
+ """Main CLI entry point."""
349
+ parser = argparse.ArgumentParser(description="Socket.IO Server Manager")
350
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
351
+
352
+ # Start command
353
+ start_parser = subparsers.add_parser('start', help='Start a Socket.IO server')
354
+ start_parser.add_argument('--port', type=int, help='Port to bind to (auto-detect if not specified)')
355
+ start_parser.add_argument('--host', default='localhost', help='Host to bind to')
356
+ start_parser.add_argument('--server-id', help='Custom server ID')
357
+
358
+ # Stop command
359
+ stop_parser = subparsers.add_parser('stop', help='Stop a Socket.IO server')
360
+ stop_parser.add_argument('--port', type=int, help='Port of server to stop')
361
+ stop_parser.add_argument('--server-id', help='Server ID to stop')
362
+
363
+ # Restart command
364
+ restart_parser = subparsers.add_parser('restart', help='Restart a Socket.IO server')
365
+ restart_parser.add_argument('--port', type=int, help='Port of server to restart')
366
+ restart_parser.add_argument('--server-id', help='Server ID to restart')
367
+
368
+ # Status command
369
+ status_parser = subparsers.add_parser('status', help='Show server status')
370
+ status_parser.add_argument('-v', '--verbose', action='store_true', help='Show detailed information')
371
+
372
+ # Health check command
373
+ health_parser = subparsers.add_parser('health', help='Perform health check')
374
+ health_parser.add_argument('--port', type=int, help='Port to check (all servers if not specified)')
375
+
376
+ # Install dependencies command
377
+ subparsers.add_parser('install-deps', help='Install required dependencies')
378
+
379
+ # List command
380
+ subparsers.add_parser('list', help='List running servers')
381
+
382
+ args = parser.parse_args()
383
+
384
+ if not args.command:
385
+ parser.print_help()
386
+ return
387
+
388
+ manager = ServerManager()
389
+
390
+ if args.command == 'start':
391
+ success = manager.start_server(
392
+ port=args.port,
393
+ server_id=args.server_id,
394
+ host=args.host
395
+ )
396
+ sys.exit(0 if success else 1)
397
+
398
+ elif args.command == 'stop':
399
+ success = manager.stop_server(
400
+ port=args.port,
401
+ server_id=args.server_id
402
+ )
403
+ sys.exit(0 if success else 1)
404
+
405
+ elif args.command == 'restart':
406
+ success = manager.restart_server(
407
+ port=args.port,
408
+ server_id=args.server_id
409
+ )
410
+ sys.exit(0 if success else 1)
411
+
412
+ elif args.command == 'status':
413
+ manager.status(verbose=args.verbose)
414
+
415
+ elif args.command == 'health':
416
+ healthy = manager.health_check(port=args.port)
417
+ sys.exit(0 if healthy else 1)
418
+
419
+ elif args.command == 'install-deps':
420
+ success = manager.install_dependencies()
421
+ sys.exit(0 if success else 1)
422
+
423
+ elif args.command == 'list':
424
+ manager.status(verbose=False)
425
+
426
+
427
+ if __name__ == "__main__":
428
+ main()
@@ -2,9 +2,14 @@
2
2
 
3
3
  from .ticket_manager import TicketManager
4
4
  from .agent_deployment import AgentDeploymentService
5
+ from .agent_memory_manager import AgentMemoryManager, get_memory_manager
6
+ from .hook_service import HookService
5
7
 
6
8
  # Import other services as needed
7
9
  __all__ = [
8
10
  "TicketManager",
9
11
  "AgentDeploymentService",
12
+ "AgentMemoryManager",
13
+ "get_memory_manager",
14
+ "HookService",
10
15
  ]
@@ -27,46 +27,97 @@ Created for ISS-0118: Agent Registry and Hierarchical Discovery System
27
27
  import asyncio
28
28
  import logging
29
29
  import time
30
+ from dataclasses import dataclass, field
30
31
  from datetime import datetime, timedelta
32
+ from enum import Enum
31
33
  from pathlib import Path
32
34
  from typing import Dict, List, Optional, Set, Any, Tuple, Union
33
35
 
34
36
  from claude_mpm.services.shared_prompt_cache import SharedPromptCache
35
- from claude_mpm.services.agent_registry import AgentRegistry
36
- from claude_mpm.services.agent_modification_tracker import AgentModificationTracker
37
- from claude_mpm.services.agent_persistence_service import AgentPersistenceService
38
- from claude_mpm.services.agent_management_service import AgentManager
39
- from claude_mpm.models.agent_definition import AgentDefinition, AgentType
40
- from claude_mpm.models.lifecycle import (
41
- LifecycleOperation,
42
- LifecycleState,
43
- AgentLifecycleRecord,
44
- LifecycleOperationResult
45
- )
46
- from claude_mpm.models.modification import (
47
- AgentModification,
48
- ModificationType,
37
+ from claude_mpm.services.agent_registry import AgentRegistry, AgentMetadata
38
+ from claude_mpm.services.agent_modification_tracker import (
39
+ AgentModificationTracker,
40
+ AgentModification,
41
+ ModificationType,
49
42
  ModificationTier
50
43
  )
51
- from claude_mpm.models.persistence import (
44
+ from claude_mpm.services.agent_persistence_service import (
45
+ AgentPersistenceService,
52
46
  PersistenceStrategy,
53
47
  PersistenceRecord,
54
48
  PersistenceOperation
55
49
  )
56
- from claude_mpm.models.registry import AgentRegistryMetadata
50
+ from claude_mpm.services.agent_management_service import AgentManager
51
+ from claude_mpm.models.agent_definition import AgentDefinition, AgentType
57
52
  from claude_mpm.core.base_service import BaseService
58
53
  from claude_mpm.utils.path_operations import path_ops
59
54
  from claude_mpm.utils.config_manager import ConfigurationManager
60
55
 
61
- # Backward compatibility imports
62
- # These allow existing code to import from this module
63
- __all__ = [
64
- 'LifecycleOperation',
65
- 'LifecycleState',
66
- 'AgentLifecycleRecord',
67
- 'LifecycleOperationResult',
68
- 'AgentLifecycleManager'
69
- ]
56
+
57
+ class LifecycleOperation(Enum):
58
+ """Agent lifecycle operations."""
59
+ CREATE = "create"
60
+ UPDATE = "update"
61
+ DELETE = "delete"
62
+ RESTORE = "restore"
63
+ MIGRATE = "migrate"
64
+ REPLICATE = "replicate"
65
+ VALIDATE = "validate"
66
+
67
+
68
+ class LifecycleState(Enum):
69
+ """Agent lifecycle states."""
70
+ ACTIVE = "active"
71
+ MODIFIED = "modified"
72
+ DELETED = "deleted"
73
+ CONFLICTED = "conflicted"
74
+ MIGRATING = "migrating"
75
+ VALIDATING = "validating"
76
+
77
+
78
+ @dataclass
79
+ class AgentLifecycleRecord:
80
+ """Complete lifecycle record for an agent."""
81
+
82
+ agent_name: str
83
+ current_state: LifecycleState
84
+ tier: ModificationTier
85
+ file_path: str
86
+ created_at: float
87
+ last_modified: float
88
+ version: str
89
+ modifications: List[str] = field(default_factory=list) # Modification IDs
90
+ persistence_operations: List[str] = field(default_factory=list) # Operation IDs
91
+ backup_paths: List[str] = field(default_factory=list)
92
+ validation_status: str = "valid"
93
+ validation_errors: List[str] = field(default_factory=list)
94
+ metadata: Dict[str, Any] = field(default_factory=dict)
95
+
96
+ @property
97
+ def age_days(self) -> float:
98
+ """Get age in days."""
99
+ return (time.time() - self.created_at) / (24 * 3600)
100
+
101
+ @property
102
+ def last_modified_datetime(self) -> datetime:
103
+ """Get last modified as datetime."""
104
+ return datetime.fromtimestamp(self.last_modified)
105
+
106
+
107
+ @dataclass
108
+ class LifecycleOperationResult:
109
+ """Result of a lifecycle operation."""
110
+
111
+ operation: LifecycleOperation
112
+ agent_name: str
113
+ success: bool
114
+ duration_ms: float
115
+ error_message: Optional[str] = None
116
+ modification_id: Optional[str] = None
117
+ persistence_id: Optional[str] = None
118
+ cache_invalidated: bool = False
119
+ registry_updated: bool = False
120
+ metadata: Dict[str, Any] = field(default_factory=dict)
70
121
 
71
122
 
72
123
  class AgentLifecycleManager(BaseService):