agmem 0.1.1__py3-none-any.whl → 0.1.2__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.
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.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]) ->
|
|
78
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FrontmatterData":
|
|
64
79
|
"""Create from dictionary."""
|
|
65
80
|
known_fields = {
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
73
|
-
last_updated=data.get(
|
|
74
|
-
source_agent_id=data.get(
|
|
75
|
-
confidence_score=data.get(
|
|
76
|
-
memory_type=data.get(
|
|
77
|
-
tags=data.get(
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|
|
204
|
-
MemoryType.EPISODIC: [
|
|
205
|
-
MemoryType.PROCEDURAL: [
|
|
206
|
-
MemoryType.CHECKPOINTS: [
|
|
207
|
-
MemoryType.SESSION_SUMMARIES: [
|
|
208
|
-
MemoryType.UNKNOWN: [
|
|
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: [
|
|
214
|
-
MemoryType.EPISODIC: [
|
|
215
|
-
MemoryType.PROCEDURAL: [
|
|
216
|
-
MemoryType.CHECKPOINTS: [
|
|
217
|
-
MemoryType.SESSION_SUMMARIES: [
|
|
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
|
|
259
|
+
|
|
260
|
+
if "episodic" in path_lower:
|
|
235
261
|
return MemoryType.EPISODIC
|
|
236
|
-
elif
|
|
262
|
+
elif "semantic" in path_lower:
|
|
237
263
|
return MemoryType.SEMANTIC
|
|
238
|
-
elif
|
|
264
|
+
elif "procedural" in path_lower:
|
|
239
265
|
return MemoryType.PROCEDURAL
|
|
240
|
-
elif
|
|
266
|
+
elif "checkpoint" in path_lower:
|
|
241
267
|
return MemoryType.CHECKPOINTS
|
|
242
|
-
elif
|
|
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(
|
|
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
|
|
277
|
-
if
|
|
278
|
-
result.add_error(
|
|
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
|
|
283
|
-
if
|
|
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(
|
|
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(
|
|
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
|
|
292
|
-
result.add_error(
|
|
293
|
-
|
|
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(
|
|
300
|
-
datetime.fromisoformat(frontmatter.last_updated.replace(
|
|
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(
|
|
305
|
-
|
|
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(
|
|
311
|
-
|
|
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(
|
|
314
|
-
|
|
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(
|
|
321
|
-
|
|
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(
|
|
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(
|
|
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() +
|
|
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(
|
|
395
|
-
return datetime.fromisoformat(ts.replace(
|
|
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:
|