memex-python 0.13.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.
memex/__init__.py ADDED
@@ -0,0 +1,336 @@
1
+ """memex — a typed, immutable, provenance-tracked memory graph for AI agents.
2
+
3
+ Pydantic port of ``@ai2070/memex``. Public surface grows phase by phase; this
4
+ barrel re-exports everything implemented so far.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._uuid import safe_extract_timestamp, uuid7
10
+ from .bulk import (
11
+ ItemTransform,
12
+ ScoreAdjustment,
13
+ apply_many,
14
+ bulk_adjust_scores,
15
+ decay_importance,
16
+ )
17
+ from .commands import (
18
+ EdgeCreate,
19
+ EdgeRetract,
20
+ EdgeUpdate,
21
+ MemoryCommand,
22
+ MemoryCommandAdapter,
23
+ MemoryCreate,
24
+ MemoryRetract,
25
+ MemoryUpdate,
26
+ )
27
+ from .envelope import wrap_edge_state_event, wrap_lifecycle_event, wrap_state_event
28
+ from .errors import (
29
+ DuplicateEdgeError,
30
+ DuplicateMemoryError,
31
+ EdgeNotFoundError,
32
+ InvalidTimestampError,
33
+ MemexError,
34
+ MemoryNotFoundError,
35
+ )
36
+ from .factories import create_edge, create_event_envelope, create_memory_item
37
+ from .graph import GraphState, clone_graph_state, create_graph_state
38
+ from .integrity import (
39
+ CascadeResult,
40
+ Contradiction,
41
+ StaleItem,
42
+ cascade_retract,
43
+ get_alias_group,
44
+ get_aliases,
45
+ get_contradictions,
46
+ get_dependents,
47
+ get_items_by_budget,
48
+ get_stale_items,
49
+ mark_alias,
50
+ mark_contradiction,
51
+ resolve_contradiction,
52
+ )
53
+ from .intent import (
54
+ DuplicateIntentError,
55
+ Intent,
56
+ IntentCommand,
57
+ IntentFilter,
58
+ IntentLifecycleEvent,
59
+ IntentNotFoundError,
60
+ IntentResult,
61
+ IntentState,
62
+ IntentStatus,
63
+ InvalidIntentTransitionError,
64
+ apply_intent_command,
65
+ create_intent,
66
+ create_intent_state,
67
+ get_child_intents,
68
+ get_intent_by_id,
69
+ get_intents,
70
+ )
71
+ from .models import (
72
+ CreatedFilter,
73
+ DecayConfig,
74
+ DecayFilter,
75
+ DecayInterval,
76
+ DecayType,
77
+ Edge,
78
+ EdgeFilter,
79
+ EventEnvelope,
80
+ KnownEdgeKind,
81
+ KnownMemoryKind,
82
+ KnownNamespace,
83
+ KnownSourceKind,
84
+ LifecycleEventType,
85
+ MemoryFilter,
86
+ MemoryItem,
87
+ MemoryLifecycleEvent,
88
+ ParentsFilter,
89
+ QueryOptions,
90
+ Range,
91
+ ScoredItem,
92
+ ScoreRanges,
93
+ ScoreWeights,
94
+ SortField,
95
+ SortOption,
96
+ )
97
+ from .query import (
98
+ ScoredQueryOptions,
99
+ compute_decay_multiplier,
100
+ compute_score,
101
+ extract_timestamp,
102
+ get_children,
103
+ get_edge_by_id,
104
+ get_edges,
105
+ get_item_by_id,
106
+ get_items,
107
+ get_parents,
108
+ get_related_items,
109
+ get_scored_items,
110
+ matches_filter,
111
+ )
112
+ from .reducer import CommandResult, apply_command, merge_edge, merge_item
113
+ from .replay import (
114
+ ReplayFailure,
115
+ ReplayResult,
116
+ replay_commands,
117
+ replay_from_envelopes,
118
+ )
119
+ from .retrieval import (
120
+ DiversityOptions,
121
+ SupportNode,
122
+ apply_diversity,
123
+ filter_contradictions,
124
+ get_support_set,
125
+ get_support_tree,
126
+ smart_retrieve,
127
+ surface_contradictions,
128
+ )
129
+ from .serialization import (
130
+ SerializedGraphState,
131
+ from_json,
132
+ parse,
133
+ stringify,
134
+ to_json,
135
+ )
136
+ from .stats import EdgeStats, GraphStats, ItemStats, get_stats
137
+ from .store import MemexStore
138
+ from .task import (
139
+ DuplicateTaskError,
140
+ InvalidTaskTransitionError,
141
+ Task,
142
+ TaskCommand,
143
+ TaskFilter,
144
+ TaskLifecycleEvent,
145
+ TaskNotFoundError,
146
+ TaskResult,
147
+ TaskState,
148
+ TaskStatus,
149
+ apply_task_command,
150
+ create_task,
151
+ create_task_state,
152
+ get_child_tasks,
153
+ get_task_by_id,
154
+ get_tasks,
155
+ get_tasks_by_intent,
156
+ )
157
+ from .transplant import (
158
+ ExportOptions,
159
+ ImportBucket,
160
+ ImportOptions,
161
+ ImportReport,
162
+ ImportResult,
163
+ MemexExport,
164
+ export_slice,
165
+ import_slice,
166
+ )
167
+
168
+ __all__ = [
169
+ # uuid
170
+ "uuid7",
171
+ "safe_extract_timestamp",
172
+ # graph state
173
+ "GraphState",
174
+ "create_graph_state",
175
+ "clone_graph_state",
176
+ # reducer
177
+ "apply_command",
178
+ "CommandResult",
179
+ "merge_item",
180
+ "merge_edge",
181
+ # query
182
+ "get_items",
183
+ "get_scored_items",
184
+ "get_edges",
185
+ "get_item_by_id",
186
+ "get_edge_by_id",
187
+ "get_related_items",
188
+ "get_parents",
189
+ "get_children",
190
+ "matches_filter",
191
+ "compute_score",
192
+ "compute_decay_multiplier",
193
+ "extract_timestamp",
194
+ "ScoredQueryOptions",
195
+ # retrieval
196
+ "get_support_tree",
197
+ "get_support_set",
198
+ "filter_contradictions",
199
+ "surface_contradictions",
200
+ "apply_diversity",
201
+ "smart_retrieve",
202
+ "SupportNode",
203
+ "DiversityOptions",
204
+ # integrity
205
+ "get_contradictions",
206
+ "mark_contradiction",
207
+ "resolve_contradiction",
208
+ "get_stale_items",
209
+ "get_dependents",
210
+ "cascade_retract",
211
+ "mark_alias",
212
+ "get_aliases",
213
+ "get_alias_group",
214
+ "get_items_by_budget",
215
+ "Contradiction",
216
+ "StaleItem",
217
+ "CascadeResult",
218
+ # bulk
219
+ "apply_many",
220
+ "bulk_adjust_scores",
221
+ "decay_importance",
222
+ "ScoreAdjustment",
223
+ "ItemTransform",
224
+ # replay
225
+ "replay_commands",
226
+ "replay_from_envelopes",
227
+ "ReplayFailure",
228
+ "ReplayResult",
229
+ # envelope
230
+ "wrap_lifecycle_event",
231
+ "wrap_state_event",
232
+ "wrap_edge_state_event",
233
+ # serialization
234
+ "to_json",
235
+ "from_json",
236
+ "stringify",
237
+ "parse",
238
+ "SerializedGraphState",
239
+ # stats
240
+ "get_stats",
241
+ "GraphStats",
242
+ "ItemStats",
243
+ "EdgeStats",
244
+ # intent graph
245
+ "create_intent_state",
246
+ "create_intent",
247
+ "apply_intent_command",
248
+ "get_intents",
249
+ "get_intent_by_id",
250
+ "get_child_intents",
251
+ "Intent",
252
+ "IntentState",
253
+ "IntentStatus",
254
+ "IntentCommand",
255
+ "IntentFilter",
256
+ "IntentLifecycleEvent",
257
+ "IntentResult",
258
+ "IntentNotFoundError",
259
+ "DuplicateIntentError",
260
+ "InvalidIntentTransitionError",
261
+ # task graph
262
+ "create_task_state",
263
+ "create_task",
264
+ "apply_task_command",
265
+ "get_tasks",
266
+ "get_task_by_id",
267
+ "get_tasks_by_intent",
268
+ "get_child_tasks",
269
+ "Task",
270
+ "TaskState",
271
+ "TaskStatus",
272
+ "TaskCommand",
273
+ "TaskFilter",
274
+ "TaskLifecycleEvent",
275
+ "TaskResult",
276
+ "TaskNotFoundError",
277
+ "DuplicateTaskError",
278
+ "InvalidTaskTransitionError",
279
+ # transplant
280
+ "export_slice",
281
+ "import_slice",
282
+ "MemexExport",
283
+ "ExportOptions",
284
+ "ImportOptions",
285
+ "ImportReport",
286
+ "ImportBucket",
287
+ "ImportResult",
288
+ # facade
289
+ "MemexStore",
290
+ # factories
291
+ "create_memory_item",
292
+ "create_edge",
293
+ "create_event_envelope",
294
+ # models
295
+ "MemoryItem",
296
+ "Edge",
297
+ "EventEnvelope",
298
+ "DecayConfig",
299
+ "ScoreWeights",
300
+ "ScoredItem",
301
+ "MemoryFilter",
302
+ "EdgeFilter",
303
+ "SortOption",
304
+ "QueryOptions",
305
+ "Range",
306
+ "ScoreRanges",
307
+ "ParentsFilter",
308
+ "DecayFilter",
309
+ "CreatedFilter",
310
+ "MemoryLifecycleEvent",
311
+ # type aliases
312
+ "KnownMemoryKind",
313
+ "KnownSourceKind",
314
+ "KnownEdgeKind",
315
+ "KnownNamespace",
316
+ "LifecycleEventType",
317
+ "SortField",
318
+ "DecayInterval",
319
+ "DecayType",
320
+ # commands
321
+ "MemoryCommand",
322
+ "MemoryCommandAdapter",
323
+ "MemoryCreate",
324
+ "MemoryUpdate",
325
+ "MemoryRetract",
326
+ "EdgeCreate",
327
+ "EdgeUpdate",
328
+ "EdgeRetract",
329
+ # errors
330
+ "MemexError",
331
+ "MemoryNotFoundError",
332
+ "EdgeNotFoundError",
333
+ "DuplicateMemoryError",
334
+ "DuplicateEdgeError",
335
+ "InvalidTimestampError",
336
+ ]
memex/_time.py ADDED
@@ -0,0 +1,26 @@
1
+ """Internal clock helpers.
2
+
3
+ Isolated in one module so tests can monkeypatch ``now_ms`` / ``now_iso``
4
+ deterministically (the analog of stubbing ``Date.now`` / ``Date.toISOString``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from datetime import datetime, timezone
11
+
12
+
13
+ def now_ms() -> int:
14
+ """Current unix time in milliseconds (mirrors JS ``Date.now()``)."""
15
+ return int(time.time() * 1000)
16
+
17
+
18
+ def now_iso() -> str:
19
+ """Current UTC time as ISO-8601 with millisecond precision and a ``Z`` suffix.
20
+
21
+ Matches JavaScript ``new Date().toISOString()`` byte-for-byte
22
+ (e.g. ``"2024-06-22T13:45:30.123Z"``), which the strict replay parser
23
+ in :mod:`memex.replay` accepts.
24
+ """
25
+ dt = datetime.now(timezone.utc)
26
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}Z"
memex/_uuid.py ADDED
@@ -0,0 +1,62 @@
1
+ """UUIDv7 generation and timestamp extraction.
2
+
3
+ Mirrors the single runtime dependency of the TypeScript library (``uuidv7``)
4
+ with a tiny internal implementation so the only third-party dependency is
5
+ ``pydantic``. UUIDv7 (RFC 9562) encodes a 48-bit big-endian millisecond
6
+ timestamp in its first six bytes; we decode exactly those bytes the same way
7
+ the TS ``safeExtractTimestamp`` does.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from uuid import UUID
14
+
15
+ from . import _time
16
+
17
+ __all__ = ["uuid7", "safe_extract_timestamp"]
18
+
19
+
20
+ def uuid7(ms: int | None = None) -> str:
21
+ """Generate a UUIDv7 string for ``ms`` (defaults to the current time)."""
22
+ if ms is None:
23
+ ms = _time.now_ms()
24
+ ts = ms & ((1 << 48) - 1)
25
+ rand = os.urandom(10)
26
+
27
+ b = bytearray(16)
28
+ b[0] = (ts >> 40) & 0xFF
29
+ b[1] = (ts >> 32) & 0xFF
30
+ b[2] = (ts >> 24) & 0xFF
31
+ b[3] = (ts >> 16) & 0xFF
32
+ b[4] = (ts >> 8) & 0xFF
33
+ b[5] = ts & 0xFF
34
+ b[6] = 0x70 | (rand[0] & 0x0F) # version 7 in the high nibble
35
+ b[7] = rand[1]
36
+ b[8] = 0x80 | (rand[2] & 0x3F) # RFC 4122 variant (0b10) in the top bits
37
+ b[9:16] = rand[3:10]
38
+ return str(UUID(bytes=bytes(b)))
39
+
40
+
41
+ def safe_extract_timestamp(value: str) -> int | None:
42
+ """Decode the millisecond timestamp from a UUIDv7 id.
43
+
44
+ Returns ``None`` for anything that is not a valid version-7 UUID, or whose
45
+ encoded timestamp is non-positive — matching the TS helper's tolerance.
46
+ """
47
+ try:
48
+ parsed = UUID(value)
49
+ except (ValueError, AttributeError, TypeError):
50
+ return None
51
+ if parsed.version != 7:
52
+ return None
53
+ b = parsed.bytes
54
+ ts = (
55
+ (b[0] << 40)
56
+ | (b[1] << 32)
57
+ | (b[2] << 24)
58
+ | (b[3] << 16)
59
+ | (b[4] << 8)
60
+ | b[5]
61
+ )
62
+ return ts if ts > 0 else None
memex/bulk.py ADDED
@@ -0,0 +1,138 @@
1
+ """Bulk operations: single-pass transforms, score adjustments, importance decay."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from . import _time
11
+ from .graph import GraphState
12
+ from .models import Edge, MemoryFilter, MemoryItem, MemoryLifecycleEvent, QueryOptions
13
+ from .query import get_items
14
+ from .reducer import CommandResult, merge_item
15
+
16
+ __all__ = ["ScoreAdjustment", "ItemTransform", "apply_many", "bulk_adjust_scores", "decay_importance"]
17
+
18
+ ItemTransform = Callable[[MemoryItem], "dict[str, Any] | None"]
19
+
20
+
21
+ class ScoreAdjustment(BaseModel):
22
+ authority: float | None = None
23
+ conviction: float | None = None
24
+ importance: float | None = None
25
+
26
+
27
+ def _clamp(value: float) -> float:
28
+ return max(0.0, min(1.0, value))
29
+
30
+
31
+ def apply_many(
32
+ state: GraphState,
33
+ filter: MemoryFilter | dict[str, Any] | None,
34
+ transform: ItemTransform,
35
+ author: str,
36
+ reason: str | None = None,
37
+ options: QueryOptions | dict[str, Any] | None = None,
38
+ ) -> CommandResult:
39
+ """Apply ``transform`` to every matching item in a single pass.
40
+
41
+ ``transform`` returns ``None`` to retract (also cleaning up incident edges),
42
+ an empty dict to skip, or a partial to update. The items dict is cloned once;
43
+ the edges dict and a reverse index are built lazily on the first retract.
44
+ """
45
+ matched = get_items(state, filter, options)
46
+ if not matched:
47
+ return CommandResult(state, [])
48
+
49
+ items = dict(state.items)
50
+ edges: dict[str, Edge] | None = None
51
+ edges_by_endpoint: dict[str, list[str]] | None = None
52
+ all_events: list[MemoryLifecycleEvent] = []
53
+ changed = False
54
+
55
+ for item in matched:
56
+ if item.id not in items:
57
+ continue
58
+
59
+ partial = transform(item)
60
+
61
+ if partial is None:
62
+ del items[item.id]
63
+ all_events.append(
64
+ MemoryLifecycleEvent(type="memory.retracted", item=item, cause_type="memory.retract")
65
+ )
66
+ changed = True
67
+ if state.edges:
68
+ if edges is None:
69
+ edges = dict(state.edges)
70
+ if edges_by_endpoint is None:
71
+ edges_by_endpoint = {}
72
+ for edge_id, edge in state.edges.items():
73
+ edges_by_endpoint.setdefault(edge.from_, []).append(edge_id)
74
+ if edge.from_ != edge.to:
75
+ edges_by_endpoint.setdefault(edge.to, []).append(edge_id)
76
+ incident_ids = edges_by_endpoint.get(item.id)
77
+ if incident_ids:
78
+ for edge_id in incident_ids:
79
+ incident_edge = edges.get(edge_id)
80
+ if incident_edge is None:
81
+ continue # already cleaned by a prior retract
82
+ del edges[edge_id]
83
+ all_events.append(
84
+ MemoryLifecycleEvent(type="edge.retracted", edge=incident_edge, cause_type="memory.retract")
85
+ )
86
+ elif len(partial) > 0:
87
+ merged = merge_item(item, partial)
88
+ items[item.id] = merged
89
+ all_events.append(
90
+ MemoryLifecycleEvent(type="memory.updated", item=merged, cause_type="memory.update")
91
+ )
92
+ changed = True
93
+
94
+ if not changed:
95
+ return CommandResult(state, [])
96
+
97
+ return CommandResult(GraphState(items, edges if edges is not None else state.edges), all_events)
98
+
99
+
100
+ def bulk_adjust_scores(
101
+ state: GraphState,
102
+ criteria: MemoryFilter | dict[str, Any],
103
+ delta: ScoreAdjustment | dict[str, Any],
104
+ author: str,
105
+ reason: str | None = None,
106
+ ) -> CommandResult:
107
+ d = delta if isinstance(delta, ScoreAdjustment) else ScoreAdjustment.model_validate(delta)
108
+
109
+ def transform(item: MemoryItem) -> dict[str, Any]:
110
+ partial: dict[str, Any] = {}
111
+ if d.authority is not None:
112
+ partial["authority"] = _clamp(item.authority + d.authority)
113
+ if d.conviction is not None:
114
+ partial["conviction"] = _clamp((item.conviction or 0) + d.conviction)
115
+ if d.importance is not None:
116
+ partial["importance"] = _clamp((item.importance or 0) + d.importance)
117
+ return partial
118
+
119
+ return apply_many(state, criteria, transform, author, reason)
120
+
121
+
122
+ def decay_importance(
123
+ state: GraphState,
124
+ older_than_ms: int,
125
+ factor: float,
126
+ author: str,
127
+ reason: str | None = None,
128
+ ) -> CommandResult:
129
+ """Decay importance on items created before a cutoff time."""
130
+ cutoff = _time.now_ms() - older_than_ms
131
+
132
+ def transform(item: MemoryItem) -> dict[str, Any]:
133
+ current = item.importance if item.importance is not None else 0
134
+ if current == 0:
135
+ return {}
136
+ return {"importance": _clamp(current * factor)}
137
+
138
+ return apply_many(state, {"created": {"before": cutoff}}, transform, author, reason or "time-based importance decay")
memex/commands.py ADDED
@@ -0,0 +1,75 @@
1
+ """Memory commands as a Pydantic discriminated union.
2
+
3
+ This single module replaces the entire ``schemas.ts`` / Zod layer: in Pydantic
4
+ the command models *are* the schema. ``apply_command`` accepts either a command
5
+ model instance or a plain dict (validated through ``MemoryCommandAdapter``),
6
+ which keeps the TS object-literal call style portable.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Annotated, Any, Literal
12
+
13
+ from pydantic import BaseModel, Field, TypeAdapter
14
+
15
+ from .models import Edge, MemoryItem
16
+
17
+ __all__ = [
18
+ "MemoryCreate",
19
+ "MemoryUpdate",
20
+ "MemoryRetract",
21
+ "EdgeCreate",
22
+ "EdgeUpdate",
23
+ "EdgeRetract",
24
+ "MemoryCommand",
25
+ "MemoryCommandAdapter",
26
+ ]
27
+
28
+
29
+ class MemoryCreate(BaseModel):
30
+ type: Literal["memory.create"] = "memory.create"
31
+ item: MemoryItem
32
+
33
+
34
+ class MemoryUpdate(BaseModel):
35
+ type: Literal["memory.update"] = "memory.update"
36
+ item_id: str
37
+ partial: dict[str, Any]
38
+ author: str
39
+ reason: str | None = None
40
+ basis: dict[str, Any] | None = None
41
+
42
+
43
+ class MemoryRetract(BaseModel):
44
+ type: Literal["memory.retract"] = "memory.retract"
45
+ item_id: str
46
+ author: str
47
+ reason: str | None = None
48
+
49
+
50
+ class EdgeCreate(BaseModel):
51
+ type: Literal["edge.create"] = "edge.create"
52
+ edge: Edge
53
+
54
+
55
+ class EdgeUpdate(BaseModel):
56
+ type: Literal["edge.update"] = "edge.update"
57
+ edge_id: str
58
+ partial: dict[str, Any]
59
+ author: str
60
+ reason: str | None = None
61
+
62
+
63
+ class EdgeRetract(BaseModel):
64
+ type: Literal["edge.retract"] = "edge.retract"
65
+ edge_id: str
66
+ author: str
67
+ reason: str | None = None
68
+
69
+
70
+ MemoryCommand = Annotated[
71
+ MemoryCreate | MemoryUpdate | MemoryRetract | EdgeCreate | EdgeUpdate | EdgeRetract,
72
+ Field(discriminator="type"),
73
+ ]
74
+
75
+ MemoryCommandAdapter: TypeAdapter[MemoryCommand] = TypeAdapter(MemoryCommand)
memex/envelope.py ADDED
@@ -0,0 +1,69 @@
1
+ """Helpers that wrap lifecycle events / state snapshots into event envelopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from . import _time
8
+ from ._uuid import uuid7
9
+ from .models import Edge, EventEnvelope, MemoryItem, MemoryLifecycleEvent
10
+
11
+ __all__ = ["wrap_lifecycle_event", "wrap_state_event", "wrap_edge_state_event"]
12
+
13
+
14
+ def _event_fields(event: MemoryLifecycleEvent) -> dict[str, Any]:
15
+ # Mirror the TS object spread: only keys that are set (item/edge/cause_type
16
+ # may be absent). Nested item/edge are kept as models until serialized.
17
+ fields: dict[str, Any] = {"namespace": event.namespace, "type": event.type}
18
+ if event.item is not None:
19
+ fields["item"] = event.item
20
+ if event.edge is not None:
21
+ fields["edge"] = event.edge
22
+ if event.cause_type is not None:
23
+ fields["cause_type"] = event.cause_type
24
+ return fields
25
+
26
+
27
+ def wrap_lifecycle_event(
28
+ event: MemoryLifecycleEvent,
29
+ cause_id: str,
30
+ trace_id: str | None = None,
31
+ ) -> EventEnvelope[dict[str, Any]]:
32
+ return EventEnvelope(
33
+ id=uuid7(),
34
+ namespace="memory",
35
+ type=event.type,
36
+ ts=_time.now_iso(),
37
+ trace_id=trace_id,
38
+ payload={**_event_fields(event), "cause_id": cause_id},
39
+ )
40
+
41
+
42
+ def wrap_state_event(
43
+ item: MemoryItem,
44
+ cause_id: str,
45
+ trace_id: str | None = None,
46
+ ) -> EventEnvelope[dict[str, Any]]:
47
+ return EventEnvelope(
48
+ id=uuid7(),
49
+ namespace="memory",
50
+ type="state.memory",
51
+ ts=_time.now_iso(),
52
+ trace_id=trace_id,
53
+ payload={"item": item, "cause_id": cause_id},
54
+ )
55
+
56
+
57
+ def wrap_edge_state_event(
58
+ edge: Edge,
59
+ cause_id: str,
60
+ trace_id: str | None = None,
61
+ ) -> EventEnvelope[dict[str, Any]]:
62
+ return EventEnvelope(
63
+ id=uuid7(),
64
+ namespace="memory",
65
+ type="state.edge",
66
+ ts=_time.now_iso(),
67
+ trace_id=trace_id,
68
+ payload={"edge": edge, "cause_id": cause_id},
69
+ )