alma-memory 0.4.0__py3-none-any.whl → 0.5.1__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 (94) hide show
  1. alma/__init__.py +121 -45
  2. alma/confidence/__init__.py +1 -1
  3. alma/confidence/engine.py +92 -58
  4. alma/confidence/types.py +34 -14
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +136 -28
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +265 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +106 -0
  24. alma/graph/backends/__init__.py +32 -0
  25. alma/graph/backends/kuzu.py +624 -0
  26. alma/graph/backends/memgraph.py +432 -0
  27. alma/graph/backends/memory.py +236 -0
  28. alma/graph/backends/neo4j.py +417 -0
  29. alma/graph/base.py +159 -0
  30. alma/graph/extraction.py +198 -0
  31. alma/graph/store.py +860 -0
  32. alma/harness/__init__.py +4 -4
  33. alma/harness/base.py +18 -9
  34. alma/harness/domains.py +27 -11
  35. alma/initializer/__init__.py +1 -1
  36. alma/initializer/initializer.py +51 -43
  37. alma/initializer/types.py +25 -17
  38. alma/integration/__init__.py +9 -9
  39. alma/integration/claude_agents.py +32 -20
  40. alma/integration/helena.py +32 -22
  41. alma/integration/victor.py +57 -33
  42. alma/learning/__init__.py +27 -27
  43. alma/learning/forgetting.py +198 -148
  44. alma/learning/heuristic_extractor.py +40 -24
  45. alma/learning/protocols.py +65 -17
  46. alma/learning/validation.py +7 -2
  47. alma/mcp/__init__.py +4 -4
  48. alma/mcp/__main__.py +2 -1
  49. alma/mcp/resources.py +17 -16
  50. alma/mcp/server.py +102 -44
  51. alma/mcp/tools.py +180 -45
  52. alma/observability/__init__.py +84 -0
  53. alma/observability/config.py +302 -0
  54. alma/observability/logging.py +424 -0
  55. alma/observability/metrics.py +583 -0
  56. alma/observability/tracing.py +440 -0
  57. alma/progress/__init__.py +3 -3
  58. alma/progress/tracker.py +26 -20
  59. alma/progress/types.py +8 -12
  60. alma/py.typed +0 -0
  61. alma/retrieval/__init__.py +11 -11
  62. alma/retrieval/cache.py +20 -21
  63. alma/retrieval/embeddings.py +4 -4
  64. alma/retrieval/engine.py +179 -39
  65. alma/retrieval/scoring.py +73 -63
  66. alma/session/__init__.py +2 -2
  67. alma/session/manager.py +5 -5
  68. alma/session/types.py +5 -4
  69. alma/storage/__init__.py +70 -0
  70. alma/storage/azure_cosmos.py +414 -133
  71. alma/storage/base.py +215 -4
  72. alma/storage/chroma.py +1443 -0
  73. alma/storage/constants.py +103 -0
  74. alma/storage/file_based.py +59 -28
  75. alma/storage/migrations/__init__.py +21 -0
  76. alma/storage/migrations/base.py +321 -0
  77. alma/storage/migrations/runner.py +323 -0
  78. alma/storage/migrations/version_stores.py +337 -0
  79. alma/storage/migrations/versions/__init__.py +11 -0
  80. alma/storage/migrations/versions/v1_0_0.py +373 -0
  81. alma/storage/pinecone.py +1080 -0
  82. alma/storage/postgresql.py +1559 -0
  83. alma/storage/qdrant.py +1306 -0
  84. alma/storage/sqlite_local.py +504 -60
  85. alma/testing/__init__.py +46 -0
  86. alma/testing/factories.py +301 -0
  87. alma/testing/mocks.py +389 -0
  88. alma/types.py +62 -14
  89. alma_memory-0.5.1.dist-info/METADATA +939 -0
  90. alma_memory-0.5.1.dist-info/RECORD +93 -0
  91. {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +1 -1
  92. alma_memory-0.4.0.dist-info/METADATA +0 -488
  93. alma_memory-0.4.0.dist-info/RECORD +0 -52
  94. {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/testing/mocks.py ADDED
@@ -0,0 +1,389 @@
1
+ """
2
+ ALMA Testing Mocks.
3
+
4
+ Provides mock implementations of ALMA interfaces for testing.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from alma.retrieval.embeddings import MockEmbedder
11
+ from alma.storage.base import StorageBackend
12
+ from alma.types import (
13
+ AntiPattern,
14
+ DomainKnowledge,
15
+ Heuristic,
16
+ Outcome,
17
+ UserPreference,
18
+ )
19
+
20
+ # Re-export MockEmbedder for convenience
21
+ __all__ = ["MockStorage", "MockEmbedder"]
22
+
23
+
24
+ class MockStorage(StorageBackend):
25
+ """
26
+ In-memory mock storage backend for testing.
27
+
28
+ Stores all memory types in dictionaries for fast, isolated testing.
29
+ Does not persist data between test runs.
30
+
31
+ Example:
32
+ >>> from alma.testing import MockStorage, create_test_heuristic
33
+ >>> storage = MockStorage()
34
+ >>> heuristic = create_test_heuristic(agent="test-agent")
35
+ >>> storage.save_heuristic(heuristic)
36
+ >>> found = storage.get_heuristics("test-project", agent="test-agent")
37
+ >>> assert len(found) == 1
38
+ """
39
+
40
+ def __init__(self):
41
+ """Initialize empty in-memory storage."""
42
+ self._heuristics: Dict[str, Heuristic] = {}
43
+ self._outcomes: Dict[str, Outcome] = {}
44
+ self._preferences: Dict[str, UserPreference] = {}
45
+ self._domain_knowledge: Dict[str, DomainKnowledge] = {}
46
+ self._anti_patterns: Dict[str, AntiPattern] = {}
47
+
48
+ # ==================== WRITE OPERATIONS ====================
49
+
50
+ def save_heuristic(self, heuristic: Heuristic) -> str:
51
+ """Save a heuristic, return its ID."""
52
+ self._heuristics[heuristic.id] = heuristic
53
+ return heuristic.id
54
+
55
+ def save_outcome(self, outcome: Outcome) -> str:
56
+ """Save an outcome, return its ID."""
57
+ self._outcomes[outcome.id] = outcome
58
+ return outcome.id
59
+
60
+ def save_user_preference(self, preference: UserPreference) -> str:
61
+ """Save a user preference, return its ID."""
62
+ self._preferences[preference.id] = preference
63
+ return preference.id
64
+
65
+ def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
66
+ """Save domain knowledge, return its ID."""
67
+ self._domain_knowledge[knowledge.id] = knowledge
68
+ return knowledge.id
69
+
70
+ def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
71
+ """Save an anti-pattern, return its ID."""
72
+ self._anti_patterns[anti_pattern.id] = anti_pattern
73
+ return anti_pattern.id
74
+
75
+ # ==================== READ OPERATIONS ====================
76
+
77
+ def get_heuristics(
78
+ self,
79
+ project_id: str,
80
+ agent: Optional[str] = None,
81
+ embedding: Optional[List[float]] = None,
82
+ top_k: int = 5,
83
+ min_confidence: float = 0.0,
84
+ ) -> List[Heuristic]:
85
+ """Get heuristics, optionally filtered by agent."""
86
+ results = []
87
+ for h in self._heuristics.values():
88
+ if h.project_id != project_id:
89
+ continue
90
+ if agent and h.agent != agent:
91
+ continue
92
+ if h.confidence < min_confidence:
93
+ continue
94
+ results.append(h)
95
+
96
+ # Sort by confidence descending
97
+ results.sort(key=lambda x: x.confidence, reverse=True)
98
+ return results[:top_k]
99
+
100
+ def get_outcomes(
101
+ self,
102
+ project_id: str,
103
+ agent: Optional[str] = None,
104
+ task_type: Optional[str] = None,
105
+ embedding: Optional[List[float]] = None,
106
+ top_k: int = 5,
107
+ success_only: bool = False,
108
+ ) -> List[Outcome]:
109
+ """Get outcomes, optionally filtered."""
110
+ results = []
111
+ for o in self._outcomes.values():
112
+ if o.project_id != project_id:
113
+ continue
114
+ if agent and o.agent != agent:
115
+ continue
116
+ if task_type and o.task_type != task_type:
117
+ continue
118
+ if success_only and not o.success:
119
+ continue
120
+ results.append(o)
121
+
122
+ # Sort by timestamp descending (most recent first)
123
+ results.sort(key=lambda x: x.timestamp, reverse=True)
124
+ return results[:top_k]
125
+
126
+ def get_user_preferences(
127
+ self,
128
+ user_id: str,
129
+ category: Optional[str] = None,
130
+ ) -> List[UserPreference]:
131
+ """Get user preferences."""
132
+ results = []
133
+ for p in self._preferences.values():
134
+ if p.user_id != user_id:
135
+ continue
136
+ if category and p.category != category:
137
+ continue
138
+ results.append(p)
139
+ return results
140
+
141
+ def get_domain_knowledge(
142
+ self,
143
+ project_id: str,
144
+ agent: Optional[str] = None,
145
+ domain: Optional[str] = None,
146
+ embedding: Optional[List[float]] = None,
147
+ top_k: int = 5,
148
+ ) -> List[DomainKnowledge]:
149
+ """Get domain knowledge."""
150
+ results = []
151
+ for dk in self._domain_knowledge.values():
152
+ if dk.project_id != project_id:
153
+ continue
154
+ if agent and dk.agent != agent:
155
+ continue
156
+ if domain and dk.domain != domain:
157
+ continue
158
+ results.append(dk)
159
+
160
+ # Sort by confidence descending
161
+ results.sort(key=lambda x: x.confidence, reverse=True)
162
+ return results[:top_k]
163
+
164
+ def get_anti_patterns(
165
+ self,
166
+ project_id: str,
167
+ agent: Optional[str] = None,
168
+ embedding: Optional[List[float]] = None,
169
+ top_k: int = 5,
170
+ ) -> List[AntiPattern]:
171
+ """Get anti-patterns."""
172
+ results = []
173
+ for ap in self._anti_patterns.values():
174
+ if ap.project_id != project_id:
175
+ continue
176
+ if agent and ap.agent != agent:
177
+ continue
178
+ results.append(ap)
179
+
180
+ # Sort by occurrence_count descending
181
+ results.sort(key=lambda x: x.occurrence_count, reverse=True)
182
+ return results[:top_k]
183
+
184
+ # ==================== UPDATE OPERATIONS ====================
185
+
186
+ def update_heuristic(
187
+ self,
188
+ heuristic_id: str,
189
+ updates: Dict[str, Any],
190
+ ) -> bool:
191
+ """Update a heuristic's fields."""
192
+ if heuristic_id not in self._heuristics:
193
+ return False
194
+
195
+ h = self._heuristics[heuristic_id]
196
+ for key, value in updates.items():
197
+ if hasattr(h, key):
198
+ setattr(h, key, value)
199
+ return True
200
+
201
+ def increment_heuristic_occurrence(
202
+ self,
203
+ heuristic_id: str,
204
+ success: bool,
205
+ ) -> bool:
206
+ """Increment heuristic occurrence count."""
207
+ if heuristic_id not in self._heuristics:
208
+ return False
209
+
210
+ h = self._heuristics[heuristic_id]
211
+ h.occurrence_count += 1
212
+ if success:
213
+ h.success_count += 1
214
+ h.last_validated = datetime.now(timezone.utc)
215
+ return True
216
+
217
+ def update_heuristic_confidence(
218
+ self,
219
+ heuristic_id: str,
220
+ new_confidence: float,
221
+ ) -> bool:
222
+ """Update a heuristic's confidence value."""
223
+ if heuristic_id not in self._heuristics:
224
+ return False
225
+
226
+ self._heuristics[heuristic_id].confidence = new_confidence
227
+ return True
228
+
229
+ def update_knowledge_confidence(
230
+ self,
231
+ knowledge_id: str,
232
+ new_confidence: float,
233
+ ) -> bool:
234
+ """Update domain knowledge confidence value."""
235
+ if knowledge_id not in self._domain_knowledge:
236
+ return False
237
+
238
+ self._domain_knowledge[knowledge_id].confidence = new_confidence
239
+ return True
240
+
241
+ # ==================== DELETE OPERATIONS ====================
242
+
243
+ def delete_heuristic(self, heuristic_id: str) -> bool:
244
+ """Delete a heuristic by ID."""
245
+ if heuristic_id not in self._heuristics:
246
+ return False
247
+ del self._heuristics[heuristic_id]
248
+ return True
249
+
250
+ def delete_outcome(self, outcome_id: str) -> bool:
251
+ """Delete an outcome by ID."""
252
+ if outcome_id not in self._outcomes:
253
+ return False
254
+ del self._outcomes[outcome_id]
255
+ return True
256
+
257
+ def delete_domain_knowledge(self, knowledge_id: str) -> bool:
258
+ """Delete domain knowledge by ID."""
259
+ if knowledge_id not in self._domain_knowledge:
260
+ return False
261
+ del self._domain_knowledge[knowledge_id]
262
+ return True
263
+
264
+ def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
265
+ """Delete an anti-pattern by ID."""
266
+ if anti_pattern_id not in self._anti_patterns:
267
+ return False
268
+ del self._anti_patterns[anti_pattern_id]
269
+ return True
270
+
271
+ def delete_outcomes_older_than(
272
+ self,
273
+ project_id: str,
274
+ older_than: datetime,
275
+ agent: Optional[str] = None,
276
+ ) -> int:
277
+ """Delete old outcomes."""
278
+ to_delete = []
279
+ for oid, o in self._outcomes.items():
280
+ if o.project_id != project_id:
281
+ continue
282
+ if agent and o.agent != agent:
283
+ continue
284
+ if o.timestamp < older_than:
285
+ to_delete.append(oid)
286
+
287
+ for oid in to_delete:
288
+ del self._outcomes[oid]
289
+ return len(to_delete)
290
+
291
+ def delete_low_confidence_heuristics(
292
+ self,
293
+ project_id: str,
294
+ below_confidence: float,
295
+ agent: Optional[str] = None,
296
+ ) -> int:
297
+ """Delete low-confidence heuristics."""
298
+ to_delete = []
299
+ for hid, h in self._heuristics.items():
300
+ if h.project_id != project_id:
301
+ continue
302
+ if agent and h.agent != agent:
303
+ continue
304
+ if h.confidence < below_confidence:
305
+ to_delete.append(hid)
306
+
307
+ for hid in to_delete:
308
+ del self._heuristics[hid]
309
+ return len(to_delete)
310
+
311
+ # ==================== STATS ====================
312
+
313
+ def get_stats(
314
+ self,
315
+ project_id: str,
316
+ agent: Optional[str] = None,
317
+ ) -> Dict[str, Any]:
318
+ """Get memory statistics."""
319
+ stats = {
320
+ "heuristics": 0,
321
+ "outcomes": 0,
322
+ "preferences": 0,
323
+ "domain_knowledge": 0,
324
+ "anti_patterns": 0,
325
+ }
326
+
327
+ for h in self._heuristics.values():
328
+ if h.project_id == project_id and (not agent or h.agent == agent):
329
+ stats["heuristics"] += 1
330
+
331
+ for o in self._outcomes.values():
332
+ if o.project_id == project_id and (not agent or o.agent == agent):
333
+ stats["outcomes"] += 1
334
+
335
+ for dk in self._domain_knowledge.values():
336
+ if dk.project_id == project_id and (not agent or dk.agent == agent):
337
+ stats["domain_knowledge"] += 1
338
+
339
+ for ap in self._anti_patterns.values():
340
+ if ap.project_id == project_id and (not agent or ap.agent == agent):
341
+ stats["anti_patterns"] += 1
342
+
343
+ # Note: preferences are user-scoped, not project-scoped
344
+ stats["preferences"] = len(self._preferences)
345
+
346
+ stats["total_count"] = sum(stats.values())
347
+ return stats
348
+
349
+ # ==================== UTILITY ====================
350
+
351
+ @classmethod
352
+ def from_config(cls, config: Dict[str, Any]) -> "MockStorage":
353
+ """Create instance from configuration dict (ignores config for mock)."""
354
+ return cls()
355
+
356
+ # ==================== MOCK-SPECIFIC METHODS ====================
357
+
358
+ def clear(self) -> None:
359
+ """Clear all stored data. Useful for test cleanup."""
360
+ self._heuristics.clear()
361
+ self._outcomes.clear()
362
+ self._preferences.clear()
363
+ self._domain_knowledge.clear()
364
+ self._anti_patterns.clear()
365
+
366
+ @property
367
+ def heuristic_count(self) -> int:
368
+ """Get total number of stored heuristics."""
369
+ return len(self._heuristics)
370
+
371
+ @property
372
+ def outcome_count(self) -> int:
373
+ """Get total number of stored outcomes."""
374
+ return len(self._outcomes)
375
+
376
+ @property
377
+ def preference_count(self) -> int:
378
+ """Get total number of stored preferences."""
379
+ return len(self._preferences)
380
+
381
+ @property
382
+ def knowledge_count(self) -> int:
383
+ """Get total number of stored domain knowledge items."""
384
+ return len(self._domain_knowledge)
385
+
386
+ @property
387
+ def anti_pattern_count(self) -> int:
388
+ """Get total number of stored anti-patterns."""
389
+ return len(self._anti_patterns)
alma/types.py CHANGED
@@ -5,13 +5,14 @@ Defines the core data structures for all memory types.
5
5
  """
6
6
 
7
7
  from dataclasses import dataclass, field
8
- from datetime import datetime
9
- from typing import Optional, List, Dict, Any
8
+ from datetime import datetime, timezone
10
9
  from enum import Enum
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
 
13
13
  class MemoryType(Enum):
14
14
  """Categories of memory that agents can store and retrieve."""
15
+
15
16
  HEURISTIC = "heuristic"
16
17
  OUTCOME = "outcome"
17
18
  USER_PREFERENCE = "user_preference"
@@ -22,13 +23,21 @@ class MemoryType(Enum):
22
23
  @dataclass
23
24
  class MemoryScope:
24
25
  """
25
- Defines what an agent is allowed to learn.
26
+ Defines what an agent is allowed to learn and share.
26
27
 
27
28
  Prevents scope creep by explicitly listing allowed and forbidden domains.
29
+ Supports multi-agent memory sharing through share_with and inherit_from.
28
30
  """
31
+
29
32
  agent_name: str
30
33
  can_learn: List[str]
31
34
  cannot_learn: List[str]
35
+ share_with: List[str] = field(
36
+ default_factory=list
37
+ ) # Agents that can read this agent's memories
38
+ inherit_from: List[str] = field(
39
+ default_factory=list
40
+ ) # Agents whose memories this agent can read
32
41
  min_occurrences_for_heuristic: int = 3
33
42
 
34
43
  def is_allowed(self, domain: str) -> bool:
@@ -39,6 +48,39 @@ class MemoryScope:
39
48
  return True
40
49
  return domain in self.can_learn
41
50
 
51
+ def get_readable_agents(self) -> List[str]:
52
+ """
53
+ Get list of agents whose memories this agent can read.
54
+
55
+ Returns:
56
+ List containing this agent's name plus all inherited agents.
57
+ """
58
+ return [self.agent_name] + list(self.inherit_from)
59
+
60
+ def can_read_from(self, other_agent: str) -> bool:
61
+ """
62
+ Check if this agent can read memories from another agent.
63
+
64
+ Args:
65
+ other_agent: Name of the agent to check
66
+
67
+ Returns:
68
+ True if this agent can read from other_agent
69
+ """
70
+ return other_agent == self.agent_name or other_agent in self.inherit_from
71
+
72
+ def shares_with(self, other_agent: str) -> bool:
73
+ """
74
+ Check if this agent shares memories with another agent.
75
+
76
+ Args:
77
+ other_agent: Name of the agent to check
78
+
79
+ Returns:
80
+ True if this agent shares with other_agent
81
+ """
82
+ return other_agent in self.share_with
83
+
42
84
 
43
85
  @dataclass
44
86
  class Heuristic:
@@ -47,11 +89,12 @@ class Heuristic:
47
89
 
48
90
  Heuristics are only created after min_occurrences validations.
49
91
  """
92
+
50
93
  id: str
51
94
  agent: str
52
95
  project_id: str
53
96
  condition: str # "form with multiple required fields"
54
- strategy: str # "test happy path first, then individual validation"
97
+ strategy: str # "test happy path first, then individual validation"
55
98
  confidence: float # 0.0 to 1.0
56
99
  occurrence_count: int
57
100
  success_count: int
@@ -75,6 +118,7 @@ class Outcome:
75
118
 
76
119
  Outcomes are raw data that can be consolidated into heuristics.
77
120
  """
121
+
78
122
  id: str
79
123
  agent: str
80
124
  project_id: str
@@ -85,7 +129,7 @@ class Outcome:
85
129
  duration_ms: Optional[int] = None
86
130
  error_message: Optional[str] = None
87
131
  user_feedback: Optional[str] = None
88
- timestamp: datetime = field(default_factory=datetime.utcnow)
132
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
89
133
  embedding: Optional[List[float]] = None
90
134
  metadata: Dict[str, Any] = field(default_factory=dict)
91
135
 
@@ -97,13 +141,14 @@ class UserPreference:
97
141
 
98
142
  Persists across sessions so users don't repeat themselves.
99
143
  """
144
+
100
145
  id: str
101
146
  user_id: str
102
147
  category: str # "communication", "code_style", "workflow"
103
148
  preference: str # "No emojis in documentation"
104
149
  source: str # "explicit_instruction", "inferred_from_correction"
105
150
  confidence: float = 1.0 # Lower for inferred preferences
106
- timestamp: datetime = field(default_factory=datetime.utcnow)
151
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
107
152
  metadata: Dict[str, Any] = field(default_factory=dict)
108
153
 
109
154
 
@@ -114,6 +159,7 @@ class DomainKnowledge:
114
159
 
115
160
  Different from heuristics - these are facts, not strategies.
116
161
  """
162
+
117
163
  id: str
118
164
  agent: str
119
165
  project_id: str
@@ -121,7 +167,7 @@ class DomainKnowledge:
121
167
  fact: str # "Login endpoint uses JWT with 24h expiry"
122
168
  source: str # "code_analysis", "documentation", "user_stated"
123
169
  confidence: float = 1.0
124
- last_verified: datetime = field(default_factory=datetime.utcnow)
170
+ last_verified: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
125
171
  embedding: Optional[List[float]] = None
126
172
  metadata: Dict[str, Any] = field(default_factory=dict)
127
173
 
@@ -133,6 +179,7 @@ class AntiPattern:
133
179
 
134
180
  Helps agents avoid repeating mistakes.
135
181
  """
182
+
136
183
  id: str
137
184
  agent: str
138
185
  project_id: str
@@ -141,7 +188,7 @@ class AntiPattern:
141
188
  better_alternative: str # "Use explicit waits with conditions"
142
189
  occurrence_count: int
143
190
  last_seen: datetime
144
- created_at: datetime = field(default_factory=datetime.utcnow)
191
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
145
192
  embedding: Optional[List[float]] = None
146
193
  metadata: Dict[str, Any] = field(default_factory=dict)
147
194
 
@@ -153,6 +200,7 @@ class MemorySlice:
153
200
 
154
201
  This is what gets injected per-call - must stay under token budget.
155
202
  """
203
+
156
204
  heuristics: List[Heuristic] = field(default_factory=list)
157
205
  outcomes: List[Outcome] = field(default_factory=list)
158
206
  preferences: List[UserPreference] = field(default_factory=list)
@@ -200,7 +248,7 @@ class MemorySlice:
200
248
 
201
249
  # Basic token estimation (rough: 1 token ~ 4 chars)
202
250
  if len(result) > max_tokens * 4:
203
- result = result[:max_tokens * 4] + "\n[truncated]"
251
+ result = result[: max_tokens * 4] + "\n[truncated]"
204
252
 
205
253
  return result
206
254
 
@@ -208,9 +256,9 @@ class MemorySlice:
208
256
  def total_items(self) -> int:
209
257
  """Total number of memory items in this slice."""
210
258
  return (
211
- len(self.heuristics) +
212
- len(self.outcomes) +
213
- len(self.preferences) +
214
- len(self.domain_knowledge) +
215
- len(self.anti_patterns)
259
+ len(self.heuristics)
260
+ + len(self.outcomes)
261
+ + len(self.preferences)
262
+ + len(self.domain_knowledge)
263
+ + len(self.anti_patterns)
216
264
  )