agmem 0.1.1__py3-none-any.whl → 0.1.3__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 (100) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
  2. agmem-0.1.3.dist-info/RECORD +105 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +45 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +83 -76
  7. memvcs/commands/audit.py +59 -0
  8. memvcs/commands/blame.py +46 -53
  9. memvcs/commands/branch.py +13 -33
  10. memvcs/commands/checkout.py +27 -32
  11. memvcs/commands/clean.py +18 -23
  12. memvcs/commands/clone.py +11 -1
  13. memvcs/commands/commit.py +40 -39
  14. memvcs/commands/daemon.py +109 -76
  15. memvcs/commands/decay.py +77 -0
  16. memvcs/commands/diff.py +56 -57
  17. memvcs/commands/distill.py +90 -0
  18. memvcs/commands/federated.py +53 -0
  19. memvcs/commands/fsck.py +86 -61
  20. memvcs/commands/garden.py +40 -35
  21. memvcs/commands/gc.py +51 -0
  22. memvcs/commands/graph.py +41 -48
  23. memvcs/commands/init.py +16 -24
  24. memvcs/commands/log.py +25 -40
  25. memvcs/commands/merge.py +69 -27
  26. memvcs/commands/pack.py +129 -0
  27. memvcs/commands/prove.py +66 -0
  28. memvcs/commands/pull.py +31 -1
  29. memvcs/commands/push.py +4 -2
  30. memvcs/commands/recall.py +145 -0
  31. memvcs/commands/reflog.py +13 -22
  32. memvcs/commands/remote.py +1 -0
  33. memvcs/commands/repair.py +66 -0
  34. memvcs/commands/reset.py +23 -33
  35. memvcs/commands/resolve.py +130 -0
  36. memvcs/commands/resurrect.py +82 -0
  37. memvcs/commands/search.py +3 -4
  38. memvcs/commands/serve.py +2 -1
  39. memvcs/commands/show.py +66 -36
  40. memvcs/commands/stash.py +34 -34
  41. memvcs/commands/status.py +27 -35
  42. memvcs/commands/tag.py +23 -47
  43. memvcs/commands/test.py +30 -44
  44. memvcs/commands/timeline.py +111 -0
  45. memvcs/commands/tree.py +26 -27
  46. memvcs/commands/verify.py +110 -0
  47. memvcs/commands/when.py +115 -0
  48. memvcs/core/access_index.py +167 -0
  49. memvcs/core/audit.py +124 -0
  50. memvcs/core/config_loader.py +3 -1
  51. memvcs/core/consistency.py +214 -0
  52. memvcs/core/crypto_verify.py +280 -0
  53. memvcs/core/decay.py +185 -0
  54. memvcs/core/diff.py +158 -143
  55. memvcs/core/distiller.py +277 -0
  56. memvcs/core/encryption.py +169 -0
  57. memvcs/core/federated.py +86 -0
  58. memvcs/core/gardener.py +176 -145
  59. memvcs/core/hooks.py +48 -14
  60. memvcs/core/ipfs_remote.py +39 -0
  61. memvcs/core/knowledge_graph.py +135 -138
  62. memvcs/core/llm/__init__.py +10 -0
  63. memvcs/core/llm/anthropic_provider.py +50 -0
  64. memvcs/core/llm/base.py +27 -0
  65. memvcs/core/llm/factory.py +30 -0
  66. memvcs/core/llm/openai_provider.py +36 -0
  67. memvcs/core/merge.py +260 -170
  68. memvcs/core/objects.py +110 -101
  69. memvcs/core/pack.py +92 -0
  70. memvcs/core/pii_scanner.py +147 -146
  71. memvcs/core/privacy_budget.py +63 -0
  72. memvcs/core/refs.py +132 -115
  73. memvcs/core/remote.py +38 -0
  74. memvcs/core/repository.py +254 -164
  75. memvcs/core/schema.py +155 -113
  76. memvcs/core/staging.py +60 -65
  77. memvcs/core/storage/__init__.py +20 -18
  78. memvcs/core/storage/base.py +74 -70
  79. memvcs/core/storage/gcs.py +70 -68
  80. memvcs/core/storage/local.py +42 -40
  81. memvcs/core/storage/s3.py +105 -110
  82. memvcs/core/temporal_index.py +121 -0
  83. memvcs/core/test_runner.py +101 -93
  84. memvcs/core/trust.py +103 -0
  85. memvcs/core/vector_store.py +56 -36
  86. memvcs/core/zk_proofs.py +26 -0
  87. memvcs/integrations/mcp_server.py +1 -3
  88. memvcs/integrations/web_ui/server.py +25 -26
  89. memvcs/retrieval/__init__.py +22 -0
  90. memvcs/retrieval/base.py +54 -0
  91. memvcs/retrieval/pack.py +128 -0
  92. memvcs/retrieval/recaller.py +105 -0
  93. memvcs/retrieval/strategies.py +314 -0
  94. memvcs/utils/__init__.py +3 -3
  95. memvcs/utils/helpers.py +52 -52
  96. agmem-0.1.1.dist-info/RECORD +0 -67
  97. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  98. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  99. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  100. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/core/schema.py CHANGED
@@ -13,6 +13,7 @@ from enum import Enum
13
13
 
14
14
  try:
15
15
  import yaml
16
+
16
17
  YAML_AVAILABLE = True
17
18
  except ImportError:
18
19
  YAML_AVAILABLE = False
@@ -22,6 +23,7 @@ from .constants import MEMORY_TYPES
22
23
 
23
24
  class MemoryType(Enum):
24
25
  """Memory types with their validation requirements."""
26
+
25
27
  EPISODIC = "episodic"
26
28
  SEMANTIC = "semantic"
27
29
  PROCEDURAL = "procedural"
@@ -33,14 +35,19 @@ class MemoryType(Enum):
33
35
  @dataclass
34
36
  class FrontmatterData:
35
37
  """Parsed frontmatter data from a memory file."""
38
+
36
39
  schema_version: str = "1.0"
37
40
  last_updated: Optional[str] = None
38
41
  source_agent_id: Optional[str] = None
39
42
  confidence_score: Optional[float] = None
40
43
  memory_type: Optional[str] = None
41
44
  tags: List[str] = field(default_factory=list)
45
+ importance: Optional[float] = None # 0.0-1.0 for recall/decay weighting
46
+ valid_from: Optional[str] = None # ISO 8601 for epistemic versioning
47
+ valid_until: Optional[str] = None # ISO 8601 for epistemic versioning
48
+ source_authority: Optional[str] = None # "human-provided" or "inferred"
42
49
  extra: Dict[str, Any] = field(default_factory=dict)
43
-
50
+
44
51
  def to_dict(self) -> Dict[str, Any]:
45
52
  """Convert to dictionary for serialization."""
46
53
  result = {
@@ -56,32 +63,53 @@ class FrontmatterData:
56
63
  result["memory_type"] = self.memory_type
57
64
  if self.tags:
58
65
  result["tags"] = self.tags
66
+ if self.importance is not None:
67
+ result["importance"] = self.importance
68
+ if self.valid_from:
69
+ result["valid_from"] = self.valid_from
70
+ if self.valid_until:
71
+ result["valid_until"] = self.valid_until
72
+ if self.source_authority:
73
+ result["source_authority"] = self.source_authority
59
74
  result.update(self.extra)
60
75
  return result
61
-
76
+
62
77
  @classmethod
63
- def from_dict(cls, data: Dict[str, Any]) -> 'FrontmatterData':
78
+ def from_dict(cls, data: Dict[str, Any]) -> "FrontmatterData":
64
79
  """Create from dictionary."""
65
80
  known_fields = {
66
- 'schema_version', 'last_updated', 'source_agent_id',
67
- 'confidence_score', 'memory_type', 'tags'
81
+ "schema_version",
82
+ "last_updated",
83
+ "source_agent_id",
84
+ "confidence_score",
85
+ "memory_type",
86
+ "tags",
87
+ "importance",
88
+ "valid_from",
89
+ "valid_until",
90
+ "source_authority",
68
91
  }
69
92
  extra = {k: v for k, v in data.items() if k not in known_fields}
70
-
93
+
71
94
  return cls(
72
- schema_version=data.get('schema_version', '1.0'),
73
- last_updated=data.get('last_updated'),
74
- source_agent_id=data.get('source_agent_id'),
75
- confidence_score=data.get('confidence_score'),
76
- memory_type=data.get('memory_type'),
77
- tags=data.get('tags', []),
78
- extra=extra
95
+ schema_version=data.get("schema_version", "1.0"),
96
+ last_updated=data.get("last_updated"),
97
+ source_agent_id=data.get("source_agent_id"),
98
+ confidence_score=data.get("confidence_score"),
99
+ memory_type=data.get("memory_type"),
100
+ tags=data.get("tags", []),
101
+ importance=data.get("importance"),
102
+ valid_from=data.get("valid_from"),
103
+ valid_until=data.get("valid_until"),
104
+ source_authority=data.get("source_authority"),
105
+ extra=extra,
79
106
  )
80
107
 
81
108
 
82
109
  @dataclass
83
110
  class ValidationError:
84
111
  """A single validation error."""
112
+
85
113
  field: str
86
114
  message: str
87
115
  severity: str = "error" # "error" or "warning"
@@ -90,16 +118,17 @@ class ValidationError:
90
118
  @dataclass
91
119
  class ValidationResult:
92
120
  """Result of validating a memory file."""
121
+
93
122
  valid: bool
94
123
  errors: List[ValidationError] = field(default_factory=list)
95
124
  warnings: List[ValidationError] = field(default_factory=list)
96
125
  frontmatter: Optional[FrontmatterData] = None
97
-
126
+
98
127
  def add_error(self, field: str, message: str):
99
128
  """Add a validation error."""
100
129
  self.errors.append(ValidationError(field=field, message=message, severity="error"))
101
130
  self.valid = False
102
-
131
+
103
132
  def add_warning(self, field: str, message: str):
104
133
  """Add a validation warning."""
105
134
  self.warnings.append(ValidationError(field=field, message=message, severity="warning"))
@@ -107,21 +136,18 @@ class ValidationResult:
107
136
 
108
137
  class FrontmatterParser:
109
138
  """Parser for YAML frontmatter in memory files."""
110
-
139
+
111
140
  # Regex to match YAML frontmatter block
112
- FRONTMATTER_PATTERN = re.compile(
113
- r'^---\s*\n(.*?)\n---\s*\n',
114
- re.DOTALL | re.MULTILINE
115
- )
116
-
141
+ FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL | re.MULTILINE)
142
+
117
143
  @classmethod
118
144
  def parse(cls, content: str) -> Tuple[Optional[FrontmatterData], str]:
119
145
  """
120
146
  Parse frontmatter from content.
121
-
147
+
122
148
  Args:
123
149
  content: Full file content
124
-
150
+
125
151
  Returns:
126
152
  Tuple of (frontmatter_data, body_content)
127
153
  frontmatter_data is None if no frontmatter found
@@ -129,37 +155,37 @@ class FrontmatterParser:
129
155
  if not YAML_AVAILABLE:
130
156
  # Without PyYAML, return None for frontmatter
131
157
  return None, content
132
-
158
+
133
159
  match = cls.FRONTMATTER_PATTERN.match(content)
134
160
  if not match:
135
161
  return None, content
136
-
162
+
137
163
  yaml_content = match.group(1)
138
- body = content[match.end():]
139
-
164
+ body = content[match.end() :]
165
+
140
166
  try:
141
167
  data = yaml.safe_load(yaml_content)
142
168
  if not isinstance(data, dict):
143
169
  return None, content
144
-
170
+
145
171
  frontmatter = FrontmatterData.from_dict(data)
146
172
  return frontmatter, body
147
173
  except yaml.YAMLError:
148
174
  return None, content
149
-
175
+
150
176
  @classmethod
151
177
  def has_frontmatter(cls, content: str) -> bool:
152
178
  """Check if content has YAML frontmatter."""
153
179
  return bool(cls.FRONTMATTER_PATTERN.match(content))
154
-
180
+
155
181
  @classmethod
156
182
  def create_frontmatter(cls, data: FrontmatterData) -> str:
157
183
  """
158
184
  Create YAML frontmatter string from data.
159
-
185
+
160
186
  Args:
161
187
  data: FrontmatterData to serialize
162
-
188
+
163
189
  Returns:
164
190
  YAML frontmatter string with delimiters
165
191
  """
@@ -173,20 +199,20 @@ class FrontmatterParser:
173
199
  elif value is not None:
174
200
  lines.append(f"{key}: {value}")
175
201
  lines.append("---")
176
- return '\n'.join(lines) + '\n'
177
-
202
+ return "\n".join(lines) + "\n"
203
+
178
204
  yaml_str = yaml.dump(data.to_dict(), default_flow_style=False, sort_keys=False)
179
205
  return f"---\n{yaml_str}---\n"
180
-
206
+
181
207
  @classmethod
182
208
  def add_or_update_frontmatter(cls, content: str, data: FrontmatterData) -> str:
183
209
  """
184
210
  Add or update frontmatter in content.
185
-
211
+
186
212
  Args:
187
213
  content: Original file content
188
214
  data: FrontmatterData to add/update
189
-
215
+
190
216
  Returns:
191
217
  Content with updated frontmatter
192
218
  """
@@ -197,144 +223,159 @@ class FrontmatterParser:
197
223
 
198
224
  class SchemaValidator:
199
225
  """Validates memory files against schema requirements."""
200
-
226
+
201
227
  # Required fields per memory type
202
228
  REQUIRED_FIELDS: Dict[MemoryType, List[str]] = {
203
- MemoryType.SEMANTIC: ['schema_version', 'last_updated'],
204
- MemoryType.EPISODIC: ['schema_version'],
205
- MemoryType.PROCEDURAL: ['schema_version', 'last_updated'],
206
- MemoryType.CHECKPOINTS: ['schema_version', 'last_updated'],
207
- MemoryType.SESSION_SUMMARIES: ['schema_version', 'last_updated'],
208
- MemoryType.UNKNOWN: ['schema_version'],
229
+ MemoryType.SEMANTIC: ["schema_version", "last_updated"],
230
+ MemoryType.EPISODIC: ["schema_version"],
231
+ MemoryType.PROCEDURAL: ["schema_version", "last_updated"],
232
+ MemoryType.CHECKPOINTS: ["schema_version", "last_updated"],
233
+ MemoryType.SESSION_SUMMARIES: ["schema_version", "last_updated"],
234
+ MemoryType.UNKNOWN: ["schema_version"],
209
235
  }
210
-
236
+
211
237
  # Recommended fields per memory type (generate warnings if missing)
212
238
  RECOMMENDED_FIELDS: Dict[MemoryType, List[str]] = {
213
- MemoryType.SEMANTIC: ['source_agent_id', 'confidence_score', 'tags'],
214
- MemoryType.EPISODIC: ['source_agent_id'],
215
- MemoryType.PROCEDURAL: ['source_agent_id', 'tags'],
216
- MemoryType.CHECKPOINTS: ['source_agent_id'],
217
- MemoryType.SESSION_SUMMARIES: ['source_agent_id'],
239
+ MemoryType.SEMANTIC: ["source_agent_id", "confidence_score", "tags"],
240
+ MemoryType.EPISODIC: ["source_agent_id"],
241
+ MemoryType.PROCEDURAL: ["source_agent_id", "tags"],
242
+ MemoryType.CHECKPOINTS: ["source_agent_id"],
243
+ MemoryType.SESSION_SUMMARIES: ["source_agent_id"],
218
244
  MemoryType.UNKNOWN: [],
219
245
  }
220
-
246
+
221
247
  @classmethod
222
248
  def detect_memory_type(cls, filepath: str) -> MemoryType:
223
249
  """
224
250
  Detect memory type from file path.
225
-
251
+
226
252
  Args:
227
253
  filepath: Path to the file
228
-
254
+
229
255
  Returns:
230
256
  MemoryType enum value
231
257
  """
232
258
  path_lower = filepath.lower()
233
-
234
- if 'episodic' in path_lower:
259
+
260
+ if "episodic" in path_lower:
235
261
  return MemoryType.EPISODIC
236
- elif 'semantic' in path_lower:
262
+ elif "semantic" in path_lower:
237
263
  return MemoryType.SEMANTIC
238
- elif 'procedural' in path_lower:
264
+ elif "procedural" in path_lower:
239
265
  return MemoryType.PROCEDURAL
240
- elif 'checkpoint' in path_lower:
266
+ elif "checkpoint" in path_lower:
241
267
  return MemoryType.CHECKPOINTS
242
- elif 'session-summar' in path_lower or 'session_summar' in path_lower:
268
+ elif "session-summar" in path_lower or "session_summar" in path_lower:
243
269
  return MemoryType.SESSION_SUMMARIES
244
-
270
+
245
271
  return MemoryType.UNKNOWN
246
-
272
+
247
273
  @classmethod
248
274
  def validate(cls, content: str, filepath: str, strict: bool = False) -> ValidationResult:
249
275
  """
250
276
  Validate a memory file's frontmatter.
251
-
277
+
252
278
  Args:
253
279
  content: File content
254
280
  filepath: Path to the file (for type detection)
255
281
  strict: If True, treat warnings as errors
256
-
282
+
257
283
  Returns:
258
284
  ValidationResult with errors and warnings
259
285
  """
260
286
  result = ValidationResult(valid=True)
261
287
  memory_type = cls.detect_memory_type(filepath)
262
-
288
+
263
289
  # Parse frontmatter
264
290
  frontmatter, body = FrontmatterParser.parse(content)
265
291
  result.frontmatter = frontmatter
266
-
292
+
267
293
  # Check for missing frontmatter
268
294
  if frontmatter is None:
269
- result.add_error('frontmatter', 'Missing YAML frontmatter block')
295
+ result.add_error("frontmatter", "Missing YAML frontmatter block")
270
296
  return result
271
-
297
+
272
298
  # Check required fields
273
299
  required = cls.REQUIRED_FIELDS.get(memory_type, [])
274
300
  frontmatter_dict = frontmatter.to_dict()
275
-
276
- for field in required:
277
- if field not in frontmatter_dict or frontmatter_dict[field] is None:
278
- result.add_error(field, f"Required field '{field}' is missing")
279
-
301
+
302
+ for field_name in required:
303
+ if field_name not in frontmatter_dict or frontmatter_dict[field_name] is None:
304
+ result.add_error(field_name, f"Required field '{field_name}' is missing")
305
+
280
306
  # Check recommended fields
281
307
  recommended = cls.RECOMMENDED_FIELDS.get(memory_type, [])
282
- for field in recommended:
283
- if field not in frontmatter_dict or frontmatter_dict[field] is None:
308
+ for field_name in recommended:
309
+ if field_name not in frontmatter_dict or frontmatter_dict[field_name] is None:
284
310
  if strict:
285
- result.add_error(field, f"Recommended field '{field}' is missing (strict mode)")
311
+ result.add_error(
312
+ field_name,
313
+ f"Recommended field '{field_name}' is missing (strict mode)",
314
+ )
286
315
  else:
287
- result.add_warning(field, f"Recommended field '{field}' is missing")
288
-
316
+ result.add_warning(field_name, f"Recommended field '{field_name}' is missing")
317
+
289
318
  # Validate schema_version format
290
319
  if frontmatter.schema_version:
291
- if not re.match(r'^\d+\.\d+$', frontmatter.schema_version):
292
- result.add_error('schema_version',
293
- f"Invalid schema_version format: '{frontmatter.schema_version}' (expected X.Y)")
294
-
320
+ if not re.match(r"^\d+\.\d+$", frontmatter.schema_version):
321
+ result.add_error(
322
+ "schema_version",
323
+ f"Invalid schema_version format: '{frontmatter.schema_version}' (expected X.Y)",
324
+ )
325
+
295
326
  # Validate last_updated format (ISO 8601)
296
327
  if frontmatter.last_updated:
297
328
  try:
298
329
  # Try parsing ISO format
299
- if frontmatter.last_updated.endswith('Z'):
300
- datetime.fromisoformat(frontmatter.last_updated.replace('Z', '+00:00'))
330
+ if frontmatter.last_updated.endswith("Z"):
331
+ datetime.fromisoformat(frontmatter.last_updated.replace("Z", "+00:00"))
301
332
  else:
302
333
  datetime.fromisoformat(frontmatter.last_updated)
303
334
  except ValueError:
304
- result.add_error('last_updated',
305
- f"Invalid last_updated format: '{frontmatter.last_updated}' (expected ISO 8601)")
306
-
335
+ result.add_error(
336
+ "last_updated",
337
+ f"Invalid last_updated format: '{frontmatter.last_updated}' (expected ISO 8601)",
338
+ )
339
+
307
340
  # Validate confidence_score range
308
341
  if frontmatter.confidence_score is not None:
309
342
  if not isinstance(frontmatter.confidence_score, (int, float)):
310
- result.add_error('confidence_score',
311
- f"confidence_score must be a number, got: {type(frontmatter.confidence_score).__name__}")
343
+ result.add_error(
344
+ "confidence_score",
345
+ f"confidence_score must be a number, got: {type(frontmatter.confidence_score).__name__}",
346
+ )
312
347
  elif not (0.0 <= frontmatter.confidence_score <= 1.0):
313
- result.add_error('confidence_score',
314
- f"confidence_score must be between 0.0 and 1.0, got: {frontmatter.confidence_score}")
315
-
348
+ result.add_error(
349
+ "confidence_score",
350
+ f"confidence_score must be between 0.0 and 1.0, got: {frontmatter.confidence_score}",
351
+ )
352
+
316
353
  # Validate memory_type if specified
317
354
  if frontmatter.memory_type:
318
355
  valid_types = [mt.value for mt in MemoryType if mt != MemoryType.UNKNOWN]
319
356
  if frontmatter.memory_type not in valid_types:
320
- result.add_warning('memory_type',
321
- f"Unknown memory_type: '{frontmatter.memory_type}' (expected one of: {valid_types})")
322
-
357
+ result.add_warning(
358
+ "memory_type",
359
+ f"Unknown memory_type: '{frontmatter.memory_type}' (expected one of: {valid_types})",
360
+ )
361
+
323
362
  # Validate tags is a list
324
363
  if frontmatter.tags and not isinstance(frontmatter.tags, list):
325
- result.add_error('tags', f"tags must be a list, got: {type(frontmatter.tags).__name__}")
326
-
364
+ result.add_error("tags", f"tags must be a list, got: {type(frontmatter.tags).__name__}")
365
+
327
366
  return result
328
-
367
+
329
368
  @classmethod
330
- def validate_batch(cls, files: Dict[str, str], strict: bool = False) -> Dict[str, ValidationResult]:
369
+ def validate_batch(
370
+ cls, files: Dict[str, str], strict: bool = False
371
+ ) -> Dict[str, ValidationResult]:
331
372
  """
332
373
  Validate multiple files.
333
-
374
+
334
375
  Args:
335
376
  files: Dict mapping filepath to content
336
377
  strict: If True, treat warnings as errors
337
-
378
+
338
379
  Returns:
339
380
  Dict mapping filepath to ValidationResult
340
381
  """
@@ -348,65 +389,66 @@ def generate_frontmatter(
348
389
  memory_type: str = "semantic",
349
390
  source_agent_id: Optional[str] = None,
350
391
  confidence_score: Optional[float] = None,
351
- tags: Optional[List[str]] = None
392
+ tags: Optional[List[str]] = None,
352
393
  ) -> FrontmatterData:
353
394
  """
354
395
  Generate frontmatter data with current timestamp.
355
-
396
+
356
397
  Args:
357
398
  memory_type: Type of memory (episodic, semantic, procedural, etc.)
358
399
  source_agent_id: ID of the agent creating this memory
359
400
  confidence_score: Confidence score (0.0 to 1.0)
360
401
  tags: List of tags for categorization
361
-
402
+
362
403
  Returns:
363
404
  FrontmatterData with populated fields
364
405
  """
365
406
  return FrontmatterData(
366
407
  schema_version="1.0",
367
- last_updated=datetime.utcnow().isoformat() + 'Z',
408
+ last_updated=datetime.utcnow().isoformat() + "Z",
368
409
  source_agent_id=source_agent_id,
369
410
  confidence_score=confidence_score,
370
411
  memory_type=memory_type,
371
- tags=tags or []
412
+ tags=tags or [],
372
413
  )
373
414
 
374
415
 
375
416
  def compare_timestamps(timestamp1: Optional[str], timestamp2: Optional[str]) -> int:
376
417
  """
377
418
  Compare two ISO 8601 timestamps.
378
-
419
+
379
420
  Args:
380
421
  timestamp1: First timestamp
381
422
  timestamp2: Second timestamp
382
-
423
+
383
424
  Returns:
384
425
  -1 if timestamp1 < timestamp2
385
426
  0 if timestamp1 == timestamp2
386
427
  1 if timestamp1 > timestamp2
387
-
428
+
388
429
  If either timestamp is None or invalid, the other is considered newer.
389
430
  """
431
+
390
432
  def parse_ts(ts: Optional[str]) -> Optional[datetime]:
391
433
  if not ts:
392
434
  return None
393
435
  try:
394
- if ts.endswith('Z'):
395
- return datetime.fromisoformat(ts.replace('Z', '+00:00'))
436
+ if ts.endswith("Z"):
437
+ return datetime.fromisoformat(ts.replace("Z", "+00:00"))
396
438
  return datetime.fromisoformat(ts)
397
439
  except ValueError:
398
440
  return None
399
-
441
+
400
442
  dt1 = parse_ts(timestamp1)
401
443
  dt2 = parse_ts(timestamp2)
402
-
444
+
403
445
  if dt1 is None and dt2 is None:
404
446
  return 0
405
447
  if dt1 is None:
406
448
  return -1
407
449
  if dt2 is None:
408
450
  return 1
409
-
451
+
410
452
  if dt1 < dt2:
411
453
  return -1
412
454
  elif dt1 > dt2: