core/generate/stream.py
Shay 3746f06898 fix: cohesive seam pass — frozen FieldState, GenerationResult, generation/vocab/algebra separation, normalization doctrine
- field/state.py: FieldState is now frozen+slotted; constructor copies and
  enforces float32 shape (32,); advance() updated to pass raw arrays.
  np.ndarray inside frozen dataclass is ref-frozen — copy() at construction
  is the explicit contract boundary.

- generate/result.py: NEW — GenerationResult frozen dataclass carrying
  tokens + final_state. Async variant yields tokens and exposes final_state
  on completion.

- generate/stream.py: generate() now returns GenerationResult, not list[str].
  vocab.edge_rotor() call replaced with:
    A = vocab.get_versor_at(current.node)
    B = vocab.get_versor_at(word_idx)
    V = word_transition_rotor(A, B)
  agenerate() updated to yield tokens and surface final_state.

- vocab/manifold.py: added get_versor_at(idx) and get_word_at(idx) indexed
  accessors. VocabManifold stores points; algebra constructs operators.
  normalize_to_versor() call-site in docstring clarified: callers must call
  unitize_versor() (algebra construction primitive) before add(), not
  normalize_to_versor() directly.

- algebra/versor.py: unitize_versor() added as the explicit construction-time
  primitive. normalize_to_versor() kept but marked internal/gate-only.
  Distinction encoded in docstrings and __all__.

- persona/motor.py + ingest/gate.py: SessionContext.respond() is not yet in
  the repo as a separate file; gate.py docstring updated to reflect the
  three-tier normalization doctrine:
    unitize_versor()    — algebra construction only
    inject()            — gate, once per raw input
    normalization       — forbidden in propagate/generate/vault recall
2026-05-13 12:32:36 -07:00

100 lines
3.1 KiB
Python

"""
Generation loop — token streaming from the versor manifold.
Every token: nearest word to current F via CGA inner product.
Every step: F <- versor_apply(V, F) where V = word_transition_rotor(A, B).
Architectural boundaries enforced here:
- VocabManifold owns manifold points only (get_versor_at, nearest).
- algebra.rotor.word_transition_rotor constructs the transition operator.
- Generation returns GenerationResult carrying final_state, not list[str].
- No normalization inside this loop. FieldState invariant is maintained
structurally by versor_apply() and the closed algebra.
No confidence gates. No IDK fallback. No attractor clamping.
F is always on the manifold. nearest() is always exact.
"""
from __future__ import annotations
from field.state import FieldState
from field.propagate import propagate_step
from algebra.rotor import word_transition_rotor
from generate.result import GenerationResult
def generate(
state: FieldState,
vocab,
persona,
max_tokens: int = 128,
record_trajectory: bool = False,
) -> GenerationResult:
"""
Generate a token sequence from an initial FieldState.
Loop:
1. Apply persona motor to current field
2. Find nearest vocab node via CGA inner product
3. Emit token
4. 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
Returns:
GenerationResult with tokens, final_state, and optional trajectory.
"""
tokens = []
trajectory = [] if record_trajectory else None
current = state
for _ in range(max_tokens):
F_voiced = persona.apply(current.F)
word, word_idx = vocab.nearest(F_voiced)
tokens.append(word)
if record_trajectory:
trajectory.append(current)
A = vocab.get_versor_at(current.node)
B = vocab.get_versor_at(word_idx)
V = word_transition_rotor(A, B)
current = propagate_step(current, V)
current = FieldState(F=current.F, node=word_idx, step=current.step)
return GenerationResult(
tokens=tokens,
final_state=current,
trajectory=trajectory,
)
async def agenerate(
state: FieldState,
vocab,
persona,
max_tokens: int = 128,
):
"""
Async streaming version — yields one token at a time.
The caller must await the generator and can retrieve final_state
by calling .athrow() or by consuming the StopAsyncIteration value.
For the final state, prefer the synchronous generate() path or
wrap in an async collector that reads the return value.
Yields: str (one token per iteration)
"""
current = state
for _ in range(max_tokens):
F_voiced = persona.apply(current.F)
word, word_idx = vocab.nearest(F_voiced)
yield word
A = vocab.get_versor_at(current.node)
B = vocab.get_versor_at(word_idx)
V = word_transition_rotor(A, B)
current = propagate_step(current, V)
current = FieldState(F=current.F, node=word_idx, step=current.step)