runtime-memory 3.0.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 (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,547 @@
1
+ """
2
+ Centralized exception hierarchy for Runtime Memory.
3
+
4
+ This module provides a consistent exception hierarchy with:
5
+ - Clear categorization (storage, retrieval, extraction, server, SDK)
6
+ - User-friendly error messages
7
+ - Structured error codes for programmatic handling
8
+ - Graceful degradation support
9
+ """
10
+
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+
15
+ class ErrorCode(str, Enum):
16
+ """Standardized error codes for programmatic handling."""
17
+
18
+ # General errors (1xxx)
19
+ UNKNOWN = "ML1000"
20
+ INVALID_INPUT = "ML1001"
21
+ CONFIGURATION_ERROR = "ML1002"
22
+ INITIALIZATION_ERROR = "ML1003"
23
+
24
+ # Storage errors (2xxx)
25
+ STORAGE_ERROR = "ML2000"
26
+ MEMORY_NOT_FOUND = "ML2001"
27
+ DATABASE_CONNECTION = "ML2002"
28
+ DATABASE_INTEGRITY = "ML2003"
29
+ MIGRATION_ERROR = "ML2004"
30
+
31
+ # Retrieval errors (3xxx)
32
+ RETRIEVAL_ERROR = "ML3000"
33
+ EMBEDDING_ERROR = "ML3001"
34
+ MODEL_NOT_FOUND = "ML3002"
35
+ SEARCH_TIMEOUT = "ML3003"
36
+
37
+ # Extraction errors (4xxx)
38
+ EXTRACTION_ERROR = "ML4000"
39
+ LLM_API_ERROR = "ML4001"
40
+ RATE_LIMIT = "ML4002"
41
+ INVALID_RESPONSE = "ML4003"
42
+
43
+ # Server errors (5xxx)
44
+ SERVER_ERROR = "ML5000"
45
+ MCP_ERROR = "ML5001"
46
+ REST_API_ERROR = "ML5002"
47
+ HOOK_ERROR = "ML5003"
48
+ DAEMON_ERROR = "ML5004"
49
+
50
+ # SDK errors (6xxx)
51
+ SDK_ERROR = "ML6000"
52
+ CONNECTION_ERROR = "ML6001"
53
+ AUTHENTICATION_ERROR = "ML6002"
54
+ VALIDATION_ERROR = "ML6003"
55
+
56
+ # Task integration errors (7xxx)
57
+ TASK_ERROR = "ML7000"
58
+ BEADS_NOT_FOUND = "ML7001"
59
+ TASK_SYNC_ERROR = "ML7002"
60
+ TASK_LINK_ERROR = "ML7003"
61
+
62
+
63
+ class RuntimeMemoryError(Exception):
64
+ """Base exception for all Runtime Memory errors.
65
+
66
+ Attributes:
67
+ message: Human-readable error message
68
+ code: Standardized error code for programmatic handling
69
+ details: Additional context about the error
70
+ recoverable: Whether the operation can be retried
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ message: str,
76
+ code: ErrorCode = ErrorCode.UNKNOWN,
77
+ details: dict[str, Any] | None = None,
78
+ recoverable: bool = False,
79
+ ):
80
+ self.message = message
81
+ self.code = code
82
+ self.details = details or {}
83
+ self.recoverable = recoverable
84
+ super().__init__(message)
85
+
86
+ def to_dict(self) -> dict[str, Any]:
87
+ """Convert to dictionary for API responses."""
88
+ return {
89
+ "error": self.message,
90
+ "code": self.code.value,
91
+ "details": self.details,
92
+ "recoverable": self.recoverable,
93
+ }
94
+
95
+ def user_message(self) -> str:
96
+ """Return a user-friendly error message."""
97
+ return self.message
98
+
99
+
100
+ # =============================================================================
101
+ # Storage Exceptions
102
+ # =============================================================================
103
+
104
+
105
+ class StorageError(RuntimeMemoryError):
106
+ """Base exception for storage-related errors."""
107
+
108
+ def __init__(
109
+ self,
110
+ message: str,
111
+ code: ErrorCode = ErrorCode.STORAGE_ERROR,
112
+ details: dict[str, Any] | None = None,
113
+ recoverable: bool = False,
114
+ ):
115
+ super().__init__(message, code, details, recoverable)
116
+
117
+
118
+ class MemoryNotFoundError(StorageError):
119
+ """Raised when a requested memory does not exist."""
120
+
121
+ def __init__(self, memory_id: str):
122
+ super().__init__(
123
+ message=f"Memory not found: {memory_id}",
124
+ code=ErrorCode.MEMORY_NOT_FOUND,
125
+ details={"memory_id": memory_id},
126
+ recoverable=False,
127
+ )
128
+
129
+
130
+ class DatabaseConnectionError(StorageError):
131
+ """Raised when database connection fails."""
132
+
133
+ def __init__(self, message: str = "Database connection failed"):
134
+ super().__init__(
135
+ message=message,
136
+ code=ErrorCode.DATABASE_CONNECTION,
137
+ recoverable=True,
138
+ )
139
+
140
+
141
+ class DatabaseIntegrityError(StorageError):
142
+ """Raised when database integrity check fails."""
143
+
144
+ def __init__(self, message: str = "Database integrity check failed"):
145
+ super().__init__(
146
+ message=message,
147
+ code=ErrorCode.DATABASE_INTEGRITY,
148
+ recoverable=False,
149
+ )
150
+
151
+
152
+ # =============================================================================
153
+ # Retrieval Exceptions
154
+ # =============================================================================
155
+
156
+
157
+ class RetrievalError(RuntimeMemoryError):
158
+ """Base exception for retrieval-related errors."""
159
+
160
+ def __init__(
161
+ self,
162
+ message: str,
163
+ code: ErrorCode = ErrorCode.RETRIEVAL_ERROR,
164
+ details: dict[str, Any] | None = None,
165
+ recoverable: bool = False,
166
+ ):
167
+ super().__init__(message, code, details, recoverable)
168
+
169
+
170
+ class EmbeddingError(RetrievalError):
171
+ """Raised when embedding generation fails."""
172
+
173
+ def __init__(self, message: str = "Embedding generation failed"):
174
+ super().__init__(
175
+ message=message,
176
+ code=ErrorCode.EMBEDDING_ERROR,
177
+ recoverable=True,
178
+ )
179
+
180
+
181
+ class ModelNotFoundError(RetrievalError):
182
+ """Raised when embedding model is not available."""
183
+
184
+ def __init__(self, model_name: str):
185
+ super().__init__(
186
+ message=f"Embedding model not found: {model_name}",
187
+ code=ErrorCode.MODEL_NOT_FOUND,
188
+ details={"model_name": model_name},
189
+ recoverable=False,
190
+ )
191
+
192
+
193
+ class SearchTimeoutError(RetrievalError):
194
+ """Raised when search operation times out."""
195
+
196
+ def __init__(self, timeout_seconds: float):
197
+ super().__init__(
198
+ message=f"Search timed out after {timeout_seconds}s",
199
+ code=ErrorCode.SEARCH_TIMEOUT,
200
+ details={"timeout_seconds": timeout_seconds},
201
+ recoverable=True,
202
+ )
203
+
204
+
205
+ # =============================================================================
206
+ # Extraction Exceptions
207
+ # =============================================================================
208
+
209
+
210
+ class ExtractionError(RuntimeMemoryError):
211
+ """Base exception for extraction-related errors."""
212
+
213
+ def __init__(
214
+ self,
215
+ message: str,
216
+ code: ErrorCode = ErrorCode.EXTRACTION_ERROR,
217
+ details: dict[str, Any] | None = None,
218
+ recoverable: bool = False,
219
+ ):
220
+ super().__init__(message, code, details, recoverable)
221
+
222
+
223
+ class LLMAPIError(ExtractionError):
224
+ """Raised when LLM API call fails."""
225
+
226
+ def __init__(
227
+ self,
228
+ message: str = "LLM API request failed",
229
+ status_code: int | None = None,
230
+ ):
231
+ super().__init__(
232
+ message=message,
233
+ code=ErrorCode.LLM_API_ERROR,
234
+ details={"status_code": status_code} if status_code else {},
235
+ recoverable=True,
236
+ )
237
+
238
+
239
+ class RateLimitError(ExtractionError):
240
+ """Raised when rate limit is exceeded."""
241
+
242
+ def __init__(
243
+ self,
244
+ message: str = "Rate limit exceeded",
245
+ retry_after: float | None = None,
246
+ ):
247
+ super().__init__(
248
+ message=message,
249
+ code=ErrorCode.RATE_LIMIT,
250
+ details={"retry_after": retry_after} if retry_after else {},
251
+ recoverable=True,
252
+ )
253
+
254
+
255
+ class InvalidResponseError(ExtractionError):
256
+ """Raised when LLM response cannot be parsed."""
257
+
258
+ def __init__(self, message: str = "Invalid response from LLM"):
259
+ super().__init__(
260
+ message=message,
261
+ code=ErrorCode.INVALID_RESPONSE,
262
+ recoverable=True,
263
+ )
264
+
265
+
266
+ # =============================================================================
267
+ # Server Exceptions
268
+ # =============================================================================
269
+
270
+
271
+ class ServerError(RuntimeMemoryError):
272
+ """Base exception for server-related errors."""
273
+
274
+ def __init__(
275
+ self,
276
+ message: str,
277
+ code: ErrorCode = ErrorCode.SERVER_ERROR,
278
+ details: dict[str, Any] | None = None,
279
+ recoverable: bool = False,
280
+ ):
281
+ super().__init__(message, code, details, recoverable)
282
+
283
+
284
+ class MCPError(ServerError):
285
+ """Raised when MCP protocol operation fails."""
286
+
287
+ def __init__(self, message: str, mcp_code: int | None = None):
288
+ super().__init__(
289
+ message=message,
290
+ code=ErrorCode.MCP_ERROR,
291
+ details={"mcp_code": mcp_code} if mcp_code else {},
292
+ recoverable=False,
293
+ )
294
+
295
+
296
+ class HookError(ServerError):
297
+ """Raised when Claude Code hook execution fails."""
298
+
299
+ def __init__(self, hook_name: str, message: str):
300
+ super().__init__(
301
+ message=f"Hook '{hook_name}' failed: {message}",
302
+ code=ErrorCode.HOOK_ERROR,
303
+ details={"hook_name": hook_name},
304
+ recoverable=True,
305
+ )
306
+
307
+
308
+ class HookNotInstalledError(HookError):
309
+ """Raised when a required hook is not installed."""
310
+
311
+ def __init__(self, hook_name: str):
312
+ super().__init__(
313
+ hook_name=hook_name,
314
+ message="not installed",
315
+ )
316
+
317
+
318
+ class DaemonError(ServerError):
319
+ """Raised when daemon operation fails."""
320
+
321
+ def __init__(self, message: str):
322
+ super().__init__(
323
+ message=message,
324
+ code=ErrorCode.DAEMON_ERROR,
325
+ recoverable=False,
326
+ )
327
+
328
+
329
+ class DaemonAlreadyRunningError(DaemonError):
330
+ """Raised when daemon is already running."""
331
+
332
+ def __init__(self):
333
+ super().__init__(message="Daemon is already running")
334
+
335
+
336
+ # =============================================================================
337
+ # SDK Exceptions
338
+ # =============================================================================
339
+
340
+
341
+ class SDKError(RuntimeMemoryError):
342
+ """Base exception for SDK client errors."""
343
+
344
+ def __init__(
345
+ self,
346
+ message: str,
347
+ code: ErrorCode = ErrorCode.SDK_ERROR,
348
+ details: dict[str, Any] | None = None,
349
+ recoverable: bool = False,
350
+ ):
351
+ super().__init__(message, code, details, recoverable)
352
+
353
+
354
+ class ConnectionError(SDKError):
355
+ """Raised when connection to server fails."""
356
+
357
+ def __init__(self, message: str = "Connection to server failed"):
358
+ super().__init__(
359
+ message=message,
360
+ code=ErrorCode.CONNECTION_ERROR,
361
+ recoverable=True,
362
+ )
363
+
364
+
365
+ class AuthenticationError(SDKError):
366
+ """Raised when authentication fails."""
367
+
368
+ def __init__(self, message: str = "Authentication failed"):
369
+ super().__init__(
370
+ message=message,
371
+ code=ErrorCode.AUTHENTICATION_ERROR,
372
+ recoverable=False,
373
+ )
374
+
375
+
376
+ class ValidationError(SDKError):
377
+ """Raised when input validation fails."""
378
+
379
+ def __init__(self, message: str, field: str | None = None):
380
+ super().__init__(
381
+ message=message,
382
+ code=ErrorCode.VALIDATION_ERROR,
383
+ details={"field": field} if field else {},
384
+ recoverable=False,
385
+ )
386
+
387
+
388
+ # =============================================================================
389
+ # Task Integration Exceptions
390
+ # =============================================================================
391
+
392
+
393
+ class TaskError(RuntimeMemoryError):
394
+ """Base exception for task integration errors."""
395
+
396
+ def __init__(
397
+ self,
398
+ message: str,
399
+ code: ErrorCode = ErrorCode.TASK_ERROR,
400
+ details: dict[str, Any] | None = None,
401
+ recoverable: bool = False,
402
+ ):
403
+ super().__init__(message, code, details, recoverable)
404
+
405
+
406
+ class BeadsNotFoundError(TaskError):
407
+ """Raised when Beads directory is not found."""
408
+
409
+ def __init__(self, path: str | None = None):
410
+ msg = "Beads directory not found"
411
+ if path:
412
+ msg += f": {path}"
413
+ super().__init__(
414
+ message=msg,
415
+ code=ErrorCode.BEADS_NOT_FOUND,
416
+ details={"path": path} if path else {},
417
+ recoverable=False,
418
+ )
419
+
420
+
421
+ class TaskSyncError(TaskError):
422
+ """Raised when task synchronization fails."""
423
+
424
+ def __init__(self, message: str, task_id: str | None = None):
425
+ super().__init__(
426
+ message=message,
427
+ code=ErrorCode.TASK_SYNC_ERROR,
428
+ details={"task_id": task_id} if task_id else {},
429
+ recoverable=True,
430
+ )
431
+
432
+
433
+ class TaskLinkError(TaskError):
434
+ """Raised when task-memory linking fails."""
435
+
436
+ def __init__(self, task_id: str, memory_id: str, reason: str):
437
+ super().__init__(
438
+ message=f"Failed to link task {task_id} to memory {memory_id}: {reason}",
439
+ code=ErrorCode.TASK_LINK_ERROR,
440
+ details={"task_id": task_id, "memory_id": memory_id},
441
+ recoverable=True,
442
+ )
443
+
444
+
445
+ # =============================================================================
446
+ # Resilience Exceptions
447
+ # =============================================================================
448
+
449
+
450
+ class CircuitOpenError(RuntimeMemoryError):
451
+ """Raised when circuit breaker is open and rejecting requests."""
452
+
453
+ def __init__(self, circuit_name: str):
454
+ super().__init__(
455
+ message=f"Service temporarily unavailable (circuit '{circuit_name}' is open)",
456
+ code=ErrorCode.SERVER_ERROR,
457
+ details={"circuit_name": circuit_name},
458
+ recoverable=True,
459
+ )
460
+
461
+
462
+ # =============================================================================
463
+ # Configuration Exceptions
464
+ # =============================================================================
465
+
466
+
467
+ class ConfigurationError(RuntimeMemoryError):
468
+ """Raised when configuration is invalid."""
469
+
470
+ def __init__(self, message: str, config_key: str | None = None):
471
+ super().__init__(
472
+ message=message,
473
+ code=ErrorCode.CONFIGURATION_ERROR,
474
+ details={"config_key": config_key} if config_key else {},
475
+ recoverable=False,
476
+ )
477
+
478
+
479
+ class InitializationError(RuntimeMemoryError):
480
+ """Raised when component initialization fails."""
481
+
482
+ def __init__(self, component: str, message: str):
483
+ super().__init__(
484
+ message=f"Failed to initialize {component}: {message}",
485
+ code=ErrorCode.INITIALIZATION_ERROR,
486
+ details={"component": component},
487
+ recoverable=False,
488
+ )
489
+
490
+
491
+ # =============================================================================
492
+ # Error Formatting Utilities
493
+ # =============================================================================
494
+
495
+
496
+ def format_error(error: Exception) -> str:
497
+ """Format an exception for user-friendly display.
498
+
499
+ Converts technical exceptions into human-readable messages.
500
+
501
+ Args:
502
+ error: The exception to format
503
+
504
+ Returns:
505
+ A user-friendly error message
506
+ """
507
+ if isinstance(error, RuntimeMemoryError):
508
+ return error.user_message()
509
+
510
+ # Map common Python exceptions to friendly messages
511
+ error_type = type(error).__name__
512
+ error_msg = str(error)
513
+
514
+ friendly_messages = {
515
+ "FileNotFoundError": f"File not found: {error_msg}",
516
+ "PermissionError": f"Permission denied: {error_msg}",
517
+ "TimeoutError": "Operation timed out. Please try again.",
518
+ "json.JSONDecodeError": "Invalid JSON data received.",
519
+ "sqlite3.OperationalError": f"Database error: {error_msg}",
520
+ "sqlite3.IntegrityError": "Database constraint violation.",
521
+ "aiohttp.ClientError": "Network request failed. Check your connection.",
522
+ "httpx.HTTPError": "HTTP request failed. Check the server status.",
523
+ }
524
+
525
+ return friendly_messages.get(error_type, f"An error occurred: {error_msg}")
526
+
527
+
528
+ def is_recoverable(error: Exception) -> bool:
529
+ """Check if an error is recoverable (can be retried).
530
+
531
+ Args:
532
+ error: The exception to check
533
+
534
+ Returns:
535
+ True if the operation can be retried
536
+ """
537
+ if isinstance(error, RuntimeMemoryError):
538
+ return error.recoverable
539
+
540
+ # Common recoverable Python exceptions
541
+ recoverable_types = (
542
+ TimeoutError,
543
+ ConnectionRefusedError,
544
+ ConnectionResetError,
545
+ BrokenPipeError,
546
+ )
547
+ return isinstance(error, recoverable_types)
@@ -0,0 +1,39 @@
1
+ """Compatibility shim for the pre-3.0 ``MEMORY_LAYER_`` environment prefix.
2
+
3
+ Every setting moved to ``RUNTIME_MEMORY_`` when the package was renamed. Rather
4
+ than make each reader check two names, this copies any legacy variable onto its
5
+ new name once, at import, leaving an explicit setting untouched. Anything
6
+ already configured with the old prefix, such as an agent config file written
7
+ before the rename, keeps working.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+
14
+ LEGACY_PREFIX = "MEMORY_LAYER_"
15
+ PREFIX = "RUNTIME_MEMORY_"
16
+
17
+
18
+ def apply_legacy_env(environ: dict[str, str] | None = None) -> list[str]:
19
+ """Copy ``MEMORY_LAYER_*`` variables onto their ``RUNTIME_MEMORY_*`` names.
20
+
21
+ A variable already set under the new prefix wins, so an explicit setting is
22
+ never overwritten by a stale one.
23
+
24
+ Args:
25
+ environ: Mapping to update. Defaults to ``os.environ``.
26
+
27
+ Returns:
28
+ The legacy names that were carried over, for callers that want to warn.
29
+ """
30
+ env = os.environ if environ is None else environ
31
+ carried = []
32
+
33
+ for name in [k for k in env if k.startswith(LEGACY_PREFIX)]:
34
+ renamed = PREFIX + name[len(LEGACY_PREFIX) :]
35
+ if renamed not in env:
36
+ env[renamed] = env[name]
37
+ carried.append(name)
38
+
39
+ return carried