grafomem 0.2.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.
Files changed (54) hide show
  1. aml/__init__.py +3 -0
  2. aml/adapter_check.py +136 -0
  3. aml/backends/__init__.py +1 -0
  4. aml/backends/bi_temporal.py +225 -0
  5. aml/backends/bounded_vector.py +197 -0
  6. aml/backends/conflict_backends.py +333 -0
  7. aml/backends/cross_session_backends.py +267 -0
  8. aml/backends/delete_backends.py +254 -0
  9. aml/backends/gmp_reference.py +285 -0
  10. aml/backends/interface.py +517 -0
  11. aml/backends/isolation_backends.py +524 -0
  12. aml/backends/persistence.py +143 -0
  13. aml/backends/retention_backends.py +215 -0
  14. aml/backends/sqlite_gmp.py +525 -0
  15. aml/backends/supersession_chain.py +187 -0
  16. aml/backends/tenant_backends.py +251 -0
  17. aml/backends/vector_only.py +226 -0
  18. aml/cli.py +627 -0
  19. aml/eval/__init__.py +1 -0
  20. aml/eval/concurrency.py +456 -0
  21. aml/eval/concurrency_runner.py +320 -0
  22. aml/eval/conformance.py +545 -0
  23. aml/eval/harness.py +279 -0
  24. aml/eval/metrics.py +268 -0
  25. aml/eval/report.py +307 -0
  26. aml/generator/__init__.py +1 -0
  27. aml/generator/oracle.py +502 -0
  28. aml/generator/trace.py +1020 -0
  29. aml/generator/validators.py +447 -0
  30. aml/generator/workloads/__init__.py +1 -0
  31. aml/generator/workloads/w1.py +369 -0
  32. aml/generator/workloads/w10.py +412 -0
  33. aml/generator/workloads/w2.py +280 -0
  34. aml/generator/workloads/w3.py +269 -0
  35. aml/generator/workloads/w4.py +234 -0
  36. aml/generator/workloads/w5.py +253 -0
  37. aml/generator/workloads/w6.py +254 -0
  38. aml/generator/workloads/w7.py +259 -0
  39. aml/generator/workloads/w8.py +242 -0
  40. aml/generator/workloads/w9.py +309 -0
  41. aml/provenance.py +114 -0
  42. aml/server/__init__.py +1 -0
  43. aml/server/app.py +362 -0
  44. aml/server/auth.py +102 -0
  45. aml/server/ingestion.py +244 -0
  46. aml/server/mcp.py +266 -0
  47. aml/server/stores.py +113 -0
  48. aml/wire.py +446 -0
  49. grafomem-0.2.0.dist-info/METADATA +227 -0
  50. grafomem-0.2.0.dist-info/RECORD +54 -0
  51. grafomem-0.2.0.dist-info/WHEEL +5 -0
  52. grafomem-0.2.0.dist-info/entry_points.txt +2 -0
  53. grafomem-0.2.0.dist-info/licenses/LICENSE +21 -0
  54. grafomem-0.2.0.dist-info/top_level.txt +1 -0
aml/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """GRAFOMEM — agent-memory benchmark framework."""
2
+
3
+ __version__ = "0.1.3"
aml/adapter_check.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ GRAFOMEM adapter pre-flight checker.
3
+
4
+ grafomem check -b my_module:MyBackend
5
+
6
+ Quick structural validation BEFORE running the full conformance suite:
7
+ checks Protocol compliance, method signatures, capability coherence,
8
+ and basic round-trip. Fails fast with actionable error messages.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import inspect
14
+ from typing import Any
15
+
16
+
17
+ def check_adapter(cls: type) -> list[str]:
18
+ """Run pre-flight checks against a backend class. Returns a list of errors.
19
+ Empty list means the adapter is structurally conformant."""
20
+ errors: list[str] = []
21
+
22
+ # 1. Check required methods exist
23
+ required_methods = ["capabilities", "write", "retrieve", "delete",
24
+ "supersede", "audit", "flush"]
25
+ for method in required_methods:
26
+ if not hasattr(cls, method):
27
+ errors.append(f"Missing required method: {method}()")
28
+ elif not callable(getattr(cls, method)):
29
+ errors.append(f"{method} exists but is not callable")
30
+
31
+ if errors:
32
+ return errors # can't proceed without basic methods
33
+
34
+ # 2. Try to instantiate
35
+ instance = None
36
+ try:
37
+ # Check if __init__ needs arguments beyond self
38
+ sig = inspect.signature(cls.__init__)
39
+ params = [p for p in sig.parameters.values()
40
+ if p.name != "self" and p.default is inspect.Parameter.empty]
41
+ if params:
42
+ errors.append(
43
+ f"Constructor requires arguments: {[p.name for p in params]}. "
44
+ f"The conformance suite needs a zero-arg factory. Consider using "
45
+ f"a wrapper lambda: lambda: MyBackend(arg1, arg2)"
46
+ )
47
+ return errors
48
+ instance = cls()
49
+ except Exception as e:
50
+ errors.append(f"Failed to instantiate {cls.__name__}(): {e}")
51
+ return errors
52
+
53
+ # 3. Check capabilities() returns set[Capability]
54
+ try:
55
+ from aml.backends.interface import Capability
56
+ caps = instance.capabilities()
57
+ if not isinstance(caps, set):
58
+ errors.append(f"capabilities() returned {type(caps).__name__}, expected set")
59
+ else:
60
+ for c in caps:
61
+ if not isinstance(c, Capability):
62
+ errors.append(f"capabilities() contains {c!r} which is not a Capability enum member")
63
+ except Exception as e:
64
+ errors.append(f"capabilities() raised: {e}")
65
+
66
+ # 4. Check Protocol compliance
67
+ try:
68
+ from aml.backends.interface import MemoryBackend
69
+ if not isinstance(instance, MemoryBackend):
70
+ errors.append(
71
+ f"{cls.__name__} does not satisfy the MemoryBackend Protocol. "
72
+ f"Check method signatures match the Protocol definition."
73
+ )
74
+ except Exception as e:
75
+ errors.append(f"Protocol check failed: {e}")
76
+
77
+ # 5. Basic round-trip: write + retrieve
78
+ try:
79
+ from aml.backends.interface import WriteOptions, RetrieveOptions
80
+ ref = instance.write("test content", WriteOptions())
81
+ if ref is None:
82
+ errors.append("write() returned None; must return a MemoryRef")
83
+ instance.flush()
84
+ mems = instance.retrieve("test", RetrieveOptions(budget_tokens=1024))
85
+ if not isinstance(mems, list):
86
+ errors.append(f"retrieve() returned {type(mems).__name__}, expected list")
87
+ except Exception as e:
88
+ errors.append(f"Basic write/retrieve round-trip failed: {e}")
89
+
90
+ return errors
91
+
92
+
93
+ def print_check(cls: type) -> bool:
94
+ """Run and print pre-flight check results. Returns True if all pass."""
95
+ print(f"GRAFOMEM adapter check — {cls.__name__}\n")
96
+
97
+ errors = check_adapter(cls)
98
+
99
+ if not errors:
100
+ # Also show declared capabilities
101
+ try:
102
+ instance = cls()
103
+ caps = instance.capabilities()
104
+ print(f"✓ Protocol compliance OK")
105
+ print(f"✓ Method signatures OK")
106
+ print(f"✓ Write/retrieve round-trip OK")
107
+ print(f"✓ Declared capabilities: {{{', '.join(sorted(c.value for c in caps))}}}")
108
+ print(f"\nAdapter is structurally conformant. Run `grafomem conformance` for full suite.")
109
+ except Exception:
110
+ print(f"✓ Structural checks passed")
111
+ return True
112
+ else:
113
+ for e in errors:
114
+ print(f"✗ {e}")
115
+ print(f"\n{len(errors)} error(s). Fix these before running the conformance suite.")
116
+ return False
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Smoke
121
+ # ---------------------------------------------------------------------------
122
+
123
+ if __name__ == "__main__":
124
+ from aml.backends.gmp_reference import GMPReferenceBackend
125
+ from aml.backends.vector_only import _stub_embedder, VectorOnlyBackend
126
+ from aml.backends.persistence import PersistenceBackend
127
+
128
+ for cls_factory in [
129
+ lambda: PersistenceBackend,
130
+ lambda: type("BadBackend", (), {}), # deliberately broken
131
+ ]:
132
+ cls = cls_factory()
133
+ print_check(cls)
134
+ print()
135
+
136
+ print("✓ Adapter check module smoke green.")
@@ -0,0 +1 @@
1
+ """Backend adapters + the MemoryBackend interface contract."""
@@ -0,0 +1,225 @@
1
+ """
2
+ GRAFOMEM bi_temporal reference adapter.
3
+
4
+ Same pinned BGE-small vector store as vector_only / supersession_chain, but it
5
+ keeps a valid-time interval [valid_from, valid_until) per version and resolves
6
+ retrieve(as_of=t) to the version valid at t. This is a strict superset of
7
+ supersession_chain:
8
+
9
+ - as_of = None -> current query: candidates are open-interval heads
10
+ (valid_until is None) -> identical to supersession_chain.
11
+ - as_of = t -> historical query: candidates are versions whose interval
12
+ contains t (valid_from <= t < valid_until) -> one version
13
+ per chain, the slice that was valid then.
14
+
15
+ It learns each interval the way the trace defines it (w2.py): a version's
16
+ valid_from arrives on write/supersede via WriteOptions; its valid_until is the
17
+ NEXT version's valid_from, which the harness hands to supersede() as the new
18
+ version's valid_from. So supersede(old, new) closes old at opts.valid_from and
19
+ reconstructs the oracle's contiguous windows exactly, with no extra plumbing.
20
+
21
+ Why it matters: historical (as_of) queries are N/A for every non-BI_TEMPORAL
22
+ backend (the harness skips them, E1). This adapter is the only one that can
23
+ answer them, so on W2 those queries flip from unscored to scored — the second
24
+ capability axis. Claims {BI_TEMPORAL, SUPERSESSION_CHAIN, AUDIT}; it must claim
25
+ SUPERSESSION_CHAIN so the harness dispatches supersede() and the intervals get
26
+ closed. Requires grafomem[backends].
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Iterator
32
+ from datetime import datetime, timezone
33
+
34
+ import numpy as np
35
+
36
+ from aml.backends.interface import (
37
+ Capability,
38
+ CapabilityNotSupported,
39
+ Memory,
40
+ RetrieveOptions,
41
+ WriteOptions,
42
+ )
43
+ from aml.backends.vector_only import REFERENCE_MODEL, EmbedFn, _default_embedder
44
+
45
+ __grafomem_interface__ = "0.1.1"
46
+
47
+
48
+ class BiTemporalVectorBackend:
49
+ """BGE vector store with per-version valid-time intervals + as_of resolution."""
50
+
51
+ __grafomem_interface__ = "0.1.1"
52
+ __grafomem_adapter_metadata__ = {
53
+ "underlying_system": "reference",
54
+ "embedding_model": REFERENCE_MODEL,
55
+ "vector_store": "numpy-bruteforce-cosine-exact",
56
+ "temporal_policy": "valid-time intervals [valid_from, valid_until); "
57
+ "as_of resolves to the version valid at t",
58
+ "notes": "Same embedder as vector_only/supersession_chain; the "
59
+ "difference is the BI_TEMPORAL capability, not the model.",
60
+ }
61
+
62
+ def __init__(self, embed_fn: EmbedFn | None = None) -> None:
63
+ self._embed_fn = embed_fn
64
+ self._store: dict[int, Memory] = {}
65
+ self._vec: dict[int, np.ndarray] = {}
66
+ self._vfrom: dict[int, datetime | None] = {}
67
+ self._vuntil: dict[int, datetime | None] = {} # None = open (head)
68
+ self._next = 0
69
+
70
+ def _embed(self, texts: list[str]) -> np.ndarray:
71
+ if self._embed_fn is None:
72
+ self._embed_fn = _default_embedder()
73
+ return self._embed_fn(texts)
74
+
75
+ def capabilities(self) -> set[Capability]:
76
+ return {Capability.BI_TEMPORAL, Capability.SUPERSESSION_CHAIN,
77
+ Capability.AUDIT}
78
+
79
+ def write(self, content: str, options: WriteOptions) -> int:
80
+ if options.tenant_id is not None:
81
+ raise CapabilityNotSupported(Capability.MULTI_TENANT, "write")
82
+ if options.signing_key is not None:
83
+ raise CapabilityNotSupported(
84
+ Capability.CRYPTOGRAPHIC_PROVENANCE, "write")
85
+ ref = self._next
86
+ self._next += 1
87
+ self._store[ref] = Memory(
88
+ ref=ref, content=content,
89
+ written_at=datetime.now(tz=timezone.utc),
90
+ metadata=dict(options.metadata),
91
+ )
92
+ self._vec[ref] = self._embed([content])[0]
93
+ self._vfrom[ref] = options.valid_from # None for non-temporal workloads
94
+ self._vuntil[ref] = None # open until superseded
95
+ return ref
96
+
97
+ def supersede(self, old_ref, content: str, options: WriteOptions) -> int:
98
+ # New version's valid_from == predecessor's valid_until (contiguous, w2.py).
99
+ new_ref = self.write(content, options)
100
+ if old_ref in self._store:
101
+ self._vuntil[old_ref] = options.valid_from
102
+ self._store[old_ref].superseded_by = new_ref
103
+ return new_ref
104
+
105
+ def delete(self, ref) -> bool:
106
+ # No deletes in W1-W6; a bi-temporal tombstone would close the interval
107
+ # with no successor, but no workload exercises it, so we don't claim it.
108
+ raise CapabilityNotSupported(Capability.HARD_DELETE, "delete")
109
+
110
+ def _valid_at(self, ref: int, t: datetime | None) -> bool:
111
+ if t is None: # current: open intervals only
112
+ return self._vuntil[ref] is None
113
+ vf, vu = self._vfrom[ref], self._vuntil[ref]
114
+ return (vf is None or vf <= t) and (vu is None or t < vu)
115
+
116
+ def retrieve(self, query: str, options: RetrieveOptions) -> list[Memory]:
117
+ if options.tenant_id is not None:
118
+ raise CapabilityNotSupported(Capability.MULTI_TENANT, "retrieve")
119
+ t = options.as_of
120
+ refs = [r for r in self._store if self._valid_at(r, t)]
121
+ if not refs:
122
+ return []
123
+ qv = self._embed([query])[0]
124
+ mat = np.stack([self._vec[r] for r in refs])
125
+ sims = mat @ qv
126
+ order = sorted(range(len(refs)),
127
+ key=lambda i: (-float(sims[i]), refs[i]))
128
+ out: list[Memory] = []
129
+ used = 0
130
+ for i in order:
131
+ m = self._store[refs[i]]
132
+ cost = len(m.content)
133
+ if used + cost > options.budget_tokens:
134
+ break
135
+ out.append(m)
136
+ used += cost
137
+ return out
138
+
139
+ def audit(self) -> Iterator[Memory]:
140
+ return iter(list(self._store.values()))
141
+
142
+ def flush(self) -> None:
143
+ pass
144
+
145
+
146
+ # ============================================================================
147
+ # Smoke check — run `python -m aml.backends.bi_temporal`
148
+ # ============================================================================
149
+
150
+ if __name__ == "__main__":
151
+ from datetime import timedelta
152
+
153
+ from aml.backends.interface import MemoryBackend
154
+ from aml.backends.vector_only import VectorOnlyBackend, _stub_embedder
155
+ from aml.eval.harness import run_trace
156
+ from aml.generator.trace import Difficulty
157
+ from aml.generator.workloads.w2 import generate_w2
158
+
159
+ print("GRAFOMEM bi_temporal.py — valid-time as_of resolution (STUB embedder)\n")
160
+
161
+ b = BiTemporalVectorBackend(embed_fn=_stub_embedder())
162
+
163
+ assert isinstance(b, MemoryBackend)
164
+ assert b.capabilities() == {Capability.BI_TEMPORAL,
165
+ Capability.SUPERSESSION_CHAIN, Capability.AUDIT}
166
+ print("✓ Protocol + capabilities ({BI_TEMPORAL, SUPERSESSION_CHAIN, AUDIT})")
167
+
168
+ # Hand-built chain with explicit contiguous windows:
169
+ # Rome [t0, t1) Milan [t1, t2) Turin [t2, open)
170
+ t0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
171
+ t1 = t0 + timedelta(days=30)
172
+ t2 = t0 + timedelta(days=60)
173
+ r0 = b.write("Aria lives in Rome", WriteOptions(valid_from=t0))
174
+ r1 = b.supersede(r0, "Aria lives in Milan", WriteOptions(valid_from=t1))
175
+ r2 = b.supersede(r1, "Aria lives in Turin", WriteOptions(valid_from=t2))
176
+ b.flush()
177
+
178
+ q = "Where does Aria live?"
179
+ now = [m.content for m in b.retrieve(q, RetrieveOptions(as_of=None))]
180
+ assert now == ["Aria lives in Turin"], now
181
+ print("✓ as_of=None -> head (Turin)")
182
+
183
+ # Time-travel: midpoint of each window resolves to that version.
184
+ for label, t, expected in (
185
+ ("inside Rome window ", t0 + timedelta(days=15), "Aria lives in Rome"),
186
+ ("inside Milan window", t1 + timedelta(days=15), "Aria lives in Milan"),
187
+ ("inside Turin window", t2 + timedelta(days=15), "Aria lives in Turin"),
188
+ ):
189
+ got = [m.content for m in b.retrieve(q, RetrieveOptions(as_of=t))]
190
+ assert got == [expected], f"{label}: {got}"
191
+ print("✓ as_of=t -> version valid at t (Rome / Milan / Turin by window)")
192
+
193
+ audited = {m.content: m for m in b.audit()}
194
+ assert len(audited) == 3 and audited["Aria lives in Milan"].superseded_by == r2
195
+ print("✓ audit() yields full history (3 versions, superseded_by linked)")
196
+
197
+ for op, call in (
198
+ ("tenant", lambda: b.retrieve("x", RetrieveOptions(tenant_id="t"))),
199
+ ("delete", lambda: b.delete(r0)),
200
+ ):
201
+ try:
202
+ call()
203
+ except CapabilityNotSupported:
204
+ pass
205
+ else:
206
+ raise AssertionError(f"{op}: expected CapabilityNotSupported")
207
+ print("✓ Capability guards (tenant -> no MULTI_TENANT; delete -> no HARD_DELETE)")
208
+
209
+ # Reclamation on a real W2 hard trace, through the actual harness:
210
+ # bi_temporal answers every query (n_na=0); vector_only skips all historical.
211
+ tr = generate_w2(seed=0, difficulty=Difficulty.HARD)
212
+ bt_run = run_trace(BiTemporalVectorBackend(embed_fn=_stub_embedder()),
213
+ tr, budget_tokens=512)
214
+ vec_run = run_trace(VectorOnlyBackend(embed_fn=_stub_embedder()),
215
+ tr, budget_tokens=512)
216
+ assert bt_run.n_na == 0, f"bi_temporal should answer all queries, n_na={bt_run.n_na}"
217
+ assert vec_run.n_na > 0, "vector_only should skip historical queries"
218
+ reclaimed = vec_run.n_na
219
+ answered = len(bt_run.per_query)
220
+ print(f"✓ Reclaims historical queries (bi_temporal n_na=0 / {answered} scored; "
221
+ f"vector_only n_na={reclaimed})")
222
+
223
+ print(f"\nAll bi_temporal smoke checks green. {reclaimed} historical queries that are "
224
+ f"N/A for every\nother backend are now answerable. Wire into run_w2 for the "
225
+ f"current/historical split.")
@@ -0,0 +1,197 @@
1
+ """
2
+ GRAFOMEM bounded_vector reference adapter.
3
+
4
+ Same pinned BGE-small vector store as vector_only, but with a fixed capacity:
5
+ on write, once the store exceeds `capacity`, the OLDEST memory is evicted
6
+ (FIFO — a recency window). This is the realistic "context window" model an agent
7
+ lives inside: bounded footprint and bounded per-query scan cost, at the price of
8
+ forgetting anything older than the window.
9
+
10
+ On W4 (Long-Horizon Dependencies) this produces the recall-vs-footprint
11
+ tradeoff: a query for a fact d facts back succeeds only if d < capacity (else it
12
+ was evicted), so recall cliffs at d = capacity — a structural fact, independent
13
+ of the embedder — while footprint and scan cost plateau at `capacity` instead of
14
+ growing linearly with the horizon like an unbounded store.
15
+
16
+ Claims {AUDIT}: no temporal, supersession, or hard-delete semantics. Eviction is
17
+ capacity management, not a HARD_DELETE (the caller did not request removal), and
18
+ is reflected in audit() — which exposes exactly the retained window.
19
+ Requires grafomem[backends].
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections import deque
25
+ from collections.abc import Iterator
26
+ from datetime import datetime, timezone
27
+
28
+ import numpy as np
29
+
30
+ from aml.backends.interface import (
31
+ Capability,
32
+ CapabilityNotSupported,
33
+ Memory,
34
+ RetrieveOptions,
35
+ WriteOptions,
36
+ )
37
+ from aml.backends.vector_only import REFERENCE_MODEL, EmbedFn, _default_embedder
38
+
39
+ __grafomem_interface__ = "0.1.1"
40
+
41
+ DEFAULT_CAPACITY = 64
42
+
43
+
44
+ class BoundedVectorBackend:
45
+ """BGE vector store with a fixed-capacity recency window (FIFO eviction)."""
46
+
47
+ __grafomem_interface__ = "0.1.1"
48
+
49
+ def __init__(self, capacity: int = DEFAULT_CAPACITY,
50
+ embed_fn: EmbedFn | None = None) -> None:
51
+ if capacity < 1:
52
+ raise ValueError("capacity must be >= 1")
53
+ self.capacity = capacity
54
+ self._embed_fn = embed_fn
55
+ self._store: dict[int, Memory] = {}
56
+ self._vec: dict[int, np.ndarray] = {}
57
+ self._order: deque[int] = deque() # FIFO eviction order
58
+ self._next = 0
59
+ self._evicted = 0 # bookkeeping (not retrievable)
60
+
61
+ __grafomem_adapter_metadata__ = {
62
+ "underlying_system": "reference",
63
+ "embedding_model": REFERENCE_MODEL,
64
+ "vector_store": "numpy-bruteforce-cosine-exact",
65
+ "retention_policy": "fixed-capacity recency window, FIFO eviction",
66
+ }
67
+
68
+ def _embed(self, texts: list[str]) -> np.ndarray:
69
+ if self._embed_fn is None:
70
+ self._embed_fn = _default_embedder()
71
+ return self._embed_fn(texts)
72
+
73
+ def capabilities(self) -> set[Capability]:
74
+ return {Capability.AUDIT}
75
+
76
+ def write(self, content: str, options: WriteOptions) -> int:
77
+ if options.tenant_id is not None:
78
+ raise CapabilityNotSupported(Capability.MULTI_TENANT, "write")
79
+ if options.signing_key is not None:
80
+ raise CapabilityNotSupported(
81
+ Capability.CRYPTOGRAPHIC_PROVENANCE, "write")
82
+ ref = self._next
83
+ self._next += 1
84
+ self._store[ref] = Memory(
85
+ ref=ref, content=content,
86
+ written_at=datetime.now(tz=timezone.utc),
87
+ metadata=dict(options.metadata),
88
+ )
89
+ self._vec[ref] = self._embed([content])[0]
90
+ self._order.append(ref)
91
+ while len(self._order) > self.capacity: # evict oldest
92
+ old = self._order.popleft()
93
+ del self._store[old]
94
+ del self._vec[old]
95
+ self._evicted += 1
96
+ return ref
97
+
98
+ def supersede(self, old_ref, content: str, options: WriteOptions) -> int:
99
+ raise CapabilityNotSupported(Capability.SUPERSESSION_CHAIN, "supersede")
100
+
101
+ def delete(self, ref) -> bool:
102
+ raise CapabilityNotSupported(Capability.HARD_DELETE, "delete")
103
+
104
+ def retrieve(self, query: str, options: RetrieveOptions) -> list[Memory]:
105
+ if options.as_of is not None:
106
+ raise CapabilityNotSupported(Capability.BI_TEMPORAL, "retrieve")
107
+ if options.tenant_id is not None:
108
+ raise CapabilityNotSupported(Capability.MULTI_TENANT, "retrieve")
109
+ refs = list(self._store.keys()) # retained window only
110
+ if not refs:
111
+ return []
112
+ qv = self._embed([query])[0]
113
+ mat = np.stack([self._vec[r] for r in refs])
114
+ sims = mat @ qv
115
+ order = sorted(range(len(refs)),
116
+ key=lambda i: (-float(sims[i]), refs[i]))
117
+ out: list[Memory] = []
118
+ used = 0
119
+ for i in order:
120
+ m = self._store[refs[i]]
121
+ cost = len(m.content)
122
+ if used + cost > options.budget_tokens:
123
+ break
124
+ out.append(m)
125
+ used += cost
126
+ return out
127
+
128
+ def audit(self) -> Iterator[Memory]:
129
+ return iter(list(self._store.values())) # the retained window
130
+
131
+ def flush(self) -> None:
132
+ pass
133
+
134
+
135
+ # ============================================================================
136
+ # Smoke check — run `python -m aml.backends.bounded_vector`
137
+ # ============================================================================
138
+
139
+ if __name__ == "__main__":
140
+ from aml.backends.interface import MemoryBackend
141
+ from aml.backends.vector_only import _stub_embedder
142
+
143
+ print("GRAFOMEM bounded_vector.py — fixed-capacity recency window (STUB)\n")
144
+
145
+ K = 10
146
+ b = BoundedVectorBackend(capacity=K, embed_fn=_stub_embedder())
147
+
148
+ assert isinstance(b, MemoryBackend)
149
+ assert b.capabilities() == {Capability.AUDIT}
150
+ print(f"✓ Protocol + capabilities ({{AUDIT}}, capacity={K})")
151
+
152
+ # Write 100 facts; only the last K survive.
153
+ refs = [b.write(f"Entity{i:03d} lives in City{i:03d}", WriteOptions())
154
+ for i in range(100)]
155
+ b.flush()
156
+ retained = {m.ref for m in b.audit()}
157
+ assert len(retained) == K, f"expected {K} retained, got {len(retained)}"
158
+ assert retained == set(refs[-K:]), "retained window is not the last K (FIFO)"
159
+ print(f"✓ FIFO eviction (100 written, last {K} retained)")
160
+
161
+ # A recent fact is retrievable; an evicted (old) one is not.
162
+ recent = b.retrieve("Where does Entity099 live?", RetrieveOptions(budget_tokens=512))
163
+ assert any("Entity099" in m.content for m in recent), "recent fact missing"
164
+ old = b.retrieve("Where does Entity001 live?", RetrieveOptions(budget_tokens=512))
165
+ assert not any("Entity001" in m.content for m in old), "evicted fact leaked"
166
+ print("✓ Recall cliff (recent retrievable; evicted absent)")
167
+
168
+ # Footprint = retained count = scan cost, plateaued at capacity.
169
+ assert len(list(b.audit())) == K
170
+ print(f"✓ Footprint plateau (audit/scan = {K}, not 100)")
171
+
172
+ # Capability guards.
173
+ for op, call in (
174
+ ("supersede", lambda: b.supersede(refs[-1], "x", WriteOptions())),
175
+ ("delete", lambda: b.delete(refs[-1])),
176
+ ("as_of", lambda: b.retrieve("x", RetrieveOptions(
177
+ as_of=datetime.now(tz=timezone.utc)))),
178
+ ):
179
+ try:
180
+ call()
181
+ except CapabilityNotSupported:
182
+ pass
183
+ else:
184
+ raise AssertionError(f"{op}: expected CapabilityNotSupported")
185
+ print("✓ Capability guards (supersede/delete/as_of refused)")
186
+
187
+ # Determinism: same writes -> same retained set + ranking.
188
+ b2 = BoundedVectorBackend(capacity=K, embed_fn=_stub_embedder())
189
+ for i in range(100):
190
+ b2.write(f"Entity{i:03d} lives in City{i:03d}", WriteOptions())
191
+ r1 = [m.ref for m in b.retrieve("Where does Entity095 live?", RetrieveOptions(budget_tokens=512))]
192
+ r2 = [m.ref for m in b2.retrieve("Where does Entity095 live?", RetrieveOptions(budget_tokens=512))]
193
+ assert r1 == r2
194
+ print("✓ Deterministic (same writes -> same window + ranking)")
195
+
196
+ print("\nAll bounded_vector smoke checks green. Next: run_w4 "
197
+ "(recall-by-distance + footprint vs unbounded).")