alma-memory 0.5.1__py3-none-any.whl → 0.7.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 (111) hide show
  1. alma/__init__.py +296 -226
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -430
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -265
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -106
  26. alma/graph/backends/__init__.py +32 -32
  27. alma/graph/backends/kuzu.py +624 -624
  28. alma/graph/backends/memgraph.py +432 -432
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -444
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -509
  55. alma/observability/__init__.py +91 -84
  56. alma/observability/config.py +302 -302
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -424
  59. alma/observability/metrics.py +583 -583
  60. alma/observability/tracing.py +440 -440
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -427
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -90
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1259
  80. alma/storage/base.py +1083 -583
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -103
  83. alma/storage/file_based.py +614 -614
  84. alma/storage/migrations/__init__.py +21 -21
  85. alma/storage/migrations/base.py +321 -321
  86. alma/storage/migrations/runner.py +323 -323
  87. alma/storage/migrations/version_stores.py +337 -337
  88. alma/storage/migrations/versions/__init__.py +11 -11
  89. alma/storage/migrations/versions/v1_0_0.py +373 -373
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1559
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1457
  95. alma/testing/__init__.py +46 -46
  96. alma/testing/factories.py +301 -301
  97. alma/testing/mocks.py +389 -389
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.1.dist-info/RECORD +0 -93
  110. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,560 @@
1
+ """
2
+ ALMA Progressive Disclosure.
3
+
4
+ Implements summary → detail retrieval pattern to optimize context usage.
5
+ Based on context engineering principle: "Must know exists" before "must see."
6
+
7
+ Features:
8
+ - Summary extraction for memory items
9
+ - Lazy-loading of full details on demand
10
+ - Reference IDs for fetch-on-demand pattern
11
+ - Tiered disclosure (summary → key details → full content)
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from alma.types import (
20
+ AntiPattern,
21
+ DomainKnowledge,
22
+ Heuristic,
23
+ Outcome,
24
+ UserPreference,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class DisclosureLevel(Enum):
31
+ """Levels of detail for progressive disclosure."""
32
+
33
+ REFERENCE = 1 # Just ID and type - agent knows it exists
34
+ SUMMARY = 2 # Brief summary - enough to decide if needed
35
+ KEY_DETAILS = 3 # Important fields only
36
+ FULL = 4 # Complete memory with all details
37
+
38
+
39
+ @dataclass
40
+ class MemorySummary:
41
+ """Compact summary of a memory item."""
42
+
43
+ id: str
44
+ memory_type: str
45
+ summary: str
46
+ relevance_hint: str # Why this might be relevant
47
+ estimated_tokens: int
48
+ disclosure_level: DisclosureLevel = DisclosureLevel.SUMMARY
49
+
50
+ # Optional key details (for KEY_DETAILS level)
51
+ key_fields: Dict[str, Any] = field(default_factory=dict)
52
+
53
+ # Reference to full item (for lazy loading)
54
+ _full_item: Optional[Any] = field(default=None, repr=False)
55
+
56
+ def get_full(self) -> Optional[Any]:
57
+ """Get full item if available."""
58
+ return self._full_item
59
+
60
+
61
+ @dataclass
62
+ class ProgressiveSlice:
63
+ """
64
+ Memory slice with progressive disclosure support.
65
+
66
+ Contains summaries by default, with ability to fetch full details
67
+ on demand.
68
+ """
69
+
70
+ # Summaries by type
71
+ heuristic_summaries: List[MemorySummary] = field(default_factory=list)
72
+ outcome_summaries: List[MemorySummary] = field(default_factory=list)
73
+ knowledge_summaries: List[MemorySummary] = field(default_factory=list)
74
+ anti_pattern_summaries: List[MemorySummary] = field(default_factory=list)
75
+ preference_summaries: List[MemorySummary] = field(default_factory=list)
76
+
77
+ # Full items (populated on demand)
78
+ _full_items: Dict[str, Any] = field(default_factory=dict, repr=False)
79
+
80
+ # Metadata
81
+ query: str = ""
82
+ agent: str = ""
83
+ total_available: int = 0
84
+ summaries_included: int = 0
85
+ estimated_summary_tokens: int = 0
86
+
87
+ @property
88
+ def all_summaries(self) -> List[MemorySummary]:
89
+ """Get all summaries across types."""
90
+ return (
91
+ self.heuristic_summaries
92
+ + self.outcome_summaries
93
+ + self.knowledge_summaries
94
+ + self.anti_pattern_summaries
95
+ + self.preference_summaries
96
+ )
97
+
98
+ def get_full_item(self, memory_id: str) -> Optional[Any]:
99
+ """Get full item by ID (lazy load if needed)."""
100
+ # Check cache first
101
+ if memory_id in self._full_items:
102
+ return self._full_items[memory_id]
103
+
104
+ # Check summaries for attached full items
105
+ for summary in self.all_summaries:
106
+ if summary.id == memory_id and summary._full_item:
107
+ self._full_items[memory_id] = summary._full_item
108
+ return summary._full_item
109
+
110
+ return None
111
+
112
+ def get_ids_by_type(self, memory_type: str) -> List[str]:
113
+ """Get all IDs for a memory type."""
114
+ type_map = {
115
+ "heuristic": self.heuristic_summaries,
116
+ "outcome": self.outcome_summaries,
117
+ "domain_knowledge": self.knowledge_summaries,
118
+ "anti_pattern": self.anti_pattern_summaries,
119
+ "preference": self.preference_summaries,
120
+ }
121
+ summaries = type_map.get(memory_type, [])
122
+ return [s.id for s in summaries]
123
+
124
+
125
+ class SummaryExtractor:
126
+ """Extracts summaries from memory items."""
127
+
128
+ def __init__(
129
+ self,
130
+ max_summary_length: int = 100,
131
+ chars_per_token: int = 4,
132
+ ):
133
+ self.max_summary_length = max_summary_length
134
+ self.chars_per_token = chars_per_token
135
+
136
+ def extract_heuristic_summary(
137
+ self,
138
+ h: Heuristic,
139
+ level: DisclosureLevel = DisclosureLevel.SUMMARY,
140
+ ) -> MemorySummary:
141
+ """Extract summary from a heuristic."""
142
+ if level == DisclosureLevel.REFERENCE:
143
+ summary = f"Heuristic: {h.condition[:30]}..."
144
+ relevance = "Learned pattern"
145
+ elif level == DisclosureLevel.SUMMARY:
146
+ summary = self._truncate(
147
+ f"When {h.condition}, {h.strategy}",
148
+ self.max_summary_length,
149
+ )
150
+ relevance = (
151
+ f"Success rate: {h.success_rate:.0%}, Confidence: {h.confidence:.0%}"
152
+ )
153
+ else: # KEY_DETAILS or FULL
154
+ summary = f"When {h.condition}, {h.strategy}"
155
+ relevance = f"Used {h.occurrence_count}x, {h.success_rate:.0%} success"
156
+
157
+ key_fields = {}
158
+ if level >= DisclosureLevel.KEY_DETAILS:
159
+ key_fields = {
160
+ "confidence": h.confidence,
161
+ "success_rate": h.success_rate,
162
+ "occurrence_count": h.occurrence_count,
163
+ }
164
+
165
+ return MemorySummary(
166
+ id=h.id,
167
+ memory_type="heuristic",
168
+ summary=summary,
169
+ relevance_hint=relevance,
170
+ estimated_tokens=len(summary) // self.chars_per_token + 10,
171
+ disclosure_level=level,
172
+ key_fields=key_fields,
173
+ _full_item=h if level == DisclosureLevel.FULL else None,
174
+ )
175
+
176
+ def extract_outcome_summary(
177
+ self,
178
+ o: Outcome,
179
+ level: DisclosureLevel = DisclosureLevel.SUMMARY,
180
+ ) -> MemorySummary:
181
+ """Extract summary from an outcome."""
182
+ status = "Success" if o.success else "Failed"
183
+
184
+ if level == DisclosureLevel.REFERENCE:
185
+ summary = f"Outcome: {o.task_type} ({status})"
186
+ relevance = f"{o.task_type} task"
187
+ elif level == DisclosureLevel.SUMMARY:
188
+ summary = self._truncate(
189
+ f"{status}: {o.task_description} using {o.strategy_used}",
190
+ self.max_summary_length,
191
+ )
192
+ relevance = f"{o.task_type} - {status}"
193
+ else:
194
+ summary = f"{status}: {o.task_description}\nStrategy: {o.strategy_used}"
195
+ if o.error_message:
196
+ summary += f"\nError: {o.error_message}"
197
+ relevance = f"{o.task_type} task outcome"
198
+
199
+ key_fields = {}
200
+ if level >= DisclosureLevel.KEY_DETAILS:
201
+ key_fields = {
202
+ "success": o.success,
203
+ "task_type": o.task_type,
204
+ "strategy_used": o.strategy_used,
205
+ }
206
+ if o.error_message:
207
+ key_fields["error"] = o.error_message[:100]
208
+
209
+ return MemorySummary(
210
+ id=o.id,
211
+ memory_type="outcome",
212
+ summary=summary,
213
+ relevance_hint=relevance,
214
+ estimated_tokens=len(summary) // self.chars_per_token + 10,
215
+ disclosure_level=level,
216
+ key_fields=key_fields,
217
+ _full_item=o if level == DisclosureLevel.FULL else None,
218
+ )
219
+
220
+ def extract_knowledge_summary(
221
+ self,
222
+ k: DomainKnowledge,
223
+ level: DisclosureLevel = DisclosureLevel.SUMMARY,
224
+ ) -> MemorySummary:
225
+ """Extract summary from domain knowledge."""
226
+ fact_str = str(k.fact)[:100] if k.fact else "N/A"
227
+
228
+ if level == DisclosureLevel.REFERENCE:
229
+ summary = f"Knowledge: {k.domain}"
230
+ relevance = f"Domain: {k.domain}"
231
+ elif level == DisclosureLevel.SUMMARY:
232
+ summary = self._truncate(
233
+ f"[{k.domain}] {fact_str}",
234
+ self.max_summary_length,
235
+ )
236
+ relevance = f"Confidence: {k.confidence:.0%}"
237
+ else:
238
+ summary = f"Domain: {k.domain}\nFact: {fact_str}\nSource: {k.source}"
239
+ relevance = f"Domain knowledge, {k.confidence:.0%} confidence"
240
+
241
+ key_fields = {}
242
+ if level >= DisclosureLevel.KEY_DETAILS:
243
+ key_fields = {
244
+ "domain": k.domain,
245
+ "confidence": k.confidence,
246
+ "source": k.source,
247
+ }
248
+
249
+ return MemorySummary(
250
+ id=k.id,
251
+ memory_type="domain_knowledge",
252
+ summary=summary,
253
+ relevance_hint=relevance,
254
+ estimated_tokens=len(summary) // self.chars_per_token + 10,
255
+ disclosure_level=level,
256
+ key_fields=key_fields,
257
+ _full_item=k if level == DisclosureLevel.FULL else None,
258
+ )
259
+
260
+ def extract_anti_pattern_summary(
261
+ self,
262
+ ap: AntiPattern,
263
+ level: DisclosureLevel = DisclosureLevel.SUMMARY,
264
+ ) -> MemorySummary:
265
+ """Extract summary from an anti-pattern."""
266
+ if level == DisclosureLevel.REFERENCE:
267
+ summary = f"Warning: {ap.pattern[:30]}..."
268
+ relevance = "Known pitfall"
269
+ elif level == DisclosureLevel.SUMMARY:
270
+ summary = self._truncate(
271
+ f"Avoid: {ap.pattern}. {ap.why_bad}",
272
+ self.max_summary_length,
273
+ )
274
+ relevance = f"Seen {ap.occurrence_count}x"
275
+ else:
276
+ summary = (
277
+ f"Pattern to avoid: {ap.pattern}\n"
278
+ f"Why bad: {ap.why_bad}\n"
279
+ f"Instead: {ap.better_alternative}"
280
+ )
281
+ relevance = f"Occurred {ap.occurrence_count}x"
282
+
283
+ key_fields = {}
284
+ if level >= DisclosureLevel.KEY_DETAILS:
285
+ key_fields = {
286
+ "pattern": ap.pattern,
287
+ "occurrence_count": ap.occurrence_count,
288
+ "alternative": ap.better_alternative,
289
+ }
290
+
291
+ return MemorySummary(
292
+ id=ap.id,
293
+ memory_type="anti_pattern",
294
+ summary=summary,
295
+ relevance_hint=relevance,
296
+ estimated_tokens=len(summary) // self.chars_per_token + 10,
297
+ disclosure_level=level,
298
+ key_fields=key_fields,
299
+ _full_item=ap if level == DisclosureLevel.FULL else None,
300
+ )
301
+
302
+ def extract_preference_summary(
303
+ self,
304
+ p: UserPreference,
305
+ level: DisclosureLevel = DisclosureLevel.SUMMARY,
306
+ ) -> MemorySummary:
307
+ """Extract summary from a user preference."""
308
+ if level == DisclosureLevel.REFERENCE:
309
+ summary = f"Preference: {p.category}"
310
+ relevance = "User constraint"
311
+ elif level == DisclosureLevel.SUMMARY:
312
+ summary = self._truncate(
313
+ f"[{p.category}] {p.preference}",
314
+ self.max_summary_length,
315
+ )
316
+ relevance = f"Priority: {p.priority}"
317
+ else:
318
+ summary = (
319
+ f"Category: {p.category}\n"
320
+ f"Preference: {p.preference}\n"
321
+ f"Context: {p.context or 'General'}"
322
+ )
323
+ relevance = f"User preference, priority {p.priority}"
324
+
325
+ key_fields = {}
326
+ if level >= DisclosureLevel.KEY_DETAILS:
327
+ key_fields = {
328
+ "category": p.category,
329
+ "priority": p.priority,
330
+ }
331
+
332
+ return MemorySummary(
333
+ id=p.id,
334
+ memory_type="preference",
335
+ summary=summary,
336
+ relevance_hint=relevance,
337
+ estimated_tokens=len(summary) // self.chars_per_token + 10,
338
+ disclosure_level=level,
339
+ key_fields=key_fields,
340
+ _full_item=p if level == DisclosureLevel.FULL else None,
341
+ )
342
+
343
+ def _truncate(self, text: str, max_length: int) -> str:
344
+ """Truncate text to max length."""
345
+ if len(text) <= max_length:
346
+ return text
347
+ return text[: max_length - 3] + "..."
348
+
349
+
350
+ class ProgressiveRetrieval:
351
+ """
352
+ Retrieval with progressive disclosure support.
353
+
354
+ Returns summaries first, allowing agents to request full details
355
+ only for items they need.
356
+
357
+ Usage:
358
+ progressive = ProgressiveRetrieval(retrieval_engine, storage)
359
+
360
+ # Get summaries
361
+ slice = progressive.retrieve_summaries(query, agent, project_id)
362
+
363
+ # Get full details for specific items
364
+ full_heuristic = progressive.get_full_item("heuristic-123", "heuristic")
365
+ """
366
+
367
+ def __init__(
368
+ self,
369
+ retrieval_engine: Any, # RetrievalEngine
370
+ storage: Any, # StorageBackend
371
+ default_level: DisclosureLevel = DisclosureLevel.SUMMARY,
372
+ ):
373
+ self.engine = retrieval_engine
374
+ self.storage = storage
375
+ self.default_level = default_level
376
+ self.extractor = SummaryExtractor()
377
+
378
+ # Cache for fetched full items
379
+ self._item_cache: Dict[str, Any] = {}
380
+
381
+ def retrieve_summaries(
382
+ self,
383
+ query: str,
384
+ agent: str,
385
+ project_id: str,
386
+ user_id: Optional[str] = None,
387
+ top_k: int = 10,
388
+ level: Optional[DisclosureLevel] = None,
389
+ **kwargs,
390
+ ) -> ProgressiveSlice:
391
+ """
392
+ Retrieve memory summaries (not full content).
393
+
394
+ Returns compact summaries that fit more items in context.
395
+ Use get_full_item() to fetch complete details when needed.
396
+ """
397
+ level = level or self.default_level
398
+
399
+ # Get full results from engine
400
+ raw_slice = self.engine.retrieve(
401
+ query=query,
402
+ agent=agent,
403
+ project_id=project_id,
404
+ user_id=user_id,
405
+ top_k=top_k,
406
+ **kwargs,
407
+ )
408
+
409
+ # Extract summaries
410
+ heuristic_summaries = [
411
+ self.extractor.extract_heuristic_summary(h, level)
412
+ for h in raw_slice.heuristics
413
+ ]
414
+ outcome_summaries = [
415
+ self.extractor.extract_outcome_summary(o, level) for o in raw_slice.outcomes
416
+ ]
417
+ knowledge_summaries = [
418
+ self.extractor.extract_knowledge_summary(k, level)
419
+ for k in raw_slice.domain_knowledge
420
+ ]
421
+ anti_pattern_summaries = [
422
+ self.extractor.extract_anti_pattern_summary(ap, level)
423
+ for ap in raw_slice.anti_patterns
424
+ ]
425
+ preference_summaries = [
426
+ self.extractor.extract_preference_summary(p, level)
427
+ for p in raw_slice.preferences
428
+ ]
429
+
430
+ # Calculate totals
431
+ all_summaries = (
432
+ heuristic_summaries
433
+ + outcome_summaries
434
+ + knowledge_summaries
435
+ + anti_pattern_summaries
436
+ + preference_summaries
437
+ )
438
+ total_tokens = sum(s.estimated_tokens for s in all_summaries)
439
+
440
+ # Cache full items for lazy loading
441
+ for h in raw_slice.heuristics:
442
+ self._item_cache[h.id] = h
443
+ for o in raw_slice.outcomes:
444
+ self._item_cache[o.id] = o
445
+ for k in raw_slice.domain_knowledge:
446
+ self._item_cache[k.id] = k
447
+ for ap in raw_slice.anti_patterns:
448
+ self._item_cache[ap.id] = ap
449
+ for p in raw_slice.preferences:
450
+ self._item_cache[p.id] = p
451
+
452
+ return ProgressiveSlice(
453
+ heuristic_summaries=heuristic_summaries,
454
+ outcome_summaries=outcome_summaries,
455
+ knowledge_summaries=knowledge_summaries,
456
+ anti_pattern_summaries=anti_pattern_summaries,
457
+ preference_summaries=preference_summaries,
458
+ query=query,
459
+ agent=agent,
460
+ total_available=raw_slice.total_items,
461
+ summaries_included=len(all_summaries),
462
+ estimated_summary_tokens=total_tokens,
463
+ )
464
+
465
+ def get_full_item(
466
+ self,
467
+ memory_id: str,
468
+ memory_type: str,
469
+ ) -> Optional[Any]:
470
+ """
471
+ Get full details for a specific memory item.
472
+
473
+ This is the "fetch on demand" part of progressive disclosure.
474
+ """
475
+ # Check cache first
476
+ if memory_id in self._item_cache:
477
+ logger.debug(f"Cache hit for {memory_type}:{memory_id}")
478
+ return self._item_cache[memory_id]
479
+
480
+ # Fetch from storage
481
+ logger.debug(f"Fetching {memory_type}:{memory_id} from storage")
482
+
483
+ item = None
484
+ if memory_type == "heuristic":
485
+ item = self.storage.get_heuristic_by_id(memory_id)
486
+ elif memory_type == "outcome":
487
+ item = self.storage.get_outcome_by_id(memory_id)
488
+ elif memory_type == "domain_knowledge":
489
+ item = self.storage.get_domain_knowledge_by_id(memory_id)
490
+ elif memory_type == "anti_pattern":
491
+ item = self.storage.get_anti_pattern_by_id(memory_id)
492
+ elif memory_type == "preference":
493
+ item = self.storage.get_preference_by_id(memory_id)
494
+
495
+ if item:
496
+ self._item_cache[memory_id] = item
497
+
498
+ return item
499
+
500
+ def get_multiple_full_items(
501
+ self,
502
+ memory_ids: List[str],
503
+ memory_type: str,
504
+ ) -> List[Any]:
505
+ """Get full details for multiple items."""
506
+ return [
507
+ item
508
+ for item in (self.get_full_item(mid, memory_type) for mid in memory_ids)
509
+ if item is not None
510
+ ]
511
+
512
+ def clear_cache(self) -> None:
513
+ """Clear the item cache."""
514
+ self._item_cache.clear()
515
+
516
+ def format_summaries_for_context(
517
+ self,
518
+ progressive_slice: ProgressiveSlice,
519
+ include_fetch_hint: bool = True,
520
+ ) -> str:
521
+ """
522
+ Format summaries for inclusion in agent context.
523
+
524
+ Returns a compact string representation suitable for prompts.
525
+ """
526
+ lines = []
527
+
528
+ if progressive_slice.heuristic_summaries:
529
+ lines.append("## Relevant Patterns")
530
+ for s in progressive_slice.heuristic_summaries:
531
+ lines.append(f"- [{s.id}] {s.summary} ({s.relevance_hint})")
532
+
533
+ if progressive_slice.anti_pattern_summaries:
534
+ lines.append("\n## Warnings")
535
+ for s in progressive_slice.anti_pattern_summaries:
536
+ lines.append(f"- [{s.id}] {s.summary} ({s.relevance_hint})")
537
+
538
+ if progressive_slice.outcome_summaries:
539
+ lines.append("\n## Recent Outcomes")
540
+ for s in progressive_slice.outcome_summaries:
541
+ lines.append(f"- [{s.id}] {s.summary}")
542
+
543
+ if progressive_slice.preference_summaries:
544
+ lines.append("\n## User Preferences")
545
+ for s in progressive_slice.preference_summaries:
546
+ lines.append(f"- [{s.id}] {s.summary}")
547
+
548
+ if progressive_slice.knowledge_summaries:
549
+ lines.append("\n## Domain Knowledge")
550
+ for s in progressive_slice.knowledge_summaries:
551
+ lines.append(f"- [{s.id}] {s.summary}")
552
+
553
+ if include_fetch_hint:
554
+ lines.append(
555
+ f"\n_({progressive_slice.summaries_included} summaries shown, "
556
+ f"~{progressive_slice.estimated_summary_tokens} tokens. "
557
+ f"Request full details by ID if needed.)_"
558
+ )
559
+
560
+ return "\n".join(lines)