Add session coherence across turns

This commit is contained in:
Shay 2026-05-13 19:59:43 -07:00
parent 531acfd40b
commit ed04fc5b15
3 changed files with 130 additions and 20 deletions

View file

@ -77,24 +77,59 @@ def _nearest_next(
return vocab.nearest(F_voiced, exclude_idx=current_node)
def _voiced_state(state: FieldState, persona) -> FieldState:
"""Compose the session persona motor into the live field path."""
return FieldState(
F=persona.apply(state.F),
node=state.node,
step=state.step,
holonomy=state.holonomy,
)
def _recall_state(state: FieldState, vault, top_k: int) -> FieldState:
"""
Feed exact vault recall back into the field as sequential operators.
Recall returns stored versors ranked by the vault's exact metric. Each hit
is treated as an additional operator in the propagation path.
"""
if vault is None or top_k <= 0:
return state
current = state
for hit in vault.recall(current.F, top_k=top_k):
current = propagate_step(current, hit["versor"])
current = FieldState(
F=current.F,
node=state.node,
step=current.step,
holonomy=state.holonomy,
)
return current
def generate(
state: FieldState,
vocab,
persona,
max_tokens: int = 128,
record_trajectory: bool = False,
vault=None,
recall_top_k: int = 3,
) -> GenerationResult:
"""
Generate a token sequence from an initial FieldState.
Loop:
1. Apply persona motor to current field
2. Find nearest non-current vocab node via CGA inner product
3. Emit token
4. Build transition rotor: V = word_transition_rotor(A, B)
1. Compose the persistent persona motor into the current field
2. Propagate exact vault recall hits into the current field
3. Find nearest non-current vocab node via CGA inner product
4. Emit token
5. Build transition rotor: V = word_transition_rotor(A, B)
where A = versor at current node, B = versor at nearest node
5. Propagate: F <- versor_apply(V, F)
6. Advance node pointer
6. Propagate: F <- versor_apply(V, F)
7. Advance node pointer
Returns:
GenerationResult with tokens, final_state, and optional trajectory.
@ -110,10 +145,10 @@ def generate(
)
for _ in range(max_tokens):
F_voiced = persona.apply(current.F)
current = _recall_state(_voiced_state(current, persona), vault, recall_top_k)
word, word_idx = _nearest_next(
vocab,
F_voiced,
current.F,
current.node,
recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes,
@ -162,10 +197,10 @@ async def agenerate(
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
)
for _ in range(max_tokens):
F_voiced = persona.apply(current.F)
current = _voiced_state(current, persona)
word, word_idx = _nearest_next(
vocab,
F_voiced,
current.F,
current.node,
recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes,

View file

@ -18,6 +18,7 @@ from persona.motor import PersonaMotor
from ingest.gate import inject
from generate.stream import generate
from generate.result import GenerationResult
from algebra.backend import versor_apply
class SessionContext:
@ -29,15 +30,23 @@ class SessionContext:
self.turn: int = 0
def ingest(self, tokens: list) -> FieldState:
"""Inject a prompt. Sets self.state. Stores the user field in vault."""
state = inject(tokens, self.vocab)
"""Inject a prompt into the running field. Stores the user field in vault."""
injected = inject(tokens, self.vocab)
node_idx = self.vocab.index_of(tokens[0])
self.state = FieldState(
F=state.F,
node=node_idx,
step=state.step,
holonomy=state.holonomy,
)
if self.state is None:
self.state = FieldState(
F=injected.F,
node=node_idx,
step=injected.step,
holonomy=injected.holonomy,
)
else:
self.state = FieldState(
F=versor_apply(injected.F, self.state.F),
node=node_idx,
step=self.state.step + 1,
holonomy=injected.holonomy,
)
self.vault.store(self.state.F, {"turn": self.turn, "role": "user"})
return self.state
@ -49,7 +58,7 @@ class SessionContext:
GenerationResult carrying emitted tokens and final_state.
"""
assert self.state is not None, "Call ingest() before respond()."
result = generate(self.state, self.vocab, self.persona, max_tokens)
result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
self.state = result.final_state
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
self.turn += 1
@ -64,7 +73,7 @@ class SessionContext:
yielding the surface tokens.
"""
assert self.state is not None, "Call ingest() before arespond()."
result = generate(self.state, self.vocab, self.persona, max_tokens)
result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
for token in result.tokens:
yield token
self.state = result.final_state

View file

@ -0,0 +1,66 @@
from __future__ import annotations
import numpy as np
from algebra.backend import cga_inner
from algebra.versor import unitize_versor
from session.context import SessionContext
from vocab.manifold import VocabManifold
def _positive_unit_reflector(seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
vec4 = rng.standard_normal(4).astype(np.float32)
norm4 = float(np.linalg.norm(vec4))
if norm4 < 1e-6:
vec4[0] = 1.0
norm4 = 1.0
vec = np.zeros(5, dtype=np.float32)
vec[:4] = vec4
vec[4] = 0.25 * norm4 * np.tanh(float(rng.standard_normal()))
mv = np.zeros(32, dtype=np.float32)
mv[1:6] = vec
return unitize_versor(mv)
def _vocab() -> VocabManifold:
vocab = VocabManifold()
vocab.add("logos", _positive_unit_reflector(1))
vocab.add("arche", _positive_unit_reflector(2))
vocab.add("pneuma", _positive_unit_reflector(3))
vocab.add("truth", _positive_unit_reflector(4))
vocab.add("nous", _positive_unit_reflector(5))
return vocab
def _farther_unrelated(result_F: np.ndarray, prompt_F: np.ndarray, start_seed: int) -> np.ndarray:
prompt_score = cga_inner(result_F, prompt_F)
for seed in range(start_seed, start_seed + 256):
candidate = _positive_unit_reflector(seed)
if prompt_score > cga_inner(result_F, candidate):
return candidate
raise AssertionError("Could not construct a deterministic farther unrelated versor.")
def test_repeated_prompt_accumulates_field_and_stays_prompt_coherent() -> None:
session = SessionContext(vocab=_vocab())
prompt = ["logos", "arche"]
initial = session.ingest(prompt)
first = session.respond(max_tokens=4)
second_prompt_state = session.ingest(prompt)
assert not np.array_equal(second_prompt_state.F, initial.F)
second = session.respond(max_tokens=4)
assert second.tokens != first.tokens
assert not np.array_equal(second.final_state.F, first.final_state.F)
for i, result in enumerate((first, second)):
random_unrelated = _farther_unrelated(result.final_state.F, initial.F, 11 + (i * 64))
prompt_score = cga_inner(result.final_state.F, initial.F)
random_score = cga_inner(result.final_state.F, random_unrelated)
assert prompt_score > random_score