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,1007 @@
1
+ """Memory Engine orchestrator for Runtime Memory.
2
+
3
+ The MemoryEngine is the main orchestrator that coordinates:
4
+ - Storage layer for persistence
5
+ - Embedding provider for vector embeddings
6
+ - Hybrid retriever for intelligent search
7
+ - Outcome tracking for learning from feedback
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import builtins
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from runtime_memory.core.embeddings import (
18
+ EmbeddingConfig,
19
+ EmbeddingProvider,
20
+ MockEmbeddingProvider,
21
+ get_embedding_provider,
22
+ )
23
+ from runtime_memory.core.logging import get_logger
24
+ from runtime_memory.core.models import (
25
+ ContextResponse,
26
+ Memory,
27
+ MemoryCategory,
28
+ MemoryCreate,
29
+ MemoryScope,
30
+ MemorySource,
31
+ MemoryUpdate,
32
+ Outcome,
33
+ SearchResult,
34
+ )
35
+ from runtime_memory.core.paths import default_db_path
36
+ from runtime_memory.core.retrieval import HybridRetriever, RetrievalConfig
37
+ from runtime_memory.core.storage import (
38
+ MemoryNotFoundError,
39
+ MemoryStorage,
40
+ StorageStats,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from collections.abc import Sequence
45
+
46
+ # Type alias to avoid conflict with list() method
47
+ _List = builtins.list
48
+
49
+ logger = get_logger(__name__)
50
+
51
+
52
+ @dataclass
53
+ class EngineConfig:
54
+ """Configuration for the Memory Engine."""
55
+
56
+ # Storage settings
57
+ db_path: str | Path = field(default_factory=lambda: str(default_db_path()))
58
+ """Path to SQLite database file."""
59
+
60
+ pool_size: int = 5
61
+ """Number of database connections in the pool."""
62
+
63
+ secure_permissions: bool = True
64
+ """Whether to set secure file permissions (0600) on database."""
65
+
66
+ # Embedding settings
67
+ embedding_provider: str = "local"
68
+ """Embedding provider type: 'local', 'openai', 'voyage', 'mock', 'null'."""
69
+
70
+ embedding_config: EmbeddingConfig | None = None
71
+ """Optional embedding provider configuration."""
72
+
73
+ # Retrieval settings
74
+ retrieval_config: RetrievalConfig | None = None
75
+ """Optional retrieval configuration."""
76
+
77
+ # Auto-archival settings
78
+ auto_archive_enabled: bool = True
79
+ """Whether to automatically archive low-score memories."""
80
+
81
+ auto_archive_threshold: float = -0.5
82
+ """Outcome score threshold below which memories are archived."""
83
+
84
+ auto_archive_on_search: bool = False
85
+ """Whether to run auto-archival after searches (can impact performance)."""
86
+
87
+ # Last search tracking
88
+ track_last_search: bool = True
89
+ """Whether to track the last search for easy outcome recording."""
90
+
91
+ def __post_init__(self) -> None:
92
+ """Expand paths and set defaults."""
93
+ if isinstance(self.db_path, str):
94
+ self.db_path = Path(self.db_path).expanduser()
95
+
96
+
97
+ @dataclass
98
+ class EngineStats:
99
+ """Statistics from the Memory Engine."""
100
+
101
+ storage_stats: StorageStats
102
+ """Underlying storage statistics."""
103
+
104
+ indexed_memories: int
105
+ """Number of memories indexed in retriever."""
106
+
107
+ indexed_with_embeddings: int
108
+ """Number of memories with embeddings in retriever."""
109
+
110
+ last_search_result_count: int
111
+ """Number of results from last search (if tracked)."""
112
+
113
+
114
+ @dataclass
115
+ class LastSearchInfo:
116
+ """Information about the last search performed."""
117
+
118
+ query: str
119
+ """The search query."""
120
+
121
+ results: _List[SearchResult]
122
+ """Search results."""
123
+
124
+ memory_ids: _List[str] = field(default_factory=list)
125
+ """IDs of memories returned in search results."""
126
+
127
+ def __post_init__(self) -> None:
128
+ """Extract memory IDs from results."""
129
+ self.memory_ids = [r.memory.id for r in self.results]
130
+
131
+
132
+ class EngineError(Exception):
133
+ """Base exception for engine errors."""
134
+
135
+ pass
136
+
137
+
138
+ class EngineNotInitializedError(EngineError):
139
+ """Raised when engine is used before initialization."""
140
+
141
+ pass
142
+
143
+
144
+ class MemoryEngine:
145
+ """Main orchestrator for the Runtime Memory.
146
+
147
+ Coordinates storage, embeddings, and retrieval to provide
148
+ a unified interface for memory operations with outcome-based learning.
149
+
150
+ Example:
151
+ ```python
152
+ engine = MemoryEngine()
153
+ await engine.initialize()
154
+
155
+ # Add a memory
156
+ memory = await engine.add(
157
+ content="Use async/await for I/O operations",
158
+ category=MemoryCategory.PATTERN,
159
+ project="my-project",
160
+ )
161
+
162
+ # Search for relevant memories
163
+ results = await engine.search("async patterns")
164
+
165
+ # Record outcome feedback
166
+ await engine.record_outcome([results[0].memory.id], Outcome.WORKED)
167
+
168
+ # Get context for injection
169
+ context = await engine.get_context(project="my-project")
170
+ print(context.to_markdown())
171
+
172
+ await engine.close()
173
+ ```
174
+ """
175
+
176
+ def __init__(
177
+ self,
178
+ config: EngineConfig | None = None,
179
+ storage: MemoryStorage | None = None,
180
+ embedding_provider: EmbeddingProvider | None = None,
181
+ retriever: HybridRetriever | None = None,
182
+ ) -> None:
183
+ """Initialize the Memory Engine.
184
+
185
+ Args:
186
+ config: Engine configuration. Uses defaults if not provided.
187
+ storage: Optional pre-configured storage instance.
188
+ embedding_provider: Optional pre-configured embedding provider.
189
+ retriever: Optional pre-configured retriever.
190
+ """
191
+ self.config = config or EngineConfig()
192
+
193
+ # Components (initialized lazily or provided)
194
+ self._storage = storage
195
+ self._embedding_provider = embedding_provider
196
+ self._retriever = retriever
197
+
198
+ # State
199
+ self._initialized = False
200
+ self._last_search: LastSearchInfo | None = None
201
+
202
+ async def initialize(self) -> None:
203
+ """Initialize the engine and all components.
204
+
205
+ Creates and initializes storage, embedding provider, and retriever.
206
+ Also loads existing memories into the retriever index.
207
+ """
208
+ if self._initialized:
209
+ return
210
+
211
+ logger.info("Initializing Memory Engine...")
212
+
213
+ # Initialize storage
214
+ if self._storage is None:
215
+ self._storage = MemoryStorage(
216
+ db_path=self.config.db_path,
217
+ pool_size=self.config.pool_size,
218
+ secure_permissions=self.config.secure_permissions,
219
+ )
220
+ await self._storage.initialize()
221
+
222
+ # Initialize embedding provider
223
+ if self._embedding_provider is None:
224
+ self._embedding_provider = get_embedding_provider(
225
+ provider_type=self.config.embedding_provider,
226
+ config=self.config.embedding_config,
227
+ )
228
+
229
+ # Initialize retriever
230
+ if self._retriever is None:
231
+ self._retriever = HybridRetriever(
232
+ embedding_provider=self._embedding_provider,
233
+ config=self.config.retrieval_config,
234
+ )
235
+
236
+ # Load existing memories into retriever
237
+ await self._load_memories_to_retriever()
238
+
239
+ self._initialized = True
240
+ logger.info("Memory Engine initialized successfully")
241
+
242
+ async def close(self) -> None:
243
+ """Close the engine and release resources."""
244
+ if self._storage:
245
+ await self._storage.close()
246
+ self._initialized = False
247
+ logger.info("Memory Engine closed")
248
+
249
+ async def __aenter__(self) -> MemoryEngine:
250
+ """Async context manager entry."""
251
+ await self.initialize()
252
+ return self
253
+
254
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
255
+ """Async context manager exit."""
256
+ await self.close()
257
+
258
+ def _ensure_initialized(self) -> None:
259
+ """Ensure the engine is initialized.
260
+
261
+ Raises:
262
+ EngineNotInitializedError: If engine is not initialized.
263
+ """
264
+ if not self._initialized:
265
+ raise EngineNotInitializedError(
266
+ "Engine not initialized. Call initialize() first or use async context manager."
267
+ )
268
+
269
+ @property
270
+ def storage(self) -> MemoryStorage:
271
+ """Get the storage instance."""
272
+ self._ensure_initialized()
273
+ assert self._storage is not None
274
+ return self._storage
275
+
276
+ @property
277
+ def embedding_provider(self) -> EmbeddingProvider:
278
+ """Get the embedding provider instance."""
279
+ self._ensure_initialized()
280
+ assert self._embedding_provider is not None
281
+ return self._embedding_provider
282
+
283
+ @property
284
+ def retriever(self) -> HybridRetriever:
285
+ """Get the retriever instance."""
286
+ self._ensure_initialized()
287
+ assert self._retriever is not None
288
+ return self._retriever
289
+
290
+ @property
291
+ def last_search(self) -> LastSearchInfo | None:
292
+ """Get information about the last search performed."""
293
+ return self._last_search
294
+
295
+ # =========================================================================
296
+ # Core CRUD Operations
297
+ # =========================================================================
298
+
299
+ async def add(
300
+ self,
301
+ content: str,
302
+ category: MemoryCategory,
303
+ project: str | None = None,
304
+ scope: MemoryScope = MemoryScope.PROJECT,
305
+ source: MemorySource = MemorySource.EXPLICIT,
306
+ confidence: float = 1.0,
307
+ importance: float = 0.5,
308
+ tags: _List[str] | None = None,
309
+ entities: _List[str] | None = None,
310
+ supersedes: str | None = None,
311
+ metadata: dict[str, Any] | None = None,
312
+ ) -> Memory:
313
+ """Add a new memory.
314
+
315
+ Creates a memory, generates embedding, stores in database, and
316
+ indexes in retriever.
317
+
318
+ Args:
319
+ content: The memory content text.
320
+ category: Classification category.
321
+ project: Project scope (None for global).
322
+ scope: Visibility scope.
323
+ source: How the memory was created.
324
+ confidence: Source reliability (0.0-1.0).
325
+ importance: Importance weight (0.0-1.0).
326
+ tags: Optional tags for categorization.
327
+ entities: Detected entities (files, functions, etc.).
328
+ supersedes: ID of memory this one replaces.
329
+ metadata: Additional metadata.
330
+
331
+ Returns:
332
+ The created Memory.
333
+ """
334
+ self._ensure_initialized()
335
+
336
+ # Validate using Pydantic model
337
+ create_model = MemoryCreate(
338
+ content=content,
339
+ category=category,
340
+ project=project,
341
+ scope=scope,
342
+ source=source,
343
+ confidence=confidence,
344
+ importance=importance,
345
+ tags=tags or [],
346
+ entities=entities or [],
347
+ supersedes=supersedes,
348
+ metadata=metadata or {},
349
+ )
350
+
351
+ # Convert to Memory dataclass
352
+ memory = create_model.to_memory()
353
+
354
+ # Generate embedding
355
+ embedding_result = await self._embedding_provider.embed(memory.content)
356
+ memory.embedding = embedding_result.embedding
357
+
358
+ # Store in database
359
+ await self._storage.create(memory)
360
+
361
+ # Add to retriever index
362
+ self._retriever.add_memory(memory, embedding_result.embedding)
363
+
364
+ # Archive superseded memory if specified
365
+ if supersedes:
366
+ try:
367
+ await self._storage.archive(supersedes)
368
+ self._retriever.remove_memory(supersedes)
369
+ except MemoryNotFoundError:
370
+ logger.warning(f"Superseded memory {supersedes} not found")
371
+
372
+ logger.debug(f"Added memory {memory.id}: {content[:50]}...")
373
+ return memory
374
+
375
+ async def add_memory(self, memory: Memory) -> Memory:
376
+ """Add an existing Memory object.
377
+
378
+ Useful when you have a pre-constructed Memory object.
379
+
380
+ Args:
381
+ memory: The Memory to add.
382
+
383
+ Returns:
384
+ The stored Memory (with embedding if not present).
385
+ """
386
+ self._ensure_initialized()
387
+
388
+ # Generate embedding if not present
389
+ if memory.embedding is None:
390
+ embedding_result = await self._embedding_provider.embed(memory.content)
391
+ memory.embedding = embedding_result.embedding
392
+
393
+ # Store in database
394
+ await self._storage.create(memory)
395
+
396
+ # Add to retriever index
397
+ self._retriever.add_memory(memory, memory.embedding)
398
+
399
+ logger.debug(f"Added memory {memory.id}")
400
+ return memory
401
+
402
+ async def add_many(self, memories: Sequence[Memory]) -> _List[Memory]:
403
+ """Add multiple memories in a batch.
404
+
405
+ Args:
406
+ memories: Memories to add.
407
+
408
+ Returns:
409
+ List of stored memories.
410
+ """
411
+ self._ensure_initialized()
412
+
413
+ # Generate embeddings for memories without them
414
+ texts_to_embed = []
415
+ indices_to_embed = []
416
+ for i, memory in enumerate(memories):
417
+ if memory.embedding is None:
418
+ texts_to_embed.append(memory.content)
419
+ indices_to_embed.append(i)
420
+
421
+ if texts_to_embed:
422
+ batch_result = await self._embedding_provider.embed_many(texts_to_embed)
423
+ for i, embedding in zip(indices_to_embed, batch_result.embeddings, strict=True):
424
+ memories[i].embedding = embedding
425
+
426
+ # Store all in database
427
+ memories_list = list(memories)
428
+ await self._storage.create_many(memories_list)
429
+
430
+ # Add all to retriever
431
+ for memory in memories_list:
432
+ self._retriever.add_memory(memory, memory.embedding)
433
+
434
+ logger.debug(f"Added {len(memories_list)} memories")
435
+ return memories_list
436
+
437
+ async def get(self, memory_id: str) -> Memory:
438
+ """Get a memory by ID.
439
+
440
+ Args:
441
+ memory_id: The memory ID.
442
+
443
+ Returns:
444
+ The Memory.
445
+
446
+ Raises:
447
+ MemoryNotFoundError: If memory not found.
448
+ """
449
+ self._ensure_initialized()
450
+ return await self._storage.get(memory_id)
451
+
452
+ async def get_many(self, memory_ids: _List[str]) -> _List[Memory]:
453
+ """Get multiple memories by ID.
454
+
455
+ Args:
456
+ memory_ids: List of memory IDs.
457
+
458
+ Returns:
459
+ List of memories (preserves order, excludes missing).
460
+ """
461
+ self._ensure_initialized()
462
+ return await self._storage.get_many(memory_ids)
463
+
464
+ async def update(
465
+ self,
466
+ memory_id: str,
467
+ content: str | None = None,
468
+ category: MemoryCategory | None = None,
469
+ confidence: float | None = None,
470
+ importance: float | None = None,
471
+ tags: _List[str] | None = None,
472
+ entities: _List[str] | None = None,
473
+ metadata: dict[str, Any] | None = None,
474
+ ) -> Memory:
475
+ """Update an existing memory.
476
+
477
+ Only provided fields are updated. If content is changed, a new
478
+ embedding is generated.
479
+
480
+ Args:
481
+ memory_id: ID of memory to update.
482
+ content: New content (optional).
483
+ category: New category (optional).
484
+ confidence: New confidence (optional).
485
+ importance: New importance (optional).
486
+ tags: New tags (optional).
487
+ entities: New entities (optional).
488
+ metadata: New metadata (optional).
489
+
490
+ Returns:
491
+ The updated Memory.
492
+
493
+ Raises:
494
+ MemoryNotFoundError: If memory not found.
495
+ """
496
+ self._ensure_initialized()
497
+
498
+ # Get existing memory
499
+ memory = await self._storage.get(memory_id)
500
+
501
+ # Apply updates using Pydantic model
502
+ update_model = MemoryUpdate(
503
+ content=content,
504
+ category=category,
505
+ confidence=confidence,
506
+ importance=importance,
507
+ tags=tags,
508
+ entities=entities,
509
+ metadata=metadata,
510
+ )
511
+ memory = update_model.apply_to(memory)
512
+
513
+ # Re-generate embedding if content changed
514
+ new_embedding = None
515
+ if content is not None:
516
+ embedding_result = await self._embedding_provider.embed(memory.content)
517
+ memory.embedding = embedding_result.embedding
518
+ new_embedding = embedding_result.embedding
519
+
520
+ # Update in database
521
+ await self._storage.update(memory)
522
+
523
+ # Update in retriever
524
+ self._retriever.update_memory(memory, new_embedding)
525
+
526
+ logger.debug(f"Updated memory {memory_id}")
527
+ return memory
528
+
529
+ async def delete(
530
+ self,
531
+ memory_id: str,
532
+ hard_delete: bool = False,
533
+ ) -> None:
534
+ """Delete a memory.
535
+
536
+ By default, performs a soft delete (archive). Use hard_delete=True
537
+ to permanently remove the memory.
538
+
539
+ Args:
540
+ memory_id: ID of memory to delete.
541
+ hard_delete: If True, permanently delete. If False, archive.
542
+
543
+ Raises:
544
+ MemoryNotFoundError: If memory not found.
545
+ """
546
+ self._ensure_initialized()
547
+
548
+ await self._storage.delete(memory_id, hard_delete=hard_delete)
549
+ self._retriever.remove_memory(memory_id)
550
+
551
+ action = "deleted" if hard_delete else "archived"
552
+ logger.debug(f"Memory {memory_id} {action}")
553
+
554
+ async def archive(self, memory_id: str) -> Memory:
555
+ """Archive a memory (soft delete).
556
+
557
+ Args:
558
+ memory_id: ID of memory to archive.
559
+
560
+ Returns:
561
+ The archived Memory.
562
+ """
563
+ self._ensure_initialized()
564
+
565
+ memory = await self._storage.archive(memory_id)
566
+ self._retriever.remove_memory(memory_id)
567
+
568
+ logger.debug(f"Archived memory {memory_id}")
569
+ return memory
570
+
571
+ async def unarchive(self, memory_id: str) -> Memory:
572
+ """Unarchive a previously archived memory.
573
+
574
+ Args:
575
+ memory_id: ID of memory to unarchive.
576
+
577
+ Returns:
578
+ The unarchived Memory.
579
+ """
580
+ self._ensure_initialized()
581
+
582
+ memory = await self._storage.unarchive(memory_id)
583
+
584
+ # Re-add to retriever
585
+ self._retriever.add_memory(memory, memory.embedding)
586
+
587
+ logger.debug(f"Unarchived memory {memory_id}")
588
+ return memory
589
+
590
+ # =========================================================================
591
+ # Search and Retrieval
592
+ # =========================================================================
593
+
594
+ async def search(
595
+ self,
596
+ query: str,
597
+ limit: int = 10,
598
+ category: MemoryCategory | None = None,
599
+ project: str | None = None,
600
+ include_archived: bool = False,
601
+ min_score: float = 0.0,
602
+ track_usage: bool = True,
603
+ ) -> _List[SearchResult]:
604
+ """Search for relevant memories.
605
+
606
+ Uses hybrid retrieval combining BM25 text search and vector
607
+ similarity for intelligent ranking.
608
+
609
+ Args:
610
+ query: Search query text.
611
+ limit: Maximum number of results.
612
+ category: Filter by category.
613
+ project: Filter by project.
614
+ include_archived: Whether to include archived memories.
615
+ min_score: Minimum relevance score threshold.
616
+ track_usage: Whether to increment use counts for returned memories.
617
+
618
+ Returns:
619
+ List of SearchResults sorted by relevance.
620
+ """
621
+ self._ensure_initialized()
622
+
623
+ # Execute search
624
+ results = await self._retriever.search(
625
+ query=query,
626
+ limit=limit,
627
+ category=category,
628
+ project=project,
629
+ include_archived=include_archived,
630
+ min_score=min_score,
631
+ )
632
+
633
+ # Track last search if enabled
634
+ if self.config.track_last_search:
635
+ self._last_search = LastSearchInfo(query=query, results=results)
636
+
637
+ # Increment use counts for returned memories
638
+ if track_usage and results:
639
+ memory_ids = [r.memory.id for r in results]
640
+ await self._storage.increment_use_counts(memory_ids)
641
+
642
+ # Run auto-archival if enabled
643
+ if self.config.auto_archive_enabled and self.config.auto_archive_on_search:
644
+ await self.auto_archive(project=project)
645
+
646
+ logger.debug(f"Search for '{query}' returned {len(results)} results")
647
+ return results
648
+
649
+ async def list(
650
+ self,
651
+ project: str | None = None,
652
+ category: MemoryCategory | None = None,
653
+ scope: MemoryScope | None = None,
654
+ source: MemorySource | None = None,
655
+ include_archived: bool = False,
656
+ min_score: float | None = None,
657
+ max_score: float | None = None,
658
+ limit: int = 100,
659
+ offset: int = 0,
660
+ order_by: str = "created_at",
661
+ descending: bool = True,
662
+ ) -> _List[Memory]:
663
+ """List memories with filtering.
664
+
665
+ Args:
666
+ project: Filter by project.
667
+ category: Filter by category.
668
+ scope: Filter by scope.
669
+ source: Filter by source.
670
+ include_archived: Whether to include archived memories.
671
+ min_score: Minimum outcome score filter.
672
+ max_score: Maximum outcome score filter.
673
+ limit: Maximum results to return.
674
+ offset: Results offset for pagination.
675
+ order_by: Column to order by.
676
+ descending: Whether to sort descending.
677
+
678
+ Returns:
679
+ List of memories matching filters.
680
+ """
681
+ self._ensure_initialized()
682
+
683
+ return await self._storage.list(
684
+ project=project,
685
+ category=category,
686
+ scope=scope,
687
+ source=source,
688
+ include_archived=include_archived,
689
+ min_score=min_score,
690
+ max_score=max_score,
691
+ limit=limit,
692
+ offset=offset,
693
+ order_by=order_by,
694
+ descending=descending,
695
+ )
696
+
697
+ # =========================================================================
698
+ # Outcome Tracking and Learning
699
+ # =========================================================================
700
+
701
+ async def record_outcome(
702
+ self,
703
+ memory_ids: _List[str] | str,
704
+ outcome: Outcome,
705
+ ) -> _List[Memory]:
706
+ """Record outcome feedback for memories.
707
+
708
+ Updates outcome scores for specified memories based on feedback.
709
+ This enables the system to learn from experience.
710
+
711
+ Args:
712
+ memory_ids: Memory ID(s) to update. Can be a single ID or list.
713
+ outcome: The outcome to record (WORKED, FAILED, PARTIAL).
714
+
715
+ Returns:
716
+ List of updated memories.
717
+ """
718
+ self._ensure_initialized()
719
+
720
+ # Normalize to list
721
+ if isinstance(memory_ids, str):
722
+ memory_ids = [memory_ids]
723
+
724
+ memories = await self._storage.record_outcomes(memory_ids, outcome)
725
+
726
+ # Update memories in retriever
727
+ for memory in memories:
728
+ self._retriever.update_memory(memory, memory.embedding)
729
+
730
+ logger.debug(f"Recorded {outcome.value} for {len(memories)} memories")
731
+ return memories
732
+
733
+ async def record_outcome_for_last_search(self, outcome: Outcome) -> _List[Memory]:
734
+ """Record outcome for the memories from the last search.
735
+
736
+ Convenience method for recording feedback on the most recent
737
+ search results.
738
+
739
+ Args:
740
+ outcome: The outcome to record.
741
+
742
+ Returns:
743
+ List of updated memories.
744
+
745
+ Raises:
746
+ EngineError: If no last search is tracked.
747
+ """
748
+ self._ensure_initialized()
749
+
750
+ if not self._last_search or not self._last_search.memory_ids:
751
+ raise EngineError("No last search results to record outcome for")
752
+
753
+ return await self.record_outcome(self._last_search.memory_ids, outcome)
754
+
755
+ async def auto_archive(
756
+ self,
757
+ project: str | None = None,
758
+ threshold: float | None = None,
759
+ ) -> int:
760
+ """Archive memories with low outcome scores.
761
+
762
+ Automatically archives memories that have accumulated
763
+ negative feedback below the threshold.
764
+
765
+ Args:
766
+ project: Optional project filter.
767
+ threshold: Score threshold (default from config).
768
+
769
+ Returns:
770
+ Number of memories archived.
771
+ """
772
+ self._ensure_initialized()
773
+
774
+ threshold = threshold or self.config.auto_archive_threshold
775
+ count = await self._storage.archive_low_score_memories(
776
+ threshold=threshold,
777
+ project=project,
778
+ )
779
+
780
+ if count > 0:
781
+ # Refresh retriever index to remove archived memories
782
+ await self._refresh_retriever(project=project)
783
+ logger.info(f"Auto-archived {count} memories below score {threshold}")
784
+
785
+ return count
786
+
787
+ # =========================================================================
788
+ # Context Generation
789
+ # =========================================================================
790
+
791
+ async def get_context(
792
+ self,
793
+ query: str | None = None,
794
+ project: str | None = None,
795
+ max_memories: int = 10,
796
+ category_distribution: dict[MemoryCategory, int] | None = None,
797
+ ) -> ContextResponse:
798
+ """Get formatted context for injection into prompts.
799
+
800
+ Retrieves relevant memories and formats them for use as context
801
+ in AI agent prompts.
802
+
803
+ Args:
804
+ query: Optional query to find relevant memories.
805
+ project: Project filter.
806
+ max_memories: Maximum memories to include.
807
+ category_distribution: Optional category -> count mapping.
808
+
809
+ Returns:
810
+ ContextResponse with formatted context.
811
+ """
812
+ self._ensure_initialized()
813
+
814
+ # Get relevant memories
815
+ if query:
816
+ results = await self._retriever.get_context_memories(
817
+ query=query,
818
+ project=project,
819
+ max_memories=max_memories,
820
+ category_distribution=category_distribution,
821
+ )
822
+ memories = [r.memory for r in results]
823
+ else:
824
+ # Get most recent/important memories without query
825
+ memories = await self._storage.list(
826
+ project=project,
827
+ include_archived=False,
828
+ limit=max_memories,
829
+ order_by="outcome_score",
830
+ descending=True,
831
+ )
832
+
833
+ # Count by category
834
+ categories: dict[str, int] = {}
835
+ for memory in memories:
836
+ cat_name = memory.category.value
837
+ categories[cat_name] = categories.get(cat_name, 0) + 1
838
+
839
+ # Get total count
840
+ total_count = await self._storage.count(project=project)
841
+
842
+ # Build response
843
+ response = ContextResponse(
844
+ memories=memories,
845
+ project=project,
846
+ total_count=total_count,
847
+ included_count=len(memories),
848
+ categories=categories,
849
+ )
850
+
851
+ # Generate formatted string
852
+ response.formatted = response.to_markdown()
853
+
854
+ return response
855
+
856
+ # =========================================================================
857
+ # Statistics and Health
858
+ # =========================================================================
859
+
860
+ async def stats(self, project: str | None = None) -> EngineStats:
861
+ """Get engine statistics.
862
+
863
+ Args:
864
+ project: Optional project filter.
865
+
866
+ Returns:
867
+ EngineStats with various metrics.
868
+ """
869
+ self._ensure_initialized()
870
+
871
+ storage_stats = await self._storage.get_stats(project=project)
872
+
873
+ return EngineStats(
874
+ storage_stats=storage_stats,
875
+ indexed_memories=self._retriever.memory_count,
876
+ indexed_with_embeddings=self._retriever.indexed_with_embeddings,
877
+ last_search_result_count=len(self._last_search.results) if self._last_search else 0,
878
+ )
879
+
880
+ async def health_check(self) -> dict[str, Any]:
881
+ """Check engine health.
882
+
883
+ Returns:
884
+ Health status dictionary.
885
+ """
886
+ status: dict[str, Any] = {
887
+ "initialized": self._initialized,
888
+ "storage": None,
889
+ "embedding_provider": None,
890
+ "retriever": None,
891
+ }
892
+
893
+ if not self._initialized:
894
+ status["status"] = "not_initialized"
895
+ return status
896
+
897
+ # Check storage health
898
+ status["storage"] = await self._storage.health_check()
899
+
900
+ # Check embedding provider
901
+ if not self._embedding_provider.available:
902
+ # Not an error: retrieval still works on keyword matching alone.
903
+ status["embedding_provider"] = {
904
+ "status": "unavailable",
905
+ "model": self._embedding_provider.model_name,
906
+ "dimensions": 0,
907
+ "detail": (
908
+ "No embedding backend installed, so semantic search is off "
909
+ "and retrieval uses keyword matching only. Enable it with: "
910
+ "pip install 'runtime-memory[embedding]'"
911
+ ),
912
+ }
913
+ else:
914
+ try:
915
+ # Quick embedding test
916
+ test_result = await self._embedding_provider.embed("test")
917
+ status["embedding_provider"] = {
918
+ "status": "healthy",
919
+ "model": self._embedding_provider.model_name,
920
+ "dimensions": test_result.dimensions,
921
+ }
922
+ except Exception as e:
923
+ status["embedding_provider"] = {
924
+ "status": "unhealthy",
925
+ "error": str(e),
926
+ }
927
+
928
+ # Check retriever
929
+ status["retriever"] = {
930
+ "status": "healthy",
931
+ "indexed_memories": self._retriever.memory_count,
932
+ "with_embeddings": self._retriever.indexed_with_embeddings,
933
+ }
934
+
935
+ # Overall status
936
+ storage_healthy = status["storage"].get("status") == "healthy"
937
+ embedding_healthy = status["embedding_provider"].get("status") == "healthy"
938
+ status["status"] = "healthy" if (storage_healthy and embedding_healthy) else "degraded"
939
+
940
+ return status
941
+
942
+ # =========================================================================
943
+ # Internal Helpers
944
+ # =========================================================================
945
+
946
+ async def _load_memories_to_retriever(self) -> None:
947
+ """Load existing memories from storage into the retriever."""
948
+ # Load all non-archived memories
949
+ memories = await self._storage.list(
950
+ include_archived=False,
951
+ limit=10000, # Load up to 10K memories
952
+ )
953
+
954
+ for memory in memories:
955
+ self._retriever.add_memory(memory, memory.embedding)
956
+
957
+ logger.info(f"Loaded {len(memories)} memories into retriever")
958
+
959
+ async def _refresh_retriever(self, project: str | None = None) -> None:
960
+ """Refresh the retriever index from storage.
961
+
962
+ Args:
963
+ project: Optional project filter.
964
+ """
965
+ # Clear and reload
966
+ self._retriever.clear()
967
+ await self._load_memories_to_retriever()
968
+
969
+
970
+ # Factory function for easy creation
971
+ async def create_engine(
972
+ db_path: str | Path | None = None,
973
+ embedding_provider: str = "local",
974
+ **kwargs: Any,
975
+ ) -> MemoryEngine:
976
+ """Create and initialize a Memory Engine.
977
+
978
+ Convenience factory function that creates and initializes an engine
979
+ in one call.
980
+
981
+ Args:
982
+ db_path: Path to database file.
983
+ embedding_provider: Embedding provider type.
984
+ **kwargs: Additional EngineConfig parameters.
985
+
986
+ Returns:
987
+ Initialized MemoryEngine.
988
+
989
+ Example:
990
+ ```python
991
+ engine = await create_engine(
992
+ db_path="~/.my-app/memories.db",
993
+ embedding_provider="local",
994
+ )
995
+ ```
996
+ """
997
+ config_kwargs: dict[str, Any] = {
998
+ "embedding_provider": embedding_provider,
999
+ **kwargs,
1000
+ }
1001
+ if db_path:
1002
+ config_kwargs["db_path"] = db_path
1003
+
1004
+ config = EngineConfig(**config_kwargs)
1005
+ engine = MemoryEngine(config=config)
1006
+ await engine.initialize()
1007
+ return engine