memex-python 0.13.0__tar.gz → 0.14.0__tar.gz
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_python-0.14.0/API.md +923 -0
- memex_python-0.14.0/PKG-INFO +686 -0
- memex_python-0.14.0/README.md +667 -0
- memex_python-0.14.0/benchmarks/perf.py +234 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/pyproject.toml +1 -1
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/integrity.py +70 -29
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/query.py +31 -11
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/transplant.py +14 -6
- memex_python-0.14.0/tests/test_perf_equivalence.py +307 -0
- memex_python-0.13.0/PKG-INFO +0 -150
- memex_python-0.13.0/README.md +0 -131
- {memex_python-0.13.0 → memex_python-0.14.0}/.github/workflows/release.yml +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/.github/workflows/test.yml +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/.gitignore +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/LICENSE +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/PLAN.md +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/renovate.json +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/__init__.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/_time.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/_uuid.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/bulk.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/commands.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/envelope.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/errors.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/factories.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/graph.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/intent.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/models.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/reducer.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/replay.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/retrieval.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/schemas.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/serialization.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/stats.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/store.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/task.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/support.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_and_coverage.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_bulk_retract_cascade.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_holes.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_regressions.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_reid_ordering.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_surface_contradictions_dedup.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_sweep.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bulk.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_cross_graph_fields.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_edge_cases.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_edge_cases_v2.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_envelope.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_graph.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_helpers.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_integrity.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_intent.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_phase1_foundation.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_query.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_query_advanced.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_reducer.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_replay.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_retrieval.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_serialization.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_setup.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_stats.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_store.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_task.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_transplant.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_types.py +0 -0
- {memex_python-0.13.0 → memex_python-0.14.0}/uv.lock +0 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
# memex-python — API reference
|
|
2
|
+
|
|
3
|
+
Complete reference for the public surface re-exported from the top-level `memex`
|
|
4
|
+
package. Everything below is importable directly:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from memex import create_graph_state, apply_command, get_scored_items, MemexStore
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This document covers the **functional core** (pure `state -> (new_state, events)`
|
|
11
|
+
functions), the typed **Pydantic models**, and the optional **`MemexStore`**
|
|
12
|
+
facade. For the conceptual overview see [`README.md`](README.md); for design
|
|
13
|
+
rationale see [`PLAN.md`](PLAN.md).
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- [Conventions](#conventions)
|
|
18
|
+
- [Core concepts](#core-concepts)
|
|
19
|
+
- [Graph state](#graph-state)
|
|
20
|
+
- [Entities & factories](#entities--factories)
|
|
21
|
+
- [Commands & the reducer](#commands--the-reducer)
|
|
22
|
+
- [Querying & filtering](#querying--filtering)
|
|
23
|
+
- [Scoring, decay & sorting](#scoring-decay--sorting)
|
|
24
|
+
- [Retrieval](#retrieval)
|
|
25
|
+
- [Integrity: contradictions, aliases, stale, cascade](#integrity)
|
|
26
|
+
- [Bulk operations](#bulk-operations)
|
|
27
|
+
- [Intent graph](#intent-graph)
|
|
28
|
+
- [Task graph](#task-graph)
|
|
29
|
+
- [Statistics](#statistics)
|
|
30
|
+
- [Replay](#replay)
|
|
31
|
+
- [Serialization](#serialization)
|
|
32
|
+
- [Event envelopes](#event-envelopes)
|
|
33
|
+
- [Transplant: export / import](#transplant)
|
|
34
|
+
- [Validation](#validation)
|
|
35
|
+
- [`MemexStore` facade](#memexstore-facade)
|
|
36
|
+
- [UUID helpers](#uuid-helpers)
|
|
37
|
+
- [Errors](#errors)
|
|
38
|
+
- [Type aliases](#type-aliases)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Conventions
|
|
43
|
+
|
|
44
|
+
**Pure & immutable.** Every mutation has the shape
|
|
45
|
+
`f(state, ...) -> (new_state, events)`. The input `state` is never modified; a
|
|
46
|
+
fresh `GraphState` is returned with the relevant dict cloned. Entities
|
|
47
|
+
(`MemoryItem`, `Edge`, `Intent`, `Task`) are **frozen** Pydantic models — an
|
|
48
|
+
"edit" produces a new instance via `model_copy(update=...)`.
|
|
49
|
+
|
|
50
|
+
**Dicts or models.** Public functions that take a filter, options, or weights
|
|
51
|
+
accept either a plain `dict` or the typed model; dicts are validated through the
|
|
52
|
+
model. The examples mix both styles freely.
|
|
53
|
+
|
|
54
|
+
**Validation is always on.** Constructing any model with an out-of-range score
|
|
55
|
+
(`authority`/`conviction`/`importance`/`weight`/`priority` must be `0..1`) raises
|
|
56
|
+
`pydantic.ValidationError`. Use `Model.model_construct(...)` to deliberately
|
|
57
|
+
bypass validation (e.g. test fixtures).
|
|
58
|
+
|
|
59
|
+
**Keyword-only constructors.** All `create_*` factories take keyword arguments
|
|
60
|
+
only.
|
|
61
|
+
|
|
62
|
+
**Reserved-word fields.** `from`, `not`, and `or` are Python keywords, so the
|
|
63
|
+
attributes are `from_`, `not_`, and `or_`; each serializes to/accepts its bare
|
|
64
|
+
JSON alias (`"from"`, `"not"`, `"or"`).
|
|
65
|
+
|
|
66
|
+
**Wire compatibility.** Command tags (`"memory.create"`), enum values
|
|
67
|
+
(`"derived_deterministic"`, `"DERIVED_FROM"`), and JSON keys are byte-identical
|
|
68
|
+
to the TypeScript `@ai2070/memex`, so a Python and a TS service can share one
|
|
69
|
+
event log.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Core concepts
|
|
74
|
+
|
|
75
|
+
Three independent graphs, each driven by the same `commands -> reducer ->
|
|
76
|
+
lifecycle events` pattern:
|
|
77
|
+
|
|
78
|
+
| Graph | State | Core type | Reducer | Namespace |
|
|
79
|
+
|--------|--------------|--------------|--------------------------|------------|
|
|
80
|
+
| Memory | `GraphState` | `MemoryItem` | `apply_command` | `"memory"` |
|
|
81
|
+
| Intent | `IntentState`| `Intent` | `apply_intent_command` | `"intent"` |
|
|
82
|
+
| Task | `TaskState` | `Task` | `apply_task_command` | `"task"` |
|
|
83
|
+
|
|
84
|
+
Each `MemoryItem` carries three orthogonal `0..1` scores — **authority** (trust),
|
|
85
|
+
**conviction** (author confidence), **importance** (current salience) — plus
|
|
86
|
+
provenance (`parents`) and typed `edges`.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from memex import create_graph_state, create_memory_item, apply_command, get_scored_items
|
|
90
|
+
|
|
91
|
+
state = create_graph_state()
|
|
92
|
+
item = create_memory_item(
|
|
93
|
+
scope="user:laz/general", kind="observation",
|
|
94
|
+
content={"key": "login_count", "value": 42},
|
|
95
|
+
author="agent:monitor", source_kind="observed",
|
|
96
|
+
authority=0.9, importance=0.7,
|
|
97
|
+
)
|
|
98
|
+
state, events = apply_command(state, {"type": "memory.create", "item": item})
|
|
99
|
+
top = get_scored_items(state, {"authority": 1.0, "importance": 0.5})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Graph state
|
|
105
|
+
|
|
106
|
+
### `class GraphState`
|
|
107
|
+
|
|
108
|
+
Frozen dataclass holding the memory graph. Not a Pydantic model (re-validating
|
|
109
|
+
every item per command would be unusably slow).
|
|
110
|
+
|
|
111
|
+
| Field | Type | Description |
|
|
112
|
+
|---------|-------------------------|------------------------|
|
|
113
|
+
| `items` | `dict[str, MemoryItem]` | id → item (insertion-ordered) |
|
|
114
|
+
| `edges` | `dict[str, Edge]` | id → edge (insertion-ordered) |
|
|
115
|
+
|
|
116
|
+
### `create_graph_state() -> GraphState`
|
|
117
|
+
|
|
118
|
+
A new empty memory graph.
|
|
119
|
+
|
|
120
|
+
### `clone_graph_state(state: GraphState) -> GraphState`
|
|
121
|
+
|
|
122
|
+
Shallow clone — new dicts, shared (immutable) item/edge instances.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Entities & factories
|
|
127
|
+
|
|
128
|
+
### `class MemoryItem`
|
|
129
|
+
|
|
130
|
+
Frozen. The core memory node.
|
|
131
|
+
|
|
132
|
+
| Field | Type | Notes |
|
|
133
|
+
|---------------|-----------------------|-----------------------------------------|
|
|
134
|
+
| `id` | `str` | usually a UUIDv7 |
|
|
135
|
+
| `scope` | `str` | namespacing key, e.g. `"user:laz/general"` |
|
|
136
|
+
| `kind` | `str` | open union; see [`KnownMemoryKind`](#type-aliases) |
|
|
137
|
+
| `content` | `dict[str, Any]` | arbitrary payload |
|
|
138
|
+
| `author` | `str` | |
|
|
139
|
+
| `source_kind` | `str` | open union; see [`KnownSourceKind`](#type-aliases) |
|
|
140
|
+
| `parents` | `list[str] \| None` | provenance ids |
|
|
141
|
+
| `authority` | `float` (0..1) | required |
|
|
142
|
+
| `conviction` | `float \| None` (0..1)| |
|
|
143
|
+
| `importance` | `float \| None` (0..1)| |
|
|
144
|
+
| `created_at` | `int \| None` | ms epoch; falls back to the UUIDv7 ts |
|
|
145
|
+
| `intent_id` | `str \| None` | cross-graph link |
|
|
146
|
+
| `task_id` | `str \| None` | cross-graph link |
|
|
147
|
+
| `meta` | `dict[str, Any] \| None` | |
|
|
148
|
+
|
|
149
|
+
### `class Edge`
|
|
150
|
+
|
|
151
|
+
Frozen, `populate_by_name=True`.
|
|
152
|
+
|
|
153
|
+
| Field | Type | Notes |
|
|
154
|
+
|---------------|-----------------------|-----------------------------------------|
|
|
155
|
+
| `edge_id` | `str` | |
|
|
156
|
+
| `from_` | `str` | JSON alias `"from"` |
|
|
157
|
+
| `to` | `str` | |
|
|
158
|
+
| `kind` | `str` | see [`KnownEdgeKind`](#type-aliases) |
|
|
159
|
+
| `weight` | `float \| None` (0..1)| |
|
|
160
|
+
| `author` | `str` | |
|
|
161
|
+
| `source_kind` | `str` | |
|
|
162
|
+
| `authority` | `float` (0..1) | required |
|
|
163
|
+
| `active` | `bool` | |
|
|
164
|
+
| `meta` | `dict[str, Any] \| None` | |
|
|
165
|
+
|
|
166
|
+
### `create_memory_item(...) -> MemoryItem`
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
create_memory_item(*, scope, kind, content, author, source_kind, authority,
|
|
170
|
+
id=None, parents=None, conviction=None, importance=None,
|
|
171
|
+
created_at=None, intent_id=None, task_id=None, meta=None)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Mints `id` (UUIDv7) when omitted. `created_at` is the explicit value, else the
|
|
175
|
+
id's UUIDv7 timestamp, else the wall clock. Validates score bounds.
|
|
176
|
+
|
|
177
|
+
### `create_edge(...) -> Edge`
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
create_edge(*, from_, to, kind, author, source_kind, authority,
|
|
181
|
+
edge_id=None, active=None, weight=None, meta=None)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`active` defaults to `True`, `edge_id` to a fresh UUIDv7.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Commands & the reducer
|
|
189
|
+
|
|
190
|
+
Commands are a Pydantic discriminated union keyed on `type`. `apply_command`
|
|
191
|
+
accepts a command **model** or a plain **dict** (validated via
|
|
192
|
+
`MemoryCommandAdapter`).
|
|
193
|
+
|
|
194
|
+
### Memory commands
|
|
195
|
+
|
|
196
|
+
| Model | `type` | Fields (besides `type`) |
|
|
197
|
+
|-----------------|--------------------|-----------------------------------------------|
|
|
198
|
+
| `MemoryCreate` | `"memory.create"` | `item: MemoryItem` |
|
|
199
|
+
| `MemoryUpdate` | `"memory.update"` | `item_id`, `partial: dict`, `author`, `reason?`, `basis?` |
|
|
200
|
+
| `MemoryRetract` | `"memory.retract"` | `item_id`, `author`, `reason?` |
|
|
201
|
+
| `EdgeCreate` | `"edge.create"` | `edge: Edge` |
|
|
202
|
+
| `EdgeUpdate` | `"edge.update"` | `edge_id`, `partial: dict`, `author`, `reason?` |
|
|
203
|
+
| `EdgeRetract` | `"edge.retract"` | `edge_id`, `author`, `reason?` |
|
|
204
|
+
|
|
205
|
+
- `MemoryCommand` — the `Annotated[Union[...], discriminator="type"]` alias.
|
|
206
|
+
- `MemoryCommandAdapter` — `TypeAdapter[MemoryCommand]` for validating raw dicts.
|
|
207
|
+
|
|
208
|
+
### `apply_command(state, cmd) -> CommandResult`
|
|
209
|
+
|
|
210
|
+
`cmd: MemoryCommand | dict`. Returns a `CommandResult` NamedTuple
|
|
211
|
+
`(state: GraphState, events: list[MemoryLifecycleEvent])`. The result is
|
|
212
|
+
iterable, so `state, events = apply_command(...)` works.
|
|
213
|
+
|
|
214
|
+
Behavior per command:
|
|
215
|
+
|
|
216
|
+
- **create** — raises `DuplicateMemoryError` / `DuplicateEdgeError` on id clash.
|
|
217
|
+
- **update** — shallow-merges `partial` (for `content`/`meta`, merges keys; keys
|
|
218
|
+
cannot be deleted via update); `id`/`created_at` are immutable for items,
|
|
219
|
+
`edge_id`/`from`/`to` for edges. Raises `MemoryNotFoundError` /
|
|
220
|
+
`EdgeNotFoundError` if absent. Updates do **not** re-validate scores.
|
|
221
|
+
- **retract (memory)** — removes the item *and* every incident edge, emitting one
|
|
222
|
+
`edge.retracted` per removed edge.
|
|
223
|
+
|
|
224
|
+
Emits [`MemoryLifecycleEvent`](#class-memorylifecycleevent)s.
|
|
225
|
+
|
|
226
|
+
### `merge_item(existing: MemoryItem, partial: dict) -> MemoryItem`
|
|
227
|
+
### `merge_edge(existing: Edge, partial: dict) -> Edge`
|
|
228
|
+
|
|
229
|
+
The merge primitives used by the reducer (no validation, mirroring the TS
|
|
230
|
+
"updates don't validate" guarantee). Exposed for advanced/bulk callers.
|
|
231
|
+
|
|
232
|
+
### `class MemoryLifecycleEvent`
|
|
233
|
+
|
|
234
|
+
| Field | Type | Notes |
|
|
235
|
+
|--------------|-------------------------------|-------------------------------|
|
|
236
|
+
| `namespace` | `Literal["memory"]` | always `"memory"` |
|
|
237
|
+
| `type` | [`LifecycleEventType`](#type-aliases) | e.g. `"memory.created"` |
|
|
238
|
+
| `item` | `MemoryItem \| None` | set for memory events |
|
|
239
|
+
| `edge` | `Edge \| None` | set for edge events |
|
|
240
|
+
| `cause_type` | `str \| None` | the command tag that caused it|
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Querying & filtering
|
|
245
|
+
|
|
246
|
+
### `get_items(state, filter=None, options=None) -> list[MemoryItem]`
|
|
247
|
+
|
|
248
|
+
Filter, then sort, then page. `filter: MemoryFilter | dict | None`,
|
|
249
|
+
`options: QueryOptions | dict | None`. With no filter, returns all items in
|
|
250
|
+
insertion order.
|
|
251
|
+
|
|
252
|
+
### `get_item_by_id(state, id) -> MemoryItem | None`
|
|
253
|
+
|
|
254
|
+
### `matches_filter(item, f: MemoryFilter) -> bool`
|
|
255
|
+
|
|
256
|
+
The predicate behind `get_items` — useful for ad-hoc filtering.
|
|
257
|
+
|
|
258
|
+
### `class MemoryFilter`
|
|
259
|
+
|
|
260
|
+
All present conditions are AND-combined (except `or_`). `populate_by_name=True`.
|
|
261
|
+
|
|
262
|
+
| Field | Type | Matches when… |
|
|
263
|
+
|----------------|----------------------------|--------------------------------------------------|
|
|
264
|
+
| `ids` | `list[str]` | `item.id` is in the list |
|
|
265
|
+
| `scope` | `str` | exact scope |
|
|
266
|
+
| `scope_prefix` | `str` | `item.scope.startswith(...)` |
|
|
267
|
+
| `author` | `str` | exact |
|
|
268
|
+
| `kind` | `str` | exact |
|
|
269
|
+
| `source_kind` | `str` | exact |
|
|
270
|
+
| `range` | `ScoreRanges` | each score within its `Range` |
|
|
271
|
+
| `intent_id` | `str` | exact |
|
|
272
|
+
| `intent_ids` | `list[str]` | `item.intent_id` in the list |
|
|
273
|
+
| `task_id` | `str` | exact |
|
|
274
|
+
| `task_ids` | `list[str]` | `item.task_id` in the list |
|
|
275
|
+
| `has_parent` | `str` | id is in `item.parents` |
|
|
276
|
+
| `is_root` | `bool` | `True`: no parents; `False`: has parents |
|
|
277
|
+
| `parents` | `ParentsFilter` | see below |
|
|
278
|
+
| `decay` | `DecayFilter` | decay multiplier ≥ `min` |
|
|
279
|
+
| `created` | `CreatedFilter` | timestamp window |
|
|
280
|
+
| `not_` | `MemoryFilter` (alias `not`) | sub-filter does **not** match |
|
|
281
|
+
| `meta` | `dict[str, Any]` | each dotted path equals the value |
|
|
282
|
+
| `meta_has` | `list[str]` | each dotted path exists |
|
|
283
|
+
| `or_` | `list[MemoryFilter]` (alias `or`) | at least one sub-filter matches |
|
|
284
|
+
|
|
285
|
+
Supporting models:
|
|
286
|
+
|
|
287
|
+
- **`Range`** — `min: float | None`, `max: float | None` (inclusive bounds).
|
|
288
|
+
- **`ScoreRanges`** — `authority`, `conviction`, `importance`, each a `Range`.
|
|
289
|
+
- **`ParentsFilter`** — `includes: str`, `includes_any: list[str]`,
|
|
290
|
+
`includes_all: list[str]`, `count: Range` (range over the number of parents).
|
|
291
|
+
- **`DecayFilter`** — `config: DecayConfig`, `min: float` (0..1).
|
|
292
|
+
- **`CreatedFilter`** — `before: int | None` (exclusive upper, `ts < before`),
|
|
293
|
+
`after: int | None` (inclusive lower, `ts >= after`); both ms epoch.
|
|
294
|
+
|
|
295
|
+
`meta`/`meta_has` paths are dotted (`"a.b.c"`) and walk nested dicts.
|
|
296
|
+
|
|
297
|
+
### `class QueryOptions`
|
|
298
|
+
|
|
299
|
+
| Field | Type | Notes |
|
|
300
|
+
|----------|-----------------------------------|--------------------------------|
|
|
301
|
+
| `sort` | `SortOption \| list[SortOption] \| None` | multi-key, applied in order |
|
|
302
|
+
| `limit` | `int \| None` (≥0) | applied after sort |
|
|
303
|
+
| `offset` | `int \| None` (≥0) | applied after sort |
|
|
304
|
+
|
|
305
|
+
### `class SortOption`
|
|
306
|
+
|
|
307
|
+
`field: "authority" | "conviction" | "importance" | "recency"`,
|
|
308
|
+
`order: "asc" | "desc"`. `recency` sorts by item timestamp. Ties preserve
|
|
309
|
+
insertion order (stable). An unknown field raises `ValueError`.
|
|
310
|
+
|
|
311
|
+
### Edges
|
|
312
|
+
|
|
313
|
+
- **`get_edges(state, filter=None) -> list[Edge]`** — `filter: EdgeFilter | dict | None`.
|
|
314
|
+
When no filter is given, only **active** edges are returned.
|
|
315
|
+
- **`get_edge_by_id(state, edge_id) -> Edge | None`**
|
|
316
|
+
|
|
317
|
+
#### `class EdgeFilter`
|
|
318
|
+
|
|
319
|
+
| Field | Type | Notes |
|
|
320
|
+
|--------------|-----------------|--------------------------------------|
|
|
321
|
+
| `from_` | `str` (alias `from`) | exact source |
|
|
322
|
+
| `to` | `str` | exact target |
|
|
323
|
+
| `kind` | `str` | exact |
|
|
324
|
+
| `min_weight` | `float` | `edge.weight >= min_weight` |
|
|
325
|
+
| `active_only`| `bool \| None` | default `True`; `False` includes retracted edges |
|
|
326
|
+
|
|
327
|
+
### Navigation
|
|
328
|
+
|
|
329
|
+
- **`get_parents(state, item_id) -> list[MemoryItem]`** — resolves `item.parents`
|
|
330
|
+
to the items that exist.
|
|
331
|
+
- **`get_children(state, item_id) -> list[MemoryItem]`** — items listing `item_id`
|
|
332
|
+
in their `parents`.
|
|
333
|
+
- **`get_related_items(state, item_id, direction="both") -> list[MemoryItem]`** —
|
|
334
|
+
items connected by active edges. `direction: "from" | "to" | "both"`. Returns
|
|
335
|
+
a deduplicated, insertion-ordered list excluding the item itself.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Scoring, decay & sorting
|
|
340
|
+
|
|
341
|
+
### `class ScoreWeights`
|
|
342
|
+
|
|
343
|
+
Multipliers (intentionally unbounded — not `0..1`).
|
|
344
|
+
|
|
345
|
+
| Field | Type | Notes |
|
|
346
|
+
|--------------|-----------------------|--------------------------------|
|
|
347
|
+
| `authority` | `float \| None` | weight on `item.authority` |
|
|
348
|
+
| `conviction` | `float \| None` | weight on `item.conviction` |
|
|
349
|
+
| `importance` | `float \| None` | weight on `item.importance` |
|
|
350
|
+
| `decay` | `DecayConfig \| None` | if set, multiplies the score |
|
|
351
|
+
|
|
352
|
+
### `class DecayConfig`
|
|
353
|
+
|
|
354
|
+
| Field | Type | Notes |
|
|
355
|
+
|------------|----------------|-----------------------------------------|
|
|
356
|
+
| `rate` | `float` (0..1) | per-interval decay rate |
|
|
357
|
+
| `interval` | `str` | `"hour"` \| `"day"` \| `"week"` |
|
|
358
|
+
| `type` | `str` | `"exponential"` \| `"linear"` \| `"step"` |
|
|
359
|
+
|
|
360
|
+
### `compute_decay_multiplier(item, decay: DecayConfig) -> float`
|
|
361
|
+
|
|
362
|
+
`intervals = age_ms / interval_ms`, where `age_ms = now - item_timestamp`.
|
|
363
|
+
|
|
364
|
+
- **exponential** → `(1 - rate) ** intervals`
|
|
365
|
+
- **linear** → `max(0, 1 - rate * intervals)`
|
|
366
|
+
- **step** → `(1 - rate) ** floor(intervals)`
|
|
367
|
+
|
|
368
|
+
Future-dated items (age ≤ 0, clock skew) return `1.0`. Unknown `interval`/`type`
|
|
369
|
+
raises `ValueError`.
|
|
370
|
+
|
|
371
|
+
### `compute_score(item, weights: ScoreWeights) -> float`
|
|
372
|
+
|
|
373
|
+
`authority*w.authority + conviction*w.conviction + importance*w.importance`,
|
|
374
|
+
then `* compute_decay_multiplier(...)` if `weights.decay` is set. Missing weights
|
|
375
|
+
and missing scores are treated as `0`.
|
|
376
|
+
|
|
377
|
+
### `get_scored_items(state, weights, options=None) -> list[ScoredItem]`
|
|
378
|
+
|
|
379
|
+
`weights: ScoreWeights | dict`, `options: ScoredQueryOptions | dict | None`.
|
|
380
|
+
Pipeline: `pre`-filter → score → sort by score descending → `min_score` →
|
|
381
|
+
`post`-filter → `offset`/`limit`.
|
|
382
|
+
|
|
383
|
+
#### `class ScoredQueryOptions`
|
|
384
|
+
|
|
385
|
+
| Field | Type | Notes |
|
|
386
|
+
|-------------|----------------------|------------------------------------|
|
|
387
|
+
| `pre` | `MemoryFilter \| None` | filter applied before scoring |
|
|
388
|
+
| `post` | `MemoryFilter \| None` | filter applied after scoring |
|
|
389
|
+
| `min_score` | `float \| None` | drop items scoring below this |
|
|
390
|
+
| `limit` | `int \| None` | |
|
|
391
|
+
| `offset` | `int \| None` | |
|
|
392
|
+
|
|
393
|
+
#### `class ScoredItem`
|
|
394
|
+
|
|
395
|
+
`item: MemoryItem`, `score: float`, `contradicted_by: list[MemoryItem] | None`.
|
|
396
|
+
Not frozen — `surface_contradictions` annotates `contradicted_by` in place.
|
|
397
|
+
|
|
398
|
+
### `extract_timestamp(uuid_id: str) -> int`
|
|
399
|
+
|
|
400
|
+
Extract the ms timestamp from a UUIDv7 id. Raises `InvalidTimestampError` on
|
|
401
|
+
anything that isn't a valid v7 UUID.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Retrieval
|
|
406
|
+
|
|
407
|
+
### Provenance walks
|
|
408
|
+
|
|
409
|
+
- **`get_support_tree(state, item_id) -> SupportNode | None`** — full provenance
|
|
410
|
+
tree, deduplicating on cycles. Returns `None` if the item is absent.
|
|
411
|
+
- **`get_support_set(state, item_id) -> list[MemoryItem]`** — flattened set of
|
|
412
|
+
items that justify a claim.
|
|
413
|
+
- **`class SupportNode`** — dataclass `item: MemoryItem`, `parents: list[SupportNode]`.
|
|
414
|
+
|
|
415
|
+
### Contradiction policies
|
|
416
|
+
|
|
417
|
+
Both remove **superseded** items (targets of active `SUPERSEDES` edges).
|
|
418
|
+
|
|
419
|
+
- **`filter_contradictions(state, scored) -> list[ScoredItem]`** — drops the
|
|
420
|
+
lower-scoring side of each unresolved `CONTRADICTS` pair (deterministic
|
|
421
|
+
tie-breaks by score then `edge_id`).
|
|
422
|
+
- **`surface_contradictions(state, scored) -> list[ScoredItem]`** — keeps both
|
|
423
|
+
sides, annotating each via `contradicted_by`. Self-edges are ignored.
|
|
424
|
+
|
|
425
|
+
### Diversity
|
|
426
|
+
|
|
427
|
+
- **`class DiversityOptions`** — `author_penalty`, `parent_penalty`,
|
|
428
|
+
`source_penalty` (each `float | None`).
|
|
429
|
+
- **`apply_diversity(scored, options) -> list[ScoredItem]`** — subtracts a
|
|
430
|
+
per-duplicate penalty (cumulative per repeated author / parent / source),
|
|
431
|
+
clamps at `0`, and re-sorts by score descending.
|
|
432
|
+
|
|
433
|
+
### `smart_retrieve(...) -> list[ScoredItem]`
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
smart_retrieve(state, *, budget, cost_fn, weights,
|
|
437
|
+
filter=None, contradictions=None, diversity=None)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Score → contradiction policy → diversity → greedy budget pack. `cost_fn:
|
|
441
|
+
Callable[[MemoryItem], float]` must return a finite, non-negative number
|
|
442
|
+
(otherwise `ValueError`). `contradictions: "filter" | "surface" | None`.
|
|
443
|
+
`diversity: DiversityOptions | dict | None`. Greedily appends items whose cost
|
|
444
|
+
fits the remaining `budget`.
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
packed = smart_retrieve(
|
|
448
|
+
state, budget=2000, cost_fn=lambda i: len(str(i.content)),
|
|
449
|
+
weights={"authority": 0.6, "importance": 0.4},
|
|
450
|
+
contradictions="surface", diversity={"author_penalty": 0.2},
|
|
451
|
+
)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### `get_items_by_budget(...) -> list[ScoredItem]`
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
get_items_by_budget(state, *, budget, cost_fn, weights, filter=None)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The budget-pack step without contradiction/diversity passes.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Integrity
|
|
465
|
+
|
|
466
|
+
Contradiction & alias management, stale detection, and cascade retraction.
|
|
467
|
+
|
|
468
|
+
### Contradictions
|
|
469
|
+
|
|
470
|
+
- **`get_contradictions(state) -> list[Contradiction]`** — active `CONTRADICTS`
|
|
471
|
+
pairs whose endpoints both exist.
|
|
472
|
+
- **`mark_contradiction(state, item_id_a, item_id_b, author, meta=None) -> CommandResult`**
|
|
473
|
+
— creates a `CONTRADICTS` edge.
|
|
474
|
+
- **`resolve_contradiction(state, winner_id, loser_id, author, reason=None) -> CommandResult`**
|
|
475
|
+
— retracts the `CONTRADICTS` edge(s) between the pair, adds a `SUPERSEDES`
|
|
476
|
+
edge (winner → loser), and drops the loser's `authority` to 10%. A stale call
|
|
477
|
+
with no matching edge is a no-op.
|
|
478
|
+
- **`class Contradiction`** — `a: MemoryItem`, `b: MemoryItem`, `edge: Edge | None`.
|
|
479
|
+
|
|
480
|
+
### Stale items & dependents
|
|
481
|
+
|
|
482
|
+
- **`get_stale_items(state) -> list[StaleItem]`** — items whose `parents`
|
|
483
|
+
reference ids no longer present.
|
|
484
|
+
- **`class StaleItem`** — `item: MemoryItem`, `missing_parents: list[str]`.
|
|
485
|
+
- **`get_dependents(state, item_id, transitive=False) -> list[MemoryItem]`** —
|
|
486
|
+
direct children, or the whole dependent subtree when `transitive=True`
|
|
487
|
+
(cycle-safe).
|
|
488
|
+
|
|
489
|
+
### Cascade retraction
|
|
490
|
+
|
|
491
|
+
- **`cascade_retract(state, item_id, author, reason=None) -> CascadeResult`** —
|
|
492
|
+
retracts an item and all transitive dependents in post-order (leaves first),
|
|
493
|
+
cleaning incident edges. Cycle- and DAG-safe; iterative (no recursion limit).
|
|
494
|
+
- **`class CascadeResult`** — `state: GraphState`,
|
|
495
|
+
`events: list[MemoryLifecycleEvent]`, `retracted: list[str]` (ids in
|
|
496
|
+
retraction order).
|
|
497
|
+
|
|
498
|
+
### Aliases (identity)
|
|
499
|
+
|
|
500
|
+
- **`mark_alias(state, item_id_a, item_id_b, author, meta=None) -> CommandResult`**
|
|
501
|
+
— creates bidirectional `ALIAS` edges. Self-alias is a no-op.
|
|
502
|
+
- **`get_aliases(state, item_id) -> list[MemoryItem]`** — direct alias targets.
|
|
503
|
+
- **`get_alias_group(state, item_id) -> list[MemoryItem]`** — the full connected
|
|
504
|
+
alias component (transitive closure), including the item itself.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Bulk operations
|
|
509
|
+
|
|
510
|
+
Single-pass transforms that clone the state once instead of per command.
|
|
511
|
+
|
|
512
|
+
### `apply_many(state, filter, transform, author, reason=None, options=None) -> CommandResult`
|
|
513
|
+
|
|
514
|
+
Applies `transform: Callable[[MemoryItem], dict | None]` to every item matching
|
|
515
|
+
`filter` (with optional `QueryOptions`). The transform returns:
|
|
516
|
+
|
|
517
|
+
- `None` → **retract** the item (and clean incident edges),
|
|
518
|
+
- an **empty dict** → skip,
|
|
519
|
+
- a **partial dict** → update.
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
ItemTransform = Callable[[MemoryItem], dict[str, Any] | None]
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### `bulk_adjust_scores(state, criteria, delta, author, reason=None) -> CommandResult`
|
|
526
|
+
|
|
527
|
+
Adds a `ScoreAdjustment` to matching items, clamping each result to `0..1`.
|
|
528
|
+
|
|
529
|
+
- **`class ScoreAdjustment`** — `authority`, `conviction`, `importance` (each
|
|
530
|
+
`float | None`); only the provided deltas are applied.
|
|
531
|
+
|
|
532
|
+
### `decay_importance(state, older_than_ms, factor, author, reason=None) -> CommandResult`
|
|
533
|
+
|
|
534
|
+
Multiplies `importance` by `factor` for items created before
|
|
535
|
+
`now - older_than_ms`. Items with zero/absent importance are skipped.
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Intent graph
|
|
540
|
+
|
|
541
|
+
Active goals with a status machine: `active ⇄ paused → completed / cancelled`.
|
|
542
|
+
|
|
543
|
+
### `class Intent`
|
|
544
|
+
|
|
545
|
+
Frozen. `id`, `parent_id?`, `label`, `description?`, `priority` (0..1), `owner`,
|
|
546
|
+
`status: IntentStatus`, `context?`, `root_memory_ids?`, `meta?`.
|
|
547
|
+
|
|
548
|
+
- **`IntentStatus`** — `"active" | "paused" | "completed" | "cancelled"`.
|
|
549
|
+
- **`class IntentState`** — frozen dataclass `intents: dict[str, Intent]`.
|
|
550
|
+
|
|
551
|
+
### State & factories
|
|
552
|
+
|
|
553
|
+
- **`create_intent_state() -> IntentState`**
|
|
554
|
+
- **`create_intent(*, label, priority, owner, id=None, parent_id=None,
|
|
555
|
+
description=None, status=None, context=None, root_memory_ids=None, meta=None)
|
|
556
|
+
-> Intent`** — `status` defaults to `"active"`.
|
|
557
|
+
|
|
558
|
+
### `apply_intent_command(state, cmd) -> IntentResult`
|
|
559
|
+
|
|
560
|
+
`cmd: IntentCommand | dict`. `IntentResult` is `(state: IntentState,
|
|
561
|
+
events: list[IntentLifecycleEvent])`.
|
|
562
|
+
|
|
563
|
+
| `type` | Fields | Effect |
|
|
564
|
+
|---------------------|------------------------------------------|---------------------------------|
|
|
565
|
+
| `"intent.create"` | `intent: Intent` | add (dup → `DuplicateIntentError`) |
|
|
566
|
+
| `"intent.update"` | `intent_id`, `partial`, `author`, `reason?` | merge (`id`/`status` ignored) |
|
|
567
|
+
| `"intent.complete"` | `intent_id`, `author`, `reason?` | → `completed` (from active/paused) |
|
|
568
|
+
| `"intent.cancel"` | `intent_id`, `author`, `reason?` | → `cancelled` (from active/paused) |
|
|
569
|
+
| `"intent.pause"` | `intent_id`, `author`, `reason?` | → `paused` (from active) |
|
|
570
|
+
| `"intent.resume"` | `intent_id`, `author`, `reason?` | → `active` (from paused) |
|
|
571
|
+
|
|
572
|
+
Invalid transitions raise `InvalidIntentTransitionError`; missing id raises
|
|
573
|
+
`IntentNotFoundError`. `IntentCommand` is the discriminated-union alias.
|
|
574
|
+
|
|
575
|
+
### Queries
|
|
576
|
+
|
|
577
|
+
- **`get_intents(state, filter=None) -> list[Intent]`** — `filter: IntentFilter | dict | None`.
|
|
578
|
+
- **`get_intent_by_id(state, id) -> Intent | None`**
|
|
579
|
+
- **`get_child_intents(state, parent_id) -> list[Intent]`**
|
|
580
|
+
|
|
581
|
+
#### `class IntentFilter`
|
|
582
|
+
|
|
583
|
+
`owner`, `status`, `statuses: list[IntentStatus]`, `min_priority`,
|
|
584
|
+
`has_memory_id` (in `root_memory_ids`), `parent_id`, `is_root` (bool).
|
|
585
|
+
|
|
586
|
+
### Events
|
|
587
|
+
|
|
588
|
+
- **`class IntentLifecycleEvent`** — `namespace="intent"`, `type`
|
|
589
|
+
(`"intent.created"` …), `intent: Intent`, `cause_type: str`.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Task graph
|
|
594
|
+
|
|
595
|
+
Units of work tied to intents: `pending → running → completed`, with
|
|
596
|
+
`running → failed → running` retry and `cancel` from any non-terminal state.
|
|
597
|
+
|
|
598
|
+
### `class Task`
|
|
599
|
+
|
|
600
|
+
Frozen. `id`, `intent_id`, `parent_id?`, `action`, `label?`, `status:
|
|
601
|
+
TaskStatus`, `priority` (0..1), `context?`, `result?`, `error?`,
|
|
602
|
+
`input_memory_ids?`, `output_memory_ids?`, `agent_id?`, `attempt?`, `meta?`.
|
|
603
|
+
|
|
604
|
+
- **`TaskStatus`** — `"pending" | "running" | "completed" | "failed" | "cancelled"`.
|
|
605
|
+
- **`class TaskState`** — frozen dataclass `tasks: dict[str, Task]`.
|
|
606
|
+
|
|
607
|
+
### State & factories
|
|
608
|
+
|
|
609
|
+
- **`create_task_state() -> TaskState`**
|
|
610
|
+
- **`create_task(*, intent_id, action, priority, id=None, parent_id=None,
|
|
611
|
+
label=None, status=None, context=None, result=None, error=None,
|
|
612
|
+
input_memory_ids=None, output_memory_ids=None, agent_id=None, attempt=None,
|
|
613
|
+
meta=None) -> Task`** — `status` defaults to `"pending"`, `attempt` to `0`.
|
|
614
|
+
|
|
615
|
+
### `apply_task_command(state, cmd) -> TaskResult`
|
|
616
|
+
|
|
617
|
+
`cmd: TaskCommand | dict`. `TaskResult` is `(state: TaskState,
|
|
618
|
+
events: list[TaskLifecycleEvent])`.
|
|
619
|
+
|
|
620
|
+
| `type` | Fields | Effect |
|
|
621
|
+
|-------------------|------------------------------------------|-------------------------------------|
|
|
622
|
+
| `"task.create"` | `task: Task` | add (dup → `DuplicateTaskError`) |
|
|
623
|
+
| `"task.update"` | `task_id`, `partial`, `author` | merge (`id`/`status` ignored) |
|
|
624
|
+
| `"task.start"` | `task_id`, `agent_id?` | → `running` (from pending/failed), `attempt++` |
|
|
625
|
+
| `"task.complete"` | `task_id`, `result?`, `output_memory_ids?` | → `completed` (from running) |
|
|
626
|
+
| `"task.fail"` | `task_id`, `error`, `retryable?` | → `failed` (from running) |
|
|
627
|
+
| `"task.cancel"` | `task_id`, `reason?` | → `cancelled` (from non-terminal) |
|
|
628
|
+
|
|
629
|
+
Invalid transitions raise `InvalidTaskTransitionError`; missing id raises
|
|
630
|
+
`TaskNotFoundError`. `TaskCommand` is the discriminated-union alias.
|
|
631
|
+
|
|
632
|
+
### Queries
|
|
633
|
+
|
|
634
|
+
- **`get_tasks(state, filter=None) -> list[Task]`** — `filter: TaskFilter | dict | None`.
|
|
635
|
+
- **`get_task_by_id(state, id) -> Task | None`**
|
|
636
|
+
- **`get_tasks_by_intent(state, intent_id) -> list[Task]`**
|
|
637
|
+
- **`get_child_tasks(state, parent_id) -> list[Task]`**
|
|
638
|
+
|
|
639
|
+
#### `class TaskFilter`
|
|
640
|
+
|
|
641
|
+
`intent_id`, `action`, `status`, `statuses: list[TaskStatus]`, `agent_id`,
|
|
642
|
+
`min_priority`, `has_input_memory_id`, `has_output_memory_id`, `parent_id`,
|
|
643
|
+
`is_root`.
|
|
644
|
+
|
|
645
|
+
### Events
|
|
646
|
+
|
|
647
|
+
- **`class TaskLifecycleEvent`** — `namespace="task"`, `type` (`"task.created"`
|
|
648
|
+
…), `task: Task`, `cause_type: str`.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Statistics
|
|
653
|
+
|
|
654
|
+
### `get_stats(state) -> GraphStats`
|
|
655
|
+
|
|
656
|
+
Aggregate counts over a `GraphState`.
|
|
657
|
+
|
|
658
|
+
- **`class GraphStats`** — `items: ItemStats`, `edges: EdgeStats`.
|
|
659
|
+
- **`class ItemStats`** — `total`, `by_kind`, `by_source_kind`, `by_author`,
|
|
660
|
+
`by_scope` (each `dict[str, int]`), `with_parents: int`, `root: int`.
|
|
661
|
+
- **`class EdgeStats`** — `total: int`, `active: int`, `by_kind: dict[str, int]`.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Replay
|
|
666
|
+
|
|
667
|
+
Rebuild a `GraphState` from an event/command log. Integrity-tolerant: per-item
|
|
668
|
+
failures are collected, not raised.
|
|
669
|
+
|
|
670
|
+
### `replay_commands(commands: list) -> ReplayResult`
|
|
671
|
+
|
|
672
|
+
Fold a list of commands (models or dicts) in order.
|
|
673
|
+
|
|
674
|
+
### `replay_from_envelopes(envelopes: list) -> ReplayResult`
|
|
675
|
+
|
|
676
|
+
Sort envelopes by their `ts` (strict ISO-8601, ms precision, explicit offset or
|
|
677
|
+
`Z`) and fold their payloads. Each envelope may be a dict or an `EventEnvelope`.
|
|
678
|
+
|
|
679
|
+
- **`class ReplayResult`** — `state: GraphState`,
|
|
680
|
+
`events: list[MemoryLifecycleEvent]`, `skipped: list[ReplayFailure]`.
|
|
681
|
+
- **`class ReplayFailure`** — dataclass `index: int`, `error: Exception`,
|
|
682
|
+
`command=None`, `envelope=None`.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## Serialization
|
|
687
|
+
|
|
688
|
+
On-disk shape matches the TS library —
|
|
689
|
+
`{"items": [[id, item], ...], "edges": [[id, edge], ...]}` — with unset
|
|
690
|
+
optionals omitted and edge `from` under its alias.
|
|
691
|
+
|
|
692
|
+
- **`to_json(state) -> SerializedGraphState`** — `dict[str, list[list[Any]]]`.
|
|
693
|
+
- **`from_json(data) -> GraphState`** — tolerates missing `items`/`edges` keys.
|
|
694
|
+
- **`stringify(state, pretty=False) -> str`** — compact, or 2-space indented.
|
|
695
|
+
- **`parse(json_str) -> GraphState`**
|
|
696
|
+
- **`SerializedGraphState`** — the serialized-dict type alias.
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
from memex import stringify, parse
|
|
700
|
+
snapshot = stringify(state, pretty=True)
|
|
701
|
+
state = parse(snapshot)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## Event envelopes
|
|
707
|
+
|
|
708
|
+
### `class EventEnvelope` (generic over `payload`)
|
|
709
|
+
|
|
710
|
+
`id`, `namespace`, `type`, `ts` (ISO string), `trace_id: str | None`, `payload: T`.
|
|
711
|
+
|
|
712
|
+
### `create_event_envelope(type, payload, *, trace_id=None, namespace="memory") -> EventEnvelope[Any]`
|
|
713
|
+
|
|
714
|
+
Mints `id` (UUIDv7) and `ts` (`now_iso`).
|
|
715
|
+
|
|
716
|
+
### Wrappers
|
|
717
|
+
|
|
718
|
+
Build envelopes from reducer output for an append-only log:
|
|
719
|
+
|
|
720
|
+
- **`wrap_lifecycle_event(event, cause_id, trace_id=None) -> EventEnvelope[dict]`**
|
|
721
|
+
— wraps a `MemoryLifecycleEvent`; payload carries the set fields plus `cause_id`.
|
|
722
|
+
- **`wrap_state_event(item, cause_id, trace_id=None) -> EventEnvelope[dict]`**
|
|
723
|
+
— a `"state.memory"` snapshot of an item.
|
|
724
|
+
- **`wrap_edge_state_event(edge, cause_id, trace_id=None) -> EventEnvelope[dict]`**
|
|
725
|
+
— a `"state.edge"` snapshot of an edge.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## Transplant
|
|
730
|
+
|
|
731
|
+
Export a slice of all three graphs and import it elsewhere, optionally re-id'ing
|
|
732
|
+
on collision.
|
|
733
|
+
|
|
734
|
+
### `export_slice(mem_state, intent_state, task_state, *, ...) -> MemexExport`
|
|
735
|
+
|
|
736
|
+
Keyword options: `memory_ids`, `intent_ids`, `task_ids` (seed sets);
|
|
737
|
+
`include_parents`, `include_children`, `include_aliases`,
|
|
738
|
+
`include_related_tasks`, `include_related_intents` (all `bool`, default
|
|
739
|
+
`False`). Walks the requested relationships and collects edges between included
|
|
740
|
+
memories.
|
|
741
|
+
|
|
742
|
+
- **`class MemexExport`** — `memories: list[MemoryItem]`, `edges: list[Edge]`,
|
|
743
|
+
`intents: list[Intent]`, `tasks: list[Task]`.
|
|
744
|
+
- **`class ExportOptions`** — the same options as a model (for callers that
|
|
745
|
+
prefer to pass a struct).
|
|
746
|
+
|
|
747
|
+
### `import_slice(mem_state, intent_state, task_state, slice, *, ...) -> ImportResult`
|
|
748
|
+
|
|
749
|
+
`slice: MemexExport | dict`. Options: `skip_existing_ids=True`,
|
|
750
|
+
`shallow_compare_existing=False`, `re_id_on_difference=False`. When re-id'ing,
|
|
751
|
+
colliding-but-different entities get fresh UUIDv7-shaped ids (1ms after the
|
|
752
|
+
original), and all cross-references (`parents`, `parent_id`, `intent_id`,
|
|
753
|
+
memory-id lists) are remapped via a per-graph pre-pass.
|
|
754
|
+
|
|
755
|
+
- **`class ImportResult`** — `mem_state`, `intent_state`, `task_state`,
|
|
756
|
+
`report: ImportReport`.
|
|
757
|
+
- **`class ImportReport`** — `created`, `updated`, `skipped`, `conflicts`, each
|
|
758
|
+
an `ImportBucket`.
|
|
759
|
+
- **`class ImportBucket`** — `memories`, `intents`, `tasks`, `edges` (each
|
|
760
|
+
`list[str]` of ids).
|
|
761
|
+
- **`class ImportOptions`** — the options as a model.
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Validation
|
|
766
|
+
|
|
767
|
+
`memex.schemas` is the validation entry point (the parity shim for
|
|
768
|
+
`@ai2070/memex/schemas`). In Pydantic the models *are* the schema.
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
from memex.schemas import validate_command
|
|
772
|
+
from memex import apply_command
|
|
773
|
+
|
|
774
|
+
cmd = validate_command(raw) # raises pydantic.ValidationError on bad shape
|
|
775
|
+
state = apply_command(state, cmd).state
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
| Function | Returns |
|
|
779
|
+
|-----------------------------------|----------------|
|
|
780
|
+
| `validate_command(raw)` | `MemoryCommand`|
|
|
781
|
+
| `validate_intent_command(raw)` | `IntentCommand`|
|
|
782
|
+
| `validate_task_command(raw)` | `TaskCommand` |
|
|
783
|
+
| `validate_memory_item(raw)` | `MemoryItem` |
|
|
784
|
+
| `validate_edge(raw)` | `Edge` |
|
|
785
|
+
|
|
786
|
+
Schema aliases (the model is the schema): `MemoryItemSchema`, `EdgeSchema`,
|
|
787
|
+
`IntentSchema`, `TaskSchema`. Adapters: `MemoryCommandAdapter`,
|
|
788
|
+
`IntentCommandAdapter`, `TaskCommandAdapter`.
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## `MemexStore` facade
|
|
793
|
+
|
|
794
|
+
A mutable, object-oriented container over the three graphs. It holds the states,
|
|
795
|
+
rebinds them on each mutation, and returns the emitted events — convenient for
|
|
796
|
+
agents and daemons that don't want to thread `state =` through every call. The
|
|
797
|
+
functional API remains the backbone; the facade just wraps it.
|
|
798
|
+
|
|
799
|
+
```python
|
|
800
|
+
from memex import MemexStore
|
|
801
|
+
|
|
802
|
+
store = MemexStore()
|
|
803
|
+
a = store.create(scope="user:laz", kind="observation", content={"v": 1},
|
|
804
|
+
author="agent:x", source_kind="observed", authority=0.9)
|
|
805
|
+
store.mark_contradiction(a.id, b_id, "system:detector")
|
|
806
|
+
snapshot = store.dumps(pretty=True)
|
|
807
|
+
restored = MemexStore.loads(snapshot)
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Constructor
|
|
811
|
+
|
|
812
|
+
`MemexStore(mem=None, intents=None, tasks=None)` — start empty or from existing
|
|
813
|
+
states. Attributes `store.mem`, `store.intents`, `store.tasks` expose the live
|
|
814
|
+
states.
|
|
815
|
+
|
|
816
|
+
### Memory
|
|
817
|
+
|
|
818
|
+
| Method | Returns | Notes |
|
|
819
|
+
|--------|---------|-------|
|
|
820
|
+
| `apply(cmd)` | `list[MemoryLifecycleEvent]` | apply any memory command |
|
|
821
|
+
| `create(**kwargs)` | `MemoryItem` | forwards to `create_memory_item`, then creates |
|
|
822
|
+
| `add(item)` | `MemoryItem` | create from an existing item |
|
|
823
|
+
| `update(item_id, partial, author, reason=None)` | events | |
|
|
824
|
+
| `retract(item_id, author, reason=None)` | events | |
|
|
825
|
+
| `add_edge(**kwargs)` | `Edge` | forwards to `create_edge`, then creates |
|
|
826
|
+
| `items(filter=None, options=None)` | `list[MemoryItem]` | |
|
|
827
|
+
| `item(id)` | `MemoryItem \| None` | |
|
|
828
|
+
| `scored(weights, options=None)` | `list[ScoredItem]` | |
|
|
829
|
+
| `edges(filter=None)` | `list[Edge]` | |
|
|
830
|
+
| `parents(item_id)` / `children(item_id)` | `list[MemoryItem]` | |
|
|
831
|
+
| `related(item_id, direction="both")` | `list[MemoryItem]` | |
|
|
832
|
+
| `smart_retrieve(**kwargs)` | `list[ScoredItem]` | |
|
|
833
|
+
| `support_tree(item_id)` | `SupportNode \| None` | |
|
|
834
|
+
| `support_set(item_id)` | `list[MemoryItem]` | |
|
|
835
|
+
| `stats()` | `GraphStats` | |
|
|
836
|
+
|
|
837
|
+
### Integrity
|
|
838
|
+
|
|
839
|
+
| Method | Returns |
|
|
840
|
+
|--------|---------|
|
|
841
|
+
| `mark_contradiction(a, b, author, meta=None)` | events |
|
|
842
|
+
| `resolve_contradiction(winner, loser, author, reason=None)` | events |
|
|
843
|
+
| `mark_alias(a, b, author, meta=None)` | events |
|
|
844
|
+
| `cascade_retract(item_id, author, reason=None)` | `list[str]` (retracted ids) |
|
|
845
|
+
| `contradictions()` | `list[Contradiction]` |
|
|
846
|
+
| `stale_items()` | `list[StaleItem]` |
|
|
847
|
+
| `aliases(item_id)` / `alias_group(item_id)` | `list[MemoryItem]` |
|
|
848
|
+
|
|
849
|
+
### Bulk
|
|
850
|
+
|
|
851
|
+
`apply_many(filter, transform, author, reason=None, options=None)`,
|
|
852
|
+
`bulk_adjust_scores(criteria, delta, author, reason=None)`,
|
|
853
|
+
`decay_importance(older_than_ms, factor, author, reason=None)` — each returns
|
|
854
|
+
events.
|
|
855
|
+
|
|
856
|
+
### Intent / Task
|
|
857
|
+
|
|
858
|
+
`apply_intent(cmd)`, `create_intent(**kwargs)`, `get_intents(filter=None)`;
|
|
859
|
+
`apply_task(cmd)`, `create_task(**kwargs)`, `get_tasks(filter=None)`.
|
|
860
|
+
|
|
861
|
+
### Transplant & serialization
|
|
862
|
+
|
|
863
|
+
`export_slice(**kwargs)` → `MemexExport`; `import_slice(slice, **kwargs)` →
|
|
864
|
+
`ImportReport` (mutates the store's states); `to_json()` →
|
|
865
|
+
`SerializedGraphState`; `dumps(pretty=False)` → `str`; `MemexStore.loads(json_str)`
|
|
866
|
+
→ `MemexStore` (classmethod, restores the memory graph).
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## UUID helpers
|
|
871
|
+
|
|
872
|
+
- **`uuid7(ms=None) -> str`** — generate a UUIDv7 string (RFC 9562) for `ms`
|
|
873
|
+
(defaults to now). Encodes a 48-bit big-endian ms timestamp in the first six
|
|
874
|
+
bytes.
|
|
875
|
+
- **`safe_extract_timestamp(value: str) -> int | None`** — decode the ms
|
|
876
|
+
timestamp from a UUIDv7; returns `None` for non-v7 input or a non-positive
|
|
877
|
+
timestamp (unlike [`extract_timestamp`](#extract_timestampuuid_id-str---int),
|
|
878
|
+
which raises).
|
|
879
|
+
|
|
880
|
+
> Install the optional `fast-uuid` extra (`uuid-utils`) for faster generation.
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## Errors
|
|
885
|
+
|
|
886
|
+
All domain errors derive from `MemexError`.
|
|
887
|
+
|
|
888
|
+
| Exception | Raised by | Attributes |
|
|
889
|
+
|-----------|-----------|------------|
|
|
890
|
+
| `MemexError` | base class | — |
|
|
891
|
+
| `MemoryNotFoundError` | update/retract of an absent item | `item_id` |
|
|
892
|
+
| `EdgeNotFoundError` | update/retract of an absent edge | `edge_id` |
|
|
893
|
+
| `DuplicateMemoryError` | create with an existing id | `item_id` |
|
|
894
|
+
| `DuplicateEdgeError` | create with an existing edge id | `edge_id` |
|
|
895
|
+
| `InvalidTimestampError` | bad UUIDv7 / envelope timestamp | — |
|
|
896
|
+
| `IntentNotFoundError` | intent reducer | `intent_id` |
|
|
897
|
+
| `DuplicateIntentError` | intent reducer | `intent_id` |
|
|
898
|
+
| `InvalidIntentTransitionError` | intent reducer | `intent_id`, `from_status`, `to_status` |
|
|
899
|
+
| `TaskNotFoundError` | task reducer | `task_id` |
|
|
900
|
+
| `DuplicateTaskError` | task reducer | `task_id` |
|
|
901
|
+
| `InvalidTaskTransitionError` | task reducer | `task_id`, `from_status`, `to_status` |
|
|
902
|
+
|
|
903
|
+
Out-of-range scores and malformed command shapes surface as
|
|
904
|
+
`pydantic.ValidationError`. `cost_fn` contract violations and unknown
|
|
905
|
+
sort/decay enums surface as `ValueError`.
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Type aliases
|
|
910
|
+
|
|
911
|
+
Open string unions documented as `Literal` aliases — fields accept any `str`,
|
|
912
|
+
but these are the canonical values.
|
|
913
|
+
|
|
914
|
+
| Alias | Values |
|
|
915
|
+
|-------|--------|
|
|
916
|
+
| `KnownMemoryKind` | `observation`, `assertion`, `assumption`, `hypothesis`, `derivation`, `simulation`, `policy`, `trait` |
|
|
917
|
+
| `KnownSourceKind` | `user_explicit`, `observed`, `derived_deterministic`, `agent_inferred`, `simulated`, `imported` |
|
|
918
|
+
| `KnownEdgeKind` | `DERIVED_FROM`, `CONTRADICTS`, `SUPPORTS`, `ABOUT`, `SUPERSEDES`, `ALIAS` |
|
|
919
|
+
| `KnownNamespace` | `memory`, `task`, `agent`, `tool`, `net`, `app`, `chat`, `system`, `debug` |
|
|
920
|
+
| `LifecycleEventType` | `memory.created`, `memory.updated`, `memory.retracted`, `edge.created`, `edge.updated`, `edge.retracted` |
|
|
921
|
+
| `SortField` | `authority`, `conviction`, `importance`, `recency` |
|
|
922
|
+
| `DecayInterval` | `hour`, `day`, `week` |
|
|
923
|
+
| `DecayType` | `exponential`, `linear`, `step` |
|