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,30 @@
1
+ """Where Runtime Memory keeps its files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ STORE_DIR_NAME = ".runtime-memory"
8
+ LEGACY_STORE_DIR_NAME = ".memory-layer"
9
+ """Store directory used before the 3.0 rename."""
10
+
11
+
12
+ def store_dir() -> Path:
13
+ """The directory holding the database, cache and extracted context.
14
+
15
+ Falls back to the pre-3.0 directory when it exists and the current one does
16
+ not, so an install that predates the rename keeps reading the memories it
17
+ already has instead of quietly starting an empty store.
18
+
19
+ Returns:
20
+ Directory path. It is not created here.
21
+ """
22
+ current = Path.home() / STORE_DIR_NAME
23
+ if not current.exists() and (Path.home() / LEGACY_STORE_DIR_NAME).exists():
24
+ return Path.home() / LEGACY_STORE_DIR_NAME
25
+ return current
26
+
27
+
28
+ def default_db_path() -> Path:
29
+ """The database used when nothing names another one."""
30
+ return store_dir() / "memories.db"
@@ -0,0 +1,511 @@
1
+ """
2
+ Resilience utilities for Runtime Memory.
3
+
4
+ Provides:
5
+ - Retry with exponential backoff
6
+ - Circuit breaker pattern
7
+ - Graceful degradation helpers
8
+ """
9
+
10
+ import asyncio
11
+ import functools
12
+ import logging
13
+ import random
14
+ import time
15
+ from collections.abc import Awaitable, Callable
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import Any, ParamSpec, TypeVar
19
+
20
+ from .exceptions import CircuitOpenError, is_recoverable
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ P = ParamSpec("P")
25
+ T = TypeVar("T")
26
+
27
+
28
+ # =============================================================================
29
+ # Retry with Exponential Backoff
30
+ # =============================================================================
31
+
32
+
33
+ @dataclass
34
+ class RetryConfig:
35
+ """Configuration for retry behavior."""
36
+
37
+ max_attempts: int = 3
38
+ base_delay: float = 1.0 # seconds
39
+ max_delay: float = 60.0 # seconds
40
+ exponential_base: float = 2.0
41
+ jitter: bool = True # Add randomness to prevent thundering herd
42
+
43
+
44
+ def calculate_backoff(
45
+ attempt: int,
46
+ config: RetryConfig,
47
+ ) -> float:
48
+ """Calculate delay for a retry attempt.
49
+
50
+ Uses exponential backoff with optional jitter.
51
+
52
+ Args:
53
+ attempt: The current attempt number (0-indexed)
54
+ config: Retry configuration
55
+
56
+ Returns:
57
+ Delay in seconds before the next retry
58
+ """
59
+ delay = config.base_delay * (config.exponential_base**attempt)
60
+ delay = min(delay, config.max_delay)
61
+
62
+ if config.jitter:
63
+ # Add up to 25% jitter
64
+ jitter_range = delay * 0.25
65
+ delay += random.uniform(-jitter_range, jitter_range)
66
+
67
+ return max(0, delay)
68
+
69
+
70
+ def retry(
71
+ max_attempts: int = 3,
72
+ base_delay: float = 1.0,
73
+ max_delay: float = 60.0,
74
+ exceptions: tuple[type[Exception], ...] | None = None,
75
+ on_retry: Callable[[Exception, int], None] | None = None,
76
+ ) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
77
+ """Decorator for retrying async functions with exponential backoff.
78
+
79
+ Args:
80
+ max_attempts: Maximum number of attempts
81
+ base_delay: Initial delay between retries (seconds)
82
+ max_delay: Maximum delay between retries (seconds)
83
+ exceptions: Tuple of exception types to retry on. If None, retries
84
+ on all recoverable exceptions.
85
+ on_retry: Optional callback called before each retry with
86
+ (exception, attempt_number)
87
+
88
+ Example:
89
+ @retry(max_attempts=3, base_delay=1.0)
90
+ async def fetch_data():
91
+ ...
92
+ """
93
+ config = RetryConfig(
94
+ max_attempts=max_attempts,
95
+ base_delay=base_delay,
96
+ max_delay=max_delay,
97
+ )
98
+
99
+ def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
100
+ @functools.wraps(func)
101
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
102
+ last_exception: Exception | None = None
103
+
104
+ for attempt in range(config.max_attempts):
105
+ try:
106
+ return await func(*args, **kwargs)
107
+ except Exception as e:
108
+ last_exception = e
109
+
110
+ # Check if we should retry this exception
111
+ should_retry = False
112
+ if exceptions is not None:
113
+ should_retry = isinstance(e, exceptions)
114
+ else:
115
+ should_retry = is_recoverable(e)
116
+
117
+ if not should_retry or attempt >= config.max_attempts - 1:
118
+ raise
119
+
120
+ # Calculate backoff delay
121
+ delay = calculate_backoff(attempt, config)
122
+
123
+ logger.warning(
124
+ f"Attempt {attempt + 1}/{config.max_attempts} failed "
125
+ f"for {func.__name__}: {e}. Retrying in {delay:.2f}s..."
126
+ )
127
+
128
+ if on_retry:
129
+ on_retry(e, attempt + 1)
130
+
131
+ await asyncio.sleep(delay)
132
+
133
+ # Should never reach here, but just in case
134
+ if last_exception:
135
+ raise last_exception
136
+ raise RuntimeError("Retry logic error")
137
+
138
+ return wrapper
139
+
140
+ return decorator
141
+
142
+
143
+ def retry_sync(
144
+ max_attempts: int = 3,
145
+ base_delay: float = 1.0,
146
+ max_delay: float = 60.0,
147
+ exceptions: tuple[type[Exception], ...] | None = None,
148
+ on_retry: Callable[[Exception, int], None] | None = None,
149
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
150
+ """Decorator for retrying sync functions with exponential backoff.
151
+
152
+ Same as retry() but for synchronous functions.
153
+ """
154
+ config = RetryConfig(
155
+ max_attempts=max_attempts,
156
+ base_delay=base_delay,
157
+ max_delay=max_delay,
158
+ )
159
+
160
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
161
+ @functools.wraps(func)
162
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
163
+ last_exception: Exception | None = None
164
+
165
+ for attempt in range(config.max_attempts):
166
+ try:
167
+ return func(*args, **kwargs)
168
+ except Exception as e:
169
+ last_exception = e
170
+
171
+ should_retry = False
172
+ if exceptions is not None:
173
+ should_retry = isinstance(e, exceptions)
174
+ else:
175
+ should_retry = is_recoverable(e)
176
+
177
+ if not should_retry or attempt >= config.max_attempts - 1:
178
+ raise
179
+
180
+ delay = calculate_backoff(attempt, config)
181
+
182
+ logger.warning(
183
+ f"Attempt {attempt + 1}/{config.max_attempts} failed "
184
+ f"for {func.__name__}: {e}. Retrying in {delay:.2f}s..."
185
+ )
186
+
187
+ if on_retry:
188
+ on_retry(e, attempt + 1)
189
+
190
+ time.sleep(delay)
191
+
192
+ if last_exception:
193
+ raise last_exception
194
+ raise RuntimeError("Retry logic error")
195
+
196
+ return wrapper
197
+
198
+ return decorator
199
+
200
+
201
+ # =============================================================================
202
+ # Circuit Breaker
203
+ # =============================================================================
204
+
205
+
206
+ class CircuitState(Enum):
207
+ """Circuit breaker states."""
208
+
209
+ CLOSED = "closed" # Normal operation
210
+ OPEN = "open" # Failing, rejecting requests
211
+ HALF_OPEN = "half_open" # Testing if service recovered
212
+
213
+
214
+ @dataclass
215
+ class CircuitBreakerConfig:
216
+ """Configuration for circuit breaker."""
217
+
218
+ failure_threshold: int = 5 # Failures before opening
219
+ success_threshold: int = 2 # Successes to close from half-open
220
+ timeout: float = 60.0 # Seconds before trying half-open
221
+ excluded_exceptions: tuple[type[Exception], ...] = () # Don't count these
222
+
223
+
224
+ @dataclass
225
+ class CircuitBreaker:
226
+ """Circuit breaker for protecting external service calls.
227
+
228
+ The circuit breaker prevents cascading failures by:
229
+ 1. Tracking failures and successes
230
+ 2. Opening the circuit (rejecting requests) after too many failures
231
+ 3. Periodically testing if the service has recovered
232
+ 4. Closing the circuit when service is healthy again
233
+
234
+ Example:
235
+ breaker = CircuitBreaker(name="embedding_api")
236
+
237
+ @breaker
238
+ async def call_embedding_api():
239
+ ...
240
+ """
241
+
242
+ name: str
243
+ config: CircuitBreakerConfig = field(default_factory=CircuitBreakerConfig)
244
+ _state: CircuitState = field(default=CircuitState.CLOSED, init=False)
245
+ _failure_count: int = field(default=0, init=False)
246
+ _success_count: int = field(default=0, init=False)
247
+ _last_failure_time: float = field(default=0.0, init=False)
248
+
249
+ @property
250
+ def state(self) -> CircuitState:
251
+ """Get current circuit state, checking for timeout transition."""
252
+ if self._state == CircuitState.OPEN:
253
+ if time.time() - self._last_failure_time >= self.config.timeout:
254
+ self._state = CircuitState.HALF_OPEN
255
+ self._success_count = 0
256
+ logger.info(f"Circuit breaker '{self.name}' entering half-open state")
257
+ return self._state
258
+
259
+ @property
260
+ def is_closed(self) -> bool:
261
+ """Check if circuit is closed (allowing requests)."""
262
+ return self.state == CircuitState.CLOSED
263
+
264
+ def record_success(self) -> None:
265
+ """Record a successful call."""
266
+ if self._state == CircuitState.HALF_OPEN:
267
+ self._success_count += 1
268
+ if self._success_count >= self.config.success_threshold:
269
+ self._state = CircuitState.CLOSED
270
+ self._failure_count = 0
271
+ logger.info(f"Circuit breaker '{self.name}' closed (service recovered)")
272
+ elif self._state == CircuitState.CLOSED:
273
+ # Reset failure count on success
274
+ self._failure_count = 0
275
+
276
+ def record_failure(self, exception: Exception) -> None:
277
+ """Record a failed call."""
278
+ # Don't count excluded exceptions
279
+ if isinstance(exception, self.config.excluded_exceptions):
280
+ return
281
+
282
+ self._failure_count += 1
283
+ self._last_failure_time = time.time()
284
+
285
+ if self._state == CircuitState.HALF_OPEN:
286
+ # Any failure in half-open immediately opens
287
+ self._state = CircuitState.OPEN
288
+ logger.warning(
289
+ f"Circuit breaker '{self.name}' opened (failure in half-open state)"
290
+ )
291
+ elif self._state == CircuitState.CLOSED:
292
+ if self._failure_count >= self.config.failure_threshold:
293
+ self._state = CircuitState.OPEN
294
+ logger.warning(
295
+ f"Circuit breaker '{self.name}' opened "
296
+ f"(reached {self._failure_count} failures)"
297
+ )
298
+
299
+ def __call__(
300
+ self, func: Callable[P, Awaitable[T]]
301
+ ) -> Callable[P, Awaitable[T]]:
302
+ """Decorator to wrap an async function with circuit breaker."""
303
+
304
+ @functools.wraps(func)
305
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
306
+ if self.state == CircuitState.OPEN:
307
+ raise CircuitOpenError(self.name)
308
+
309
+ try:
310
+ result = await func(*args, **kwargs)
311
+ self.record_success()
312
+ return result
313
+ except Exception as e:
314
+ self.record_failure(e)
315
+ raise
316
+
317
+ return wrapper
318
+
319
+ def call_sync(self, func: Callable[P, T]) -> Callable[P, T]:
320
+ """Decorator to wrap a sync function with circuit breaker."""
321
+
322
+ @functools.wraps(func)
323
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
324
+ if self.state == CircuitState.OPEN:
325
+ raise CircuitOpenError(self.name)
326
+
327
+ try:
328
+ result = func(*args, **kwargs)
329
+ self.record_success()
330
+ return result
331
+ except Exception as e:
332
+ self.record_failure(e)
333
+ raise
334
+
335
+ return wrapper
336
+
337
+ def reset(self) -> None:
338
+ """Manually reset the circuit breaker to closed state."""
339
+ self._state = CircuitState.CLOSED
340
+ self._failure_count = 0
341
+ self._success_count = 0
342
+ logger.info(f"Circuit breaker '{self.name}' manually reset")
343
+
344
+
345
+ # =============================================================================
346
+ # Graceful Degradation
347
+ # =============================================================================
348
+
349
+
350
+ def with_fallback(
351
+ fallback_value: T,
352
+ exceptions: tuple[type[Exception], ...] = (Exception,),
353
+ log_level: int = logging.WARNING,
354
+ ) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
355
+ """Decorator that returns a fallback value on failure.
356
+
357
+ Useful for graceful degradation where partial functionality
358
+ is better than total failure.
359
+
360
+ Args:
361
+ fallback_value: Value to return on failure
362
+ exceptions: Exception types to catch
363
+ log_level: Logging level for failures
364
+
365
+ Example:
366
+ @with_fallback(fallback_value=[])
367
+ async def get_optional_data():
368
+ ...
369
+ """
370
+
371
+ def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
372
+ @functools.wraps(func)
373
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
374
+ try:
375
+ return await func(*args, **kwargs)
376
+ except exceptions as e:
377
+ logger.log(
378
+ log_level,
379
+ f"Function {func.__name__} failed, using fallback: {e}",
380
+ )
381
+ return fallback_value
382
+
383
+ return wrapper
384
+
385
+ return decorator
386
+
387
+
388
+ def with_fallback_sync(
389
+ fallback_value: T,
390
+ exceptions: tuple[type[Exception], ...] = (Exception,),
391
+ log_level: int = logging.WARNING,
392
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
393
+ """Synchronous version of with_fallback."""
394
+
395
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
396
+ @functools.wraps(func)
397
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
398
+ try:
399
+ return func(*args, **kwargs)
400
+ except exceptions as e:
401
+ logger.log(
402
+ log_level,
403
+ f"Function {func.__name__} failed, using fallback: {e}",
404
+ )
405
+ return fallback_value
406
+
407
+ return wrapper
408
+
409
+ return decorator
410
+
411
+
412
+ async def try_multiple(
413
+ *operations: tuple[Callable[[], Awaitable[T]], str],
414
+ default: T | None = None,
415
+ ) -> T | None:
416
+ """Try multiple operations in order, returning first success.
417
+
418
+ Useful for fallback chains where you want to try multiple
419
+ approaches to get data.
420
+
421
+ Args:
422
+ operations: Tuples of (async_callable, description)
423
+ default: Value to return if all operations fail
424
+
425
+ Example:
426
+ result = await try_multiple(
427
+ (fetch_from_cache, "cache"),
428
+ (fetch_from_api, "api"),
429
+ (fetch_from_backup, "backup"),
430
+ default=None,
431
+ )
432
+ """
433
+ for operation, description in operations:
434
+ try:
435
+ result = await operation()
436
+ logger.debug(f"Operation '{description}' succeeded")
437
+ return result
438
+ except Exception as e:
439
+ logger.debug(f"Operation '{description}' failed: {e}")
440
+ continue
441
+
442
+ logger.warning("All operations failed, returning default")
443
+ return default
444
+
445
+
446
+ # =============================================================================
447
+ # Timeout Utilities
448
+ # =============================================================================
449
+
450
+
451
+ async def with_timeout(
452
+ coro: Awaitable[T],
453
+ timeout: float,
454
+ timeout_message: str | None = None,
455
+ ) -> T:
456
+ """Execute a coroutine with a timeout.
457
+
458
+ Args:
459
+ coro: The coroutine to execute
460
+ timeout: Timeout in seconds
461
+ timeout_message: Custom message for timeout error
462
+
463
+ Returns:
464
+ The result of the coroutine
465
+
466
+ Raises:
467
+ asyncio.TimeoutError: If the operation times out
468
+ """
469
+ try:
470
+ return await asyncio.wait_for(coro, timeout=timeout)
471
+ except asyncio.TimeoutError:
472
+ msg = timeout_message or f"Operation timed out after {timeout}s"
473
+ raise asyncio.TimeoutError(msg) from None
474
+
475
+
476
+ # =============================================================================
477
+ # Shared Circuit Breakers (Singletons)
478
+ # =============================================================================
479
+
480
+ # Pre-configured circuit breakers for common external services
481
+ _circuit_breakers: dict[str, CircuitBreaker] = {}
482
+
483
+
484
+ def get_circuit_breaker(
485
+ name: str,
486
+ config: CircuitBreakerConfig | None = None,
487
+ ) -> CircuitBreaker:
488
+ """Get or create a named circuit breaker.
489
+
490
+ Circuit breakers are cached by name, so the same breaker
491
+ is returned for the same name across the application.
492
+
493
+ Args:
494
+ name: Unique name for the circuit breaker
495
+ config: Configuration (only used on first creation)
496
+
497
+ Returns:
498
+ The circuit breaker instance
499
+ """
500
+ if name not in _circuit_breakers:
501
+ _circuit_breakers[name] = CircuitBreaker(
502
+ name=name,
503
+ config=config or CircuitBreakerConfig(),
504
+ )
505
+ return _circuit_breakers[name]
506
+
507
+
508
+ def reset_all_circuit_breakers() -> None:
509
+ """Reset all circuit breakers to closed state."""
510
+ for breaker in _circuit_breakers.values():
511
+ breaker.reset()