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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- 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
|