holo-curiosity

A memory that proposes questions it wants answered.

Given a store of items with optional role structure, the engine proposes queries whose answers would most reduce the store's own uncertainty. Five mechanisms generate candidate questions. Each is scored by expected information gain and returned with a per-mechanism diversity constraint.

The engine is closed-loop. It proposes questions, the caller answers some of them, the store updates, and the next proposal reflects the new state.

What it does

from holo_curiosity import CuriosityMemory, CuriosityEngine

mem = CuriosityMemory(d=2048)
mem.observe("france", capital="paris", continent="europe")
mem.observe("germany", capital="berlin")
mem.observe("japan", capital="tokyo", continent="asia")
mem.observe("italy", capital="rome", continent="europe")
mem.observe("spain", weight=0.2, capital="madrid")

engine = CuriosityEngine(mem)
for q in engine.propose(n=6):
    print(q["query"], q["kind"], q["expected_info_gain"])

Output:

what relates to germany?       sparse       0.900
what relates to spain?         sparse       0.900
what relates to japan?         sparse       0.891
tell me more about spain       low_weight   0.800
what is germany's continent?   slot_gap     0.540
what is spain's continent?     slot_gap     0.540

The five mechanisms

Mechanism Question Trigger
low_weight "tell me more about X" X has weight far below the max
sparse "what relates to X?" X has no close neighbors
slot_gap "what is X's R?" R appears on other items but not on X
reverse_pair "what does B say about A?" A mentions B, B doesn't mention A
interpolation "what is between X and Y?" X and Y are highly similar

Each mechanism returns a list of candidates with an expected information gain. Candidates from all mechanisms are merged, sorted by gain, and truncated with a per-mechanism cap (default 3) so that no single mechanism dominates the proposal list.

Installation

pip install numpy

No other dependencies. Single file, approximately 600 lines.

Usage

CLI

python holo_curiosity.py
python holo_curiosity.py --output results/

Runs six demonstrations and writes a JSON state file.

Python

from holo_curiosity import CuriosityMemory, CuriosityEngine

# Storage
mem = CuriosityMemory(d=2048)
mem.observe("france", weight=1.0, capital="paris", continent="europe")
mem.observe("germany", weight=1.0, capital="berlin")
mem.observe("spain", weight=0.2, capital="madrid")

# Query the store directly
mem.query("france")          # ~1.0
mem.similarity("france", "germany")
mem.nearest("france", k=3)

# Propose questions
engine = CuriosityEngine(mem, mechanism_cap=3)
proposals = engine.propose(n=6)
for p in proposals:
    print(p["query"], p["kind"], p["expected_info_gain"])

# Closed loop: answer a proposal, re-propose
for p in proposals:
    if p["kind"] == "slot_gap":
        label, role = p["target"]
        mem.observe(label, weight=0.3, **{role: "placeholder"})
        break
new_proposals = engine.propose(n=6)

Results

All results at D=2048.

Self-test

Check Result
bind/unbind identity PASS
observe + query (france sim=0.964) PASS
slot-gap detects germany PASS

Basic proposals

Store of five countries with partial field coverage. Proposal ranking with mechanism_cap=3:

Rank Gain Kind Query
1 0.900 sparse what relates to germany?
2 0.900 sparse what relates to spain?
3 0.891 sparse what relates to japan?
4 0.800 low_weight tell me more about spain
5 0.540 slot_gap what is germany's continent?
6 0.540 slot_gap what is spain's continent?

The engine correctly identifies that germany and spain lack continents (while france, japan, italy have them). It also identifies spain as under-weighted. And it flags three items as isolated.

Each mechanism in isolation

low_weight. Items stored with weights 1.0, 0.9, 0.2, 0.05. Only the two weak items generate proposals. Gain is 1 βˆ’ weight/max.

sparse. Eight items with no close neighbors. All eight produce proposals at gain ~0.89. Sparsity is 1 βˆ’ mean nn similarity.

slot_gap. Four items with capital set; continent set on the first only. Two items are missing continent, one is missing both continent and currency. Only the missing slots with 2+ coverage generate proposals. Gain scales with how many other items cover the slot.

reverse_pair. Five items with pairwise mentions. Proposes the reverse of every one-sided mention: "what does paris say about alice?", "what does france say about paris?", "what does bob say about alice?", etc. Gain is 0.4 + 0.4 Γ— similarity.

interpolation. With four scattered items, no pair is above the similarity threshold of 0.15, so no proposals. The mechanism is conservative by design.

Diversity constraint

Same store, two cap values:

cap=2 produces a mix of mechanisms:

low_weight: 2
sparse:     2
slot_gap:   2

cap=6 produces only the top mechanism:

low_weight: 6

The cap ensures the top-N proposals represent multiple kinds of uncertainty rather than many instances of the same kind.

Closed loop

Round 1 proposals include slot_gap: what is germany's continent? at rank 4. After answering with mem.observe("germany", continent="europe"), the slot is filled. Round 2 proposals include reverse_pair: what does rome say about italy? at rank 4 instead.

The engine adapts to what it has learned. The slot-gap question disappears once filled; new questions surface from other mechanisms.

Store growth under guidance

Six rounds of propose β†’ answer β†’ re-propose on a five-item store. The top proposal was always a sparse query ("what relates to germany?"), which the demo loop does not answer (it only answers slot_gap and low_weight). The store stays at 5 items.

This is a limitation of the demonstration loop, not the engine. A full loop would handle all five mechanism kinds. The engine itself proposed the correct questions; the loop just didn't act on all of them.

Proposal statistics

Same store, mechanism_cap=100, top 50 proposals:

Kind Count Mean gain
low_weight 5 0.900
reverse_pair 10 0.400
slot_gap 19 0.587
sparse 16 0.888

With no diversity constraint, sparse and low_weight dominate because their gains are highest. Slot_gap proposals have moderate gain. Reverse_pair proposals cluster near 0.4 because similarity is often low. Interpolation is absent because no pair meets the threshold.

API reference

CuriosityMemory

CuriosityMemory(d=2048)
  • observe(label, weight=1.0, **fields) β€” store an item with optional role fields. The composite vector (name + role-bound field values) goes into the trace. Fields accumulate: calling observe twice on the same label merges the fields.
  • query(label) -> float β€” raw projection of the composite against the trace.
  • similarity(a, b) -> float β€” cosine between two composite items.
  • nearest(label, k=5, exclude_self=True) -> [(label, sim)]
  • stats() -> dict

CuriosityEngine

CuriosityEngine(
    memory,
    mechanism_cap=3,
    max_weight_ratio=0.5,
    min_sparsity=0.5,
    min_interp_similarity=0.15,
)
  • propose(n=5) -> list[dict] β€” top-n proposals, ranked by gain with a per-mechanism cap.

Each proposal is:

{
    "query": str,                    # human-readable question
    "kind": str,                     # mechanism name
    "target": str or tuple,          # the label(s) involved
    "expected_info_gain": float,     # 0.0 to 1.0
    "explanation": str,              # why this was proposed
    # mechanism-specific extras:
    "weight_ratio": float,           # low_weight only
    "sparsity": float,               # sparse only
    "coverage": int,                 # slot_gap only
    "similarity": float,             # reverse_pair, interpolation
}

Design notes

Composite items

An item is stored as norm(name + Ξ£ bind(role, value)). The name vector identifies the label; the field bindings make the item's structure retrievable. The composite is what lives in the trace.

Field values share the name-vector namespace with labels. The string "paris" as a capital and the label "paris" both resolve to the same underlying name vector. This means reverse-pair detection works for any pair of strings that appear as either a label or a value.

Why diversity constraint

Without a cap, the top-N proposals are often all from the same mechanism. sparse and low_weight both produce gain values near 0.9, so they crowd out everything else. The cap ensures the user sees a mix. The default cap of 3 means at most 3 candidates from any one mechanism.

Why information gain, not entropy

The engine does not use Shannon entropy over the value space. The substrate does not maintain a distribution over values; it maintains a set of stored observations. The gain score is a heuristic on the store's own geometry: how far below the max is this item's weight, how sparse is this item's neighborhood, how many other items cover this field. The score is not a probability; it is a ranking signal.

Limitations

The demonstration loop does not handle all mechanisms. DEMO 5 shows the store staying at 5 items because the loop only answers slot_gap and low_weight queries. Sparse, reverse_pair, and interpolation queries require the caller to supply new items or relationships, which the demo does not do.

Interpolation rarely fires. With min_interp_similarity=0.15 and composite items built from random vectors, most pairs are below threshold. In practice, interpolation would trigger only for items that happen to share field values or that are otherwise correlated in the codebook. The threshold is configurable.

No contradiction detection between proposals. If two questions imply different answers for the same slot, the engine does not report this. The slot_gap mechanism proposes missing slots but does not check whether the answer would conflict with anything.

The gain formulas are heuristic. Each mechanism has a hand-tuned formula. There is no principled derivation of the scores. A different designer would choose different formulas. The ranking is stable within each mechanism but the relative ordering across mechanisms is a design choice.

No "already asked" tracking. The engine does not remember which proposals the caller has already answered. Running propose(n) twice on the same state returns the same list. A closed loop that answers proposals will see different lists because the state has changed, but the engine itself does not track proposal history.

No mechanism for proposing new roles. The slot_gap mechanism finds missing values for existing roles. It cannot propose that a new role should be introduced (e.g., "does france have a population field?").

Similarity has no semantic meaning. Two items are "similar" if their composite vectors have high cosine similarity. With random name vectors, this similarity is dominated by shared field values and by coincidence. Two unrelated items that happen to share a field value will look more similar than they semantically are.

Citation

@misc{holo-curiosity2026,
  title  = {holo-curiosity: A memory that proposes questions it
            wants answered},
  author = {zeechimp},
  year   = {2026},
  note   = {Five-mechanism question generator with information-gain
            ranking and per-mechanism diversity constraint.}
}

References

  • Plate, T. A. "Holographic Reduced Representations." IEEE Transactions on Neural Networks 6:3 (1995), 623–641.
  • Kanerva, P. "Hyperdimensional Computing." Cognitive Computation 1:2 (2009), 139–159.
  • Schmidhuber, J. "Formal Theory of Creativity, Fun, and Intrinsic Motivation." IEEE Transactions on Autonomous Mental Development 2:3 (2010), 230–247.
  • Pathak, D., Agrawal, P., Efros, A. A., Darrell, T. "Curiosity-driven Exploration by Self-supervised Prediction." ICML 2017.

License

Apache 2.0

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support