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) 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( def generate(
state: FieldState, state: FieldState,
vocab, vocab,
persona, persona,
max_tokens: int = 128, max_tokens: int = 128,
record_trajectory: bool = False, record_trajectory: bool = False,
vault=None,
recall_top_k: int = 3,
) -> GenerationResult: ) -> GenerationResult:
""" """
Generate a token sequence from an initial FieldState. Generate a token sequence from an initial FieldState.
Loop: Loop:
1. Apply persona motor to current field 1. Compose the persistent persona motor into the current field
2. Find nearest non-current vocab node via CGA inner product 2. Propagate exact vault recall hits into the current field
3. Emit token 3. Find nearest non-current vocab node via CGA inner product
4. Build transition rotor: V = word_transition_rotor(A, B) 4. Emit token
5. Build transition rotor: V = word_transition_rotor(A, B)
where A = versor at current node, B = versor at nearest node where A = versor at current node, B = versor at nearest node
5. Propagate: F <- versor_apply(V, F) 6. Propagate: F <- versor_apply(V, F)
6. Advance node pointer 7. Advance node pointer
Returns: Returns:
GenerationResult with tokens, final_state, and optional trajectory. GenerationResult with tokens, final_state, and optional trajectory.
@ -110,10 +145,10 @@ def generate(
) )
for _ in range(max_tokens): 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( word, word_idx = _nearest_next(
vocab, vocab,
F_voiced, current.F,
current.node, current.node,
recent_nodes=tuple(recent_nodes), recent_nodes=tuple(recent_nodes),
stop_nodes=stop_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))} if token in {vocab.get_word_at(i) for i in range(len(vocab))}
) )
for _ in range(max_tokens): for _ in range(max_tokens):
F_voiced = persona.apply(current.F) current = _voiced_state(current, persona)
word, word_idx = _nearest_next( word, word_idx = _nearest_next(
vocab, vocab,
F_voiced, current.F,
current.node, current.node,
recent_nodes=tuple(recent_nodes), recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes, stop_nodes=stop_nodes,

View file

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