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 +450 -0
- nooa_memory/__init__.py +144 -0
- nooa_memory/config.py +202 -0
- nooa_memory/descriptors.py +58 -0
- nooa_memory/embeddings.py +145 -0
- nooa_memory/forgetting.py +98 -0
- nooa_memory/generative.py +205 -0
- nooa_memory/manager.py +933 -0
- nooa_memory/memory_skill/__init__.py +88 -0
- nooa_memory/monitoring.py +120 -0
- nooa_memory/observability.py +174 -0
- nooa_memory/references.py +163 -0
- nooa_memory/reflection.py +348 -0
- nooa_memory/retrieval.py +369 -0
- nooa_memory/schema.py +372 -0
- nooa_memory/store.py +487 -0
- nooa_memory/tracing_bridge.py +90 -0
- nooa_memory/vector_backends.py +225 -0
- nooa_memory-0.0.7.dist-info/METADATA +31 -0
- nooa_memory-0.0.7.dist-info/RECORD +22 -0
- nooa_memory-0.0.7.dist-info/WHEEL +4 -0
- nooa_memory-0.0.7.dist-info/entry_points.txt +2 -0
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.
|
nooa_memory/__init__.py
ADDED
|
@@ -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
|
+
]
|