Composes the FieldState (A) and VaultStore (B) codecs with new codecs for
SessionGraph/TurnNode, ReferentRegistry/ReferentEntry, Proposition, and
DialogueTurn into SessionContext.snapshot()/restore() — the complete lived
session state that must survive reboot for resume-as-same-life.
- session/graph.py: TurnNode + SessionGraph to_dict/from_dict (versors bit-exact).
- session/referents.py: ReferentEntry + ReferentRegistry, preserving the
_slots<->_history object aliasing via slot->history-index (update_turn_versor
relies on `is` identity).
- generate/proposition.py + generate/dialogue.py: Proposition + DialogueTurn
codecs (relation_norm is derived in __post_init__, not persisted).
- vault/store.py: complete the metadata codec — vault metadata can hold a
Proposition ({"kind":"proposition",...} from generate/proposition.py), tagged
on encode and reconstructed on decode (lazy import, cycle-free). This closes a
gap Phase B assumed away ("metadata is primitives only"); surfaced by the
Phase C JSON-safe integration test.
- session/context.py: snapshot()/restore(). vocab/persona are NOT serialized
(shared, supplied at restore); restore() mutates self by design (a load).
Exit gate: a real 4-turn session, snapshotted and restored into a fresh context,
is field-equal — field bit-exact, vault recall identical, graph/referents/
dialogue preserved (incl. the referent aliasing). 9 new tests; INV-02 +
session-coherence regression green (68 passed).
Part of the A->E Shape B+ scope (Phase C).
152 lines
6.2 KiB
Python
152 lines
6.2 KiB
Python
"""SessionContext.snapshot/restore — Shape B+ Phase C.
|
|
|
|
Composes the FieldState (Phase A) and VaultStore (Phase B) codecs with new
|
|
codecs for SessionGraph, ReferentRegistry, Proposition, and DialogueTurn. Exit
|
|
gate: a real session, snapshotted and restored into a fresh context, is
|
|
field-equal — field bit-exact, vault recall identical, graph/referents/dialogue
|
|
preserved (including the _slots<->_history object aliasing).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from chat.runtime import ChatRuntime
|
|
from core.config import RuntimeConfig
|
|
from core.cognition.pipeline import CognitiveTurnPipeline
|
|
from generate.dialogue import DialogueTurn
|
|
from generate.proposition import Proposition
|
|
from session.context import SessionContext
|
|
from session.graph import SessionGraph, TurnNode
|
|
from session.referents import ReferentEntry, ReferentRegistry
|
|
|
|
_PROMPTS = ("What causes light?", "What is a concept?", "What causes rain?", "Hello.")
|
|
|
|
|
|
def _v(seed: int) -> np.ndarray:
|
|
v = np.zeros(32, dtype=np.float32)
|
|
v[0] = 1.0
|
|
v[(seed % 31) + 1] = 0.01 * (seed + 1)
|
|
return v
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Component round-trips #
|
|
# --------------------------------------------------------------------------- #
|
|
def test_turnnode_and_graph_round_trip() -> None:
|
|
g = SessionGraph()
|
|
g.add_turn(0, _v(1), _v(2), ("a", "b"), ("c",), "assert", {"neut_sg": 0}, [])
|
|
g.add_turn(1, _v(3), _v(4), ("d",), ("e", "f"), "question", {"neut_sg": 0}, [0])
|
|
r = SessionGraph.from_dict(g.to_dict())
|
|
assert len(r) == 2
|
|
for orig, rec in zip(g.all_nodes(), r.all_nodes()):
|
|
assert rec.input_versor.tobytes() == orig.input_versor.tobytes()
|
|
assert rec.output_versor.tobytes() == orig.output_versor.tobytes()
|
|
assert rec.tokens_in == orig.tokens_in
|
|
assert rec.backward_edges == orig.backward_edges
|
|
assert rec.referent_slots == orig.referent_slots
|
|
|
|
|
|
def test_referent_registry_round_trip_preserves_slot_history_aliasing() -> None:
|
|
reg = ReferentRegistry()
|
|
reg.register("cat", _v(5), turn=0, slot="neut_sg")
|
|
reg.register("dog", _v(6), turn=1, slot="neut_sg") # same slot, twice
|
|
restored = ReferentRegistry.from_dict(reg.to_dict())
|
|
# Active slot points to the latest ("dog") and is the SAME object as the
|
|
# corresponding history entry (the aliasing update_turn_versor relies on).
|
|
active = restored.active_referent("neut_sg")
|
|
assert active is not None and active.surface == "dog"
|
|
assert active is restored.history()[-1]
|
|
assert restored.history()[0].versor.tobytes() == reg.history()[0].versor.tobytes()
|
|
|
|
|
|
def test_proposition_and_dialogue_turn_round_trip() -> None:
|
|
prop = Proposition(
|
|
subject="s", predicate="p", object_="o", surface="s p o",
|
|
frame_id="f", subject_versor=_v(7), predicate_versor=_v(8),
|
|
object_versor=_v(9), relation=_v(10),
|
|
)
|
|
turn = DialogueTurn(proposition=prop, outer_product_blade=_v(11))
|
|
rec = DialogueTurn.from_dict(turn.to_dict())
|
|
assert rec.proposition.subject == "s"
|
|
assert rec.proposition.subject_versor.tobytes() == prop.subject_versor.tobytes()
|
|
assert rec.proposition.object_versor is not None
|
|
assert rec.proposition.relation.tobytes() == prop.relation.tobytes()
|
|
assert rec.outer_product_blade.tobytes() == turn.outer_product_blade.tobytes()
|
|
|
|
|
|
def test_proposition_none_object_versor_round_trips() -> None:
|
|
prop = Proposition(
|
|
subject="s", predicate="p", object_=None, surface="s p",
|
|
frame_id="f", subject_versor=_v(1), predicate_versor=_v(2),
|
|
)
|
|
rec = Proposition.from_dict(prop.to_dict())
|
|
assert rec.object_versor is None
|
|
assert rec.object_ is None
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Integration: a real session snapshot -> restore is field-equal #
|
|
# --------------------------------------------------------------------------- #
|
|
def _drive_session(tmp_path: Path) -> SessionContext:
|
|
runtime = ChatRuntime(config=RuntimeConfig(), engine_state_path=tmp_path / "es")
|
|
pipe = CognitiveTurnPipeline(runtime=runtime)
|
|
for p in _PROMPTS:
|
|
pipe.run(p)
|
|
return runtime._context
|
|
|
|
|
|
def test_session_context_snapshot_restore_is_field_equal(tmp_path: Path) -> None:
|
|
ctx = _drive_session(tmp_path)
|
|
snap = ctx.snapshot()
|
|
|
|
restored = SessionContext(vocab=ctx.vocab, persona=ctx.persona)
|
|
restored.restore(snap)
|
|
|
|
# Field bit-exact + closure preserved.
|
|
assert (ctx.state is None) == (restored.state is None)
|
|
if ctx.state is not None:
|
|
assert restored.state.F.tobytes() == ctx.state.F.tobytes()
|
|
assert restored.turn == ctx.turn
|
|
|
|
# Anchor bit-exact.
|
|
if ctx._anchor_field is not None:
|
|
assert restored._anchor_field.tobytes() == ctx._anchor_field.tobytes()
|
|
|
|
# Vault recall identical (exact CGA preserved through the whole compose).
|
|
query = ctx.state.F if ctx.state is not None else _v(0)
|
|
before = ctx.vault.recall(query, top_k=5)
|
|
after = restored.vault.recall(query, top_k=5)
|
|
assert [(r["index"], r["score"]) for r in before] == [
|
|
(r["index"], r["score"]) for r in after
|
|
]
|
|
|
|
# Graph preserved.
|
|
assert len(restored.graph) == len(ctx.graph)
|
|
for orig, rec in zip(ctx.graph.all_nodes(), restored.graph.all_nodes()):
|
|
assert rec.output_versor.tobytes() == orig.output_versor.tobytes()
|
|
assert rec.tokens_in == orig.tokens_in
|
|
|
|
# Referents + dialogue history preserved.
|
|
assert restored.referents.active_slots() == ctx.referents.active_slots()
|
|
assert len(restored._dialogue_history_compat) == len(ctx._dialogue_history_compat)
|
|
assert (ctx.running_dialogue_blade is None) == (
|
|
restored.running_dialogue_blade is None
|
|
)
|
|
if ctx.running_dialogue_blade is not None:
|
|
assert (
|
|
restored.running_dialogue_blade.tobytes()
|
|
== ctx.running_dialogue_blade.tobytes()
|
|
)
|
|
|
|
|
|
def test_session_context_snapshot_is_json_safe(tmp_path: Path) -> None:
|
|
import json
|
|
|
|
ctx = _drive_session(tmp_path)
|
|
blob = json.dumps(ctx.snapshot())
|
|
restored = SessionContext(vocab=ctx.vocab, persona=ctx.persona)
|
|
restored.restore(json.loads(blob))
|
|
assert restored.turn == ctx.turn
|