Fix post-contract runtime regressions

- remove normalization and unitization calls from generation path
- skip invalid recalled fields instead of repairing them in generation
- punctuate selected articulation surfaces
- stabilize assertive dialogue roles
- anchor proposition slots to live field
- preserve session anchor orientation for coherence
This commit is contained in:
Shay 2026-05-14 18:57:24 -07:00 committed by GitHub
parent 7401eae7ae
commit a683912ad2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 84 additions and 52 deletions

View file

@ -40,6 +40,8 @@ _SEED_ALIASES = {
"arche": "ἀρχή",
"aletheia": "ἀλήθεια",
}
_QUESTION_WORDS = frozenset({"what", "who", "how", "why", "when", "where", "which"})
_TERMINALS = frozenset({".", "?", ";", "!"})
def _energy_scalar(energy_obj) -> float:
@ -54,6 +56,33 @@ def _energy_scalar(energy_obj) -> float:
return 1.0
def _is_question_input(raw_text: str, tokens: Sequence[str]) -> bool:
if raw_text.strip().endswith("?"):
return True
return bool(tokens and tokens[0].casefold() in _QUESTION_WORDS)
def _stable_dialogue_role(role: DialogueRole, *, raw_text: str, tokens: Sequence[str]) -> DialogueRole:
if role == "question" and not _is_question_input(raw_text, tokens):
return "elaborate"
return role
def _terminal_for_role(role: DialogueRole, output_language: str) -> str:
if role == "question":
return ";" if output_language == "grc" else "?"
return "."
def _terminate_surface(surface: str, *, role: DialogueRole, output_language: str) -> str:
stripped = surface.strip()
if not stripped:
return stripped
if stripped[-1] in _TERMINALS:
return stripped
return f"{stripped}{_terminal_for_role(role, output_language)}"
@dataclass
class _StubBindingFrame:
frame_id: str
@ -272,9 +301,10 @@ class ChatRuntime:
self._frame_registry,
output_lang=self.config.output_language,
)
dialogue_role = classify_dialogue_blade(
base_proposition.relation,
reference_blade,
dialogue_role = _stable_dialogue_role(
classify_dialogue_blade(base_proposition.relation, reference_blade),
raw_text=text,
tokens=tokens,
)
proposition = propose_dialogue(
field_state,
@ -346,7 +376,12 @@ class ChatRuntime:
)
walk_surface = sentence_plan.surface
surface = articulation.surface
surface = _terminate_surface(
articulation.surface,
role=dialogue_role,
output_language=self.config.output_language,
)
articulation_surface = surface
vault_hits = int(result.vault_hits)
turn_event = TurnEvent(
@ -354,7 +389,7 @@ class ChatRuntime:
input_tokens=tuple(filtered),
surface=surface,
walk_surface=walk_surface,
articulation_surface=articulation.surface,
articulation_surface=articulation_surface,
dialogue_role=str(dialogue_role),
identity_score=identity_score,
cycle_cost_total=cycle_cost.total,
@ -369,7 +404,7 @@ class ChatRuntime:
surface=surface,
proposition=proposition,
articulation=articulation,
articulation_surface=articulation.surface,
articulation_surface=articulation_surface,
dialogue_role=dialogue_role,
versor_condition=versor_condition(result.final_state.F),
output_language=self.config.output_language,

View file

@ -153,9 +153,6 @@ def propose(
preferred_pos=frozenset({"noun", "pronoun"}),
candidate_indices=candidate_indices,
)
# Predicate selection must remain anchored to the prompt field, not a
# recall-contaminated or drive-biased current field, so slot evidence stays
# closer to prompt than unrelated vault points.
predicate_word, predicate_idx = _nearest_content_word(
vocab,
prompt,
@ -275,7 +272,7 @@ def _first_existing(vocab, candidates: tuple[str, ...]) -> str | None:
def _prompt_versor(field_state: FieldState) -> np.ndarray:
return field_state.holonomy if field_state.holonomy is not None else field_state.F
return field_state.F
def _nearest_content_word(

View file

@ -3,6 +3,11 @@ Generation loop — token streaming from the versor manifold.
Every token: nearest non-current word to current F via CGA inner product.
Every step: F <- versor_apply(V, F) where V = word_transition_rotor(A, B).
Generation is not a normalization boundary. Raw prompt normalization belongs
at ingest/gate.py; construction normalization belongs in algebra/vocab/persona.
If vault recall returns a non-operator-like field that cannot form a stable
transition, recall skips that hit instead of repairing it here.
"""
from __future__ import annotations
@ -13,7 +18,6 @@ import numpy as np
from field.state import FieldState
from field.propagate import propagate_step
from algebra.rotor import word_transition_rotor
from algebra.versor import normalize_to_versor, unitize_versor
from generate.attention import AttentionOperator
from generate.result import GenerationResult
from generate.salience import SalienceOperator
@ -22,29 +26,6 @@ _RECENT_WINDOW = 3
_STOP_TOKENS = frozenset({"it", "to", "word"})
def _closed_F(F: np.ndarray) -> np.ndarray:
arr = np.asarray(F, dtype=np.float64)
try:
return unitize_versor(arr)
except ValueError:
return normalize_to_versor(arr)
def _renorm(state: FieldState) -> FieldState:
"""Return state with F reclosed onto the versor manifold."""
closed = _closed_F(state.F)
if np.allclose(closed, state.F, atol=1e-12, rtol=1e-12):
return state
return FieldState(
F=closed,
node=state.node,
step=state.step,
holonomy=state.holonomy,
energy=state.energy,
valence=state.valence,
)
def _articulate(vocab, word: str) -> str:
morphology_for_word = getattr(vocab, "morphology_for_word", None)
if morphology_for_word is None:
@ -117,26 +98,33 @@ def _nearest_with_optional_candidates(
def _voiced_state(state: FieldState, persona) -> FieldState:
return _renorm(FieldState(
return FieldState(
F=persona.apply(state.F),
node=state.node,
step=state.step,
holonomy=state.holonomy,
energy=state.energy,
valence=state.valence,
))
)
def _recall_state(state: FieldState, vault, top_k: int) -> tuple[FieldState, int]:
if vault is None or top_k <= 0:
return state, 0
current = _renorm(state)
current = state
hits_applied = 0
for hit in vault.recall(current.F, top_k=top_k):
recalled_F = _closed_F(np.asarray(hit["versor"], dtype=np.float64))
V = word_transition_rotor(current.F, recalled_F)
current = _renorm(propagate_step(current, V))
recalled_F = np.asarray(hit["versor"], dtype=np.float64)
try:
V = word_transition_rotor(current.F, recalled_F)
except ValueError:
# Vault stores field states as well as proposition/memory payloads.
# Not every recalled versor is a valid transition target for the
# live generation operator. Generation must fail closed here rather
# than normalizing or repairing recalled memory in the hot path.
continue
current = propagate_step(current, V)
current = FieldState(
F=current.F,
node=state.node,
@ -203,7 +191,7 @@ def generate(
tokens = []
trajectory = [] if record_trajectory else None
vault_hits = 0
current = _renorm(state)
current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
language_candidates = None if allow_cross_language_generation else _candidate_indices_for_language(vocab, output_lang)
salience_candidates, salience_budget, candidates_used = _attention_candidates(
@ -245,7 +233,7 @@ def generate(
B = vocab.get_versor_at(word_idx)
V = word_transition_rotor(A, B)
current = _renorm(propagate_step(current, V))
current = propagate_step(current, V)
current = FieldState(
F=current.F,
node=word_idx,
@ -258,7 +246,7 @@ def generate(
return GenerationResult(
tokens=tokens,
final_state=_renorm(current),
final_state=current,
trajectory=trajectory,
salience_top_k=salience_budget,
candidates_used=candidates_used,
@ -274,7 +262,7 @@ async def agenerate(
vault=None,
recall_top_k: int = 3,
):
current = _renorm(state)
current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
stop_nodes = frozenset(
vocab.index_of(token)
@ -300,7 +288,7 @@ async def agenerate(
B = vocab.get_versor_at(word_idx)
V = word_transition_rotor(A, B)
current = _renorm(propagate_step(current, V))
current = propagate_step(current, V)
current = FieldState(
F=current.F,
node=word_idx,

View file

@ -110,9 +110,21 @@ class SessionContext:
valence=self.state.valence,
)
result = generate(pivot, self.vocab, self.persona, max_tokens, vault=self.vault)
result = self._orient_result_to_anchor(result)
self.state = result.final_state
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
self.turn += 1
self._last_response_tokens = result.tokens
return result
def _orient_result_to_anchor(self, result: GenerationResult) -> GenerationResult:
final_state = result.final_state
coherence_anchor = self._anchor_field if self._anchor_field is not None else self.state.F
if cga_inner(final_state.F, coherence_anchor) < 0.0:
if coherence_anchor is None:
return result
cga_score = cga_inner(final_state.F, coherence_anchor)
euclidean_score = float(np.dot(final_state.F, coherence_anchor))
if cga_score < 0.0 or euclidean_score < 0.0:
final_state = FieldState(
F=-final_state.F,
node=final_state.node,
@ -121,17 +133,15 @@ class SessionContext:
energy=final_state.energy,
valence=final_state.valence,
)
result = GenerationResult(
return GenerationResult(
tokens=result.tokens,
final_state=final_state,
trajectory=result.trajectory,
salience_top_k=result.salience_top_k,
candidates_used=result.candidates_used,
vault_hits=result.vault_hits,
identity_score=result.identity_score,
)
self.state = result.final_state
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
self.turn += 1
self._last_response_tokens = result.tokens
return result
async def arespond(self, max_tokens: int = 128):
@ -143,7 +153,9 @@ 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, vault=self.vault)
result = self._orient_result_to_anchor(
generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
)
for token in result.tokens:
yield token
self.state = result.final_state