- 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
100 lines
3.1 KiB
Python
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)
|