dandelion-memory 1.0.0rc15__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 (232) hide show
  1. benchmarks/__init__.py +3 -0
  2. benchmarks/adapters/__init__.py +29 -0
  3. benchmarks/adapters/ablations.py +406 -0
  4. benchmarks/adapters/base.py +72 -0
  5. benchmarks/adapters/controlled/__init__.py +13 -0
  6. benchmarks/adapters/controlled/core_archival.py +115 -0
  7. benchmarks/adapters/controlled/extract_resolve.py +130 -0
  8. benchmarks/adapters/controlled/hybrid_rag.py +120 -0
  9. benchmarks/adapters/controlled/no_late_binding.py +113 -0
  10. benchmarks/adapters/dandelion.py +94 -0
  11. benchmarks/adapters/native/__init__.py +31 -0
  12. benchmarks/adapters/native/base.py +45 -0
  13. benchmarks/adapters/native/dandelion_native.py +164 -0
  14. benchmarks/adapters/native/dandelion_native_b0.py +165 -0
  15. benchmarks/adapters/native/dandelion_native_b1.py +170 -0
  16. benchmarks/adapters/native/dandelion_native_b2.py +169 -0
  17. benchmarks/adapters/native/dandelion_native_b3.py +171 -0
  18. benchmarks/adapters/native/dandelion_native_c4.py +261 -0
  19. benchmarks/adapters/native/dandelion_native_c4_1.py +281 -0
  20. benchmarks/adapters/native/dandelion_native_c4_2.py +435 -0
  21. benchmarks/adapters/native/event_evidence_b2.py +616 -0
  22. benchmarks/adapters/native/evidence_b2.py +609 -0
  23. benchmarks/adapters/native/letta_native.py +253 -0
  24. benchmarks/adapters/native/mem0_native.py +219 -0
  25. benchmarks/adapters/native/store_vector_b0.py +792 -0
  26. benchmarks/adapters/native/store_vector_b2.py +808 -0
  27. benchmarks/agent_adapter_stdio_acceptance.py +86 -0
  28. benchmarks/bootstrap_cross_stack.py +218 -0
  29. benchmarks/canonical_profile_instrumentation.py +167 -0
  30. benchmarks/canonical_public_baseline.py +288 -0
  31. benchmarks/cli.py +525 -0
  32. benchmarks/collector_public30_offline.py +1653 -0
  33. benchmarks/datasets/__init__.py +45 -0
  34. benchmarks/datasets/conversational_generator.py +238 -0
  35. benchmarks/datasets/generator.py +283 -0
  36. benchmarks/datasets/holdout_topology_generator.py +290 -0
  37. benchmarks/datasets/holdout_topology_generator_v2.py +294 -0
  38. benchmarks/datasets/multi_world.py +1643 -0
  39. benchmarks/datasets/scale_generator.py +289 -0
  40. benchmarks/datasets/schema.py +57 -0
  41. benchmarks/deep_collision_stress.py +199 -0
  42. benchmarks/evaluators/__init__.py +12 -0
  43. benchmarks/evaluators/manifest.py +142 -0
  44. benchmarks/evaluators/metrics.py +277 -0
  45. benchmarks/evaluators/runner.py +258 -0
  46. benchmarks/f1_step0/build_dev_measurement_manifest_v1.py +127 -0
  47. benchmarks/f1_step0/build_dev_measurement_manifest_v2.py +127 -0
  48. benchmarks/f1_step0/build_dev_measurement_manifest_v3.py +146 -0
  49. benchmarks/f1_step0/build_ledger.py +143 -0
  50. benchmarks/f1_step0/build_ledger_v2.py +296 -0
  51. benchmarks/f1_step0/compare_dev52_readiness_v1.py +106 -0
  52. benchmarks/f1_step0/locator_public_smoke.py +172 -0
  53. benchmarks/f1_step0/prepare_dev52_owner_v1.py +133 -0
  54. benchmarks/f1_step0/prepare_dev52_owner_v2.py +133 -0
  55. benchmarks/f1_step0/public_identity_probe.py +191 -0
  56. benchmarks/f1_step0/public_response_mapper.py +101 -0
  57. benchmarks/f1_step0/real_source_public_preflight.py +244 -0
  58. benchmarks/f1_step0/run_exact_fusion_dev_experiment_v1.py +83 -0
  59. benchmarks/f1_step0/scorer.py +110 -0
  60. benchmarks/f1_step0/source_aware_dev_measurement_v1.py +627 -0
  61. benchmarks/f1_step0/source_aware_dev_measurement_v2.py +659 -0
  62. benchmarks/f1_step0/source_aware_dev_measurement_v3.py +776 -0
  63. benchmarks/f1_step0/test_public_response_mapper.py +110 -0
  64. benchmarks/f1_step0/test_real_source_public_preflight.py +25 -0
  65. benchmarks/f1_step0/test_scorer.py +158 -0
  66. benchmarks/f1_step0/test_source_aware_dev_measurement.py +189 -0
  67. benchmarks/f1_step0/test_source_aware_dev_measurement_v2.py +263 -0
  68. benchmarks/f1_step0/test_source_aware_dev_measurement_v3.py +334 -0
  69. benchmarks/f1_step0/typed_readback_public_smoke.py +181 -0
  70. benchmarks/forensic_c4_1_dev.py +258 -0
  71. benchmarks/forensic_c4_dev.py +255 -0
  72. benchmarks/forensic_deep25_c4_1.py +478 -0
  73. benchmarks/legacy_migration_acceptance.py +29 -0
  74. benchmarks/longmemeval_actor.py +497 -0
  75. benchmarks/longmemeval_agent_judge.py +593 -0
  76. benchmarks/longmemeval_budget.py +139 -0
  77. benchmarks/longmemeval_full.py +410 -0
  78. benchmarks/longmemeval_judge.py +327 -0
  79. benchmarks/longmemeval_pilot.py +316 -0
  80. benchmarks/longmemeval_s.py +318 -0
  81. benchmarks/memory_bench.py +248 -0
  82. benchmarks/memory_formation/__init__.py +1 -0
  83. benchmarks/memory_formation/audit.py +801 -0
  84. benchmarks/memory_formation/real_audit.py +688 -0
  85. benchmarks/memory_session_acceptance.py +279 -0
  86. benchmarks/native_codex_host.py +513 -0
  87. benchmarks/owner_dense_acceptance.py +126 -0
  88. benchmarks/pilot_seeker_vault.py +266 -0
  89. benchmarks/prepare_public30_ripgrep_range_owner.py +115 -0
  90. benchmarks/profile_canonical.py +373 -0
  91. benchmarks/profile_canonical_lanes.py +405 -0
  92. benchmarks/profile_search_chunks.py +351 -0
  93. benchmarks/public_ranges_faq_proof.py +121 -0
  94. benchmarks/public_repos/build_history_scope_v3.py +330 -0
  95. benchmarks/public_repos/build_oracle.py +108 -0
  96. benchmarks/public_repos/build_oracle_v2.py +169 -0
  97. benchmarks/public_repos/build_oracle_v3.py +176 -0
  98. benchmarks/public_repos/map_public_v2.py +296 -0
  99. benchmarks/public_repos/project_prompts_v2.py +31 -0
  100. benchmarks/public_repos/project_prompts_v3.py +29 -0
  101. benchmarks/public_repos/ready_admin_v2.py +214 -0
  102. benchmarks/public_repos/score_public_v2.py +258 -0
  103. benchmarks/public_repos/test_map_public_v2.py +171 -0
  104. benchmarks/public_repos/test_public_repo_v2.py +44 -0
  105. benchmarks/public_repos/test_public_repo_v3.py +46 -0
  106. benchmarks/public_repos/test_ready_admin_v2.py +117 -0
  107. benchmarks/public_repos/test_score_public_v2.py +163 -0
  108. benchmarks/public_repos/test_wire_v2.py +292 -0
  109. benchmarks/public_repos/wire_v2.py +603 -0
  110. benchmarks/real_blocker_fix_acceptance.py +155 -0
  111. benchmarks/real_scan_rescan_acceptance.py +41 -0
  112. benchmarks/recall_v2_20k_smoke.py +112 -0
  113. benchmarks/recall_v2_e1_retrieve.py +70 -0
  114. benchmarks/recall_v2_e1_score.py +271 -0
  115. benchmarks/recall_v2_e2_prepare.py +69 -0
  116. benchmarks/recall_v2_e2_public_score.py +91 -0
  117. benchmarks/recall_v2_e2_score.py +108 -0
  118. benchmarks/recall_v2_navigation_mcp_replay.py +308 -0
  119. benchmarks/recall_v2_reader_capture.py +118 -0
  120. benchmarks/recall_v2_reader_replay.py +136 -0
  121. benchmarks/run_blind_h10_holdout.py +284 -0
  122. benchmarks/run_blind_h9_holdout.py +275 -0
  123. benchmarks/run_c4_2_nightly_scale.py +446 -0
  124. benchmarks/run_context_scale_simulation.py +189 -0
  125. benchmarks/run_conversational_bench.py +170 -0
  126. benchmarks/run_h11_evaluation.py +316 -0
  127. benchmarks/run_h11_v2_evaluation.py +431 -0
  128. benchmarks/run_native_fixed12.py +469 -0
  129. benchmarks/run_native_preflight.py +280 -0
  130. benchmarks/run_scale_stress.py +225 -0
  131. benchmarks/solanazation_brownfield_01/build_oracle.py +970 -0
  132. benchmarks/solanazation_brownfield_01/check_level_a.py +78 -0
  133. benchmarks/solanazation_brownfield_01/debug_scope.py +13 -0
  134. benchmarks/solanazation_brownfield_01/evaluate_bootstrap_02.py +570 -0
  135. benchmarks/solanazation_brownfield_01/evaluate_first_pass.py +306 -0
  136. benchmarks/solanazation_brownfield_01/generate_dryrun_report.py +222 -0
  137. benchmarks/solanazation_brownfield_01/generate_manifests.py +146 -0
  138. benchmarks/solanazation_brownfield_01/inspect_misses.py +29 -0
  139. benchmarks/solanazation_brownfield_01/run_miss_forensic.py +206 -0
  140. benchmarks/solanazation_brownfield_01/test_facts.py +15 -0
  141. benchmarks/statistics/__init__.py +15 -0
  142. benchmarks/statistics/significance.py +326 -0
  143. benchmarks/test_native_mcp_preflight.py +229 -0
  144. benchmarks/test_prepare_public30_range_owner.py +35 -0
  145. benchmarks/test_public30_source_reference.py +698 -0
  146. benchmarks/test_recall_v2_e1_reader_input.py +89 -0
  147. benchmarks/test_recall_v2_navigation_mcp_replay.py +155 -0
  148. benchmarks/test_recall_v2_reader_replay.py +41 -0
  149. dandelion/__init__.py +3 -0
  150. dandelion/__main__.py +11 -0
  151. dandelion/agent_adapter.py +2369 -0
  152. dandelion/bootstrap/__init__.py +872 -0
  153. dandelion/bootstrap/alias_policy.py +50 -0
  154. dandelion/bootstrap/authority.py +225 -0
  155. dandelion/bootstrap/canonical_history.py +412 -0
  156. dandelion/bootstrap/classifier.py +403 -0
  157. dandelion/bootstrap/component_graph.py +280 -0
  158. dandelion/bootstrap/discovery.py +279 -0
  159. dandelion/bootstrap/domain_induction.py +180 -0
  160. dandelion/bootstrap/domain_topology.py +673 -0
  161. dandelion/bootstrap/evidence_envelope.py +132 -0
  162. dandelion/bootstrap/extractors/base.py +171 -0
  163. dandelion/bootstrap/extractors/exact.py +1234 -0
  164. dandelion/bootstrap/extractors/generic.py +100 -0
  165. dandelion/bootstrap/extractors/git_temporal.py +111 -0
  166. dandelion/bootstrap/extractors/registry.py +345 -0
  167. dandelion/bootstrap/extractors/relations.py +89 -0
  168. dandelion/bootstrap/extractors/semantic.py +424 -0
  169. dandelion/bootstrap/extractors/status.py +218 -0
  170. dandelion/bootstrap/git_sweeper.py +135 -0
  171. dandelion/bootstrap/manifest.py +246 -0
  172. dandelion/bootstrap/quarantine.py +264 -0
  173. dandelion/bootstrap/report.py +259 -0
  174. dandelion/bootstrap/root_resolver.py +221 -0
  175. dandelion/bootstrap/tech_profiler.py +125 -0
  176. dandelion/bootstrap/topology.py +75 -0
  177. dandelion/bootstrap/topology_index.py +110 -0
  178. dandelion/bootstrap/transaction.py +315 -0
  179. dandelion/canonical_bootstrap.py +257 -0
  180. dandelion/canonical_memory.py +4130 -0
  181. dandelion/captured_input.py +295 -0
  182. dandelion/cli.py +767 -0
  183. dandelion/config.py +40 -0
  184. dandelion/derived_index.py +745 -0
  185. dandelion/domains.py +706 -0
  186. dandelion/embedder.py +229 -0
  187. dandelion/event_evidence.py +571 -0
  188. dandelion/evidence.py +812 -0
  189. dandelion/evidence_response.py +453 -0
  190. dandelion/evidence_workspace.py +2360 -0
  191. dandelion/exact_context.py +129 -0
  192. dandelion/hydrator.py +124 -0
  193. dandelion/inspector.py +442 -0
  194. dandelion/inspector_assets/dandelion-mark.svg +33 -0
  195. dandelion/inspector_assets/dandelion_graph.js +361 -0
  196. dandelion/inspector_assets/inspector.css +223 -0
  197. dandelion/inspector_assets/inspector.html +135 -0
  198. dandelion/inspector_assets/inspector.js +1059 -0
  199. dandelion/legacy_import.py +431 -0
  200. dandelion/live_source_grants.py +133 -0
  201. dandelion/local_owner.py +114 -0
  202. dandelion/memory_client.py +252 -0
  203. dandelion/memory_contract.py +107 -0
  204. dandelion/memory_owner.py +560 -0
  205. dandelion/reader_providers.py +118 -0
  206. dandelion/recall_v2.py +767 -0
  207. dandelion/reconciler.py +219 -0
  208. dandelion/request_budget.py +125 -0
  209. dandelion/retained_basis.py +223 -0
  210. dandelion/retrieval.py +485 -0
  211. dandelion/router.py +405 -0
  212. dandelion/search_terms.py +53 -0
  213. dandelion/security.py +124 -0
  214. dandelion/server.py +810 -0
  215. dandelion/source_content_fts.py +219 -0
  216. dandelion/source_delivery.py +89 -0
  217. dandelion/source_history.py +241 -0
  218. dandelion/source_identity.py +88 -0
  219. dandelion/source_navigator.py +5 -0
  220. dandelion/source_reader.py +444 -0
  221. dandelion/source_references.py +379 -0
  222. dandelion/source_sections.py +265 -0
  223. dandelion/store_kv.py +684 -0
  224. dandelion/store_vector.py +1289 -0
  225. dandelion/studio.py +325 -0
  226. dandelion/wal.py +227 -0
  227. dandelion_memory-1.0.0rc15.dist-info/METADATA +48 -0
  228. dandelion_memory-1.0.0rc15.dist-info/RECORD +232 -0
  229. dandelion_memory-1.0.0rc15.dist-info/WHEEL +5 -0
  230. dandelion_memory-1.0.0rc15.dist-info/entry_points.txt +5 -0
  231. dandelion_memory-1.0.0rc15.dist-info/licenses/LICENSE +21 -0
  232. dandelion_memory-1.0.0rc15.dist-info/top_level.txt +2 -0
benchmarks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """DandelionBench — Independent Evaluation Suite for Agent Memory Architectures."""
2
+
3
+ __version__ = "1.0.0-rc1"
@@ -0,0 +1,29 @@
1
+ """Adapters module for DandelionBench."""
2
+
3
+ from benchmarks.adapters.base import BaseMemoryAdapter
4
+ from benchmarks.adapters.dandelion import DandelionAdapter
5
+ from benchmarks.adapters.ablations import (
6
+ DenseOnlyAdapter,
7
+ NoSnapshotAdapter,
8
+ NoReverseAdapter,
9
+ NoLexicalAdapter,
10
+ )
11
+ from benchmarks.adapters.controlled import (
12
+ HybridRAGAdapter,
13
+ ExtractResolveAdapter,
14
+ CoreArchivalAdapter,
15
+ NoLateBindingAdapter,
16
+ )
17
+
18
+ __all__ = [
19
+ "BaseMemoryAdapter",
20
+ "DandelionAdapter",
21
+ "DenseOnlyAdapter",
22
+ "NoSnapshotAdapter",
23
+ "NoReverseAdapter",
24
+ "NoLexicalAdapter",
25
+ "HybridRAGAdapter",
26
+ "ExtractResolveAdapter",
27
+ "CoreArchivalAdapter",
28
+ "NoLateBindingAdapter",
29
+ ]
@@ -0,0 +1,406 @@
1
+ """Ablations and baseline adapters for DandelionBench."""
2
+
3
+ import re
4
+ from typing import Any, Optional
5
+ import dandelion.store_kv
6
+ from dandelion.store_kv import KVStore
7
+ from dandelion.store_vector import VectorStore
8
+ from dandelion.hydrator import hydrate_template
9
+ from benchmarks.adapters.base import BaseMemoryAdapter
10
+
11
+
12
+ class DenseOnlyAdapter(BaseMemoryAdapter):
13
+ """Standard Dense Vector Memory baseline (e.g. naive vector DB / Mem0 / LangChain RAG).
14
+
15
+ Characteristics:
16
+ - Pure vector embeddings (FastEmbed cosine similarity).
17
+ - Eager binding: facts are inlined directly into text at ingestion time.
18
+ - No symbolic late-binding: when facts mutate, stored memories remain stale.
19
+ - No reverse reference index: cannot trace fact values back to referencing memories.
20
+ - No secret redaction policy: sensitive tokens are stored and recalled in plaintext.
21
+ - No identifier / BM25 lexical boost.
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ self.facts: dict[str, str] = {}
26
+ self.vec: Optional[VectorStore] = None
27
+ self.reset()
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ return "dense_only"
32
+
33
+ def reset(self) -> None:
34
+ self.facts.clear()
35
+ self.vec = VectorStore(in_memory=True)
36
+ # Clear global KV store to prevent state leakage from prior adapters
37
+ import dandelion.store_kv
38
+ dandelion.store_kv._store = None
39
+
40
+ def ingest_fact(
41
+ self,
42
+ key: str,
43
+ value: str,
44
+ timestamp: float,
45
+ domain: str = "general",
46
+ source: str = "config_read",
47
+ ) -> None:
48
+ self.facts[key] = value
49
+ # Also index fact as plain passage in vector store
50
+ fact_sentence = f"Configuration fact '{key}' is set to '{value}' in domain '{domain}'."
51
+ self.vec.save_chunk(
52
+ domain=domain,
53
+ template=fact_sentence,
54
+ source=source,
55
+ created_at=timestamp,
56
+ )
57
+
58
+ def ingest_memory(
59
+ self,
60
+ event_id: str,
61
+ template: str,
62
+ domain: str,
63
+ timestamp: float,
64
+ supersedes: Optional[str] = None,
65
+ source: str = "tool_observed",
66
+ ) -> str:
67
+ # Eager resolution at store time: inlines current fact value directly
68
+ def _resolve_eager(m: re.Match) -> str:
69
+ raw_key = m.group(1).split("@rev:")[0]
70
+ # Try domain-qualified first
71
+ if domain:
72
+ qual_key = f"{domain}.{raw_key}"
73
+ if qual_key in self.facts:
74
+ return self.facts[qual_key]
75
+ return self.facts.get(raw_key, f"[MISSING: {raw_key}]")
76
+
77
+ inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_eager, template)
78
+ if domain:
79
+ inlined = inlined.replace("{{domain}}", domain)
80
+
81
+ return self.vec.save_chunk(
82
+ domain=domain,
83
+ template=inlined,
84
+ supersedes=supersedes,
85
+ source=source,
86
+ created_at=timestamp,
87
+ event_id=event_id,
88
+ )
89
+
90
+ def recall(
91
+ self,
92
+ query: str,
93
+ domain: Optional[str] = None,
94
+ limit: int = 3,
95
+ ) -> list[dict[str, Any]]:
96
+ domains = [domain] if domain and not domain.endswith(".*") else None
97
+ # Dense vector search only: no lexical rerank, no reverse index
98
+ raw_results = self.vec.search_chunks(
99
+ query=query,
100
+ domains=domains,
101
+ limit=limit,
102
+ use_reverse_index=False,
103
+ use_lexical_rerank=False,
104
+ )
105
+
106
+ out = []
107
+ for r in raw_results:
108
+ out.append({
109
+ "id": r["id"],
110
+ "event_id": r.get("event_id", r["id"]),
111
+ "text": r["template"],
112
+ "template": r["template"],
113
+ "domain": r["domain"],
114
+ "score": r["score"],
115
+ })
116
+ return out
117
+
118
+ def get_fact(
119
+ self,
120
+ key: str,
121
+ domain: Optional[str] = None,
122
+ at: Optional[float] = None,
123
+ ) -> Optional[str]:
124
+ if domain:
125
+ scoped = f"{domain}.{key}"
126
+ if scoped in self.facts:
127
+ return self.facts[scoped]
128
+ return self.facts.get(key)
129
+
130
+
131
+ class NoSnapshotAdapter(BaseMemoryAdapter):
132
+ """Ablation: Dandelion without Snapshot Pinning.
133
+
134
+ All {{snapshot:key}} references are treated as live {{ref:key}}.
135
+ When a fact mutates, historical memories also resolve to the new value,
136
+ violating temporal fidelity (tests Hypothesis H2).
137
+ """
138
+
139
+ def __init__(self) -> None:
140
+ self.kv: Optional[KVStore] = None
141
+ self.vec: Optional[VectorStore] = None
142
+ self.reset()
143
+
144
+ @property
145
+ def name(self) -> str:
146
+ return "no_snapshot"
147
+
148
+ def reset(self) -> None:
149
+ self.kv = KVStore(path=None)
150
+ dandelion.store_kv._store = self.kv
151
+ self.vec = VectorStore(in_memory=True)
152
+
153
+ def ingest_fact(
154
+ self,
155
+ key: str,
156
+ value: str,
157
+ timestamp: float,
158
+ domain: str = "general",
159
+ source: str = "config_read",
160
+ ) -> None:
161
+ self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
162
+
163
+ def ingest_memory(
164
+ self,
165
+ event_id: str,
166
+ template: str,
167
+ domain: str,
168
+ timestamp: float,
169
+ supersedes: Optional[str] = None,
170
+ source: str = "tool_observed",
171
+ ) -> str:
172
+ # Intentionally convert snapshot to live ref instead of pinning
173
+ unpinned = re.sub(r"\{\{snapshot:([\w.:\-]+)\}\}", r"{{ref:\1}}", template)
174
+ return self.vec.save_chunk(
175
+ domain=domain,
176
+ template=unpinned,
177
+ supersedes=supersedes,
178
+ source=source,
179
+ created_at=timestamp,
180
+ event_id=event_id,
181
+ )
182
+
183
+ def recall(
184
+ self,
185
+ query: str,
186
+ domain: Optional[str] = None,
187
+ limit: int = 3,
188
+ ) -> list[dict[str, Any]]:
189
+ domains = [domain] if domain and not domain.endswith(".*") else None
190
+ raw_results = self.vec.search_chunks(query=query, domains=domains, limit=limit)
191
+
192
+ out = []
193
+ for r in raw_results:
194
+ hydrated = hydrate_template(
195
+ r["template"],
196
+ kv_resolver=self.kv.resolve_ref,
197
+ chunk_domain=r["domain"],
198
+ allow_secrets=False,
199
+ )
200
+ out.append({
201
+ "id": r["id"],
202
+ "event_id": r.get("event_id", r["id"]),
203
+ "text": hydrated,
204
+ "template": r["template"],
205
+ "domain": r["domain"],
206
+ "score": r["score"],
207
+ })
208
+ return out
209
+
210
+ def get_fact(
211
+ self,
212
+ key: str,
213
+ domain: Optional[str] = None,
214
+ at: Optional[float] = None,
215
+ ) -> Optional[str]:
216
+ if at is not None:
217
+ return self.kv.get_fact(key, at_timestamp=at)
218
+ return self.kv.resolve_ref(key, domain=domain)
219
+
220
+
221
+ class NoReverseAdapter(BaseMemoryAdapter):
222
+ """Ablation: Dandelion without Fact Reverse-Lookup Index.
223
+
224
+ Querying for fact values (e.g. '5433') relies solely on dense/lexical
225
+ matching without checking which chunks reference that fact key.
226
+ """
227
+
228
+ def __init__(self) -> None:
229
+ self.kv: Optional[KVStore] = None
230
+ self.vec: Optional[VectorStore] = None
231
+ self.reset()
232
+
233
+ @property
234
+ def name(self) -> str:
235
+ return "no_reverse"
236
+
237
+ def reset(self) -> None:
238
+ self.kv = KVStore(path=None)
239
+ dandelion.store_kv._store = self.kv
240
+ self.vec = VectorStore(in_memory=True)
241
+
242
+ def ingest_fact(
243
+ self,
244
+ key: str,
245
+ value: str,
246
+ timestamp: float,
247
+ domain: str = "general",
248
+ source: str = "config_read",
249
+ ) -> None:
250
+ self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
251
+
252
+ def ingest_memory(
253
+ self,
254
+ event_id: str,
255
+ template: str,
256
+ domain: str,
257
+ timestamp: float,
258
+ supersedes: Optional[str] = None,
259
+ source: str = "tool_observed",
260
+ ) -> str:
261
+ from dandelion.hydrator import pin_snapshots
262
+ pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
263
+ return self.vec.save_chunk(
264
+ domain=domain,
265
+ template=pinned,
266
+ supersedes=supersedes,
267
+ source=source,
268
+ created_at=timestamp,
269
+ event_id=event_id,
270
+ )
271
+
272
+ def recall(
273
+ self,
274
+ query: str,
275
+ domain: Optional[str] = None,
276
+ limit: int = 3,
277
+ ) -> list[dict[str, Any]]:
278
+ domains = [domain] if domain and not domain.endswith(".*") else None
279
+ # Explicitly disable reverse index in hybrid search
280
+ raw_results = self.vec.search_chunks(
281
+ query=query,
282
+ domains=domains,
283
+ limit=limit,
284
+ use_reverse_index=False,
285
+ use_lexical_rerank=True,
286
+ )
287
+
288
+ out = []
289
+ for r in raw_results:
290
+ hydrated = hydrate_template(
291
+ r["template"],
292
+ kv_resolver=self.kv.resolve_ref,
293
+ chunk_domain=r["domain"],
294
+ allow_secrets=False,
295
+ )
296
+ out.append({
297
+ "id": r["id"],
298
+ "event_id": r.get("event_id", r["id"]),
299
+ "text": hydrated,
300
+ "template": r["template"],
301
+ "domain": r["domain"],
302
+ "score": r["score"],
303
+ })
304
+ return out
305
+
306
+ def get_fact(
307
+ self,
308
+ key: str,
309
+ domain: Optional[str] = None,
310
+ at: Optional[float] = None,
311
+ ) -> Optional[str]:
312
+ if at is not None:
313
+ return self.kv.get_fact(key, at_timestamp=at)
314
+ return self.kv.resolve_ref(key, domain=domain)
315
+
316
+
317
+ class NoLexicalAdapter(BaseMemoryAdapter):
318
+ """Ablation: Dandelion without BM25 / Lexical Identifier Matching."""
319
+
320
+ def __init__(self) -> None:
321
+ self.kv: Optional[KVStore] = None
322
+ self.vec: Optional[VectorStore] = None
323
+ self.reset()
324
+
325
+ @property
326
+ def name(self) -> str:
327
+ return "no_lexical"
328
+
329
+ def reset(self) -> None:
330
+ self.kv = KVStore(path=None)
331
+ dandelion.store_kv._store = self.kv
332
+ self.vec = VectorStore(in_memory=True)
333
+
334
+ def ingest_fact(
335
+ self,
336
+ key: str,
337
+ value: str,
338
+ timestamp: float,
339
+ domain: str = "general",
340
+ source: str = "config_read",
341
+ ) -> None:
342
+ self.kv.set_fact(key=key, value=value, source=source, timestamp=timestamp)
343
+
344
+ def ingest_memory(
345
+ self,
346
+ event_id: str,
347
+ template: str,
348
+ domain: str,
349
+ timestamp: float,
350
+ supersedes: Optional[str] = None,
351
+ source: str = "tool_observed",
352
+ ) -> str:
353
+ from dandelion.hydrator import pin_snapshots
354
+ pinned = pin_snapshots(template, self.kv.get_current_revision, domain)
355
+ return self.vec.save_chunk(
356
+ domain=domain,
357
+ template=pinned,
358
+ supersedes=supersedes,
359
+ source=source,
360
+ created_at=timestamp,
361
+ event_id=event_id,
362
+ )
363
+
364
+ def recall(
365
+ self,
366
+ query: str,
367
+ domain: Optional[str] = None,
368
+ limit: int = 3,
369
+ ) -> list[dict[str, Any]]:
370
+ domains = [domain] if domain and not domain.endswith(".*") else None
371
+ # Explicitly disable lexical reranking
372
+ raw_results = self.vec.search_chunks(
373
+ query=query,
374
+ domains=domains,
375
+ limit=limit,
376
+ use_reverse_index=True,
377
+ use_lexical_rerank=False,
378
+ )
379
+
380
+ out = []
381
+ for r in raw_results:
382
+ hydrated = hydrate_template(
383
+ r["template"],
384
+ kv_resolver=self.kv.resolve_ref,
385
+ chunk_domain=r["domain"],
386
+ allow_secrets=False,
387
+ )
388
+ out.append({
389
+ "id": r["id"],
390
+ "event_id": r.get("event_id", r["id"]),
391
+ "text": hydrated,
392
+ "template": r["template"],
393
+ "domain": r["domain"],
394
+ "score": r["score"],
395
+ })
396
+ return out
397
+
398
+ def get_fact(
399
+ self,
400
+ key: str,
401
+ domain: Optional[str] = None,
402
+ at: Optional[float] = None,
403
+ ) -> Optional[str]:
404
+ if at is not None:
405
+ return self.kv.get_fact(key, at_timestamp=at)
406
+ return self.kv.resolve_ref(key, domain=domain)
@@ -0,0 +1,72 @@
1
+ """Base interface for agent memory benchmark adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Optional
5
+
6
+
7
+ class BaseMemoryAdapter(ABC):
8
+ """Abstract interface for memory systems under evaluation."""
9
+
10
+ @property
11
+ @abstractmethod
12
+ def name(self) -> str:
13
+ """System or ablation name (e.g. 'dandelion', 'dense_only')."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def reset(self) -> None:
18
+ """Clear all internal memory stores."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def ingest_fact(
23
+ self,
24
+ key: str,
25
+ value: str,
26
+ timestamp: float,
27
+ domain: str = "general",
28
+ source: str = "config_read",
29
+ ) -> None:
30
+ """Store or mutate a deterministic fact."""
31
+ pass
32
+
33
+ @abstractmethod
34
+ def ingest_memory(
35
+ self,
36
+ event_id: str,
37
+ template: str,
38
+ domain: str,
39
+ timestamp: float,
40
+ supersedes: Optional[str] = None,
41
+ source: str = "tool_observed",
42
+ ) -> str:
43
+ """Store an episodic/semantic memory event."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def recall(
48
+ self,
49
+ query: str,
50
+ domain: Optional[str] = None,
51
+ limit: int = 3,
52
+ ) -> list[dict[str, Any]]:
53
+ """Retrieve top candidate memories for query.
54
+
55
+ Each returned dict MUST contain at least:
56
+ - 'id': str
57
+ - 'event_id': str
58
+ - 'text': str (hydrated or inlined content shown to agent)
59
+ - 'domain': str
60
+ - 'score': float
61
+ """
62
+ pass
63
+
64
+ @abstractmethod
65
+ def get_fact(
66
+ self,
67
+ key: str,
68
+ domain: Optional[str] = None,
69
+ at: Optional[float] = None,
70
+ ) -> Optional[str]:
71
+ """Directly query a deterministic fact."""
72
+ pass
@@ -0,0 +1,13 @@
1
+ """Controlled architectural baseline adapters for DandelionBench Track A."""
2
+
3
+ from benchmarks.adapters.controlled.hybrid_rag import HybridRAGAdapter
4
+ from benchmarks.adapters.controlled.extract_resolve import ExtractResolveAdapter
5
+ from benchmarks.adapters.controlled.core_archival import CoreArchivalAdapter
6
+ from benchmarks.adapters.controlled.no_late_binding import NoLateBindingAdapter
7
+
8
+ __all__ = [
9
+ "HybridRAGAdapter",
10
+ "ExtractResolveAdapter",
11
+ "CoreArchivalAdapter",
12
+ "NoLateBindingAdapter",
13
+ ]
@@ -0,0 +1,115 @@
1
+ """Controlled Architectural Baseline: Core + Archival Memory (MemGPT / Letta paradigm).
2
+
3
+ Characteristics:
4
+ - Two-tier memory hierarchy:
5
+ 1. In-context Core Memory (structured blocks, editable in place, always visible).
6
+ 2. Long-term Archival Memory (dense vector search over past event logs).
7
+ - Facts immediately update Core Memory blocks.
8
+ - Episodic memories are stored into Archival Memory as static passages.
9
+ - Evaluates the effect of in-context core state versus late-bound symbolic hydration.
10
+ """
11
+
12
+ import re
13
+ from typing import Any, Optional
14
+ from dandelion.store_vector import VectorStore
15
+ from benchmarks.adapters.base import BaseMemoryAdapter
16
+
17
+
18
+ class CoreArchivalAdapter(BaseMemoryAdapter):
19
+ """Core + Archival memory baseline representing two-tier agent memory systems."""
20
+
21
+ def __init__(self) -> None:
22
+ self.core_memory: dict[str, str] = {}
23
+ self.archival_vec: Optional[VectorStore] = None
24
+ self.reset()
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ return "core_archival"
29
+
30
+ def reset(self) -> None:
31
+ self.core_memory.clear()
32
+ self.archival_vec = VectorStore(in_memory=True)
33
+ import dandelion.store_kv
34
+ dandelion.store_kv._store = None
35
+
36
+ def ingest_fact(
37
+ self,
38
+ key: str,
39
+ value: str,
40
+ timestamp: float,
41
+ domain: str = "general",
42
+ source: str = "config_read",
43
+ ) -> None:
44
+ # Core Memory update (in-place state edit)
45
+ self.core_memory[key] = value
46
+
47
+ def ingest_memory(
48
+ self,
49
+ event_id: str,
50
+ template: str,
51
+ domain: str,
52
+ timestamp: float,
53
+ supersedes: Optional[str] = None,
54
+ source: str = "tool_observed",
55
+ ) -> str:
56
+ # Archival memory stores static passages resolved against current Core Memory
57
+ def _resolve_core(m: re.Match) -> str:
58
+ raw_key = m.group(1).split("@rev:")[0]
59
+ if domain:
60
+ qual_key = f"{domain}.{raw_key}"
61
+ if qual_key in self.core_memory:
62
+ return self.core_memory[qual_key]
63
+ return self.core_memory.get(raw_key, f"[MISSING: {raw_key}]")
64
+
65
+ inlined = re.sub(r"\{\{(?:ref|snapshot):([\w.:\-]+)\}\}", _resolve_core, template)
66
+ if domain:
67
+ inlined = inlined.replace("{{domain}}", domain)
68
+
69
+ return self.archival_vec.save_chunk(
70
+ domain=domain,
71
+ template=inlined,
72
+ supersedes=supersedes,
73
+ source=source,
74
+ created_at=timestamp,
75
+ event_id=event_id,
76
+ )
77
+
78
+ def recall(
79
+ self,
80
+ query: str,
81
+ domain: Optional[str] = None,
82
+ limit: int = 3,
83
+ ) -> list[dict[str, Any]]:
84
+ domains = [domain] if domain and not domain.endswith(".*") else None
85
+ # Archival vector search
86
+ raw_results = self.archival_vec.search_chunks(
87
+ query=query,
88
+ domains=domains,
89
+ limit=limit,
90
+ use_reverse_index=False,
91
+ use_lexical_rerank=False,
92
+ )
93
+ return [
94
+ {
95
+ "id": r["id"],
96
+ "event_id": r.get("event_id", ""),
97
+ "text": r["template"],
98
+ "domain": r["domain"],
99
+ "score": r.get("score", 0.0),
100
+ "vector_score": r.get("vector_score", 0.0),
101
+ }
102
+ for r in raw_results
103
+ ]
104
+
105
+ def get_fact(
106
+ self,
107
+ key: str,
108
+ domain: Optional[str] = None,
109
+ at: Optional[float] = None,
110
+ ) -> Optional[str]:
111
+ if domain:
112
+ qual_key = f"{domain}.{key}"
113
+ if qual_key in self.core_memory:
114
+ return self.core_memory[qual_key]
115
+ return self.core_memory.get(key)