Replace plain ValueError at both inner-loop exhaustion sites in
generate/stream.py with InnerLoopExhaustion, a typed ValueError
subclass carrying machine-readable refusal evidence:
reason : RefusalReason (INNER_LOOP_EXHAUSTION)
region_label : which AdmissibilityRegion blocked
step_index : -1 = pre-walk empty intersection;
>=0 = in-walk per-step exhaustion
rejected_attempts : ordered (idx, word, score) triples
Backward-compat by construction: subclassing ValueError preserves
every pre-Phase-2 `except ValueError` handler in chat/runtime.py,
eval lanes, and tests. No edits to chat/runtime.py, field/propagate.py,
algebra/versor.py, or vault/store.py.
Trace path wired:
- CognitiveTurnResult.refusal_reason (str, default "")
- compute_trace_hash folds refusal_reason only when non-empty
-> byte-identical hashes preserved for non-refused turns
- CognitiveTurnPipeline reads via getattr from ChatResponse and
forwards into both trace_hash and result construction
Contract documented in docs/runtime_contracts.md §"Refusal contract".
Tests (tests/test_refusal_contract.py — 10 passing):
- InnerLoopExhaustion isinstance(ValueError) at both raise sites
- In-walk site carries reason/region_label/step_index>=0/
rejected_attempts with (int,str,float) triples
- Pre-walk site uses step_index=-1 sentinel + empty
rejected_attempts
- Pre-walk fires even when inner_loop_admissibility=False
- Trace hash: empty refusal_reason preserves legacy bytes;
non-empty differs; same inputs are stable
Suite results:
smoke: 67 passed
cognition: 121 passed
runtime: 19 passed
full: 1024 passed, 2 skipped
core eval cognition: 13/13, 100% intent accuracy, 100% versor closure
Residual silent path (documented as out-of-scope for Phase 2):
chat/runtime.respond()/arespond() still convert any ValueError to
"" for their public str return contract. So a refused turn today
produces surface == "" with refusal_reason == "" — the typed
evidence is unread between the raise site and the result. The
plumbing on result + trace + pipeline is in place so a future ADR
can wire materialisation (propagate exception to
ChatResponse.refusal_reason, or catch at the pipeline seam) without
re-deriving the contract.
Phase 1 (commit 3940290) and Phase 2 (this commit) were developed
in parallel with disjoint file scope to avoid conflicts.
105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
"""
|
|
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.graph_planner import ArticulationTarget, PropositionGraph
|
|
from generate.intent import DialogueIntent
|
|
from generate.proposition import Proposition
|
|
from core.physics.identity import IdentityScore
|
|
from teaching.correction import CorrectionCandidate
|
|
from teaching.review import ReviewedTeachingExample
|
|
from teaching.store import PackMutationProposal
|
|
|
|
|
|
@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
|
|
|
|
# --- intent / graph telemetry ---
|
|
intent: DialogueIntent | None = None
|
|
proposition_graph: PropositionGraph | None = None
|
|
articulation_target: ArticulationTarget | None = None
|
|
|
|
# --- teaching loop ---
|
|
teaching_candidate: CorrectionCandidate | None = None
|
|
reviewed_teaching_example: ReviewedTeachingExample | None = None
|
|
pack_mutation_proposal: PackMutationProposal | None = None
|
|
|
|
# --- inference operators (ADR-0018) ---
|
|
# Deterministic serialisation of any typed operator invoked during the
|
|
# turn (e.g. transitive_walk over the teaching-store typed-relation
|
|
# graph). Empty string when no operator ran. Folded into trace_hash
|
|
# so operator invocation is a load-bearing part of replay equality.
|
|
operator_invocation: str = ""
|
|
|
|
# --- forward semantic control evidence (ADR-0023) ---
|
|
# ``admissibility_trace`` is the per-transition record produced by
|
|
# ``generate()`` (empty tuple when no admissibility ran).
|
|
# ``admissibility_trace_hash`` is its canonical SHA-256, folded
|
|
# into ``trace_hash`` only when non-empty so pre-ADR-0023 turn
|
|
# hashes are byte-preserved.
|
|
# ``ratification_outcome`` is the enum value ("ratified" /
|
|
# "demoted" / "passthrough") from the field ratifier; empty
|
|
# string when no ratification ran.
|
|
# ``region_was_unconstrained`` records whether forward semantic
|
|
# control was active on this turn — observation only, no
|
|
# production fail-closed yet (see ADR-0023 §Out of scope).
|
|
admissibility_trace: tuple = ()
|
|
admissibility_trace_hash: str = ""
|
|
ratification_outcome: str = ""
|
|
region_was_unconstrained: bool = True
|
|
|
|
# --- inner-loop refusal evidence (ADR-0024 Phase 2) ---
|
|
# ``refusal_reason`` is the stable string value of a
|
|
# ``generate.exhaustion.RefusalReason`` when the walk refused this
|
|
# turn, or the empty string otherwise. Empty-string default is
|
|
# the contract for "no refusal materialised"; folding into
|
|
# trace_hash is gated on non-emptiness so non-refused turns keep
|
|
# byte-identical hashes relative to pre-Phase-2 (CLAUDE.md
|
|
# determinism invariant). Phase 2 leaves the materialisation site
|
|
# in chat/runtime.py untouched per the ADR-0024 Phase 2 scope
|
|
# decision — this field exists so the trace contract is already
|
|
# in place when a future ADR wires the materialisation path.
|
|
refusal_reason: str = ""
|
|
|
|
# --- invariant bookkeeping ---
|
|
versor_condition: float = 0.0 # must be < 1e-6
|
|
trace_hash: str = "" # SHA-256 over deterministic key fields
|