Tighten session node tracking and generation selection

This commit is contained in:
Shay 2026-05-13 14:35:31 -07:00
parent 4c3004c73a
commit d997b88d32
6 changed files with 138 additions and 19 deletions

View file

@ -4,7 +4,6 @@ import re
from language_packs import OOVPolicy, load_pack, load_pack_entries
from persona.motor import PersonaMotor
from field.state import FieldState
from session.context import SessionContext
_TOKEN_RE = re.compile(r"\w+", re.UNICODE)
@ -58,13 +57,6 @@ class ChatRuntime:
if not filtered:
return ""
self._context.ingest(filtered)
node_idx = self._context.vocab.index_of(filtered[0])
self._context.state = FieldState(
F=self._context.state.F,
node=node_idx,
step=self._context.state.step,
holonomy=self._context.state.holonomy,
)
result = self._context.respond(max_tokens=max_tokens)
guarded = self._syntactic_guard(result.tokens)
return " ".join(guarded)

View file

@ -16,22 +16,50 @@ F is always on the manifold. nearest() is exact.
"""
from __future__ import annotations
from collections import deque
from field.state import FieldState
from field.propagate import propagate_step
from algebra.rotor import word_transition_rotor
from generate.result import GenerationResult
_RECENT_WINDOW = 3
_STOP_TOKENS = frozenset({"it", "to", "word"})
def _nearest_next(vocab, F_voiced, current_node: int) -> tuple[str, int]:
def _nearest_next(
vocab,
F_voiced,
current_node: int,
recent_nodes: tuple[int, ...] = (),
stop_nodes: frozenset[int] = frozenset(),
) -> tuple[str, int]:
"""
Select the nearest non-current vocabulary point when possible.
Select the nearest vocabulary point while avoiding short loops.
Allowing the current node to win makes V = transition(A, A), which is an
identity-like transition and can stall generation forever on one token.
VocabManifold already exposes exclude_idx for this exact seam.
Recent-node exclusion reduces two- and three-token attractor cycles.
Stop-node exclusion keeps function-word wells from dominating when more
informative neighbors are available.
"""
exclude_idx = current_node if len(vocab) > 1 else -1
return vocab.nearest(F_voiced, exclude_idx=exclude_idx)
if len(vocab) <= 1:
return vocab.nearest(F_voiced)
recent = set(recent_nodes)
stop = set(stop_nodes)
fallback_orders = (
recent | stop,
stop,
recent,
set(),
)
for extra in fallback_orders:
try:
return vocab.nearest(F_voiced, exclude_idx=current_node, exclude_indices=extra)
except ValueError:
continue
return vocab.nearest(F_voiced, exclude_idx=current_node)
def generate(
@ -59,10 +87,22 @@ def generate(
tokens = []
trajectory = [] if record_trajectory else None
current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
stop_nodes = frozenset(
vocab.index_of(token)
for token in _STOP_TOKENS
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)
word, word_idx = _nearest_next(vocab, F_voiced, current.node)
word, word_idx = _nearest_next(
vocab,
F_voiced,
current.node,
recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes,
)
tokens.append(word)
if record_trajectory:
@ -74,6 +114,7 @@ def generate(
current = propagate_step(current, V)
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
recent_nodes.append(word_idx)
return GenerationResult(
tokens=tokens,
@ -99,9 +140,21 @@ async def agenerate(
Yields: str (one token per iteration)
"""
current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
stop_nodes = frozenset(
vocab.index_of(token)
for token in _STOP_TOKENS
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)
word, word_idx = _nearest_next(vocab, F_voiced, current.node)
word, word_idx = _nearest_next(
vocab,
F_voiced,
current.node,
recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes,
)
yield word
A = vocab.get_versor_at(current.node)
@ -110,3 +163,4 @@ async def agenerate(
current = propagate_step(current, V)
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
recent_nodes.append(word_idx)

View file

@ -30,7 +30,14 @@ class SessionContext:
def ingest(self, tokens: list) -> FieldState:
"""Inject a prompt. Sets self.state. Stores the user field in vault."""
self.state = inject(tokens, self.vocab)
state = 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,
)
self.vault.store(self.state.F, {"turn": self.turn, "role": "user"})
return self.state

View file

@ -95,6 +95,7 @@ def test_minimum_engine_loop_is_deterministic_and_stores_generated_state() -> No
def test_session_context_respond_preserves_and_vaults_final_state() -> None:
session = SessionContext(vocab=_minimal_vocab())
initial = session.ingest(["logos", "arche"])
assert initial.node == session.vocab.index_of("logos")
result = session.respond(max_tokens=3)

View file

@ -0,0 +1,54 @@
from __future__ import annotations
from generate.stream import _nearest_next
class _StubVocab:
def __init__(self, words: list[str]):
self._words = words
self.calls: list[tuple[int, frozenset[int]]] = []
def __len__(self) -> int:
return len(self._words)
def nearest(self, F, exclude_idx: int = -1, exclude_indices=None):
blocked = frozenset(exclude_indices or ())
self.calls.append((exclude_idx, blocked))
for idx, word in enumerate(self._words):
if idx == exclude_idx or idx in blocked:
continue
return word, idx
raise ValueError("No candidate word available after exclusions.")
def test_nearest_next_excludes_recent_and_stop_nodes_when_possible():
vocab = _StubVocab(["seed", "to", "it", "meaning", "truth"])
word, idx = _nearest_next(
vocab,
F_voiced=None,
current_node=0,
recent_nodes=(3,),
stop_nodes=frozenset({1, 2}),
)
assert (word, idx) == ("truth", 4)
assert vocab.calls[0] == (0, frozenset({1, 2, 3}))
def test_nearest_next_relaxes_recent_window_before_stop_tokens():
vocab = _StubVocab(["seed", "to", "truth"])
word, idx = _nearest_next(
vocab,
F_voiced=None,
current_node=0,
recent_nodes=(2,),
stop_nodes=frozenset({1}),
)
assert (word, idx) == ("truth", 2)
assert vocab.calls == [
(0, frozenset({1, 2})),
(0, frozenset({1})),
]

View file

@ -92,7 +92,12 @@ class VocabManifold:
except ValueError:
raise KeyError(f"Word '{word}' not in vocabulary.")
def nearest(self, F: np.ndarray, exclude_idx: int = -1) -> tuple[str, int]:
def nearest(
self,
F: np.ndarray,
exclude_idx: int = -1,
exclude_indices: set[int] | frozenset[int] | None = None,
) -> tuple[str, int]:
"""
Find the word whose versor is closest to F by CGA inner product.
Returns (word, index). O(|vocab|), exact, no approximation.
@ -100,15 +105,21 @@ class VocabManifold:
Hot path: cga_inner routes through algebra.backend.
"""
blocked = set(exclude_indices or ())
if exclude_idx >= 0:
blocked.add(exclude_idx)
best_score = -np.inf
best_idx = 0
best_idx = -1
for i, v in enumerate(self._versors):
if i == exclude_idx:
if i in blocked:
continue
score = cga_inner(F, v)
if score > best_score:
best_score = score
best_idx = i
if best_idx < 0:
raise ValueError("No candidate word available after exclusions.")
return self._words[best_idx], best_idx
def __len__(self) -> int: