feat(cognition): add CognitiveTurnPipeline spine (#19)
This commit is contained in:
parent
249592c37e
commit
92be98fbdf
5 changed files with 387 additions and 0 deletions
15
core/cognition/__init__.py
Normal file
15
core/cognition/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""
|
||||
core.cognition — the cognitive spine.
|
||||
|
||||
Exports the public surface of the pipeline layer.
|
||||
"""
|
||||
|
||||
from core.cognition.pipeline import CognitiveTurnPipeline
|
||||
from core.cognition.result import CognitiveTurnResult
|
||||
from core.cognition.trace import compute_trace_hash
|
||||
|
||||
__all__ = [
|
||||
"CognitiveTurnPipeline",
|
||||
"CognitiveTurnResult",
|
||||
"compute_trace_hash",
|
||||
]
|
||||
103
core/cognition/pipeline.py
Normal file
103
core/cognition/pipeline.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
CognitiveTurnPipeline — the cognitive spine.
|
||||
|
||||
Architecture:
|
||||
listen -> ingest -> understand -> recall -> think -> articulate
|
||||
-> learn_proposal -> trace
|
||||
|
||||
This first-pass implementation delegates to ChatRuntime internals so
|
||||
future intelligence modules (IntentPropositionGraph, ArticulationRealizerV2,
|
||||
ReviewedTeachingLoop, CognitiveEvalHarness) have a clean plug-in surface
|
||||
without requiring a full ChatRuntime rewrite.
|
||||
|
||||
Constraint: ChatRuntime.chat() and ChatResponse contract are unchanged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from field.state import FieldState
|
||||
from core.cognition.result import CognitiveTurnResult
|
||||
from core.cognition.trace import compute_trace_hash
|
||||
|
||||
|
||||
class CognitiveTurnPipeline:
|
||||
"""Thin pipeline wrapper over ChatRuntime.
|
||||
|
||||
Phase 1 goal: extract the observability path so downstream modules have
|
||||
a place to plug in. No new intelligence is added here.
|
||||
"""
|
||||
|
||||
def __init__(self, runtime) -> None: # runtime: ChatRuntime (no import cycle)
|
||||
self.runtime = runtime
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def run(self, text: str, max_tokens: int | None = None) -> CognitiveTurnResult:
|
||||
"""Execute one full cognitive turn and return a complete result record."""
|
||||
|
||||
# 1. LISTEN — capture pre-turn field state
|
||||
field_state_before: FieldState | None = self._capture_field_state()
|
||||
|
||||
# 2–7. INGEST / UNDERSTAND / RECALL / THINK / ARTICULATE / LEARN
|
||||
# Delegated to ChatRuntime.chat() in Phase 1.
|
||||
# ChatResponse is the stable contract surface.
|
||||
response = self.runtime.chat(text, max_tokens=max_tokens)
|
||||
|
||||
# 8. CAPTURE post-turn field state
|
||||
field_state_after: FieldState = self.runtime.session.state
|
||||
|
||||
# 9. Reconstruct input-layer tokens from the turn log
|
||||
# (turn_log is appended inside chat(); last entry matches this turn)
|
||||
last_turn = self.runtime.turn_log[-1]
|
||||
input_tokens = last_turn.input_tokens # already filtered
|
||||
filtered_tokens = last_turn.input_tokens # same at Phase 1
|
||||
|
||||
# Raw tokenization is identical to filtered for Phase 1 — the
|
||||
# runtime's _tokenize() runs before _apply_oov_policy(). We
|
||||
# expose input_tokens separately so Phase 2 can diverge them.
|
||||
raw_tokens = tuple(self.runtime.tokenize(text))
|
||||
|
||||
# 10. TRACE — deterministic hash
|
||||
trace_hash = compute_trace_hash(
|
||||
input_text=text,
|
||||
filtered_tokens=filtered_tokens,
|
||||
surface=response.surface,
|
||||
walk_surface=response.walk_surface,
|
||||
articulation_surface=response.articulation_surface,
|
||||
dialogue_role=str(response.dialogue_role),
|
||||
versor_condition=response.versor_condition,
|
||||
vault_hits=response.vault_hits,
|
||||
)
|
||||
|
||||
return CognitiveTurnResult(
|
||||
input_text=text,
|
||||
input_tokens=raw_tokens,
|
||||
filtered_tokens=filtered_tokens,
|
||||
field_state_before=field_state_before,
|
||||
field_state_after=field_state_after,
|
||||
proposition=response.proposition,
|
||||
articulation=response.articulation,
|
||||
surface=response.surface,
|
||||
walk_surface=response.walk_surface,
|
||||
articulation_surface=response.articulation_surface,
|
||||
dialogue_role=response.dialogue_role,
|
||||
identity_score=response.identity_score,
|
||||
vault_hits=response.vault_hits,
|
||||
versor_condition=response.versor_condition,
|
||||
trace_hash=trace_hash,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _capture_field_state(self) -> FieldState | None:
|
||||
"""Return current session field state, or None if not yet initialised."""
|
||||
try:
|
||||
state = self.runtime.session.state
|
||||
# SessionContext.state may be None before the first ingest
|
||||
return state if state is not None else None
|
||||
except AttributeError:
|
||||
return None
|
||||
53
core/cognition/result.py
Normal file
53
core/cognition/result.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""
|
||||
CognitiveTurnResult — the complete record of one cognitive turn.
|
||||
|
||||
This is the canonical output of CognitiveTurnPipeline.run(). It is
|
||||
frozen and slot-based so it can be passed safely across module boundaries
|
||||
without mutation risk.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from field.state import FieldState
|
||||
from generate.articulation import ArticulationPlan
|
||||
from generate.dialogue import DialogueRole
|
||||
from generate.proposition import Proposition
|
||||
from core.physics.identity import IdentityScore
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CognitiveTurnResult:
|
||||
"""Full observability record for a single pipeline turn."""
|
||||
|
||||
# --- input layer ---
|
||||
input_text: str
|
||||
input_tokens: tuple[str, ...]
|
||||
filtered_tokens: tuple[str, ...]
|
||||
|
||||
# --- field layer ---
|
||||
field_state_before: FieldState | None # None on the very first turn
|
||||
field_state_after: FieldState
|
||||
|
||||
# --- understanding / recall layer ---
|
||||
proposition: Proposition
|
||||
articulation: ArticulationPlan
|
||||
|
||||
# --- output surfaces ---
|
||||
surface: str # final voiced surface (what the user sees)
|
||||
walk_surface: str # sentence-assembled walk surface
|
||||
articulation_surface: str # bare articulation surface before assembly
|
||||
|
||||
# --- dialogue ---
|
||||
dialogue_role: DialogueRole
|
||||
|
||||
# --- identity telemetry ---
|
||||
identity_score: IdentityScore | None
|
||||
|
||||
# --- vault / memory ---
|
||||
vault_hits: int
|
||||
|
||||
# --- invariant bookkeeping ---
|
||||
versor_condition: float # must be < 1e-6
|
||||
trace_hash: str # SHA-256 over deterministic key fields
|
||||
67
core/cognition/trace.py
Normal file
67
core/cognition/trace.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Deterministic trace hashing for cognitive turns.
|
||||
|
||||
The hash captures every meaningful output of a pipeline run so that:
|
||||
- identical inputs on identical field state → identical hash
|
||||
- any output change → different hash
|
||||
|
||||
Only stable, semantically meaningful fields are included. Floating-point
|
||||
values are rounded to 9 decimal places before hashing so that numeric
|
||||
noise from different hardware does not break determinism within a run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.cognition.result import CognitiveTurnResult
|
||||
|
||||
|
||||
def _round_float(v: float, ndigits: int = 9) -> float:
|
||||
return round(float(v), ndigits)
|
||||
|
||||
|
||||
def compute_trace_hash(
|
||||
input_text: str,
|
||||
filtered_tokens: tuple[str, ...],
|
||||
surface: str,
|
||||
walk_surface: str,
|
||||
articulation_surface: str,
|
||||
dialogue_role: str,
|
||||
versor_condition: float,
|
||||
vault_hits: int,
|
||||
) -> str:
|
||||
"""Return a deterministic SHA-256 hex digest over the turn's key outputs.
|
||||
|
||||
Parameters match the subset of CognitiveTurnResult that is both
|
||||
semantically meaningful and stable across hardware.
|
||||
"""
|
||||
payload = {
|
||||
"input_text": input_text,
|
||||
"filtered_tokens": list(filtered_tokens),
|
||||
"surface": surface,
|
||||
"walk_surface": walk_surface,
|
||||
"articulation_surface": articulation_surface,
|
||||
"dialogue_role": str(dialogue_role),
|
||||
"versor_condition": _round_float(versor_condition),
|
||||
"vault_hits": int(vault_hits),
|
||||
}
|
||||
serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def trace_hash_from_result(result: "CognitiveTurnResult") -> str:
|
||||
"""Convenience wrapper — compute the hash directly from a result object."""
|
||||
return compute_trace_hash(
|
||||
input_text=result.input_text,
|
||||
filtered_tokens=result.filtered_tokens,
|
||||
surface=result.surface,
|
||||
walk_surface=result.walk_surface,
|
||||
articulation_surface=result.articulation_surface,
|
||||
dialogue_role=str(result.dialogue_role),
|
||||
versor_condition=result.versor_condition,
|
||||
vault_hits=result.vault_hits,
|
||||
)
|
||||
149
tests/test_cognitive_turn_pipeline.py
Normal file
149
tests/test_cognitive_turn_pipeline.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
Tests for CognitiveTurnPipeline — the cognitive spine.
|
||||
|
||||
Five tests, no micro-test explosion:
|
||||
1. test_pipeline_known_token_turn — happy-path turn with known tokens
|
||||
2. test_pipeline_unknown_token_grounding — OOV token handled; field still valid
|
||||
3. test_pipeline_two_turn_memory_continuity — field evolves across turns
|
||||
4. test_pipeline_trace_hash_deterministic — identical inputs → identical hash
|
||||
5. test_pipeline_preserves_versor_closure — versor_condition < 1e-6 per turn
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
from core.cognition import CognitiveTurnPipeline, CognitiveTurnResult
|
||||
from core.cognition.trace import trace_hash_from_result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture()
|
||||
def runtime() -> ChatRuntime:
|
||||
return ChatRuntime()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def pipeline(runtime: ChatRuntime) -> CognitiveTurnPipeline:
|
||||
return CognitiveTurnPipeline(runtime)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Known token turn
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pipeline_known_token_turn(pipeline: CognitiveTurnPipeline) -> None:
|
||||
"""A single turn with known tokens yields a fully populated result."""
|
||||
result = pipeline.run("light logos", max_tokens=8)
|
||||
|
||||
assert isinstance(result, CognitiveTurnResult)
|
||||
|
||||
# Input layer
|
||||
assert result.input_text == "light logos"
|
||||
assert len(result.input_tokens) >= 1
|
||||
assert len(result.filtered_tokens) >= 1
|
||||
|
||||
# Field layer
|
||||
assert result.field_state_before is None # first turn: no prior state
|
||||
assert result.field_state_after is not None
|
||||
assert result.field_state_after.F.shape == (32,)
|
||||
|
||||
# Output surfaces
|
||||
assert result.surface.strip()
|
||||
assert isinstance(result.walk_surface, str)
|
||||
assert isinstance(result.articulation_surface, str)
|
||||
|
||||
# Dialogue
|
||||
assert result.dialogue_role in {"assert", "elaborate", "question", "refute"}
|
||||
|
||||
# Bookkeeping
|
||||
assert isinstance(result.versor_condition, float)
|
||||
assert isinstance(result.trace_hash, str) and len(result.trace_hash) == 64
|
||||
assert isinstance(result.vault_hits, int)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Unknown / OOV token grounding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pipeline_unknown_token_grounding(pipeline: CognitiveTurnPipeline) -> None:
|
||||
"""OOV token in an open pack should not prevent field from staying valid."""
|
||||
result = pipeline.run("what is דברית", max_tokens=4)
|
||||
|
||||
# Runtime must still produce a valid result
|
||||
assert result.surface.strip()
|
||||
assert result.field_state_after is not None
|
||||
assert result.versor_condition < 1e-6
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Two-turn memory continuity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pipeline_two_turn_memory_continuity(pipeline: CognitiveTurnPipeline) -> None:
|
||||
"""Field state evolves between turns, confirming the pipeline threads memory."""
|
||||
first = pipeline.run("light logos", max_tokens=8)
|
||||
second = pipeline.run("truth logos", max_tokens=8)
|
||||
|
||||
# second turn knows about first
|
||||
assert second.field_state_before is not None
|
||||
assert second.field_state_before.F.shape == (32,)
|
||||
|
||||
# field genuinely moved between turns
|
||||
assert not np.array_equal(
|
||||
first.field_state_after.F,
|
||||
second.field_state_after.F,
|
||||
), "Field state must evolve across turns."
|
||||
|
||||
# Both versor conditions are closed
|
||||
assert first.versor_condition < 1e-6
|
||||
assert second.versor_condition < 1e-6
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Trace hash determinism
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pipeline_trace_hash_deterministic() -> None:
|
||||
"""Identical inputs on a fresh runtime produce the same trace hash."""
|
||||
rt1 = ChatRuntime()
|
||||
rt2 = ChatRuntime()
|
||||
|
||||
r1 = CognitiveTurnPipeline(rt1).run("light truth", max_tokens=6)
|
||||
r2 = CognitiveTurnPipeline(rt2).run("light truth", max_tokens=6)
|
||||
|
||||
# Re-derive via the helper to confirm the hash formula is stable
|
||||
assert r1.trace_hash == trace_hash_from_result(r1)
|
||||
assert r2.trace_hash == trace_hash_from_result(r2)
|
||||
|
||||
# Same hash across two independent runtimes with same prompt
|
||||
assert r1.trace_hash == r2.trace_hash, (
|
||||
f"Expected deterministic hash, got:\n r1={r1.trace_hash}\n r2={r2.trace_hash}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Versor closure preserved across all turns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pipeline_preserves_versor_closure(pipeline: CognitiveTurnPipeline) -> None:
|
||||
"""versor_condition must stay below 1e-6 for every turn in the session."""
|
||||
prompts = [
|
||||
"logos light",
|
||||
"truth word",
|
||||
"what is λόγος",
|
||||
"spirit breath",
|
||||
]
|
||||
for prompt in prompts:
|
||||
result = pipeline.run(prompt, max_tokens=6)
|
||||
assert result.versor_condition < 1e-6, (
|
||||
f"Versor closure broken after prompt {prompt!r}: "
|
||||
f"versor_condition={result.versor_condition:.2e}"
|
||||
)
|
||||
# Field state invariant: shape must be intact
|
||||
assert result.field_state_after.F.shape == (32,)
|
||||
Loading…
Reference in a new issue