Skip to content

Commit 0cc9a4f

Browse files
authored
Merge pull request #19 from nickroci/feat/reflective-abstraction
feat(daemon): reflective abstraction — aha-gated abstract_entries action
2 parents f884f92 + 0536800 commit 0cc9a4f

13 files changed

Lines changed: 832 additions & 43 deletions

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -308,19 +308,21 @@ Doesn't require architectural change — it's a small addition in `priming.py` b
308308

309309
### Reflective abstraction (offline integration of leaf episodes)
310310

311-
Ultan curates incoming items well but has no bottom-up pass that synthesises higher-order rules from accumulated leaf entries. The Scholar is *reactive* — it judges incoming proposals — not *reflective*. Decay (PR #7) removes unused noise; reflection would consolidate the *used* patterns that would otherwise stay as N parallel leaves. Without it the library has *flat breadth* — every fact stays a leaf forever; nothing graduates to a higher-order rule. This is one of the largest deltas from a mammalian system: hippocampal–neocortical dialogue during offline periods produces transitive inferences and schema abstractions that no single episode contains (Schlichting & Preston 2015; Eichenbaum 2017; Preston & Eichenbaum 2013). The canonical LLM-side analog is the *reflection* mechanism in Park et al.'s Generative Agents (2023). A-MEM (Xu et al., NeurIPS 2025) — already cited below in the Reconsolidation row — also does hierarchical evolution of entries on use; reflection here would add the *net-new parent abstraction* step that A-MEM doesn't propose.
311+
**Slice 1 shipped (`abstract_entries`).** Ultan now has a bottom-up pass that synthesises higher-order rules from accumulated leaf entries, closing one of the largest deltas from a mammalian system: hippocampal–neocortical dialogue during offline periods produces transitive inferences and schema abstractions that no single episode contains (Schlichting & Preston 2015; Eichenbaum 2017; Preston & Eichenbaum 2013). The Scholar was *reactive* — it judged incoming proposals — and is now also *reflective*: it approves or vetoes net-new parent abstractions the Librarian proposes over related leaves. Decay (PR #7) removes unused noise; reflection consolidates the *used* patterns that would otherwise stay as N parallel leaves, so the library no longer has only *flat breadth* — facts can graduate to a higher-order rule. The canonical LLM-side analog is the *reflection* mechanism in Park et al.'s Generative Agents (2023). A-MEM (Xu et al., NeurIPS 2025) — cited below in the Reconsolidation row — does hierarchical evolution of entries on use; reflection here adds the *net-new parent abstraction* step that A-MEM doesn't propose.
312312

313-
**The Librarian already does most of this shape — extend its action vocabulary, don't add a separate clustering daemon.** The Librarian routinely reads multiple entries and proposes structural changes to the Scholar — `merge_entries`, `split_folder`, `update_readme`, `add_wikilink` are all multi-entry-reading-with-judgment actions today. Adding "synthesise an abstract parent over related leaves" is *one more verb* in that vocabulary, not a periodic offline cluster-and-summarise job. Cosine clustering would force a topical-summary shape (Park et al. 2023's flavour); the agent-driven path gives us the schema-inference flavour the cited Eichenbaum/Schlichting biology actually describes — *"rule A about python + rule B about Go + rule C about Node → abstract rule 'always pin dependency versions across all language ecosystems'"*, not "5 entries about python deps → 1 themed summary." Same pattern as [[projects/agent-mem/conventions/retrieval/no-regex-seed-extraction-use-parallel-search]]: the Librarian is a tool-using agent making judgments, not an algorithm running offline.
313+
**It is one more verb in the Librarian's vocabulary, not a separate clustering daemon.** The Librarian already reads multiple entries and proposes structural changes to the Scholar (`merge_entries`, `split_folder`, `add_wikilink` are all multi-entry-reading-with-judgment actions). `abstract_entries` is *one more verb* in that vocabulary. Cosine clustering would force a topical-summary shape (Park et al. 2023's flavour); the agent-driven path gives us the schema-inference flavour the cited Eichenbaum/Schlichting biology actually describes — *"rule A about python + rule B about JS → abstract rule 'user likes linting across languages'"* (predicts wanting lint in a new language like Rust), not "5 entries about python deps → 1 themed summary." Same pattern as [[projects/agent-mem/conventions/retrieval/no-regex-seed-extraction-use-parallel-search]]: the Librarian is a tool-using agent making judgments, not an algorithm running offline.
314314

315-
Concretely:
315+
**This is surprise-gated, not similarity-gated — the same encoding bar the leaf writes use.** A proposal is approved only when it's a genuine "aha", an integration event with reward-prediction-error signature, not a topical cluster. The biology is specific: only strongly novel-or-congruent items encode (the "mild middle" is dropped) (van Kesteren et al. 2012, SLIMM); insight is the binding of a *remote* association (Jung-Beeman et al. 2004); the aha moment is a reward-prediction-error that drives durable encoding (Kizilirmak et al. 2016); and integration is what lets the hippocampal–prefrontal system make a *novel inference* about an unseen case from separately-stored episodes (Schlichting & Preston 2015; Tse et al. 2007; Behrens et al. 2018). So the gate, enforced in BOTH the Librarian's propose-side prompt and the Scholar's veto-side prompt, has four parts: (1) **remote children** — leaves from different domains, so the link is non-obvious; (2) **predictive lift** — the parent enables a confident call on an unseen case no single child supports; (3) **non-obvious** — a competent assistant wouldn't have stated the rule unprompted; (4) **compresses** — the rule is shorter than its children and regenerates them. The Scholar treats failing the bar as a precision veto. Explicit must-reject patterns (same-surface groupings like "all about yellow things", vague ones like "likes uv + likes ruff → likes fast tools", true-but-worthless ones like "likes lint + likes types → likes good code") are baked into the prompts so the model doesn't over-abstract.
316316

317-
- A new Librarian action, e.g. `abstract_entries` or `propose_parent_entry` — takes a list of child paths, a proposed parent title + body capturing the schema, and the target folder. The Librarian identifies candidates while doing its normal scan (using BM25 + embedding-search tools to find related leaves), not via a periodic clustering pass.
318-
- The Scholar evaluates the proposal like any other action — approves and writes the parent + the `[[wikilink]]` backlinks, or vetoes if the abstraction is premature, too narrow, too generic, or conflicts with an existing parent.
319-
- Children stay in place; they remain individually retrievable and subject to normal decay. The parent gains its own `reinforced` lifecycle.
317+
How it works:
320318

321-
The parent's `encoding_strength` derivation (once that field exists — see *Forgetting* below) and whether the parent earns a `reinforced` bump when any child is used are still open below.
319+
- **Action.** `abstract_entries` takes `child_paths` (≥2 existing leaves, enforced by a model validator), a `parent_path`, `parent_title`, and a `parent_body` (full markdown with `type: abstraction` frontmatter and a `[[wikilink]]` to each child). The Librarian identifies candidates during its normal scan using its BM25 + embedding-search tools, not via a periodic clustering pass.
320+
- **Validation.** The same boundary validators every action passes: parent + child paths must be well-formed `.md` inside the knowledge root; `parent_body` frontmatter must parse, carry the required fields, and agree (id/scope) with `parent_path`; every child must already exist (children are being abstracted, so they must be there); the parent is registered as a batch-created path so its own body wikilinks resolve; the parent's directory is counted toward the flat-dir cap.
321+
- **Execution.** The deterministic executor atomically writes the parent, upserts its `index.md` row, appends to `log.md`, and adds a reverse `[[parent]]` backlink into each child's `## Related` section (idempotent — a re-run doesn't pile up duplicate bullets). **Children stay in place** — they are not archived or moved, remain individually retrievable, and stay subject to normal decay. The parent gains its own `reinforced`/decay lifecycle.
322322

323-
Open questions:
323+
`encoding_strength`-derived durability for the parent (a flatter decay slope for high-surprise abstractions) remains a follow-up — it depends on the separate `encoding_strength` field (see *Forgetting* below). Until then the parent ships on the existing `reinforced`/decay lifecycle like any other entry.
324+
325+
Open questions (follow-up slices):
324326

325327
- **Triggering** — under the agent-driven framing, "cadence" mostly evaporates: the Librarian considers abstraction during every normal scan. The real question is what *prompts* the Librarian to look. Options: the existing applies-when match against the rolling buffer (cheap, but biased toward whatever the user just touched); a periodic deep-scan mode that ignores the buffer and just sweeps the library for under-abstracted clusters (more thorough; costs Sonnet tokens); or both (deep-scan as a separate Librarian invocation, e.g. weekly).
326328
- **Restraint heuristics** for the Librarian — what's the minimum number of related leaves before proposing an abstraction is worth the Scholar's attention? What signals "this cluster is cohesive enough to abstract" vs "this is a coincidental keyword overlap"? Probably needs explicit prompt guidance ("only propose when at least N entries clearly imply the same general rule, and the rule isn't already captured by an existing parent").
@@ -336,6 +338,11 @@ References:
336338
- Preston, A.R. & Eichenbaum, H. (2013). *Interplay of hippocampus and prefrontal cortex in memory.* Current Biology, 23(17), R764–R773.
337339
- Schlichting, M.L. & Preston, A.R. (2015). *Memory integration: neural mechanisms and implications for behavior.* Current Opinion in Behavioral Sciences, 1, 1–8.
338340
- Eichenbaum, H. (2017). *On the integration of space, time, and memory.* Neuron, 95(5), 1007–1018.
341+
- van Kesteren, M.T.R., Ruiter, D.J., Fernández, G. & Henson, R.N. (2012). *How schema and novelty augment memory formation.* Trends in Neurosciences, 35(4), 211–219. (SLIMM — only strongly novel/congruent items encode; the mild middle is dropped.)
342+
- Jung-Beeman, M., Bowden, E.M., Haberman, J., Frymiare, J.L., Arambel-Liu, S., Greenblatt, R., Reber, P.J. & Kounios, J. (2004). *Neural activity when people solve verbal problems with insight.* PLoS Biology, 2(4), e97. (Insight = binding of a remote association.)
343+
- Kizilirmak, J.M., Thuerich, H., Folta-Schoofs, K., Schott, B.H. & Richardson-Klavehn, A. (2016). *Neural correlates of learning from induced insight: a case for reward-based encoding.* Frontiers in Psychology, 7, 1693. (Aha = reward-prediction-error driving durable encoding.)
344+
- Tse, D., Langston, R.F., Kakeyama, M., Bethus, I., Spooner, P.A., Wood, E.R., Witter, M.P. & Morris, R.G.M. (2007). *Schemas and memory consolidation.* Science, 316(5821), 76–82.
345+
- Behrens, T.E.J., Muller, T.H., Whittington, J.C.R., Mark, S., Baram, A.B., Stachenfeld, K.L. & Kurth-Nelson, Z. (2018). *What is a cognitive map? Organizing knowledge for flexible behavior.* Neuron, 100(2), 490–509.
339346
- Park, J.S., O'Brien, J.C., Cai, C.J., Morris, M.R., Liang, P. & Bernstein, M.S. (2023). *Generative agents: Interactive simulacra of human behavior.* UIST '23.
340347

341348
### Forgetting (LTD) with surprise-calibrated encoding strength
@@ -362,7 +369,7 @@ We don't need to replicate biology exactly. We need the mechanisms to be *presen
362369
| **Prefrontal inhibition of retrieval** (Anderson & Green 2001 Nature, Think/No-Think paradigm) | **Agent seeing a surfaced memory and not acting on it is functionally a no-think signal.** Counts as weak negative evidence — accelerates decay for low/moderate-strength entries; for high-strength entries it triggers a reconsolidation/update review (relevance-drift, not irrelevance). Asymmetric: "agent explicitly cited / `ultan-search`-fetched" is *strong positive* evidence; "surfaced but ignored" is *weak negative* evidence, requiring multiple instances. Mirrors the brain's asymmetric weighting of presence-of-use vs absence-of-use. | TODO |
363370
| **Reconsolidation** — retrieved memories become labile and are re-stored mutated (Nader et al. 2000; Schiller et al. 2010; Lee et al. 2017; Hupbach et al. 2007) | Librarian gets a `drift` salience signal alongside `contradicts`/`novel`/`reinforces`: *"high-strength entry [[X]] keeps surfacing in contexts that don't quite match its current text — propose an `update`."* Scholar evaluates as a partial mutation, not full replacement. Closest published OSS analog: A-MEM's Zettelkasten evolution (Xu et al., NeurIPS 2025). | Partial — `update` action exists; drift-driven proposal pathway does not |
364371
| Sleep-based selective consolidation (Diekelmann & Born 2010; Stickgold 2005; Wilhelm et al. 2011) | The Scholar's batch reconciliation phase plays this role architecturally — periodic, deliberate, prioritises high-salience entries; no behavioural analog of replay yet | Partial |
365-
| **Reflective abstraction — offline integration of leaf episodes into higher-order rules** (Preston & Eichenbaum 2013; Schlichting & Preston 2015; Eichenbaum 2017; LLM analog: Park et al. 2023 Generative Agents reflection) | New Librarian action (e.g. `abstract_entries`) that proposes a parent abstraction over related leaves during its normal scan — agent judgment, not cosine clustering. Scholar approves or vetoes; on approve, writes the parent with `[[wikilink]]` backlinks to children. See *Reflective abstraction* above. | TODO |
372+
| **Reflective abstraction — offline integration of leaf episodes into higher-order rules** (van Kesteren et al. 2012; Jung-Beeman et al. 2004; Kizilirmak et al. 2016; Schlichting & Preston 2015; Tse et al. 2007; Behrens et al. 2018; Preston & Eichenbaum 2013; Eichenbaum 2017; LLM analog: Park et al. 2023 Generative Agents reflection) | `abstract_entries` Librarian action proposes a parent abstraction over ≥2 related leaves during its normal scan — agent judgment, not cosine clustering. Surprise-/aha-gated (remote children + predictive lift + non-obvious + compresses) on both the Librarian propose-side and Scholar veto-side; the Scholar approves or vetoes; on approve, the executor writes the parent (`type: abstraction`) with `[[wikilink]]` backlinks into each child, which stay in place. See *Reflective abstraction* above. | **Done (slice 1)** — encoding_strength-derived parent durability awaits the `encoding_strength` field; ships on the existing `reinforced`/decay lifecycle |
366373
| Rational analysis of memory — retention tracks environmental utility (Anderson & Schooler 1991) | The whole loop is enacting this if encoding-strength + decay + use-tracking work together. The Anderson & Schooler framing is the cleanest theoretical anchor for the system as a whole. | Emergent if the rest lands |
367374
| Arousal modulation of consolidation (McGaugh 2000) | Folded into `encoding_strength` rather than a separate field — keep schema lean | TODO |
368375
| Active accuracy override (truth beats salience) | `contradicts` from a new entry sharply discounts the target's effective strength regardless of accumulated reinforcement; flashbulb-memory guard | TODO |

daemon/agent_mem_daemon/_schemas.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,67 @@ class SplitFolder(_BaseAction):
267267
)
268268

269269

270+
class AbstractEntries(_BaseAction):
271+
"""Synthesise a higher-order PARENT abstraction over ≥2 related leaf
272+
entries (reflective abstraction — bottom-up schema formation).
273+
274+
The children STAY in place (each remains individually retrievable and
275+
subject to normal decay); a new parent entry is created at
276+
``parent_path`` whose ``parent_body`` states a single general rule
277+
abstracted FROM them and links to each via ``[[wikilink]]``. The
278+
executor also writes a reverse ``[[parent]]`` backlink into each child.
279+
280+
This is NOT dedup (``merge_entries`` archives its sources) and NOT
281+
reorg (``split_folder``/``move_entry`` relocate without synthesising):
282+
nothing is archived or moved. Propose ONLY for a genuine "aha" — a
283+
surprising, non-obvious, PREDICTIVE rule that connects leaves from
284+
DIFFERENT domains and would let a competent assistant make a confident
285+
call on an unseen case no single child supports. ``parent_body`` is
286+
full markdown including YAML frontmatter and MUST use ``type:
287+
abstraction``."""
288+
289+
action: Literal["abstract_entries"] = "abstract_entries"
290+
child_paths: List[str] = Field(
291+
default_factory=list,
292+
description=(
293+
"≥2 existing leaf entry paths (relative to knowledge/) the "
294+
"parent abstracts. Must contain at least two; an abstraction "
295+
"over a single entry is not an abstraction."
296+
),
297+
)
298+
parent_path: str = Field(
299+
default="",
300+
description=(
301+
"Where the synthesised parent entry should live, relative to knowledge/, ending in .md."
302+
),
303+
)
304+
parent_title: str = Field(
305+
default="",
306+
description="Human-readable title of the parent abstraction.",
307+
)
308+
parent_body: str = Field(
309+
default="",
310+
description=(
311+
"Full markdown body of the parent entry, including YAML "
312+
"frontmatter with ``type: abstraction`` and ``[[wikilink]]``s "
313+
"to each child."
314+
),
315+
)
316+
317+
@model_validator(mode="after")
318+
def _require_two_children(self) -> "AbstractEntries":
319+
"""An abstraction needs ≥2 children. The Librarian sometimes emits
320+
partial JSON, but a single-child (or empty) ``child_paths`` is never
321+
a valid abstraction — reject it at parse time so neither the Scholar
322+
nor the executor sees a degenerate action."""
323+
if len(self.child_paths) < 2:
324+
raise ValueError(
325+
"abstract_entries requires at least two child_paths "
326+
"(an abstraction over fewer than two leaves is not an abstraction)"
327+
)
328+
return self
329+
330+
270331
# The bare union of proposed-action classes — usable as a type annotation
271332
# and for ``isinstance`` in the Librarian's boundary validator. The
272333
# discriminated alias below adds Pydantic's ``action`` dispatch on top.
@@ -280,6 +341,7 @@ class SplitFolder(_BaseAction):
280341
UpdateReadme,
281342
AddWikilink,
282343
SplitFolder,
344+
AbstractEntries,
283345
]
284346

285347
# Discriminated union — Pydantic dispatches on the ``action`` literal.
@@ -641,6 +703,29 @@ def _validate_paths(self) -> "ScholarDeprecateEntry":
641703
return self
642704

643705

706+
class ScholarAbstractEntries(AbstractEntries):
707+
"""Scholar-validated ``abstract_entries`` — ``parent_body`` held to the
708+
same frontmatter bar as a fresh write (parseable, required fields, id
709+
matches the filename slug, scope agrees with the path) against
710+
``parent_path``. The ≥2-children rule is inherited from
711+
``AbstractEntries``; per-child existence is a whole-batch check the
712+
executor cannot do in isolation, so it lives in ``scholar`` /
713+
``librarian`` validators that see the knowledge root, not here."""
714+
715+
@model_validator(mode="after")
716+
def _validate_body(self) -> "ScholarAbstractEntries":
717+
errors = _frontmatter_field_errors(self.parent_body, path=self.parent_path)
718+
if not self.parent_path.strip():
719+
errors.append("abstract_entries requires a non-empty `parent_path`")
720+
if not self.parent_body.strip():
721+
errors.append(
722+
f"abstract_entries for {self.parent_path!r} requires a non-empty `parent_body`"
723+
)
724+
if errors:
725+
raise ValueError("; ".join(errors))
726+
return self
727+
728+
644729
# Discriminated union of the Scholar's validated actions. Same
645730
# ``action`` discriminator keys as ``ProposedAction`` so the prompt
646731
# enumeration generated from those models still matches.
@@ -652,6 +737,7 @@ def _validate_paths(self) -> "ScholarDeprecateEntry":
652737
ScholarMoveEntry,
653738
ScholarArchiveEntry,
654739
ScholarDeprecateEntry,
740+
ScholarAbstractEntries,
655741
],
656742
Field(discriminator="action"),
657743
]
@@ -727,6 +813,7 @@ def describe_scholar_decisions_shape() -> str:
727813
UpdateReadme,
728814
AddWikilink,
729815
SplitFolder,
816+
AbstractEntries,
730817
]
731818

732819

0 commit comments

Comments
 (0)