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).
133 lines
4.6 KiB
Python
133 lines
4.6 KiB
Python
"""
|
|
Dialogue move selection from proposition relation blades.
|
|
|
|
The proposition layer constructs X_prompt ^ X_field. Dialogue reads that
|
|
grade-2 relation against the prior relation blade and chooses the next move
|
|
kind: assertion, elaboration, question/contrast, or refutation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Literal
|
|
|
|
import numpy as np
|
|
|
|
from algebra.cga import cga_inner, outer_product
|
|
from core.array_codec import decode_array, encode_array
|
|
from field.state import FieldState
|
|
from generate.proposition import FrameRegistry, Proposition, propose
|
|
|
|
DialogueRole = Literal["assert", "elaborate", "question", "refute"]
|
|
|
|
_PARALLEL_THRESHOLD = 0.35
|
|
_ANTI_PARALLEL_THRESHOLD = -0.35
|
|
_ORTHOGONAL_THRESHOLD = 0.20
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DialogueTurn:
|
|
proposition: Proposition
|
|
outer_product_blade: np.ndarray = field(repr=False)
|
|
|
|
def __post_init__(self) -> None:
|
|
blade = np.asarray(self.outer_product_blade, dtype=np.float32).copy()
|
|
object.__setattr__(self, "outer_product_blade", blade)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"proposition": self.proposition.to_dict(),
|
|
"outer_product_blade": encode_array(self.outer_product_blade),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, payload: dict) -> "DialogueTurn":
|
|
return cls(
|
|
proposition=Proposition.from_dict(payload["proposition"]),
|
|
outer_product_blade=decode_array(payload["outer_product_blade"]),
|
|
)
|
|
|
|
|
|
def blade_alignment(blade: np.ndarray, reference: np.ndarray) -> float:
|
|
"""
|
|
Return signed orientation of two relation blades.
|
|
|
|
Positive values mean same plane orientation, near-zero values mean
|
|
orthogonal relation, and negative values mean the same relation reversed.
|
|
The scalar is derived from CORE's algebraic inner product, not an external
|
|
approximate metric.
|
|
"""
|
|
blade = np.asarray(blade, dtype=np.float32)
|
|
reference = np.asarray(reference, dtype=np.float32)
|
|
blade_norm = abs(cga_inner(blade, blade)) ** 0.5
|
|
reference_norm = abs(cga_inner(reference, reference)) ** 0.5
|
|
if blade_norm < 1e-8 or reference_norm < 1e-8:
|
|
return 0.0
|
|
return float(-cga_inner(blade, reference) / (blade_norm * reference_norm))
|
|
|
|
|
|
def classify_dialogue_blade(
|
|
blade: np.ndarray,
|
|
reference_blade: np.ndarray | None = None,
|
|
) -> DialogueRole:
|
|
"""
|
|
Classify a relation blade as the next dialogue move.
|
|
|
|
With no prior relation the engine has no contrastive plane yet, so the
|
|
first move is an assertion. After that:
|
|
same orientation -> elaboration
|
|
near-orthogonal -> question/contrast
|
|
reversed orientation -> refutation
|
|
"""
|
|
if reference_blade is None:
|
|
return "assert"
|
|
|
|
alignment = blade_alignment(blade, reference_blade)
|
|
if alignment >= _PARALLEL_THRESHOLD:
|
|
return "elaborate"
|
|
if alignment <= _ANTI_PARALLEL_THRESHOLD:
|
|
return "refute"
|
|
if abs(alignment) <= _ORTHOGONAL_THRESHOLD:
|
|
return "question"
|
|
return "question"
|
|
|
|
|
|
def trajectory_blade(blades: tuple[np.ndarray, ...] | list[np.ndarray]) -> np.ndarray | None:
|
|
"""Fold a dialogue path into the running outer product of its blades."""
|
|
if not blades:
|
|
return None
|
|
running = np.asarray(blades[0], dtype=np.float32).copy()
|
|
for blade in blades[1:]:
|
|
running = outer_product(running, blade)
|
|
return running
|
|
|
|
|
|
def propose_dialogue(
|
|
field_state: FieldState,
|
|
vault,
|
|
vocab,
|
|
frame_registry: FrameRegistry,
|
|
reference_blade: np.ndarray | None = None,
|
|
output_lang: str | None = None,
|
|
) -> Proposition:
|
|
"""
|
|
Generate a proposition through a dialogue-role constrained frame choice.
|
|
"""
|
|
base = propose(field_state, None, vocab, frame_registry, output_lang=output_lang)
|
|
role = classify_dialogue_blade(base.relation, reference_blade)
|
|
frame = frame_registry.select_dialogue(base.relation, role)
|
|
role_registry = FrameRegistry((frame,))
|
|
proposition = propose(field_state, vault, vocab, role_registry, output_lang=output_lang)
|
|
if reference_blade is not None and blade_alignment(proposition.relation, reference_blade) < 0.0:
|
|
proposition = Proposition(
|
|
subject=proposition.subject,
|
|
predicate=proposition.predicate,
|
|
object_=proposition.object_,
|
|
surface=proposition.surface,
|
|
frame_id=proposition.frame_id,
|
|
subject_versor=proposition.subject_versor,
|
|
predicate_versor=proposition.predicate_versor,
|
|
object_versor=proposition.object_versor,
|
|
relation=-proposition.relation,
|
|
)
|
|
return proposition
|