nooa-memory 0.0.7__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.
nooa_memory/README.md ADDED
@@ -0,0 +1,450 @@
1
+ # `nooa_memory` — long-term memory for agents
2
+
3
+ An **opt-in, additive, brain-inspired long-term memory subsystem** for nooa
4
+ Agents. Toggle it on per agent; it changes no core framework code. Its defining
5
+ idea: **the agent authors and curates its own memories** through native tools —
6
+ rather than a harness extracting them behind the agent's back.
7
+
8
+ - Runnable demos + measured results: [`examples/memory_bench/`](../../../examples/memory_bench/)
9
+ and the quickstart [`examples/quickstart/12_memory.py`](../../../examples/quickstart/12_memory.py).
10
+
11
+ ---
12
+
13
+ ## Table of contents
14
+ 1. [Why](#why) · 2. [Quick start](#quick-start) · 3. [Architecture](#architecture) ·
15
+ 4. [Memory records & schema](#memory-records--schema) · 5. [The operations](#the-operations) ·
16
+ 6. [Agent-facing tools](#agent-facing-tools) · 7. [Retrieval & scoring](#retrieval--scoring) ·
17
+ 8. [Spontaneous retrieval](#spontaneous-retrieval-automatic-recall) · 9. [Reflection](#reflection-offline-consolidation) ·
18
+ 10. [Forgetting](#forgetting) · 11. [Storage & embeddings](#storage--embedding-backends) ·
19
+ 12. [Configuration](#configuration-reference) · 13. [Monitoring](#monitoring--debugging) ·
20
+ 14. [What it's good for](#when-does-memory-help) · 15. [Module map](#module-map) · 16. [Limitations](#limitations)
21
+
22
+ ---
23
+
24
+ ## Why
25
+
26
+ A nooa agent is normally amnesic between method calls and sessions. This add-on
27
+ gives it durable memory across **long-horizon autonomous tasks** and **tasks
28
+ accreted over time**, modeled on how the brain uses memory.
29
+
30
+ | | other systems (Mem0, Zep, RAG) | **this system** |
31
+ |---|---|---|
32
+ | who creates memories | a harness pipeline extracts/stores them | **the agent** writes them via tools |
33
+ | instruction | implicit / external | the agent is **told it owns its memory** and given the schema (`MEMORY_SCHEMA_GUIDE`, injected at install) |
34
+ | refinement | system-side | the agent **`update_memory` / `forget` / `associate`** as it goes |
35
+
36
+ When `enabled=False` (or not installed), the agent is byte-for-byte unchanged — the
37
+ *additive guarantee* (regression-tested).
38
+
39
+ ---
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ from nooa import Agent
45
+ from nooa_memory import MemoryConfig, MemoryManager, MemoryToolsMixin
46
+
47
+ class MyAgent(MemoryToolsMixin, Agent, llm=my_llm):
48
+ async def work(self, task: str) -> str:
49
+ # Do {task}; recall with self.recall(...) and save with self.remember(...).
50
+ ...
51
+
52
+ agent = MyAgent()
53
+ MemoryManager.install(agent, config=MemoryConfig(enabled=True))
54
+
55
+ # The agent now writes/reads its own memory; you can too:
56
+ agent.remember("Deploy with `make ship`.", type="skill", importance="HIGH")
57
+ hits = agent.recall("how do I deploy?") # list[Memory]
58
+ report = agent.reflect() # offline consolidation
59
+ print(agent.memory_stats().summary()) # usage counters
60
+ ```
61
+
62
+ Default config needs **no external services**: a local SQLite file + a
63
+ deterministic offline embedder. Point it at real models when you want (below).
64
+ `MemoryManager.install(...)` remains available as lower-level plumbing for hosts
65
+ that need manual lifecycle control, but normal users should enable `nemo.memory`
66
+ as a skill.
67
+
68
+ ---
69
+
70
+ ## Architecture
71
+
72
+ `MemoryManager.install(agent, config=...)` wires three surfaces onto the agent's
73
+ **existing** machinery (mirrors `agents/summarization.py`; **no core edits**):
74
+
75
+ ```
76
+ ┌─────────────────────── your Agent ───────────────────────┐
77
+ conscious tools │ remember / recall / search / update_memory / forget / │
78
+ (MemoryToolsMixin)│ associate ── visible via doc(self) ── │
79
+ │ │
80
+ pre-turn inject │ BeforeTurn ─▶ recall_for_context() ─▶ ContextManager │ "spontaneous association"
81
+ │ (dynamic block, configurable cadence) │
82
+ │ │
83
+ write-on-event │ EventManager.on("Error"/"Notification"/…) ─▶ remember() │
84
+ │ │
85
+ post-task reflect│ intercept("agent_call", top-level) ─▶ reflect() │ "consolidation"
86
+ └────────────────────────────┬───────────────────────────-─┘
87
+ │
88
+ MemoryManager ────────── owns:
89
+ • MemoryStore (SQLite: records + graph + vectors)
90
+ • RetrievalEngine (hybrid recall + ACT-R scoring + k-hop)
91
+ • ReflectionEngine (merge · reconsolidate · abstract · prune)
92
+ • ForgettingEngine (decay + prune)
93
+ • Embedder (hashing | litellm)
94
+ • VectorIndex (numpy | sqlite-vec | chroma)
95
+ ```
96
+
97
+ `install()` also injects `MEMORY_SCHEMA_GUIDE` (the "you own your memory + here's the
98
+ schema" instruction) as a static context block, and `register`s runtime
99
+ memory-events. `uninstall()` removes every hook and the instruction block.
100
+
101
+ ---
102
+
103
+ ## Memory records & schema
104
+
105
+ A memory (`schema.Memory`) is intentionally **loose** — only `id`, `type`,
106
+ `content`, `created_at` are required; everything else has sane defaults.
107
+
108
+ - **types** (`MemoryType`): `info` (semantic fact), `skill` (verified procedure),
109
+ `episode` (a specific experience), `intent` (future reminder/TODO),
110
+ `reflection` (insight distilled from episodes), `scratch` (transient working memory).
111
+ - **descriptors:** `importance` (verbal: CRITICAL/HIGH/MEDIUM/LOW/TRIVIAL), `salience`, `confidence`, `mood`, `strength`
112
+ (spaced-repetition counter), `reinforcement_count`.
113
+ - **metadata:** `created_at`, `last_accessed_at`, `access_log`, `access_count`,
114
+ `source_task_ref`, `related_files`, `chat_turn_ref`, `valid_from/valid_to`.
115
+ - **graph:** typed directed `edges` (`EdgeType`): causal `derived_from` / `created_by`
116
+ / `causes`, plus `refines`, `supports`, `contradicts`, `related`, `precedes`,
117
+ `part_of`, `triggers`.
118
+
119
+ The directed graph lets retrieval do **multi-hop associative spread** and lets
120
+ reflection record provenance.
121
+
122
+ ---
123
+
124
+ ## The operations
125
+
126
+ | operation | what | how it fires |
127
+ |---|---|---|
128
+ | **Encode (write)** | create a memory; dedup-on-write reinforces near-duplicates | conscious `remember()` + automatic event-driven |
129
+ | **Spontaneous association** | inject relevant memories into context each turn | `BeforeTurn` → dynamic block (cadence: `self_gated`/`per_task`/`every_turn`) |
130
+ | **Deliberate recall** | the agent looks something up | `recall()` / `search()` tools |
131
+ | **Reflection** | offline consolidation after a task | top-level `intercept("agent_call")` or `manager.reflect()` |
132
+ | **Forgetting** | decay + prune | online (lazy) + offline (in reflection) |
133
+
134
+ ---
135
+
136
+ ## Agent-facing tools
137
+
138
+ Mixed in by `MemoryToolsMixin`, enabled/disabled per `MemoryConfig.tools`:
139
+
140
+ | tool | signature | purpose |
141
+ |---|---|---|
142
+ | `remember` | `(content, *, type="info", importance="MEDIUM", tags=None, title=None) -> id` | write a new memory (dedup-on-write) |
143
+ | `recall` | `(query, k=5) -> list[Memory]` | associative recall (similarity + graph spread) |
144
+ | `search` | `(query, k=5) -> list[Memory]` | term/keyword recall (no graph hop) |
145
+ | `update_memory` | `(id, *, content=, importance=, type=, tags=) -> bool` | refine an existing memory (re-embeds if content changed) |
146
+ | `forget` | `(id) -> bool` | archive an obsolete/wrong memory |
147
+ | `associate` | `(a_id, b_id, relation="related") -> None` | add a directed graph edge |
148
+
149
+ The same operations are available programmatically on `agent._memory`
150
+ (`MemoryManager`), which also exposes `reflect()`, `recall_for_context()`,
151
+ `memory_stats()`, `log_summary()`.
152
+
153
+ ---
154
+
155
+ ## Retrieval & scoring
156
+
157
+ `recall(query)` runs (see `retrieval.py`):
158
+
159
+ 1. **Hybrid candidates** — dense (embedding KNN) ∪ sparse (keyword) pools.
160
+ 2. **ACT-R-style score** per candidate (each term min-max normalised, then weighted):
161
+ ```
162
+ rel = λ·cos(query, mem) + (1−λ)·context-overlap # encoding specificity
163
+ rec = σ( ln Σ_k (t_now − access_k)^−d ) # ACT-R base-level activation
164
+ imp = importance / 10
165
+ score = α_rel·rel̂ + α_rec·reĉ + α_imp·imp̂
166
+ ```
167
+ 3. **Multi-hop spread** (`hops ≥ 1`) — activation propagates over graph edges with
168
+ per-hop decay, surfacing memories linked to (but not directly similar to) the query.
169
+
170
+ On retrieval, memories are "touched" (recency + strength bumped) so hot paths
171
+ self-strengthen. Weights/decays/hops are all in `RetrievalConfig`/`ScoringWeights`.
172
+
173
+ ---
174
+
175
+ ## Spontaneous retrieval (automatic recall)
176
+
177
+ The agent does **not** have to call `recall()` to benefit from memory. Before each
178
+ turn, a `BeforeTurn` hook **synthesises a query from the conversation** ("the anchor"),
179
+ runs the **same full `recall()` pipeline** (dense ANN ∪ keyword → ACT-R score → graph
180
+ spread — *not* ANN-only), and injects the top hits into a context block the model reads
181
+ that turn (`manager.py` → `ContextManager.set_dynamic`). As the conversation moves, the
182
+ anchor changes and different memories surface — "spontaneous association".
183
+
184
+ **How to use it.** Nothing — it's automatic once memory is installed (`enabled=True`).
185
+ The recalled memories appear in the agent's context under the block:
186
+
187
+ ```
188
+ ## Recalled memories (associative)
189
+ - [info] prod DB is pg-east-1
190
+ - [skill] deploy with `make ship`
191
+ ```
192
+
193
+ Use explicit `self.recall(query)` only when you want to *deliberately* fetch for a
194
+ specific query; spontaneous recall covers the passive "what's relevant right now" case.
195
+
196
+ **The anchor** is built by `SpontaneousConfig.query_strategies` (composable, de-duped):
197
+
198
+ | strategy | anchor it builds |
199
+ |---|---|
200
+ | `last_message` *(default)* | the most recent user-text message |
201
+ | `recent_events` | the last `recent_events_n` events, concatenated |
202
+ | `working_state` | recent `PythonOutput`/`LLMOutput` (the agent's scratch state) |
203
+ | `distilled` | LLM-distilled query — falls back to `recent_events` with no LLM |
204
+
205
+ If no anchor can be derived (e.g. empty turn) **nothing is injected** — it never
206
+ "retrieves from nothing". Spontaneous recall uses **`touch=False`**: passively surfaced
207
+ memories are *not* reinforced (only memories the agent deliberately `recall()`s get their
208
+ recency/strength bumped), so the ACT-R signal stays meaningful.
209
+
210
+ **How to configure** (`SpontaneousConfig`):
211
+
212
+ ```python
213
+ from nooa_memory import MemoryConfig, MemoryManager
214
+ from nooa_memory.config import SpontaneousConfig
215
+
216
+ MemoryManager.install(agent, config=MemoryConfig(
217
+ enabled=True,
218
+ spontaneous=SpontaneousConfig(
219
+ enabled=True, # set False to disable auto-injection entirely
220
+ query_strategies=("last_message",), # anchor source(s), tried in order
221
+ inject_cadence="self_gated", # self_gated | per_task | every_turn
222
+ top_k=5, # memories injected (0 -> retrieval.top_k)
223
+ context_char_budget=2000, # hard cap on the injected block
224
+ context_block_key="recalled_memories",
225
+ recent_events_n=5, # window for recent_events / working_state
226
+ ),
227
+ ))
228
+ ```
229
+
230
+ - **`inject_cadence`** — `self_gated` (default): re-inject only when the derived anchor
231
+ changes (cheapest); `per_task`: inject once per task; `every_turn`: refresh every turn.
232
+ - Turn it **off** with `spontaneous=SpontaneousConfig(enabled=False)` and rely on the
233
+ agent's deliberate `recall`/`search` tools instead.
234
+
235
+ ---
236
+
237
+ ## Reflection (offline consolidation)
238
+
239
+ `manager.reflect()` (see `reflection.py`) runs an ordered, brain-inspired pipeline:
240
+
241
+ 1. **merge** near-identical memories into one canonical record;
242
+ 2. **reconsolidate** — cluster related memories and let an optional LLM `reconciler`
243
+ resolve outdated/contradicted values (**keep-latest**: archive stale, store the
244
+ current consolidated one);
245
+ 3. **edge formation** — link memories whose embeddings are close;
246
+ 4. **re-score** importance (salience + access-frequency aware);
247
+ 5. **abstract** — an optional LLM `reasoner` turns clusters of episodes into compact
248
+ `reflection` memories (the generative abstraction step);
249
+ 6. **prune** — forget decayed memories.
250
+
251
+ Deterministic steps (1, 3, 4, 6) run with no LLM. The generative steps (2, 5) only
252
+ run when you pass `reasoner=` / `reconciler=` to `install()`.
253
+
254
+ **How to use it.** Two ways to trigger a pass:
255
+
256
+ - **Automatic (default):** with `ReflectionPolicy(trigger="post_task")`, a top-level
257
+ `agent_call` interception runs `reflect()` after each task (only top-level calls, not
258
+ nested subagent calls, when `only_top_level=True`).
259
+ - **Manual:** call `manager.reflect()` (or `self.memory.reflect()` via the skill) whenever
260
+ you want — returns a `ReflectionReport` (`merged`, `reconciled`, `superseded`,
261
+ `edges_added`, `rescored`, `created`, `pruned`).
262
+
263
+ **How to configure** (`ReflectionPolicy`):
264
+
265
+ ```python
266
+ from nooa_memory import MemoryConfig, MemoryManager
267
+ from nooa_memory.config import ReflectionPolicy
268
+
269
+ MemoryManager.install(
270
+ agent,
271
+ config=MemoryConfig(enabled=True, reflection=ReflectionPolicy(
272
+ enabled=True,
273
+ trigger="post_task", # post_task (auto) | manual
274
+ only_top_level=True, # nested subagent calls do NOT each reflect
275
+ background=False, # False = await inline (deterministic); True = create_task
276
+ merge_threshold=0.95, # cosine >= this -> merge duplicates
277
+ edge_threshold=0.80, # this <= cosine < merge_threshold -> add a 'related' edge
278
+ recon_threshold=0.6, # cluster radius for reconsolidation
279
+ recon_max_cluster=6,
280
+ max_episodes_per_reflection=50,
281
+ )),
282
+ reasoner=my_llm_reasoner, # OPTIONAL — enables step 5 (abstraction)
283
+ reconciler=my_llm_reconciler, # OPTIONAL — enables step 2 (reconsolidation)
284
+ )
285
+ ```
286
+
287
+ **LLM or not.** Without `reasoner`/`reconciler`, `reflect()` makes **zero LLM calls** —
288
+ the deterministic steps (merge, edges, re-score, prune) run over the already-stored
289
+ embeddings + ACT-R math (the skill installs this way by default). Wire the two callables
290
+ to enable the generative steps. They're plain callables you build from your LLM, e.g.:
291
+
292
+ ```python
293
+ def my_llm_reasoner(episodes): # list[Memory] -> list[Memory] (new reflections)
294
+ text = my_llm.call([{ "role": "user",
295
+ "content": "Distil durable insights from these episodes:\n" + format(episodes)}]).content
296
+ return [Memory(content=line, type=MemoryType.REFLECTION) for line in parse(text)]
297
+
298
+ def my_llm_reconciler(cluster): # list[Memory] (old->new) -> (current|None, [archive_ids])
299
+ # ask the LLM which value is current; return the consolidated memory + ids to retire
300
+ ...
301
+ ```
302
+
303
+ See `examples/memory_bench/reflecting.py` (`make_llm_reasoner`) and `longmemeval.py`
304
+ (`make_llm_reconciler`) for working implementations.
305
+
306
+ **When does reflection help?** Empirically (real gpt-5.4):
307
+
308
+ | task shape | retrieval bottleneck? | reflect effect |
309
+ |---|---|---|
310
+ | synthesis of many scattered facts | yes (small `top_k` ≪ #facts) | **+50%** |
311
+ | aggregation / latest value, memories already fit budget | no | **+0%** (neutral) |
312
+ | one pinpoint fact | no | **−20%** (abstraction blurs it) |
313
+
314
+ Consolidation pays off **only when retrieval is the bottleneck** and the model
315
+ can't reconcile in-context. It's off by default for pure retrieval-QA.
316
+
317
+ ---
318
+
319
+ ## Forgetting
320
+
321
+ `forgetting.py`: every memory has a retention that **decays with time** (Ebbinghaus
322
+ `R = e^(−Δt/S)`), slowed by `strength` (retrieval boosts it). The offline prune
323
+ archives memories below threshold, with guards: never prune very young memories,
324
+ high-importance memories, or `protected_types` (default: `skill`). Archive (soft
325
+ tombstone) vs hard delete is configurable.
326
+
327
+ ---
328
+
329
+ ## Storage & embedding backends
330
+
331
+ **One SQLite file** holds records + the association graph + vector blobs (separate
332
+ from the agent's session DB; WAL). The vector index is pluggable behind one
333
+ protocol (`vector_backends.py`):
334
+
335
+ | `vector.backend` | what | extra dep |
336
+ |---|---|---|
337
+ | `numpy` (default) | exact brute-force cosine, in-process | none |
338
+ | `sqlite_vec` | ANN inside the same SQLite file | `sqlite-vec` |
339
+ | `chroma_embedded` | Chroma over a local dir | `chromadb` |
340
+ | `chroma_http` | a running Chroma server | `chromadb` |
341
+
342
+ All four return identical ranking; swap freely. Embeddings (`embeddings.py`):
343
+
344
+ | `embedding.backend` | what |
345
+ |---|---|
346
+ | `hashing` (default) | deterministic, offline, dependency-free (great for tests) |
347
+ | `litellm` | OpenAI-compatible `/embeddings` (e.g. `text-embedding-3-large` via the NVIDIA gateway) |
348
+
349
+ ```python
350
+ MemoryConfig(
351
+ enabled=True,
352
+ vector=VectorConfig(backend="sqlite_vec"),
353
+ embedding=EmbeddingConfig(backend="litellm",
354
+ model="openai/azure/openai/text-embedding-3-large",
355
+ endpoint="https://inference-api.nvidia.com/v1",
356
+ dimensions=1024),
357
+ )
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Configuration reference
363
+
364
+ `MemoryConfig` (frozen Pydantic; `.merge_with(**overrides)`) with nested sub-configs:
365
+
366
+ | group | key knobs |
367
+ |---|---|
368
+ | **top-level** | `enabled`, `path`, `tools`, `instruct`, `chunk_size/overlap` |
369
+ | **spontaneous** | `enabled`, `query_strategies` (`last_message`/`recent_events`/`working_state`), `inject_cadence`, `context_char_budget`, `top_k` |
370
+ | **retrieval** | `top_k`, `hops`, `n_dense`, `n_sparse`, `per_hop_decay`, `min_similarity`, `weights` (relevance/recency/importance/λ/decay/spread) |
371
+ | **embedding** | `backend`, `model`, `endpoint`, `api_key`, `dim`, `dimensions`, `batch_size` |
372
+ | **vector** | `backend`, `collection`, `host`, `port` |
373
+ | **write** | `on_events`, `salience_min`, `dedup_threshold`, `dedup_top_k`, `write_episodic` |
374
+ | **reflection** | `enabled`, `trigger`, `only_top_level`, `merge_threshold`, `edge_threshold`, `recon_threshold`, budgets |
375
+ | **forget** | `enabled`, `decay_half_life_hours`, `prune_activation_threshold`, `prune_min_age_hours`, `archive_vs_delete`, `protected_types` |
376
+
377
+ ---
378
+
379
+ ## Monitoring & debugging
380
+
381
+ Reuses the framework's existing observability (no new system):
382
+
383
+ - **logger** `nooa_memory` — `setLevel(logging.DEBUG)` for per-op traces.
384
+ - **events** on the agent's `EventManager`: `MemoryWritten`, `MemoryRecalled`,
385
+ `MemoryInjected`, `ReflectionCompleted` (RUNTIME_EVENT role → visible to any telemetry
386
+ subscriber, never injected into the LLM context).
387
+ - **counters** `manager.memory_stats()` → `MemoryStats` (writes, reinforced, recalls,
388
+ recalled_items, injections, reflections, merged, edges_added, pruned, store_size);
389
+ `manager.log_summary()` logs a one-liner.
390
+
391
+ ```python
392
+ import logging; logging.getLogger("nooa_memory").setLevel(logging.DEBUG)
393
+ agent.event_manager.on("MemoryWritten", lambda e: print("wrote", e.memory_id, e.op))
394
+ ```
395
+
396
+ ---
397
+
398
+ ## When does memory help?
399
+
400
+ Measured with real gpt-5.4 + text-embedding-3-large (see
401
+ [`examples/memory_bench/`](../../../examples/memory_bench/)):
402
+
403
+ | demo | what it isolates | result |
404
+ |---|---|---|
405
+ | `recall_qa.py` | unguessable cross-session facts | memory **+75%** (ON 75% / OFF 0%) |
406
+ | `locomo.py` | LoCoMo, agent-authored | memory **+44–60%** vs no-memory |
407
+ | `reflecting.py` | synthesis under a tight retrieval budget | reflection **+50%** |
408
+ | `longmemeval.py` | LongMemEval + reconsolidation | memory **+70%**, reflection neutral (+0%) |
409
+ | `locomo.py --reflect` | pinpoint lookup | reflection **−20%** |
410
+ | `memory_effect.py` | useful *and* detrimental | memory can backfire when stale |
411
+
412
+ **Principle:** memory helps when the answer isn't derivable in-context (past-session
413
+ or unguessable facts). Consolidation (reflection) is a deliberate tool — it helps
414
+ synthesis/aggregation under a retrieval bottleneck, is neutral when memories already
415
+ fit the budget, and *hurts* pinpoint lookups.
416
+
417
+ ---
418
+
419
+ ## Module map
420
+
421
+ | file | contents |
422
+ |---|---|
423
+ | `schema.py` | `Memory`, `MemoryType`, `Edge`, `EdgeType` |
424
+ | `config.py` | `MemoryConfig` + sub-configs (frozen Pydantic) |
425
+ | `embeddings.py` | `Embedder` protocol, `HashingEmbedder`, `LiteLLMEmbedder`, `get_embedder` |
426
+ | `store.py` | `MemoryStore` (SQLite: records + graph + vectors) |
427
+ | `vector_backends.py` | `VectorIndex` protocol + numpy / sqlite-vec / chroma impls + `make_vector_index` |
428
+ | `retrieval.py` | `RetrievalEngine` (hybrid recall, ACT-R scoring, k-hop), query strategies |
429
+ | `forgetting.py` | `ForgettingEngine`, `retention()` |
430
+ | `reflection.py` | `ReflectionEngine` (merge · reconsolidate · edges · re-score · abstract · prune), `ReflectionReport` |
431
+ | `monitoring.py` | `MemoryStats` + runtime memory events |
432
+ | `manager.py` | `MemoryManager` (install/hooks/ops) + `MemoryToolsMixin` + `MEMORY_SCHEMA_GUIDE` |
433
+
434
+ Tests: [`tests/memory/`](../../../tests/memory/).
435
+
436
+ ---
437
+
438
+ ## Limitations
439
+
440
+ - **Agentic authoring costs LLM calls** (one per session/task) — the deterministic
441
+ offline path (hashing embedder, no reasoner/reconciler) is free and good for tests.
442
+ - **Reconsolidation** only fires when contradictory memories cluster *and* the agent
443
+ authored both values; a missing fact can't be reconciled.
444
+ - **Reflection's win** needs a real retrieval bottleneck (large/noisy store under a
445
+ tight budget); on small stores a strong model reconciles in-context, so it's neutral.
446
+ - `numpy`/`sqlite-vec` backends are exact/brute-force — fine to ~10⁵ vectors; beyond
447
+ that prefer a Chroma backend.
448
+
449
+ See [`examples/memory_bench/`](../../../examples/memory_bench/) for runnable
450
+ benchmarks and measured behavior.
@@ -0,0 +1,144 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Opt-in long-term memory subsystem for NVIDIA OO Agents.
4
+
5
+ A brain-inspired, fully *additive* memory add-on: spontaneous association
6
+ (similarity recall injected each turn), deliberate recall (conscious tools),
7
+ encoding (write on important events), reflection (offline consolidation), and
8
+ forgetting (online decay + offline pruning).
9
+
10
+ Quick start::
11
+
12
+ from nooa import Agent
13
+ from nooa_memory import MemoryConfig, MemoryManager, MemoryToolsMixin
14
+
15
+ class MyAgent(MemoryToolsMixin, Agent, llm=my_llm):
16
+ async def work(self, task: str) -> str:
17
+ # Do {task}. Use self.recall(...) to consult memory.
18
+ ...
19
+
20
+ agent = MyAgent()
21
+ MemoryManager.install(agent, config=MemoryConfig(enabled=True))
22
+ agent.remember("Deploy with `make ship`.", type="skill", importance="HIGH")
23
+ """
24
+
25
+ from nooa_memory.config import (
26
+ EmbeddingConfig,
27
+ ForgetPolicy,
28
+ MemoryConfig,
29
+ ObservabilityConfig,
30
+ ReflectionPolicy,
31
+ RetrievalConfig,
32
+ ScoringWeights,
33
+ SpontaneousConfig,
34
+ VectorConfig,
35
+ WritePolicy,
36
+ )
37
+ from nooa_memory.descriptors import (
38
+ TODO_STATUSES,
39
+ ladder,
40
+ to_label,
41
+ to_numeric,
42
+ to_status,
43
+ )
44
+ from nooa_memory.embeddings import (
45
+ Embedder,
46
+ HashingEmbedder,
47
+ LiteLLMEmbedder,
48
+ get_embedder,
49
+ )
50
+ from nooa_memory.generative import llm_reasoner, llm_reconciler
51
+ from nooa_memory.manager import MemoryManager, MemoryToolsMixin
52
+ from nooa_memory.monitoring import (
53
+ MemoryInjected,
54
+ MemoryRecalled,
55
+ MemoryStats,
56
+ MemoryWritten,
57
+ ReflectionCompleted,
58
+ ReflectionStarted,
59
+ )
60
+ from nooa_memory.observability import per_memory_usage, prune_eta, store_kpis
61
+ from nooa_memory.references import ResolvedRef, capture, parse_ref, render, resolve
62
+ from nooa_memory.schema import (
63
+ ACCESS_CHANNELS,
64
+ REF_KINDS,
65
+ AccessRecord,
66
+ Edge,
67
+ EdgeType,
68
+ Memory,
69
+ MemoryRef,
70
+ MemoryType,
71
+ )
72
+ from nooa_memory.store import SCHEMA_VERSION, MemorySchemaError, MemoryStore
73
+ from nooa_memory.vector_backends import (
74
+ ChromaVectorIndex,
75
+ NumpyVectorIndex,
76
+ SqliteVecVectorIndex,
77
+ VectorIndex,
78
+ make_vector_index,
79
+ )
80
+
81
+ __all__ = [
82
+ # schema
83
+ "Memory",
84
+ "MemoryType",
85
+ "Edge",
86
+ "EdgeType",
87
+ "MemoryRef",
88
+ "REF_KINDS",
89
+ "AccessRecord",
90
+ "ACCESS_CHANNELS",
91
+ # references (pass-by-reference resolution)
92
+ "ResolvedRef",
93
+ "parse_ref",
94
+ "capture",
95
+ "resolve",
96
+ "render",
97
+ # config
98
+ "MemoryConfig",
99
+ "RetrievalConfig",
100
+ "SpontaneousConfig",
101
+ "ScoringWeights",
102
+ "EmbeddingConfig",
103
+ "VectorConfig",
104
+ "WritePolicy",
105
+ "ReflectionPolicy",
106
+ "ForgetPolicy",
107
+ "ObservabilityConfig",
108
+ # embeddings
109
+ "Embedder",
110
+ "HashingEmbedder",
111
+ "LiteLLMEmbedder",
112
+ "get_embedder",
113
+ # store
114
+ "MemoryStore",
115
+ "MemorySchemaError",
116
+ "SCHEMA_VERSION",
117
+ # vector backends
118
+ "VectorIndex",
119
+ "NumpyVectorIndex",
120
+ "SqliteVecVectorIndex",
121
+ "ChromaVectorIndex",
122
+ "make_vector_index",
123
+ # verbal descriptors (agent-facing vocabulary)
124
+ "ladder",
125
+ "to_numeric",
126
+ "to_label",
127
+ "to_status",
128
+ "TODO_STATUSES",
129
+ # manager / integration
130
+ "MemoryManager",
131
+ "MemoryToolsMixin",
132
+ # monitoring / observability
133
+ "MemoryStats",
134
+ "llm_reasoner",
135
+ "llm_reconciler",
136
+ "per_memory_usage",
137
+ "store_kpis",
138
+ "prune_eta",
139
+ "MemoryWritten",
140
+ "MemoryRecalled",
141
+ "MemoryInjected",
142
+ "ReflectionCompleted",
143
+ "ReflectionStarted",
144
+ ]