from __future__ import annotations from dataclasses import dataclass, replace import hashlib import json import re import warnings from collections.abc import Sequence from typing import Any, List from generate.lexicon import BE_FINITE import numpy as np from algebra.versor import versor_condition from chat.pack_grounding import ( pack_grounded_surface, pack_grounded_comparison_surface, pack_grounded_correction_surface, pack_grounded_procedure_surface, pack_grounded_relation_confirmation_surface, pack_grounded_unknown_surface, gloss_aware_cause_surface, PACK_ID as _COGNITION_PACK_ID, ) from chat.teaching_grounding import ( teaching_grounded_surface, teaching_grounded_surface_composed, teaching_grounded_surface_transitive, TEACHING_CORPUS_ID as _TEACHING_CORPUS_ID, ) from chat.refusal import ( TYPED_REFUSAL_PREFIX, build_hedge_prefix, build_refusal_surface, inject_hedge, should_inject_hedge, ) from core.epistemic_state import ( clearance_from_verdicts, epistemic_state_for_grounding_source, normative_detail_from_verdicts, ) from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary from core.response_governance import govern_response, shape_surface from chat.telemetry import ( TurnEventSink, format_correction_event_jsonl, format_reboot_event_jsonl, format_turn_event_jsonl, ) from chat.verdicts import TurnVerdicts from chat.dispatch_trace import DispatchAttempt, DispatchTrace from teaching.discovery import ( DiscoveryCandidate, extract_discovery_candidates, format_candidate_jsonl, ) from teaching.discovery_sink import DiscoveryCandidateSink from engine_state import EngineStateStore from core.engine_identity import ( ENGINE_IDENTITY_SCHEME, IdentityReconciliation, engine_identity_for_config, reconcile_loaded_identity, compute_engine_identity, DEFAULT_SAFETY_PACK, DEFAULT_REGISTER_PACK, DEFAULT_ANCHOR_LENS, ) from recognition.anti_unifier import derive_recognizer from recognition.outcome import FeatureBundle from recognition.registry import RecognizerRegistry from core.config import DEFAULT_CONFIG, DEFAULT_IDENTITY_PACK, RuntimeConfig from core.physics.drive import DriveGradientMap, GradientField from core.physics.energy import EnergyClass, EnergyProfile from core.physics.exertion import CycleCost, ExertionMeter from core.physics.learning import VaultPromotionPolicy from core.physics.identity import ( CharacterProfile, IdentityCheck, IdentityScore, TurnEvent, ) from core.physics.identity import advance_session_identity_path from core.physics.identity_action import AdmissionPolicy from packs.ethics.check import EthicsCheck, EthicsContext from packs.ethics.loader import ( DEFAULT_ETHICS_PACK as _DEFAULT_ETHICS_PACK, EthicsPackError, load_ethics_pack, ) from packs.identity.loader import load_identity_manifold from chat.register_substantive import apply_substantive_register from chat.register_variation import decorate_surface from chat.atom_equivalence import atoms_for_graph_nodes, compare_atom_sets from generate.realizer_guard import ( DISCLOSURE_SURFACE as _GUARD_DISCLOSURE_SURFACE, QUOTED_TEMPLATE_EXEMPT_VERDICT as _GUARD_QUOTED_EXEMPT, check_surface as _check_realizer_surface, ) from packs.anchor_lens.loader import AnchorLens, load_anchor_lens from packs.register.loader import RegisterPack, load_register_pack from packs.safety.check import SafetyCheck, SafetyContext from packs.safety.loader import load_safety_pack from field.state import FieldState from generate.articulation import ArticulationPlan, realize from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue from generate.graph_constraint import build_graph_constraint from generate.intent_bridge import articulate_with_intent, build_graph_from_input from generate.proposition import FrameRegistry, Proposition, propose from generate.result import GenerationResult from generate.stream import generate from generate.surface import SentenceAssembler, SentencePlan, SurfaceContext from ingest.gate import inject from packs import OOVPolicy, load_mounted_packs, load_pack, load_pack_entries from persona.motor import PersonaMotor from session.context import SessionContext from session.correction import CorrectionPass from vault.decompose import default_decomposer, default_gate from core.cognition.backpressure import BackpressureRecord, build_backpressure_record _TOKEN_RE = re.compile(r"\w+", re.UNICODE) # ADR-0073d (L1.4) — extracts the engaged ``cognitive_mode_label`` from a # composer-emitted ``[lens():]`` annotation. The runtime # uses this read-only to populate the TurnEvent telemetry field; the # composer remains the only source of truth for engagement. _ANCHOR_LENS_ANNOTATION_RE = re.compile(r"\[lens\(([^):]+)\):([^\]]+)\]") def _auto_propose_from_candidates(candidates: list[DiscoveryCandidate]) -> None: from teaching.proposals import ( ProposalError, ProposalLog, _current_revision, propose_from_candidate, ) from teaching.source import ProposalSource log = ProposalLog() for candidate in candidates: source = ProposalSource( kind="contemplation", source_id=candidate.candidate_id, emitted_at_revision=_current_revision(), ) try: propose_from_candidate(candidate, log=log, source=source) except ProposalError: pass def _extract_anchor_lens_mode_label(surface: str, lens_id: str) -> str: """Return the engaged mode_label if *surface* carries a ``[lens():]`` annotation for the given ``lens_id``. Returns ``""`` when: * surface is empty or contains no lens annotation * lens_id is empty (no lens loaded) * the annotation in surface is for a different lens_id (defensive) Pure read; no side effects. Telemetry-only — the composer is the sole source of truth for engagement (ADR-0073c). """ if not surface or not lens_id: return "" for match in _ANCHOR_LENS_ANNOTATION_RE.finditer(surface): if match.group(1) == lens_id: return match.group(2) return "" _SEED_ALIASES = { "logos": "\u03bb\u03cc\u03b3\u03bf\u03c2", "dabar": "\u05d3\u05d1\u05e8", "or": "\u05d0\u05d5\u05e8", "phos": "\u03c6\u03c9\u03c2", "zoe": "\u03b6\u03c9\u03ae", "arche": "\u1f00\u03c1\u03c7\u03ae", "aletheia": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", } _QUESTION_WORDS = frozenset({"what", "who", "how", "why", "when", "where", "which"}) # Comb pass 2026-05-21 — module-level constant so ``_prefer_prompt_anchor`` # does not allocate a fresh set on every English turn. Aux-verbs that # precede the prompt's content noun ("is", "are", "was", "were") get # filtered out so the content-noun search lands on the actual subject. _BE_FORMS: frozenset[str] = BE_FINITE _TERMINALS = frozenset({".", "?", ";", "!"}) _UNKNOWN_DOMAIN_SURFACE = "I don't know — insufficient grounding for that yet." def _build_vault_probe(vault, vocab): """Return a vault probe for discovery contemplation (W-016). Queries the session vault at EpistemicStatus.COHERENT using the subject lemma's versor as the lookup key. The probe is a pure read; it never writes to the vault or changes runtime state. Trust boundary: vault entries from SPECULATIVE/CONTESTED/FALSIFIED tiers are excluded by passing min_status=COHERENT to vault.recall. The probe returns only ``vault_coherent`` EvidencePointers per _VaultProbe contract (teaching/contemplation.py). """ from teaching.discovery import EvidencePointer from teaching.epistemic import EpistemicStatus def _probe(subject: str, obj: str) -> tuple[EvidencePointer, ...]: try: query = vocab.get_versor(subject) except KeyError: return () hits = vault.recall(query, top_k=3, min_status=EpistemicStatus.COHERENT) return tuple( EvidencePointer( source="vault_coherent", ref=str(hit["index"]), polarity="affirms", epistemic_status="coherent", ) for hit in hits ) return _probe def _vault_probe_for_context(context: SessionContext | None) -> Any | None: """Return a _VaultProbe callable or None for the given session context.""" if context is None: return None return _build_vault_probe(context.vault, context.vocab) def _energy_scalar(energy_obj) -> float: if energy_obj is None: return 1.0 if isinstance(energy_obj, EnergyProfile): return float(energy_obj.raw) try: return float(energy_obj) except (TypeError, ValueError): return 1.0 def _recall_energy_class_from_hits(hits: Sequence[dict]) -> str | None: if not hits: return None profile = hits[0].get("energy_profile") energy_class = getattr(profile, "energy_class", None) value = getattr(energy_class, "value", None) return value if isinstance(value, str) else None 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 in {"question", "refute"} 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)}" def _prefer_prompt_anchor( articulation: ArticulationPlan, filtered_tokens: Sequence[str], *, output_language: str, ) -> ArticulationPlan: if output_language != "en" or len(filtered_tokens) < 2: return articulation # Comb pass 2026-05-21 — find the last content-bearing token by # reverse iteration with short-circuit; pre-fix this built a full # ``content_tokens`` list and then took ``[-1]``. Also: cache # ``token.casefold()`` once per token via walrus operator instead # of calling it twice (against ``_QUESTION_WORDS`` and the # historical inline ``{"is", "are", "was", "were"}`` literal). anchor: str | None = None for token in reversed(filtered_tokens): lower = token.casefold() if lower in _QUESTION_WORDS or lower in _BE_FORMS: continue anchor = token break if anchor is None or anchor == articulation.subject: return articulation return replace( articulation, subject=anchor, surface=" ".join(part for part in (anchor, articulation.predicate, articulation.object) if part), ) @dataclass class _StubBindingFrame: frame_id: str coherence_magnitude: float region_ids: frozenset cycle_index: int @dataclass(frozen=True, slots=True) class _FieldStateWithVersor: """Adapter exposing ``versor_condition`` for SafetyContext. ``FieldState`` itself does not carry a precomputed ``versor_condition`` attribute; it is computed on demand from ``versor_condition(state.F)``. The SafetyCheck predicate for ``preserve_versor_closure`` reads ``ctx.field_state.versor_condition`` via ``getattr``. This adapter exposes the precomputed value so the predicate is runtime-checkable each turn. """ versor_condition: float def _hash_identity_manifold(manifold) -> str: """Deterministic SHA-256 of the load-bearing identity-manifold fields. ADR-0035 — feeds the ``no_identity_override`` predicate in :class:`SafetyCheck`. The runtime never mutates ``identity_manifold`` after composition, so before- and after-turn hashes are equal by construction; an unequal hash would indicate the predicate's exact failure mode. """ payload = { "value_axes": [ { "axis_id": axis.axis_id, "name": axis.name, "direction": list(axis.direction), "weight": axis.weight, } for axis in manifold.value_axes ], "boundary_ids": sorted(manifold.boundary_ids), "alignment_threshold": manifold.alignment_threshold, } blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") return hashlib.sha256(blob).hexdigest() def _surface_contains_hedge(surface: str, manifold) -> bool: """Detect whether the realized surface emitted a hedge phrase. Compares case-insensitively against the manifold's preferred hedge phrases (ADR-0028). False when surface is empty. Coarse but deterministic: the predicate downstream is observational, so occasional false negatives are surfaced as ``acknowledge_uncertainty`` violations in audit and corrected by refining hedge detection, not by silently passing. """ if not surface: return False prefs = getattr(manifold, "surface_preferences", None) if prefs is None: return False candidates: list[str] = [] for field_name in ( "preferred_hedge_strong", "preferred_hedge_soft", "preferred_qualifier", ): value = getattr(prefs, field_name, "") if value: candidates.append(value) for _, hedge in getattr(prefs, "axis_hedges", ()) or (): for sub in ("strong", "soft", "qualifier"): value = getattr(hedge, sub, "") if value: candidates.append(value) surface_fold = surface.casefold() return any(c.casefold() in surface_fold for c in candidates if c) def _make_trajectory_from_result(result, turn: int): from core.physics.reasoning import TrajectoryOperator operator = TrajectoryOperator() states = result.trajectory or (result.final_state,) frames = [ _StubBindingFrame( frame_id=f"t{turn}_s{i}", coherence_magnitude=_energy_scalar(getattr(fs, "energy", None)), region_ids=frozenset({str(getattr(fs, "node", 0))}), cycle_index=turn, ) for i, fs in enumerate(states) ] return operator.build(frames, trajectory_id=f"turn_{turn}") @dataclass(frozen=True, slots=True) class TurnAccrual: """The inline-realization outcome of one turn (Step B / E). ``kind`` is ``"realized"`` (a declarative fact was accrued into the held self), ``"determined"`` (a question was answered over realized knowledge), ``"estimated"`` (Step E — a converse query DETERMINE refused, for which a calibrated converse-guess exists), or ``"none"`` (nothing comprehensible to accrue/determine). The payload carries the typed result for introspection. B-1 records, does not surface; B-2 and E surface only when their flag is on. """ kind: str realized: Any = None # generate.realize.Realized | NotRealized | None determination: Any = None # generate.determine.Determined | Undetermined | None # Step E — the converse-guess candidate and its SERVE license, when ``kind`` is # ``"estimated"``. ``estimate`` is a ``ConverseEstimate``; ``license`` a # ``LicenseDecision`` (None if the predicate-class is absent from the ratified ledger). estimate: Any = None license: Any = None @dataclass(frozen=True, slots=True) class ChatResponse: surface: str proposition: Proposition articulation: ArticulationPlan articulation_surface: str dialogue_role: DialogueRole versor_condition: float output_language: str frame_pack: str walk_surface: str salience_top_k: int | None candidates_used: int | None vault_hits: int identity_score: IdentityScore | None character_profile: CharacterProfile flagged: bool recall_energy_class: str | None = None # ADR-0023 §2 — per-transition admissibility evidence and region # provenance flag. An empty tuple is the contract for "no # admissibility was checked this turn" (cold start, refusal, stub). admissibility_trace: tuple = () region_was_unconstrained: bool = True # ADR-0035 — verdicts surfaced from SafetyCheck and EthicsCheck. # ``None`` only on stub/refusal paths that bypass the turn loop. safety_verdict: object = None ethics_verdict: object = None # ADR-0039 — unified TurnVerdicts bundle carrying identity / safety # / ethics verdicts and the two remediation flags # (refusal_emitted, hedge_injected). Typed as ``object`` to avoid # coupling at module-resolution time; downcast at use site. verdicts: object = None # ADR-0048 / ADR-0050 / ADR-0052 — provenance tag for the surface's # grounding. One of: # "vault" — answer drawn from session vault evidence (main path). # "pack" — answer drawn from the ratified language pack # (cold-start DEFINITION/RECALL/COMPARISON on pack-known # lemmas — ADR-0048 / ADR-0050). # "teaching" — answer drawn from a reviewed teaching-chain corpus # (cold-start CAUSE/VERIFICATION — ADR-0052). # "none" — universal "insufficient grounding" disclosure on stub. # "deduction" — answer decided by the verified propositional # entailment engine (deduction-serve arc, Phase 1; # chat/deduction_surface.py — ADR-0256). # "curriculum" — answer decided from the ratified domain-chain # curriculum of the question's subject # (chat/curriculum_surface.py — ADR-0262). # Both are registered in core.epistemic_state.GroundingSource (the # Workbench-coupled closed Literal) and map to DECODED. That registration # is load-bearing rather than cosmetic: workbench/api.py's live chat route # builds a bare ChatRuntime(), so with deduction_serving_enabled ratified # ON these composers decide Workbench turns too. While the labels were # unregistered, the API's coercion floored them to "none" and recorded a # proved answer as ungrounded (pinned by # tests/test_workbench_deduction_provenance.py). Adding a grounding source # here means registering it there in the same change — the workbench-ui # enum-coverage test fails the build otherwise. # The string is preserved verbatim in TurnEvent for downstream audit. grounding_source: str = "none" # ADR-0071 (R4) — pre-decoration surface. ``surface`` is the # user-facing string AFTER seeded discourse-marker decoration; # ``pre_decoration_surface`` is the realizer's output BEFORE the # decoration step. The cognition pipeline reads this field to # compute ``trace_hash`` so register decoration cannot leak into # the truth path (ADR-0069 invariant C). Empty string ⇒ identical # to ``surface`` (no decoration applied this turn). pre_decoration_surface: str = "" # Phase 3 epistemic taxonomy — first-class state axes per turn. # Values are lower_snake_case strings matching core.epistemic_state # enum values so the field serializes stably without importing the # enum here. Defaults match TurnEvent defaults (undetermined / # unassessable) so pre-Phase-3 callers that omit these fields are # treated conservatively. epistemic_state: str = "undetermined" normative_clearance: str = "unassessable" # Comma-separated violated boundary/commitment IDs when normative # clearance is VIOLATED or SUPPRESSED; empty string otherwise. normative_detail: str = "" # ADR-0206 — Response Governance Bridge reach level for this turn. # Mirrors the TurnEvent field so callers (CLI, demos, tests) can read # the governed reach level from ChatResponse. Scaffold contract: # always "strict" (govern_response is STRICT-only until widening is # built). ``"strict"`` default preserves byte-identity for callers # that construct ChatResponse without this field. reach_level: str = "strict" # ADR-0072 (R5) — operator-visible register identity per turn. # Mirrors the TurnEvent fields so callers (CLI, demos, tests) can # read the register state from ChatResponse without re-parsing the # telemetry JSONL. ``""`` defaults preserve pre-R5 byte-identity # for callers that construct ChatResponse without these fields. register_id: str = "" register_variant_id: str = "" # ADR-0073d (L1.4) — operator-visible anchor-lens identity per turn. # Mirrors the TurnEvent fields so callers (CLI, demos, tests) can # read the lens state from ChatResponse without re-parsing the # telemetry JSONL. ``""`` defaults preserve pre-L1.4 byte-identity. anchor_lens_id: str = "" anchor_lens_mode_label: str = "" # ADR-0075 (C1) — realizer slot-type guard verdict. Mirrors the # TurnEvent fields so callers (CLI, demos, tests) can read the # guard state from ChatResponse without re-parsing the telemetry # JSONL. ``""`` defaults preserve pre-C1 byte-identity. realizer_guard_status: str = "" realizer_guard_rule: str = "" # ADR-0077 (R6) — register layering boundary surface. Carries the # composer output BEFORE any register transformation (substantive # or decorative). The cognition pipeline hashes this field for # ``trace_hash`` when present, preserving R5's load-bearing # invariant — substantive register transforms must not move # ``trace_hash``. Empty string ⇒ pre-R6 caller; pipeline falls # back to ``pre_decoration_surface`` (byte-identity preserved). register_canonical_surface: str = "" # ADR-0078 (Phase 1) — observational composer/graph atom # equivalence telemetry mirrored from TurnEvent. composer_graph_atom_status: str = "" composer_atom_set_hash: str = "" graph_atom_set_hash: str = "" composer_graph_atom_overlap_count: int = 0 # ADR-0088 Phase B (audit Finding 2, 2026-05-20) — alphabetic- # filtered walk tokens from the recall step. Populated only on # the main path; the stub / refusal paths leave this empty. # Consumed by ``CognitiveTurnPipeline`` when # ``RuntimeConfig.realizer_grounded_authority`` is True so the # proposition graph can be grounded before ``realize_semantic`` # is invoked. Empty tuple preserves pre-ADR-0088 byte-identity # for every caller that constructs ChatResponse without this # field. recalled_words: tuple[str, ...] = () # ADR-0024 Phase 2 — stable refusal reason value refusal_reason: str = "" dispatch_trace: DispatchTrace | None = None @dataclass(frozen=True, slots=True) class IdleTickResult: """Outcome of one ``idle_tick``. The proposal pass is PROPOSAL-ONLY learning, never ratification (HITL ratifies). The Step D consolidation pass is SESSION-memory learning: ``facts_consolidated`` derived facts were written back into the held self so the next ``determine`` reaches them directly — still never corpus mutation, never a proposal. The proposal-review sub-pass (``proposal_review``) is READ-ONLY: it surfaces pending comprehension-failure proposals for review and mutates nothing. """ candidates_contemplated: int proposals_created: int pending_proposals: int #: Step D — derived facts consolidated into the held self this tick (0 unless #: ``config.consolidate_determinations`` and the closure had a new layer to add). facts_consolidated: int = 0 #: ADR-0080 — SPECULATIVE frontier findings autonomously mined + persisted this tick #: (0 unless ``config.contemplate_frontier_during_idle`` AND a NOT-yet-mined frontier; #: a mined frontier converges to 0). Persisted as a reviewable artifact under #: ``/contemplation_runs/`` for later review tooling — NOT yet surfaced in #: the existing HITL queue / workbench contemplation reader (those scan #: ``/contemplation/runs``; wiring that discovery is a follow-up). Never ratified here. frontier_findings: int = 0 #: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``). #: Surfaces pending comprehension-failure proposals for review; mutates nothing. proposal_review: ProposalReviewIdleSummary | None = None #: PR-2 — count of new reviewable proposal artifacts emitted this tick from proof-gated #: derived CLOSE facts (member/subset + TRANSITIVE_PREDICATES). Only non-zero when #: ``config.review_derived_close_proposals``. Proposal-only, review-gated, SPECULATIVE; #: does not set did_work or trigger engine checkpoint (lives in teaching/proposals sink). derived_close_proposals_emitted: int = 0 #: Observational backpressure telemetry — backlog depth, headroom, inflow vs drain. #: None only when the tick was never called (not possible in normal use); always #: present on a completed tick. Never gates or refuses; see ADR-0161 for that. backpressure: BackpressureRecord | None = None class ChatRuntime: def __init__( self, pack_id: str | Sequence[str] | None = None, *, frame_pack: str | None = None, config: RuntimeConfig = DEFAULT_CONFIG, no_load_state: bool = False, engine_state_path: Any | None = None, ) -> None: if pack_id is not None or frame_pack is not None: pack_ids = (pack_id,) if isinstance(pack_id, str) else tuple(pack_id or config.input_packs) # Use dataclasses.replace so newer RuntimeConfig fields # (identity_pack, ethics_pack, forward_graph_constraint, # composed_surface, thread_anaphora, etc.) survive the # pack_id / frame_pack override path. The previous manual # reconstruction silently dropped any field not enumerated # here, which would let a caller like # ``ChatRuntime(pack_id="x", config=RuntimeConfig(composed_surface=True))`` # lose composed_surface without warning. from dataclasses import replace as _dc_replace resolved_config = _dc_replace( config, input_packs=pack_ids, frame_pack=frame_pack or config.frame_pack, ) else: resolved_config = config pack_ids = tuple(config.input_packs) self.config = resolved_config self._last_node_depths: dict | None = None # 3-lang PropGraph depth propagation (he/grc roots) to contemplate paths; set by pipeline manifests = [] manifolds = [] entries = [] for mounted_pack_id in pack_ids: manifest, manifold = load_pack(mounted_pack_id) manifests.append(manifest) manifolds.append(manifold) entries.extend(load_pack_entries(mounted_pack_id)) manifold = manifolds[0] if len(pack_ids) == 1 else load_mounted_packs(pack_ids) self._manifests = tuple(manifests) # Comb pass 2026-05-21 — precompute OOV-policy aggregates so # ``_apply_oov_policy`` doesn't rescan every manifest per OOV # token. Manifests are immutable post-construction, so a # one-time aggregate is safe and cuts the hot path from # O(packs × OOV) to O(OOV). self._all_manifests_fail_closed: bool = all( m.oov_policy is OOVPolicy.FAIL_CLOSED for m in self._manifests ) self._any_manifest_proposes_vocab: bool = any( m.oov_policy is OOVPolicy.PROPOSE_VOCAB_EXPANSION for m in self._manifests ) identity_pack_id = resolved_config.identity_pack or DEFAULT_IDENTITY_PACK identity_manifold = load_identity_manifold(identity_pack_id) self.safety_pack = load_safety_pack() ethics_pack_id = resolved_config.ethics_pack or _DEFAULT_ETHICS_PACK try: self.ethics_pack = load_ethics_pack(ethics_pack_id) except EthicsPackError: if ethics_pack_id == _DEFAULT_ETHICS_PACK: raise self.ethics_pack = load_ethics_pack(_DEFAULT_ETHICS_PACK) ethics_pack_id = _DEFAULT_ETHICS_PACK self.ethics_pack_id = ethics_pack_id # ADR-0068 / ADR-0069 — register pack load. None resolves to the # in-memory unregistered sentinel (structurally identical to # default_neutral_v1). Invalid ids fail-fast at runtime init, # not at first turn. At R2 the register is loaded but no # composer consumes it; byte-identity invariants pin this. if resolved_config.register_pack_id is None: self.register_pack: RegisterPack = RegisterPack.unregistered() else: self.register_pack = load_register_pack( resolved_config.register_pack_id ) self.register_pack_id = resolved_config.register_pack_id # ADR-0073b — anchor-lens load. ``None`` resolves to the # in-memory unanchored sentinel (structurally identical to # ``default_unanchored_v1``). Invalid ids fail-fast at # runtime init, not at first turn. At L1.2 the lens is # loaded and stored but no composer consumes it; the # ``anchor_lens_byte_identity_null_lift`` invariant pins this. if resolved_config.anchor_lens_id is None: self.anchor_lens: AnchorLens = AnchorLens.unanchored() else: self.anchor_lens = load_anchor_lens( resolved_config.anchor_lens_id ) self.anchor_lens_id = resolved_config.anchor_lens_id self.identity_manifold = type(identity_manifold)( value_axes=identity_manifold.value_axes, boundary_ids=( identity_manifold.boundary_ids | self.safety_pack.boundary_ids | self.ethics_pack.commitment_ids ), alignment_threshold=identity_manifold.alignment_threshold, surface_preferences=identity_manifold.surface_preferences, ) self.identity_pack_id = identity_pack_id persona_motor = PersonaMotor.identity() self._context = SessionContext( manifold, persona=persona_motor, vault_reproject_interval=resolved_config.vault_reproject_interval, ) self._frame_registry = FrameRegistry.from_pack(resolved_config.frame_pack, self._context.vocab) self._surface_by_fold = {e.surface.casefold(): e.surface for e in entries} self._surface_by_fold.update(_SEED_ALIASES) self._pos_by_surface = {e.surface: (e.pos or e.part_of_speech or "X") for e in entries} self.exertion_meter = ExertionMeter(capacity_ceiling=128.0) self.drive_gradients = tuple(GradientField(axis=axis, magnitude=0.75) for axis in self.identity_manifold.value_axes) self._drive_map = DriveGradientMap(gradients=self.drive_gradients) self.character_profile = CharacterProfile.from_manifold( self.identity_manifold, drive_summaries={g.axis.name: g.magnitude for g in self.drive_gradients}, fatigue_index=0.0, ) self._identity_check = IdentityCheck() self.safety_check = SafetyCheck() self.ethics_check = EthicsCheck() self._identity_manifold_hash: str = _hash_identity_manifold( self.identity_manifold, ) self._last_refusal_was_typed: bool = True # ADR-0246 §3.4/§3.5 — lawful-only session identity-path ledger # (observe-only; advanced per-turn only when identity_action_surface + # identity_wave_gate are both on). Instance lifetime IS the §3.5 # session boundary: a fresh runtime starts from None → hard break. self._identity_path_ledger = None self.turn_log: List[TurnEvent] = [] from chat.thread_context import ThreadContext self.thread_context = ThreadContext() self._telemetry_sink: TurnEventSink | None = None self._telemetry_include_content: bool = False self._discovery_sink: DiscoveryCandidateSink | None = None self._oov_sink: Any = None self._contemplate_discoveries: bool = False self._correction_pass = CorrectionPass() self._last_valence: float = 0.0 # Phase 3 — most-recent plan-contemplation findings (tuple of # SPECULATIVE ``ContemplationFinding`` records). Reset to ``()`` # on every turn; populated only when ``config.discourse_contemplation`` # is True AND the planner actually engaged on the turn. Exposed # via the ``last_plan_findings`` property below. self._last_plan_findings: tuple[Any, ...] = () # Phase 4 — most-recent plan-articulation metrics (PlanMetrics). # Reset to ``None`` between turns. Populated under the same # gating discipline as ``_last_plan_findings``: requires # ``config.discourse_contemplation`` + an engaged planner. self._last_plan_metrics: Any | None = None # Phase 5 — articulation-observation sink (per-turn JSONL stream # consumed by the offline ``mine_articulation_observations`` # miner). Attached via ``attach_articulation_sink``; ``None`` # by default so the runtime emits nothing until an operator # opts in. Behaviour mirrors ``attach_telemetry_sink``: # append-only, fail-fast on sink errors, deterministic JSONL. self._articulation_sink: Any | None = None self._articulation_turn_counter: int = 0 # W-013 — last classified intent, updated each turn for /explain REPL use. self._last_intent: Any | None = None self._last_input_text: str = "" # Step B (inline realization) — the last turn's accrual outcome (what the turn # realized into the held self, or determined over it). Introspectable; the # surface contract is unchanged (slice B-1 records, does not surface). self._last_turn_accrual: TurnAccrual | None = None self._relational_pack_lemmas: frozenset[str] | None = None self._engine_state_store: EngineStateStore | None = ( None if no_load_state else EngineStateStore(engine_state_path) ) self._recognizer_registry: RecognizerRegistry = RecognizerRegistry() # Memoized frontier mine (ADR-0080 idle pass): the frontier reports are static within # a session, so a converged life mines once instead of re-reading every idle beat. A # reboot re-mines (a fresh runtime), picking up any frontier change across downtime. self._frontier_run_cache: Any = None self._turn_count: int = 0 self._pending_candidates: list[DiscoveryCandidate] = [] self._pending_recognizer_examples: list[ tuple[tuple[str, ...], FeatureBundle] ] = [] # W-024 / ADR-0158 — reboot event JSONL line buffered here when a # checkpoint is loaded, flushed to the sink on attach_telemetry_sink. # None means no reboot was detected this session. self._pending_reboot_payload: str | None = None # Audit-ledger R7 — when a CognitiveTurnPipeline owns this turn's sink # emission, the turn event is staged as an INDEX into ``turn_log`` and # emitted at the pipeline's serve boundary, after the surface and # trace_hash back-stamps land. Opt-in (M3): a runtime used directly # emits inline exactly as it did before R7. self._deferred_turn_emission: bool = False self._staged_turn_index: int | None = None # L11 — the engine's content-derived identity (who am I), and the # identity stamped in the loaded checkpoint (the lineage parent for the # next checkpoint). ``_loaded_engine_identity`` stays "" at genesis. # ADR-0220: identity is the ratified PACKS only — the build revision is # provenance (manifest written_at_revision), not an identity input. pack_ids = { "identity": self.identity_pack_id, "safety": DEFAULT_SAFETY_PACK, "ethics": self.ethics_pack_id, "register": self.register_pack_id or DEFAULT_REGISTER_PACK, "anchor_lens": self.anchor_lens_id or DEFAULT_ANCHOR_LENS, } self._engine_identity: str = compute_engine_identity(pack_ids) self._loaded_engine_identity: str = "" # CL — the persistent reviewed-learning proposal log. ``idle_tick()`` # advances it during idle (proposal-only); it lives alongside the engine # state so the learning backlog survives reboot. None for no_load_state # (ephemeral runtimes don't accumulate a learning lineage). self._proposal_log: Any | None = None if self._engine_state_store is not None: from teaching.proposals import ProposalLog self._proposal_log = ProposalLog( path=self._engine_state_store.path / "proposals.jsonl" ) # L11 — set True on reboot when the stamped checkpoint identity differs # from the recomputed identity (the ratified substrate changed while down). self.identity_continuity_break: bool = False if self._engine_state_store is not None and self._engine_state_store.exists(): self._load_engine_state() def _load_engine_state(self) -> None: store = self._engine_state_store if store is None: return # Schema-version compatibility gates the whole load: load_manifest() # refuses (raises) a checkpoint written by newer code BEFORE we read any # recognizers/candidates (L10 step-2 migration discipline). manifest = store.load_manifest() or {} recognizers = store.load_recognizers() self._recognizer_registry = RecognizerRegistry.from_recognizers(recognizers) self._pending_candidates = store.load_discovery_candidates() self._turn_count = int(manifest.get("turn_count", 0)) # L11 / ADR-0220 — the identity this checkpoint was written under becomes # the lineage parent of the next checkpoint. Reconcile it against the # identity we recomputed at boot (packs-only). A genuine ratified-PACK # divergence is a different identity (refuse under strict / warn+flag); a # legacy (code_revision-folded) stamp whose packs verify identical is the # SAME identity under an old stamp scheme (migrate + re-stamp, no break); # a build-revision-only change is not a divergence at all (packs-only # hash is rev-independent → MATCH). reconcile_loaded_identity is the # single source of truth shared with the workbench continuity reader. self._loaded_engine_identity = str(manifest.get("engine_identity", "")) if self._loaded_engine_identity: # A malformed identity_scheme (non-int) falls back to the legacy # scheme — the verifying migration then reconciles it safely rather # than crashing the load. A null/absent revision becomes "" so the # legacy branch treats it as unverifiable (conservative DIVERGED). try: stored_scheme = int(manifest.get("identity_scheme", 1)) except (TypeError, ValueError): stored_scheme = 1 reconciliation = reconcile_loaded_identity( self.config, self._engine_identity, stored_identity=self._loaded_engine_identity, stored_scheme=stored_scheme, stored_revision=str(manifest.get("written_at_revision") or ""), ) if reconciliation is IdentityReconciliation.DIVERGED: self.identity_continuity_break = True message = ( "engine identity continuity break: checkpoint was written under " f"{self._loaded_engine_identity[:12]}… but this build computes " f"{self._engine_identity[:12]}… — the ratified identity substrate " "(packs) changed while the engine was down. Resuming would carry " "the lived state into a different identity." ) if self.config.strict_identity_continuity: from core.engine_identity import IdentityContinuityError raise IdentityContinuityError(message) warnings.warn(message, RuntimeWarning, stacklevel=2) elif reconciliation is IdentityReconciliation.MIGRATED: # Same identity, legacy stamp scheme — resume and migrate the # lineage parent to the packs-only hash so the chain stays # coherent (the next checkpoint re-stamps under scheme 2). warnings.warn( "migrating engine-identity stamp scheme (1→2): re-stamping the " "packs-only identity. Same identity — the build revision is " "provenance (written_at_revision), not identity.", RuntimeWarning, stacklevel=2, ) self._loaded_engine_identity = self._engine_identity # IdentityReconciliation.MATCH → clean same-identity resume, no-op. # Shape B+ (schema v2): restore the lived session state into the live # context so a reboot resumes the SAME life (field/vault/anchor/graph/ # referents/dialogue). Opt-in (config.persist_session_state); None for a # v1 checkpoint -> fresh session (the historical Shape B behavior), so old # checkpoints stay loadable. if self.config.persist_session_state and self._context is not None: session_snapshot = store.load_session_state() if session_snapshot is not None: self._context.restore(session_snapshot) # W-024 / ADR-0158 — buffer reboot event for emission when sink attaches. from engine_state import _git_revision self._pending_reboot_payload = format_reboot_event_jsonl( restored_turn_count=self._turn_count, stored_revision=str(manifest.get("written_at_revision", "unknown")), current_revision=_git_revision(), recognizers_count=len(recognizers), candidates_count=len(self._pending_candidates), ) if self.config.auto_proposal_enabled and self._pending_candidates: _auto_propose_from_candidates(self._pending_candidates) def checkpoint_engine_state(self) -> None: store = self._engine_state_store if store is None: return if ( self.config.recognition_grounded_graph and self._pending_recognizer_examples ): recognizer = derive_recognizer(tuple(self._pending_recognizer_examples)) self._recognizer_registry.register(recognizer) self._pending_recognizer_examples.clear() candidates_to_save = self._pending_candidates if self.config.auto_contemplate and candidates_to_save: # 3-lang depth propagation contract (AC5 / review): # _last_node_depths is written by CognitiveTurnPipeline after PropGraph construction # (from resolver + build_node_depths on enriched GraphNodes). It is forwarded here # as depth= into teaching.contemplation.contemplate so that framing findings and # proposed_chain carry depth_roots for he/grc. Same contract used for runtime # contemplate and candidate paths. See also core/cognition/result.py (node_depths / # graph_anti_unify on result) + pipeline.py. from teaching.contemplation import contemplate vault_probe = _vault_probe_for_context(self._context) if self._context else None depth = getattr(self, '_last_node_depths', None) candidates_to_save = [ contemplate(c, vault_probe=vault_probe, depth=depth) for c in candidates_to_save ] # ADR-0219 — generation-dir atomic checkpoint. All files are written # into a fresh gen-NNNN/ directory; the commit is the atomic pointer # swap in commit_generation. A kill before the swap leaves the prior # committed generation intact. The manifest is the last file written # into the gen dir (audit convention); the pointer swap is the true # commit boundary. gen_num, gen_dir = store.begin_generation() gen_store = EngineStateStore(gen_dir) gen_store.save_recognizers(self._recognizer_registry.all()) gen_store.save_discovery_candidates(candidates_to_save) # Shape B+ (schema v2): persist the lived session state (field, vault, # anchor, graph, referents, dialogue) BEFORE the manifest. # Opt-in (config.persist_session_state): off by default. if self._context is not None and self.config.persist_session_state: gen_store.save_session_state(self._context.snapshot()) # L11 — stamp the engine's identity and its lineage parent. gen_store.save_manifest( self._turn_count, engine_identity=self._engine_identity, parent_engine_identity=self._loaded_engine_identity, identity_scheme=ENGINE_IDENTITY_SCHEME, ) store.commit_generation(gen_num) self._loaded_engine_identity = self._engine_identity def _count_pending_proposals(self) -> int: if self._proposal_log is None: return 0 return sum( 1 for entry in self._proposal_log.current_state().values() if entry.get("state") == "pending" ) def idle_tick(self) -> "IdleTickResult": """Advance learning during idle (NO user turn). Two disjoint passes: 1. PROPOSAL pass (the reviewed-learning flywheel): turn the pending discovery backlog into reviewable teaching proposals. Contemplate each pending candidate (enrichment) and run the replay-gated ``propose_from_candidate``, which leaves a PROPOSAL-ONLY ``pending`` entry in the persistent proposal log. An idle tick NEVER ratifies — ratification (appending to the corpus) stays HITL via ``teaching/review`` (CLAUDE.md teaching safety). The tick only *proposes*; the reviewed loop is not bypassed or duplicated. 2. CONSOLIDATION pass (Step D — CLOSE): the loop learns from *determined* facts. Run one semi-naive layer of the member/subset deductive closure over the held self (``generate.determine.consolidate_once``); each soundly-derived, proof_chain-verified fact is written back as a SPECULATIVE realized record so the next ``determine`` reaches it directly. SESSION memory (immediate, allowed) — NOT corpus mutation, NOT a proposal; the HITL path is untouched. Gated by ``config.consolidate_determinations``. The closure converges (a saturated tick consolidates nothing — ``at_fixed_point``). The proposal log, the candidate backlog, and (with ``persist_session_state``) the consolidated facts all live in the engine-state dir, so this learning progress persists across reboot (CL-2). """ contemplated_count = 0 created = 0 facts_consolidated = 0 did_work = False # 1. Proposal pass — unchanged behavior, runs only with a log + backlog. if self._proposal_log is not None and self._pending_candidates: from teaching.contemplation import contemplate from teaching.proposals import ( ProposalError, TeachingChainProposal, _current_revision, propose_from_candidate, ) from teaching.source import ProposalSource vault_probe = ( _vault_probe_for_context(self._context) if self._context else None ) # 3-lang depth propagation contract (see checkpoint_engine_state) depth = getattr(self, '_last_node_depths', None) contemplated = [ contemplate(candidate, vault_probe=vault_probe, depth=depth) for candidate in self._pending_candidates ] contemplated_count = len(contemplated) for candidate in contemplated: source = ProposalSource( kind="contemplation", source_id=candidate.candidate_id, emitted_at_revision=_current_revision(), ) try: result = propose_from_candidate( candidate, log=self._proposal_log, source=source ) except ProposalError: continue if isinstance(result, TeachingChainProposal): created += 1 did_work = True # 2. Consolidation pass (Step D) — runs independently of the backlog. if self.config.consolidate_determinations and self._context is not None: from generate.determine import consolidate_once facts_consolidated = consolidate_once(self._context).consolidated did_work = True derived_close_proposals_emitted = 0 # PR-2 derived CLOSE proposal bridge — after consolidation so newly derived # proof-gated facts (member/subset + relational transitive) can surface as # reviewable proposal artifacts. Best-effort, proposal-only, review-gated. # Never sets did_work (proposals live outside engine_state; no unrelated checkpoint). # Gated by explicit config flag (default off). if getattr(self.config, "review_derived_close_proposals", False): try: from generate.determine.derived_close_proposals import ( emit_derived_close_proposals, ) res = emit_derived_close_proposals(self._context) derived_close_proposals_emitted = res.get("emitted", 0) except Exception as exc: # noqa: BLE001 — proposal emission must not crash idle warnings.warn( f"derived close proposal emission skipped: {exc!r}", RuntimeWarning, stacklevel=2, ) derived_close_proposals_emitted = 0 # 2b. Frontier-contemplation pass (ADR-0080 into the idle loop) — autonomously MINE # the frontier-compare reports into persisted SPECULATIVE reviewable findings, so # the always-on life proposes its OWN frontier with no user turn. Idempotent per # frontier (an already-mined frontier is NOT re-persisted → the life converges, no # churn); SPECULATIVE-only (ADR-0080: never COHERENT/ratified → the HITL path is # untouched). Persists per-life under the engine-state dir. # # This is an OPTIONAL background pass on an indefinite-uptime life: a malformed # frontier report (parse error) or a transient filesystem fault must DEGRADE to # "no findings this beat", never crash the continuous loop. So the whole pass is # best-effort (the exit-checkpoint boundary idiom) — on failure it warns, leaves # did_work untouched, and a later beat retries (the run-cache stays unset on a # mining failure; a write failure simply re-attempts while the target is absent). frontier_findings = 0 if ( self.config.contemplate_frontier_during_idle and self._engine_state_store is not None ): try: from core.contemplation.runner import ( run_contemplation, write_contemplation_run, ) # Mine once per session (the frontier is static within a run); a reboot re-mines. if self._frontier_run_cache is None: self._frontier_run_cache = run_contemplation() run = self._frontier_run_cache target = ( self._engine_state_store.path / "contemplation_runs" / f"idle_{run.substrate_hash}.json" ) if run.findings and not target.exists(): write_contemplation_run(run, target) frontier_findings = len(run.findings) did_work = True except Exception as exc: # noqa: BLE001 — optional idle pass is best-effort # An optional frontier mine must never crash the always-on life. Degrade to # "no findings this beat" and surface the cause; the loop, its closure # evidence, and the rest of idle_tick continue uninterrupted. warnings.warn( "idle frontier-contemplation pass skipped " f"(degraded to 0 findings): {exc!r}", RuntimeWarning, stacklevel=2, ) # 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure # proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an # artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies # or mounts. A reporter failure is CAPTURED, not propagated, so it cannot corrupt the # tick. Gated off by default. proposal_review = None if self.config.review_pending_proposals: try: proposal_review = idle_summary() except Exception as exc: # noqa: BLE001 — a reporter failure must not corrupt the tick proposal_review = ProposalReviewIdleSummary( safe=False, total=0, review_needed=0, malformed=0, by_family=(), errors=(f"proposal_review_failed:{type(exc).__name__}",), ) # Persist the advanced state once (backlog + lineage +, with # persist_session_state, the consolidated facts). Skipped on a no-op tick so an # idle engine with nothing to learn does not churn the checkpoint. if did_work: self.checkpoint_engine_state() # Backpressure telemetry — observational only, never gates, never alters # trace_hash. Built from counts already computed above; the cap is resolved # from the env exactly as ADR-0161 does in teaching.proposals. pending_count = self._count_pending_proposals() bp_record = build_backpressure_record( pending_proposals=pending_count, candidate_backlog=len(self._pending_candidates), contemplated_this_tick=contemplated_count, created_this_tick=created, did_work=did_work, ) # Append-only telemetry line in the engine-state dir so backpressure # history survives reboot. Best-effort: a write failure must not crash # the continuous life. if self._engine_state_store is not None: try: telem_path = self._engine_state_store.path / "idle_telemetry.jsonl" line = json.dumps( { "pending_proposals": bp_record.pending_proposals, "candidate_backlog": bp_record.candidate_backlog, "cap": bp_record.cap, "headroom": bp_record.headroom, "contemplated_this_tick": bp_record.contemplated_this_tick, "created_this_tick": bp_record.created_this_tick, "at_fixed_point": bp_record.at_fixed_point, "did_work": bp_record.did_work, }, separators=(",", ":"), ) with open(telem_path, "a", encoding="utf-8") as fh: fh.write(line + "\n") except Exception: # noqa: BLE001 — telemetry write must never crash the life pass return IdleTickResult( candidates_contemplated=contemplated_count, proposals_created=created, pending_proposals=pending_count, facts_consolidated=facts_consolidated, frontier_findings=frontier_findings, proposal_review=proposal_review, derived_close_proposals_emitted=derived_close_proposals_emitted, backpressure=bp_record, ) def record_recognition_example( self, tokens: tuple[str, ...], bundle: FeatureBundle, ) -> None: self._pending_recognizer_examples.append((tuple(tokens), bundle)) def finalize_turn_trace_hash(self, trace_hash: str) -> None: """ADR-0153 (W-020a) — back-stamp the canonical trace_hash. Called by ``CognitiveTurnPipeline.process`` after ``compute_trace_hash`` produces the turn's canonical SHA-256. Stamps the trace_hash onto the most recent TurnEvent and any DiscoveryCandidate emitted during this turn (i.e., the unstamped tail of ``_pending_candidates``). The stamped candidates are held in memory and persisted at the next ``checkpoint_engine_state`` call (ADR-0219: only ``checkpoint_engine_state`` writes to the checkpoint dir, so the trace-hash back-stamp is always part of a coherent committed generation rather than a mid-turn partial write). No-op when ``trace_hash`` is empty (pre-pipeline call sites, refusal stub path). Idempotent: stamping a tail whose ``source_turn_trace`` is already non-empty halts the back-walk. """ if not trace_hash: return from dataclasses import replace if self.turn_log: last_event = self.turn_log[-1] if not last_event.trace_hash: self.turn_log[-1] = replace(last_event, trace_hash=trace_hash) for idx in range(len(self._pending_candidates) - 1, -1, -1): cand = self._pending_candidates[idx] if cand.source_turn_trace: break self._pending_candidates[idx] = replace( cand, source_turn_trace=trace_hash ) def finalize_turn_surface( self, surface: str, articulation_surface: str | None = None, ) -> None: """Back-stamp the pipeline's final *served* surface onto the turn. Weekly-audit T13 (2026-07-22): ``ChatRuntime.chat`` seals a ``TurnEvent`` with the runtime-owned surface, but ``CognitiveTurnPipeline`` applies the Shadow-Coherence / logos-morph surface authority AFTER ``chat`` returns. The telemetry record therefore captured a surface the user never saw — ``TurnEvent.surface != pipeline.run().surface`` — breaking the ``warmed_session_consistency`` telemetry-consistency invariant and audit/replay trust (Absolute Provenance). The pipeline owns the final surface decision, so — exactly like ``finalize_turn_trace_hash`` back-stamps the pipeline-owned ``trace_hash`` — it back-stamps the resolved surface here after ``resolve_surface`` + logos-morph. Mirrors that method's contract: operates on the most recent ``TurnEvent`` (main or refusal-stub path), no-ops when unchanged or when no turn exists. The opt-in external telemetry sink receives the POST-override event: the pipeline stages its turn event and flushes it at the serve boundary, after this back-stamp and ``finalize_turn_trace_hash`` have both landed (audit-ledger R7, closed — see ``_emit_turn_event``). """ if not self.turn_log: return from dataclasses import replace last_event = self.turn_log[-1] updates: dict[str, str] = {} if surface != last_event.surface: updates["surface"] = surface if ( articulation_surface is not None and articulation_surface != last_event.articulation_surface ): updates["articulation_surface"] = articulation_surface if updates: self.turn_log[-1] = replace(last_event, **updates) def first_admitted_recognizer(self): if not self.config.recognition_grounded_graph: return None return self._recognizer_registry.first_admitted() def _checkpointed_response(self, response: ChatResponse) -> ChatResponse: self._turn_count += 1 # Step B — inline realization, BEFORE the checkpoint so an accrued fact is in # the snapshot this turn (survives reboot with persist_session_state). Gated; # off by default. The surface contract is unchanged (the outcome is recorded, # not surfaced). if self.config.accrue_realized_knowledge: self._accrue_in_turn(self._last_input_text) response = self._maybe_surface_determination(response) self.checkpoint_engine_state() return response def _maybe_surface_determination(self, response: ChatResponse) -> ChatResponse: """Step B-2 / E — select the user-facing surface from the turn's accrual. B-2: when the turn DETERMINED an answer over realized knowledge, select the rendered determination as ``surface`` (the realizer's ``articulation_surface`` is retained as evidence). An ``Undetermined`` turn keeps the default surface. E: when the turn is ``estimated`` (a refused converse query with a calibrated guess) AND ``estimation_enabled``, route the guess through the ADR-0206 bridge — ``govern_response`` widens to APPROXIMATE iff the predicate-class holds a genuine SERVE license, and ``shape_surface`` DISCLOSES it as ``[approximate] …``. An unlicensed class stays STRICT (the surface is unchanged — the honest refusal). Off-flag turns never reach here. See ``docs/specs/runtime_contracts.md``. """ accrual = self._last_turn_accrual if accrual is None: return response if accrual.kind == "determined": from generate.determine import Determined, render_determination if not isinstance(accrual.determination, Determined): return response # Undetermined → keep the default surface return replace(response, surface=render_determination(accrual.determination)) if accrual.kind == "estimated" and self.config.estimation_enabled: return self._surface_estimate(response, accrual) return response def _surface_estimate(self, response: ChatResponse, accrual: "TurnAccrual") -> ChatResponse: """Surface a licensed converse-guess as a DISCLOSED ``[approximate]`` estimate. The license gates the widening (``govern_response`` returns STRICT for an unlicensed class → surface unchanged); ``shape_surface`` guarantees the disclosure prefix because a converse guess is ``UNVERIFIED_POSSIBLE``, never in APPROXIMATE's admissible (fully-grounded) set. So a wrong estimate is always a DISCLOSED wrong — wrong=0 (silent) is preserved. """ from core.epistemic_state import EpistemicState from core.response_governance import ReachLevel, govern_response, shape_surface from generate.determine import ConverseEstimate, render_estimate estimate, license_decision = accrual.estimate, accrual.license if not isinstance(estimate, ConverseEstimate): return response policy = govern_response( epistemic_state=EpistemicState.UNVERIFIED_POSSIBLE, license_decision=license_decision, ) if policy.level is ReachLevel.STRICT: return response # unlicensed → no widening, honest refusal stands disclosed = shape_surface( policy, committed_surface=response.surface, decode_state=EpistemicState.UNVERIFIED_POSSIBLE, disclosed_alternative=render_estimate(estimate), ) return replace(response, surface=disclosed, reach_level=policy.level.value) def last_turn_accrual(self) -> TurnAccrual | None: """The most recent turn's inline-realization outcome (Step B), or None when accrual is off or did not complete. Introspection only — never surfaced.""" return self._last_turn_accrual def _accrue_in_turn(self, text: str) -> None: """Inline realization (Step B): a comprehensible declarative turn accrues a realized fact into the held self (session vault, SPECULATIVE / as-told); a comprehensible question turn is determined over realized knowledge. Records the typed outcome on ``self._last_turn_accrual``. Never raises into the turn — accrual is ADDITIVE, so any failure is a clean no-op (the turn's response is untouched). This is SESSION memory (immediate), NOT ratified learning: it proposes nothing and leaves the teaching/review HITL path untouched. Realization writes go through ``generate.realize`` (the INV-21 allowed vault writer); DETERMINE and the readers are total (typed results, no raises), so the broad guard is a defensive backstop, not expected control flow. """ self._last_turn_accrual = None if self._context is None or not text or not text.strip(): return try: from generate.determine import determine from generate.meaning_graph.reader import Comprehension, comprehend from generate.meaning_graph.relational import comprehend_relational from generate.realize import realize_comprehension if self._relational_pack_lemmas is None: from generate.meaning_graph.relational import load_relational_pack_lemmas self._relational_pack_lemmas = load_relational_pack_lemmas() readings = ( comprehend(text), comprehend_relational(text, self._relational_pack_lemmas), ) comprehensions = [c for c in readings if isinstance(c, Comprehension)] # A question turn (query-bearing) is DETERMINED over realized knowledge. for c in comprehensions: if c.queries: determination = determine(c, self._context) self._last_turn_accrual = self._accrue_estimate_if_refused( c, determination ) return # A declarative turn (a single told fact) is REALIZED into the held self. for c in comprehensions: if not c.queries and c.meaning_graph.relations: self._last_turn_accrual = TurnAccrual( kind="realized", realized=realize_comprehension(c, self._context) ) return self._last_turn_accrual = TurnAccrual(kind="none") except Exception: # additive: accrual must never crash a turn # noqa: BLE001 self._last_turn_accrual = None def _accrue_estimate_if_refused(self, comprehension: Any, determination: Any) -> "TurnAccrual": """Step E: turn a REFUSED converse query into an ``estimated`` accrual. When DETERMINE refused (``Undetermined``) a single non-negated binary query whose converse was told (``p(a,b)`` realized, ``p(b,a)`` asked), produce the calibrated converse-guess + its SERVE license. Off-flag (or any non-converse refusal) returns the plain ``determined`` accrual unchanged — nothing widens. The license is only *attached* here; the surface decision (and the disclosure) is the bridge's, in ``_maybe_surface_determination``. """ from generate.determine import Undetermined if not (self.config.estimation_enabled and isinstance(determination, Undetermined)): return TurnAccrual(kind="determined", determination=determination) queries = getattr(comprehension, "queries", ()) if len(queries) != 1: return TurnAccrual(kind="determined", determination=determination) query = queries[0] if getattr(query, "negated", False) or len(getattr(query, "arguments", ())) != 2: return TurnAccrual(kind="determined", determination=determination) from generate.determine import estimate_converse, serve_license subject, target = query.arguments[0], query.arguments[1] estimate = estimate_converse(self._context, query.predicate, subject, target) if estimate is None: # no told converse to generalize from → plain refusal return TurnAccrual(kind="determined", determination=determination) return TurnAccrual( kind="estimated", determination=determination, estimate=estimate, license=serve_license(query.predicate), ) @property def session(self) -> SessionContext: return self._context @property def last_plan_findings(self) -> tuple[Any, ...]: """Phase 3 — most-recent plan-contemplation findings. Tuple of ``core.contemplation.schema.ContemplationFinding`` records (always SPECULATIVE per ADR-0080). Populated only when ``config.discourse_contemplation`` is True and the discourse planner engaged on the turn — empty tuple otherwise. Read-only observation surface; the runtime itself never acts on findings, the offline contemplation miner does. """ return self._last_plan_findings @property def last_plan_metrics(self) -> Any | None: """Phase 4 — most-recent plan articulation metrics. ``core.contemplation.plan_metrics.PlanMetrics`` instance when the discourse planner engaged on the most recent turn AND ``config.discourse_contemplation`` is True; ``None`` otherwise. Read-only quantitative companion to ``last_plan_findings`` (which carries the qualitative SPECULATIVE concerns). Designed for downstream aggregation — Phase 5's offline contemplation miner streams these across turns to score plan-quality patterns the runtime never tries to act on alone. """ return self._last_plan_metrics def explain_last_turn(self) -> str: """Return a canonical natural-language restatement of the last turn (W-013). Feeds the last classified intent through ``core.cognition.explain``'s dispatch table and returns the resulting canonical prompt string. This is the ``/explain`` REPL command's backing method. Returns an empty string when no turn has been processed yet or when the intent could not be classified (UNKNOWN tag). """ from core.cognition.explain import explain_from_intent return explain_from_intent( self._last_intent, correction_text=self._last_input_text, ) def attach_telemetry_sink( self, sink: TurnEventSink | None, *, include_content: bool = False, ) -> None: """ADR-0040 — attach a structured-logging sink. A buffered REBOOT payload is flushed here; a staged TURN event (:meth:`begin_deferred_turn_emission`) deliberately is NOT. The asymmetry is load-bearing (audit-ledger R7, mechanism M2): the reboot payload is already final when it is buffered, whereas a staged turn event is mid-flight and may not yet carry the pipeline's back-stamped surface and ``trace_hash``. Flushing one on attach would emit the pre-override event and silently restore the defect R7 fixed. """ self._telemetry_sink = sink self._telemetry_include_content = bool(include_content) # W-024 / ADR-0158 — flush buffered reboot event now that sink is live. if sink is not None and self._pending_reboot_payload is not None: sink.emit(self._pending_reboot_payload) self._pending_reboot_payload = None def attach_articulation_sink(self, sink: Any | None) -> None: """Phase 5 — attach a sink for per-turn articulation observations. ``sink`` must satisfy ``chat.articulation_telemetry.ArticulationObservationSink`` (any object with ``def emit(line: str) -> None``). Pass ``None`` to detach. The sink receives one canonical JSONL line per turn that engages the discourse planner AND has ``config.discourse_contemplation == True``; non-engaged turns emit nothing. Lines are byte-identical for byte-equal plans — the offline miner relies on this for deterministic aggregation. """ self._articulation_sink = sink def attach_oov_sink(self, sink: Any) -> None: """Phase 2.3 — attach an OOV candidate sink.""" self._oov_sink = sink def attach_discovery_sink( self, sink: DiscoveryCandidateSink | None, ) -> None: """ADR-0055 Phase B — attach a DiscoveryCandidate sink.""" self._discovery_sink = sink def attach_contemplation(self, *, enabled: bool = True) -> None: """ADR-0056 Phase C1 — opt-in inline contemplation.""" self._contemplate_discoveries = bool(enabled) def _push_thread_summary( self, *, turn_event: TurnEvent, intent_tag: Any, intent_subject: str | None, grounding_source: str | None, surface: str | None = None, ) -> None: """P3.1 — append one TurnSummary to the bounded session-thread context.""" from chat.thread_context import TurnSummary turn_index = len(self.turn_log) - 1 if intent_tag is not None and hasattr(intent_tag, "name"): intent_name = str(intent_tag.name).lower() else: intent_name = "" subject = (intent_subject or "").strip().lower() source = (grounding_source or "none").lower() chain_id: str | None = None corpus_id: str | None = None if source == "teaching" and subject and intent_name in {"cause", "verification"}: from chat.teaching_grounding import _all_chains_index chain = _all_chains_index().get((subject, intent_name)) if chain is not None: chain_id = chain.chain_id corpus_id = chain.corpus_id _ = surface self.thread_context.push( TurnSummary( turn_index=turn_index, intent_tag_name=intent_name, subject=subject, grounding_source=source, chain_id=chain_id, corpus_id=corpus_id, ) ) def _emit_oov_candidate( self, *, turn_event: TurnEvent, intent_tag: Any, token: str | None, ) -> None: """P2.3 — emit one OOVCandidate per OOV-grounded turn.""" sink = self._oov_sink if sink is None or not token: return from teaching.oov_sink import ( OOVCandidate, format_oov_candidate_jsonl, hash_oov_candidate_id, ) from generate.intent import IntentTag if intent_tag is None or not isinstance(intent_tag, IntentTag): return intent_name = intent_tag.name.lower() trace_hash = getattr(turn_event, "trace_hash", "") or "" boundary_clean = ( not getattr(turn_event, "refusal_emitted", False) and not getattr(turn_event, "hedge_injected", False) ) cleaned_token = (token or "").strip().lower() if not cleaned_token: return candidate_id = hash_oov_candidate_id(cleaned_token, intent_name, trace_hash) candidate = OOVCandidate( candidate_id=candidate_id, token=cleaned_token, intent=intent_name, # type: ignore[arg-type] trigger="unresolved_subject", source_turn_trace=trace_hash, boundary_clean=boundary_clean, ) sink.emit(format_oov_candidate_jsonl(candidate)) def _emit_discovery_candidates( self, *, turn_event: TurnEvent, intent_tag: Any, intent_subject: str | None, grounding_source: str | None, ) -> None: candidates = extract_discovery_candidates( turn_event, intent_tag, intent_subject, grounding_source=grounding_source, ) if self._contemplate_discoveries and candidates: from teaching.contemplation import contemplate vault_probe = ( _build_vault_probe(self._context.vault, self._context.vocab) if self.config.vault_probe_discoveries else None ) # 3-lang depth propagation contract (see checkpoint_engine_state) depth = getattr(self, '_last_node_depths', None) candidates = tuple( contemplate(c, vault_probe=vault_probe, depth=depth) for c in candidates ) self._pending_candidates.extend(candidates) sink = self._discovery_sink if sink is None: return for candidate in candidates: sink.emit(format_candidate_jsonl(candidate)) def _format_turn_event(self, event: TurnEvent) -> str: return format_turn_event_jsonl( event, safety_pack_id=self.safety_pack.pack_id, ethics_pack_id=self.ethics_pack_id, identity_pack_id=self.identity_pack_id, include_content=self._telemetry_include_content, ) def _emit_turn_event(self, event: TurnEvent) -> None: """Emit one turn event to the sink, or STAGE it for the serve boundary. Audit-ledger R7. Emitting inline is correct whenever this runtime owns the final surface — which it does for every direct ``ChatRuntime.chat`` caller, including the eval and demo paths. When a :class:`CognitiveTurnPipeline` wraps the runtime it does not: the pipeline applies the surface authority and computes the canonical ``trace_hash`` AFTER ``chat`` returns, back-stamping both onto ``turn_log[-1]``. Inline emission therefore put a stale ``surface`` and a stale ``trace_hash`` into the durable stream while ``turn_log`` was repaired — the record that persists was the wrong one. The runtime cannot detect whether a pipeline will run, because the pipeline wraps it rather than the reverse (M3). So deferral is opt-in: the pipeline declares that it owns the emission. """ sink = self._telemetry_sink if sink is None: return if self._deferred_turn_emission and self.turn_log and self.turn_log[-1] is event: # M1 — stage the INDEX, never a copy of the event. At flush time the # log is re-read, so both back-stamps are picked up for free and the # staged record cannot go stale. Capturing the TurnEvent object here # would be the original defect with extra steps. # # I7 — a second staging while one is unflushed flushes the first # rather than dropping it, which bounds the worst case to the final # turn of a session whose pipeline raised before its flush (and I5's # ``finally`` covers that). self._flush_staged_turn_event() self._staged_turn_index = len(self.turn_log) - 1 return sink.emit(self._format_turn_event(event)) def begin_deferred_turn_emission(self) -> None: """Claim ownership of this turn's sink emission (audit-ledger R7, M3). Called by :class:`CognitiveTurnPipeline` before it delegates to :meth:`chat`. The caller MUST pair this with :meth:`flush_deferred_turn_event` in a ``finally`` — otherwise a raising pipeline swallows a telemetry record, which is worse than the staleness this repairs. """ self._deferred_turn_emission = True def _flush_staged_turn_event(self) -> None: index = self._staged_turn_index self._staged_turn_index = None if index is None: return sink = self._telemetry_sink if sink is None or index >= len(self.turn_log): return # Re-read the log: this is where the pipeline's back-stamped surface and # trace_hash enter the durable stream. sink.emit(self._format_turn_event(self.turn_log[index])) def flush_deferred_turn_event(self) -> None: """Emit the staged turn event and stop deferring (idempotent). Idempotent by construction — the staged index is cleared before the emit — so the exactly-once invariant survives a caller that flushes in both a normal return and a ``finally``. """ self._flush_staged_turn_event() self._deferred_turn_emission = False def _tokenize(self, text: str) -> list[str]: return [self._surface_by_fold.get(m.group(0).casefold(), m.group(0)) for m in _TOKEN_RE.finditer(text)] def tokenize(self, text: str) -> list[str]: return self._tokenize(text) def _apply_oov_policy(self, tokens: list[str]) -> list[str]: # Comb pass 2026-05-21 — OOV-policy aggregates are precomputed # at ``__init__`` so this method stays O(OOV tokens) rather # than O(packs × OOV tokens). See ``_all_manifests_fail_closed`` # / ``_any_manifest_proposes_vocab``. kept: list[str] = [] for token in tokens: try: self._context.vocab.get_versor(token) kept.append(token) except KeyError: if self._all_manifests_fail_closed: raise if self._any_manifest_proposes_vocab: raise KeyError(f"OOV token requires vocab proposal: {token}") kept.append(token) return kept def _syntactic_guard(self, tokens: tuple[str, ...]) -> list[str]: out: list[str] = [] prev_pos: str | None = None for token in tokens: pos = self._pos_by_surface.get(token, "X") if pos == prev_pos: continue out.append(token) prev_pos = pos return out def _dialogue_reference(self) -> np.ndarray | None: blade = self._context.last_dialogue_blade if blade is None or float(np.linalg.norm(blade)) < 1e-8: return None return blade def _apply_drive_bias(self, field_state: FieldState) -> FieldState: return field_state def _build_surface_context(self, identity_score, current_valence: float) -> SurfaceContext: active = self._context.referents.active_referent() alignment = float(identity_score.alignment) if identity_score is not None else 1.0 deviation_axes = ( frozenset(identity_score.deviation_axes) if identity_score is not None else frozenset() ) prefs = self.identity_manifold.surface_preferences axis_hedges = tuple( (axis_id, hedge.strong, hedge.soft, hedge.qualifier) for axis_id, hedge in prefs.axis_hedges ) return SurfaceContext( active_referent_surface=active.surface if active is not None else "", active_referent_slot=active.slot if active is not None else "neut_sg", identity_alignment=alignment, valence_delta=current_valence - self._last_valence, elab_conjunction="", hedge_threshold_strong=prefs.hedge_threshold_strong, hedge_threshold_soft=prefs.hedge_threshold_soft, preferred_hedge_strong=prefs.preferred_hedge_strong, preferred_hedge_soft=prefs.preferred_hedge_soft, claim_strength=prefs.claim_strength, qualified_band_high=prefs.qualified_band_high, preferred_qualifier=prefs.preferred_qualifier, deviation_axes=deviation_axes, axis_hedges=axis_hedges, ) def _maybe_pack_grounded_surface( self, text: str, gate_source: str, *, allow_warm: bool = False, attempts: list[DispatchAttempt] | None = None ) -> tuple[str, str, tuple[str, ...]] | None: """Return ``(surface, grounding_source)`` or ``None``. ADR-0048 / ADR-0050 / ADR-0052 — three reviewed sources of cold-start grounding share this dispatcher. ``allow_warm=True`` bypasses the empty-vault gate so the warm path can engage pack-grounding for pack-resident DEFINITION / RECALL / NARRATIVE / EXAMPLE / COMPARISON / PROCEDURE intents — addresses ``warm_grounding_stability`` regression where turn-2 of the same prompt drifted from a coherent pack surface to a walk fragment. CAUSE / VERIFICATION still return None when no teaching chain exists, preserving the discovery signal. """ if self.config.output_language != "en": if attempts is not None: for src in ("pack", "teaching", "partial", "oov"): attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="non_english_output")) return None # Deduction-serve arc, Phase 1 — checked BEFORE the empty-vault gate # below (unlike pack/teaching/partial/oov, deduction serving is not a # cold-start-only fallback: a user may ask a logic question at any # point in a conversation, warm or cold vault). A pure function of # the input text — no vault/field dependency — so it is safe to # decide unconditionally of ``gate_source``/``allow_warm``. if self.config.deduction_serving_enabled: from chat.deduction_surface import deduction_grounded_surface from generate.intent import DialogueIntent, IntentTag as _IntentTag deduction_surface = deduction_grounded_surface(text) if deduction_surface is not None: self._last_intent = DialogueIntent(tag=_IntentTag.DEDUCTION, subject=text) # W-013 if attempts is not None: attempts.append(DispatchAttempt(source="deduction", outcome="admitted", reason="deduction_composer_committed")) return (deduction_surface, "deduction", ()) if attempts is not None: attempts.append(DispatchAttempt(source="deduction", outcome="skipped", reason="not_argument_shaped")) # Generalization arc, Phase 2 (ADR-0262) — curriculum-grounded exam # questions, checked beside deduction serving for the same reason: a # pure function of the input text and the ratified curriculum, with no # vault/field dependency, so warm/cold state cannot change the answer. if self.config.curriculum_serving_enabled: from chat.curriculum_surface import curriculum_grounded_surface from generate.intent import DialogueIntent, IntentTag as _IntentTag curriculum_surface = curriculum_grounded_surface(text) if curriculum_surface is not None: self._last_intent = DialogueIntent(tag=_IntentTag.DEDUCTION, subject=text) if attempts is not None: attempts.append(DispatchAttempt(source="curriculum", outcome="admitted", reason="curriculum_composer_committed")) return (curriculum_surface, "curriculum", ()) if attempts is not None: attempts.append(DispatchAttempt(source="curriculum", outcome="skipped", reason="not_curriculum_question")) if not allow_warm and gate_source != "empty_vault": if attempts is not None: for src in ("pack", "teaching", "partial", "oov"): attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="warm_path_disabled")) return None from generate.intent import IntentTag from generate.intent_bridge import classify_intent_from_input intent = classify_intent_from_input(text) self._last_intent = intent # W-013: expose for /explain if intent.tag is IntentTag.COMPARISON: lemma_a = (intent.subject or "").strip().rstrip(".,?!;:") lemma_b = (intent.secondary_subject or "").strip().rstrip(".,?!;:") if lemma_a and lemma_b: surface = pack_grounded_comparison_surface( lemma_a, lemma_b, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_comparison_surface_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_comparison_surface")) from chat.partial_surface import partial_comparison_surface partial = partial_comparison_surface(lemma_a, lemma_b) if partial is not None: if attempts is not None: attempts.append(DispatchAttempt(source="partial", outcome="admitted", reason="partial_comparison_surface_found")) return (partial[0], "partial", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="partial", outcome="fell_through", reason="no_partial_comparison_surface")) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="missing_comparison_lemmas")) attempts.append(DispatchAttempt(source="partial", outcome="fell_through", reason="missing_comparison_lemmas")) if intent.tag is IntentTag.NARRATIVE: lemma = (intent.subject or "").strip() if lemma: from chat.narrative_surface import narrative_grounded_surface surface = narrative_grounded_surface( lemma, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="admitted", reason="narrative_surface_found")) return (surface, "teaching", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="no_narrative_surface")) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="missing_subject")) if intent.tag is IntentTag.EXAMPLE: lemma = (intent.subject or "").strip() if lemma: from chat.example_surface import example_grounded_surface surface = example_grounded_surface( lemma, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="admitted", reason="example_surface_found")) return (surface, "teaching", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="no_example_surface")) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="missing_subject")) if intent.tag in (IntentTag.CAUSE, IntentTag.VERIFICATION): lemma = (intent.subject or "").strip() if lemma: if ( intent.tag is IntentTag.VERIFICATION and intent.relation and intent.secondary_subject ): surface = pack_grounded_relation_confirmation_surface( lemma, intent.relation, intent.object or intent.secondary_subject, negated=intent.negated, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_relation_confirmation_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_relation_confirmation")) elif intent.tag is IntentTag.VERIFICATION: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="skipped", reason="missing_relation_or_secondary_subject")) # ADR-0085 — gloss-aware CAUSE surface (opt-in). Tried # FIRST so a lemma with a ratified gloss gets an # explanation-shaped answer drawn from the gloss text # instead of the chain-walk's structurally-correct-but- # bureaucratic domain-tag walk. Falls through to the # chain-walk on None (no gloss for this lemma), so the # null-drop invariant holds: every case that lifted # pre-ADR-0085 still lifts; only the *frame* shifts on # lemmas where a gloss exists. if ( self.config.gloss_aware_cause and intent.tag is IntentTag.CAUSE ): surface = gloss_aware_cause_surface( lemma, register=self.register_pack, anchor_lens=self.anchor_lens, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="gloss_aware_cause_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_gloss_aware_cause")) elif intent.tag is IntentTag.CAUSE: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="skipped", reason="gloss_aware_cause_disabled")) if self.config.transitive_surface: # ADR-0083 — transitive supersedes composed. At # max_depth=1 this degrades byte-identically to the # single-chain surface; at max_depth=2 byte-identical # to ADR-0062 when no second hop exists. surface = teaching_grounded_surface_transitive( lemma, intent.tag, register=self.register_pack, max_depth=self.config.transitive_max_depth, ) reason_type = "transitive" elif self.config.composed_surface: surface = teaching_grounded_surface_composed( lemma, intent.tag, register=self.register_pack, ) reason_type = "composed" else: surface = teaching_grounded_surface( lemma, intent.tag, register=self.register_pack, ) reason_type = "standard" if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="admitted", reason=f"teaching_{reason_type}_surface_found")) return (surface, "teaching", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason=f"no_teaching_{reason_type}_surface")) from chat.cross_pack_grounding import cross_pack_grounded_surface surface = cross_pack_grounded_surface( lemma, intent.tag, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="admitted", reason="cross_pack_grounded_surface_found")) return (surface, "teaching", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="no_cross_pack_grounded_surface")) # Deliberate non-fallback: when CAUSE / VERIFICATION # has no teaching chain or cross-pack chain rooted on # the subject, return None so the discovery layer logs # a "would_have_grounded" candidate identifying the # teaching-content gap. Emitting the bare pack # disclosure here would mask that signal and give the # user a non-answer (a definition rather than a cause). # See ``tests/test_discovery_candidates``. else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="missing_subject")) attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="missing_subject")) if intent.tag is IntentTag.CORRECTION: surface = pack_grounded_correction_surface( text, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_correction_surface_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_correction_surface")) if intent.tag is IntentTag.PROCEDURE: subject_text = (intent.subject or "").strip() if subject_text: surface = pack_grounded_procedure_surface( subject_text, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_procedure_surface_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_procedure_surface")) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="missing_subject")) if intent.tag in (IntentTag.DEFINITION, IntentTag.RECALL): lemma = (intent.subject or "").strip() if not lemma: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="missing_subject")) return None surface = pack_grounded_surface( lemma, register=self.register_pack, anchor_lens=self.anchor_lens, ) if surface is not None: # ADR-0077 (R6) — expose the resolving lemma's # semantic_domains so the runtime's substantive-register # hook can fuel ``append_semantic_domain_clause``. All # other composers return ``()`` because only the gloss # DEFINITION/RECALL path participates in convivial's # bounded propositional expansion in R6. from chat.pack_resolver import resolve_lemma resolved = resolve_lemma(lemma) domains = resolved[1] if resolved is not None else () if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_grounded_surface_found")) return (surface, "pack", domains) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_grounded_surface")) if intent.tag is IntentTag.UNKNOWN: # ADR-0086 — UNKNOWN intent with pack-resident prompt # tokens. The classifier could not assign a known dialogue # shape, but the prompt itself may contain lemmas that are # ratified in mounted lexicon packs (e.g. ``"light logos"``, # ``"spirit wisdom truth"``). Surface those lemmas with # their semantic_domains rather than emit the bare # _UNKNOWN_DOMAIN_SURFACE disclosure. Null-lift invariant: # when no prompt token resolves, composer returns None and # the caller falls through to the universal disclosure # byte-identically (preserves the ADR-0053 honesty contract # for fully-OOV prompts). surface = pack_grounded_unknown_surface( text, register=self.register_pack, ) if surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="admitted", reason="pack_grounded_unknown_surface_found")) return (surface, "pack", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_grounded_unknown_surface")) # Check if any attempts have been recorded for pack/teaching/partial. If not, record them as skipped. if attempts is not None: has_pack = any(a.source == "pack" for a in attempts) has_teaching = any(a.source == "teaching" for a in attempts) has_partial = any(a.source == "partial" for a in attempts) if not has_pack: attempts.append(DispatchAttempt(source="pack", outcome="skipped", reason=f"intent_tag_{intent.tag.name.lower()}_not_targeted")) if not has_teaching: attempts.append(DispatchAttempt(source="teaching", outcome="skipped", reason=f"intent_tag_{intent.tag.name.lower()}_not_targeted")) if not has_partial: attempts.append(DispatchAttempt(source="partial", outcome="skipped", reason=f"intent_tag_{intent.tag.name.lower()}_not_targeted")) oov_lemma = (intent.subject or "").strip() if oov_lemma: from chat.oov_surface import oov_learning_invitation_surface oov_surface = oov_learning_invitation_surface(oov_lemma, intent.tag) if oov_surface is not None: if attempts is not None: attempts.append(DispatchAttempt(source="oov", outcome="admitted", reason="oov_learning_invitation_surface_found")) return (oov_surface, "oov", ()) else: if attempts is not None: attempts.append(DispatchAttempt(source="oov", outcome="fell_through", reason="no_oov_learning_invitation_surface")) else: if attempts is not None: attempts.append(DispatchAttempt(source="oov", outcome="skipped", reason="missing_subject")) return None def _graph_atom_context( self, text: str, articulation: ArticulationPlan, *, region=None, ) -> tuple[tuple[str, ...], bool]: """Return ``(graph_atoms, graph_unconstrained)`` for observational telemetry.""" if self.config.output_language != "en": return ((), True) graph = build_graph_from_input(text, articulation) graph_atoms = atoms_for_graph_nodes(graph) unconstrained = len(graph_atoms) == 0 if region is not None: unconstrained = unconstrained or getattr(region, "allowed_indices", None) is None return (graph_atoms, unconstrained) def _composer_graph_atom_equivalence( self, *, grounding_source: str, composer_atoms: tuple[str, ...], graph_atoms: tuple[str, ...], graph_unconstrained: bool, ): applicable = grounding_source in {"pack", "teaching"} return compare_atom_sets( composer_atoms=composer_atoms, graph_atoms=graph_atoms, graph_unconstrained=graph_unconstrained, applicable=applicable, ) def _maybe_apply_discourse_planner( self, text: str, source_tag: str ) -> tuple[str, str] | None: """Build and render a :class:`DiscoursePlan` for *text*. Returns ``(rendered_surface, new_source_tag)`` when the planner engages and produces more than one move, else ``None``. Callers own assignment. The returned ``new_source_tag`` is the source the planner actually used (``"teaching"`` when the plan contains any teaching fact, else ``"pack"``) so downstream labels reflect the surface's true provenance — particularly important when the planner engaged via the compound bypass (upstream tagged "oov" but rendered output is pack/teaching content). Gating discipline (must match both cold-start and warm hooks): * Returns ``None`` unless ``self.config.discourse_planner`` is True. * Returns ``None`` unless *source_tag* is one of ``pack`` or ``teaching``. Vault / none / oov / empty paths are not replaced — the discovery-signal disclosure and the existing vault-grounded walk surfaces stay intact. * Returns ``None`` when the classified intent carries no subject (no head noun ⇒ no grounding bundle to plan over). * Returns ``None`` when the resulting plan has ≤ 1 move (BRIEF mode or empty bundle) — render in that case would just duplicate the existing single-sentence pack-grounded surface. * Returns ``None`` when the renderer produces an empty string. """ # Phase 3 + 4 — reset plan-contemplation findings AND plan # metrics at the start of every call so they never leak across # turns; only successfully rendered plans (with contemplation # enabled) repopulate them. self._last_plan_findings = () self._last_plan_metrics = None if not self.config.discourse_planner: return None from generate.discourse_planner import ( GroundingBundle, plan_compound_discourse, plan_discourse, render_plan, ) from generate.grounding_accessors import grounding_bundle_for from generate.intent import ( classify_compound_intent, classify_response_mode, ) from generate.intent_bridge import classify_intent_from_input compound = classify_compound_intent(text) mode = classify_response_mode(text) # Compound prompts implicitly request more depth than BRIEF # can express — a multi-part compound in BRIEF mode produces # one ANCHOR per part, which on shared-subject compounds # ("What is X, and why does it matter?") would emit duplicate # anchor sentences. Upgrade to EXPLAIN so each sub-plan has # ANCHOR+SUPPORT+RELATION budget and the parts differentiate. from generate.intent import ResponseMode as _ResponseMode if compound.is_compound() and mode is _ResponseMode.BRIEF: mode = _ResponseMode.EXPLAIN # Fast path: BRIEF mode on a non-compound prompt can never # emit > 1 move (``_MODE_BUDGETS[BRIEF] = (1, 1)``). The # downstream ``len(plan.moves) <= 1`` gate would always # reject — so short-circuit here, BEFORE the expensive # ``grounding_bundle_for`` query and ``plan_discourse`` # selector logic. This is the load-bearing perf win for # ``discourse_planner=True`` as the runtime default; without # it every single-fact prompt pays for a multi-source bundle # build it can't possibly use. Confirmed empirically: # ``tests/test_cognition_eval_register_matrix.py`` runtime # collapsed from ~14 minutes to seconds after this gate # landed. if mode is _ResponseMode.BRIEF and not compound.is_compound(): return None # Standard gate: when upstream grounded the surface in pack or # teaching, the planner is free to engage. standard_gate = source_tag in {"pack", "teaching"} # Compound bypass: when upstream produced an OOV / none surface # because the flat classifier saw a polluted subject (e.g. # ``"truth, and why does it matter"``), but the compound # decomposition reveals at least one pack-resident primary # part, the substrate exists — the planner engages on the # decomposed parts rather than the polluted flat surface. compound_bypass = False if not standard_gate and compound.is_compound(): primary = compound.primary if primary.subject: probe = grounding_bundle_for(primary.subject) if not probe.is_empty(): compound_bypass = True if not standard_gate and not compound_bypass: return None if compound.is_compound(): bundles = tuple( grounding_bundle_for(part.subject) if part.subject else GroundingBundle() for part in compound.parts ) plan = plan_compound_discourse(compound, mode, bundles) else: # Use the intent_bridge classifier on single-part prompts to # preserve the pre-compound behavior exactly. intent = classify_intent_from_input(text) if not intent.subject: return None bundle = grounding_bundle_for(intent.subject) plan = plan_discourse(intent, mode, bundle) if len(plan.moves) <= 1: return None # Phase 3 + 4 — plan-level contemplation pre-flight + metrics. # Read-only, SPECULATIVE-only on the findings side; pure # measurements on the metrics side. Stores both on the # runtime for offline miner consumption. Does not mutate the # plan or block rendering — emits side observations only. if self.config.discourse_contemplation: from core.contemplation.plan_metrics import compute_plan_metrics from core.contemplation.plan_preflight import contemplate_plan self._last_plan_findings = contemplate_plan(plan) self._last_plan_metrics = compute_plan_metrics(plan) else: self._last_plan_findings = () self._last_plan_metrics = None # Phase 2 — reflective rendering pronominalizes the focus # subject across consecutive same-subject moves, eliminating # the mechanical "Truth ... Truth ... Truth ..." cascade the # Phase 1 flat renderer produced. Deterministic, replayable, # adds no new content — purely a rendering improvement. rendered = render_plan(plan, reflective=True) if not rendered: return None from generate.discourse_planner import FactSource plan_uses_teaching = any( m.fact is not None and m.fact.source is FactSource.TEACHING for m in plan.moves ) new_source = "teaching" if plan_uses_teaching else "pack" # Phase 5 — emit one articulation observation per engaged turn. # Gated by both ``discourse_contemplation`` (so metrics + # findings exist to package) AND the presence of an attached # sink (so the runtime does no JSON work when nobody is # listening). Sink errors are NOT swallowed — same fail-fast # contract as the telemetry sink. if ( self._articulation_sink is not None and self.config.discourse_contemplation and self._last_plan_metrics is not None ): from chat.articulation_telemetry import ( ArticulationObservation, format_articulation_observation_jsonl, prompt_hash, ) anchor = plan.anchor() anchor_subject = ( anchor.fact.subject if anchor is not None and anchor.fact is not None else (plan.intent.subject or "") ) import hashlib as _hashlib plan_substrate_hash = _hashlib.sha256( plan.to_json().encode("utf-8") ).hexdigest()[:16] observation = ArticulationObservation( turn_id=self._articulation_turn_counter, anchor_subject=anchor_subject, prompt_hash=prompt_hash(text), plan_substrate_hash=plan_substrate_hash, metrics=self._last_plan_metrics.as_dict(), findings=tuple( { "kind": f.kind.value, "subject": f.subject, "predicate": f.predicate, "object": f.object, } for f in self._last_plan_findings ), ) self._articulation_sink.emit( format_articulation_observation_jsonl(observation) ) self._articulation_turn_counter += 1 return rendered, new_source def _stub_response( self, field_state: FieldState, *, tokens: tuple[str, ...] = (), pack_grounded_surface: str | None = None, grounded_source_tag: str = "pack", pack_semantic_domains: tuple[str, ...] = (), graph_atoms: tuple[str, ...] = (), graph_unconstrained: bool = True, discovery_intent_tag: Any = None, discovery_intent_subject: str | None = None, dispatch_trace: DispatchTrace | None = None, ) -> ChatResponse: zero = np.zeros(field_state.F.shape, dtype=np.float32) prop = Proposition( subject="", predicate="", object_=None, surface=_UNKNOWN_DOMAIN_SURFACE, frame_id="unknown_domain", subject_versor=zero, predicate_versor=zero, object_versor=None, relation=zero, ) art = ArticulationPlan( subject="", predicate="", object=None, surface=_UNKNOWN_DOMAIN_SURFACE, output_language=self.config.output_language, frame_id="unknown_domain", ) safety_ctx = SafetyContext( field_state=_FieldStateWithVersor( versor_condition=float(versor_condition(field_state.F)), ), last_refusal_was_typed=self._last_refusal_was_typed, identity_manifold_hash_before=self._identity_manifold_hash, identity_manifold_hash_after=_hash_identity_manifold(self.identity_manifold), ) safety_verdict = self.safety_check.check(safety_ctx, self.safety_pack) ethics_ctx = EthicsContext( alignment_score=0.0, hedge_threshold_soft=float( self.identity_manifold.surface_preferences.hedge_threshold_soft ), hedge_emitted=False, grounded_in_evidence=False, disclosure_emitted=True, ) ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack) refusal_surface = build_refusal_surface( safety_verdict, ethics_verdict, self.ethics_pack, ) refusal_emitted = refusal_surface is not None if refusal_emitted: response_surface = refusal_surface self._last_refusal_was_typed = True elif pack_grounded_surface is not None: response_surface = pack_grounded_surface if ( self.config.thread_anaphora and grounded_source_tag in {"pack", "teaching"} and discovery_intent_subject and discovery_intent_tag is not None ): from chat.anaphora import thread_anaphora_prefix prefix = thread_anaphora_prefix( self.thread_context, discovery_intent_subject, discovery_intent_tag.name.lower(), grounded_source_tag, ) if prefix is not None: response_surface = prefix + response_surface else: response_surface = _UNKNOWN_DOMAIN_SURFACE if pack_grounded_surface is not None and not refusal_emitted: grounding_source = grounded_source_tag else: grounding_source = "none" # ADR-0075 (C1) — realizer slot-type guard. Runs BEFORE # register decoration so a register cannot accidentally heal # an illegal articulation by wrapping it, and BEFORE anchor- # lens annotation extraction so the lens annotation never # rides on a guard-rejected surface. On rejection, route to # the bounded disclosure string and force grounding_source to # ``"none"`` (an illegal surface is ungrounded by construction). # The pre-guard candidate is preserved on walk_surface_stub # for telemetry — the stub path normally leaves walk_surface as # _UNKNOWN_DOMAIN_SURFACE, so this swap strictly increases # observability under rejection. # Deduction surfaces (ADR-0257) are OUT of C1's regime: a fixed, # test-audited template quoting the user's clauses verbatim, not a # slot-composed articulation — pack POS describes the composed sense, # not the user's quoted sense (e.g. pack ``open``=VERB rejecting an # honest "the door is not open"). Exempt, like empty surfaces. guard_verdict_stub = ( _GUARD_QUOTED_EXEMPT if grounding_source == "deduction" else _check_realizer_surface( response_surface, pos_lookup=self._pos_by_surface.get, ) ) realizer_guard_status_stub = guard_verdict_stub.status realizer_guard_rule_stub = guard_verdict_stub.rule_id walk_surface_stub = _UNKNOWN_DOMAIN_SURFACE if guard_verdict_stub.status == "rejected": walk_surface_stub = response_surface response_surface = _GUARD_DISCLOSURE_SURFACE grounding_source = "none" # ADR-0077 (R6) — register layering separation. # ``register_canonical_surface`` is the composer / guard output # BEFORE any register transformation; the pipeline hashes this # field for ``trace_hash`` so substantive register transforms # cannot move the truth-path identity. Substantive transforms # are skipped on ``grounding_source == "none"`` so the bounded # disclosure stays sacrosanct under terse_v1's drop_articles. register_canonical_surface_stub = response_surface if grounding_source == "none": substantive_surface_stub = response_surface else: substantive_surface_stub = apply_substantive_register( response_surface, self.register_pack, semantic_domains=pack_semantic_domains, ) response_surface = substantive_surface_stub # ADR-0071 (R4) — apply seeded discourse-marker decoration to # the realized surface AFTER substantive register transforms. # Empty marker buckets ⇒ no-op (UNREGISTERED / neutral / terse). # Preserve the pre-decoration string so the pipeline can hash # the truth-path surface and trace_hash stays invariant under # register (ADR-0069 invariant C, strengthened by ADR-0077). pre_decoration_surface_stub = response_surface decoration_stub = decorate_surface( response_surface, self.register_pack, turn_idx=len(self.turn_log), ) response_surface = decoration_stub.surface register_id_stub = ( "" if self.register_pack.is_unregistered() else self.register_pack.register_id ) # ADR-0073d — anchor-lens telemetry. ``id`` reflects the loaded # pack (empty for UNANCHORED); ``mode_label`` reflects the # engaged label this turn (empty when the lens didn't fire on # this turn's lemma). Mode is extracted from the pre-decoration # surface so register decoration cannot interfere. anchor_lens_id_stub = ( "" if self.anchor_lens.is_unanchored() else self.anchor_lens.lens_id ) anchor_lens_mode_label_stub = _extract_anchor_lens_mode_label( pre_decoration_surface_stub, anchor_lens_id_stub, ) atom_equivalence_stub = self._composer_graph_atom_equivalence( grounding_source=grounding_source, composer_atoms=pack_semantic_domains, graph_atoms=graph_atoms, graph_unconstrained=graph_unconstrained, ) verdicts_bundle = TurnVerdicts( identity_score=None, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, refusal_emitted=refusal_emitted, hedge_injected=False, ) stub_epistemic_state = epistemic_state_for_grounding_source(grounding_source).value stub_normative_clearance = clearance_from_verdicts(verdicts_bundle).value stub_normative_detail = normative_detail_from_verdicts(verdicts_bundle) if tokens: stub_event = TurnEvent( turn=max(self._context.turn - 1, 0), input_tokens=tokens, surface=response_surface, walk_surface=walk_surface_stub, articulation_surface=_UNKNOWN_DOMAIN_SURFACE, dialogue_role="assert", identity_score=None, cycle_cost_total=0.0, vault_hits=0, versor_condition=float(versor_condition(field_state.F)), flagged=False, elaboration=None, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, verdicts=verdicts_bundle, grounding_source=grounding_source, register_id=register_id_stub, register_variant_id=decoration_stub.variant_id, anchor_lens_id=anchor_lens_id_stub, anchor_lens_mode_label=anchor_lens_mode_label_stub, realizer_guard_status=realizer_guard_status_stub, realizer_guard_rule=realizer_guard_rule_stub, register_canonical_surface=register_canonical_surface_stub, composer_graph_atom_status=atom_equivalence_stub.status, composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash, graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash, composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, epistemic_state=stub_epistemic_state, normative_clearance=stub_normative_clearance, normative_detail=stub_normative_detail, ) self.turn_log.append(stub_event) self._emit_turn_event(stub_event) if discovery_intent_tag is not None: self._emit_discovery_candidates( turn_event=stub_event, intent_tag=discovery_intent_tag, intent_subject=discovery_intent_subject, grounding_source=grounding_source, ) if grounding_source == "oov": self._emit_oov_candidate( turn_event=stub_event, intent_tag=discovery_intent_tag, token=discovery_intent_subject, ) self._push_thread_summary( turn_event=stub_event, intent_tag=discovery_intent_tag, intent_subject=discovery_intent_subject, grounding_source=grounding_source, surface=response_surface, ) return ChatResponse( surface=response_surface, proposition=prop, articulation=art, articulation_surface=_UNKNOWN_DOMAIN_SURFACE, dialogue_role="assert", versor_condition=versor_condition(field_state.F), output_language=self.config.output_language, frame_pack=self.config.frame_pack, walk_surface=walk_surface_stub, salience_top_k=None, candidates_used=None, vault_hits=0, identity_score=None, character_profile=self.character_profile, flagged=False, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, verdicts=verdicts_bundle, grounding_source=grounding_source, pre_decoration_surface=pre_decoration_surface_stub, register_id=register_id_stub, register_variant_id=decoration_stub.variant_id, anchor_lens_id=anchor_lens_id_stub, anchor_lens_mode_label=anchor_lens_mode_label_stub, realizer_guard_status=realizer_guard_status_stub, realizer_guard_rule=realizer_guard_rule_stub, register_canonical_surface=register_canonical_surface_stub, composer_graph_atom_status=atom_equivalence_stub.status, composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash, graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash, composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, epistemic_state=stub_epistemic_state, normative_clearance=stub_normative_clearance, normative_detail=stub_normative_detail, refusal_reason=refusal_surface if refusal_emitted else "", dispatch_trace=dispatch_trace, ) def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse: self._last_plan_findings = () self._last_plan_metrics = None self._last_input_text = text # W-013: store for explain_last_turn() tokens = self._tokenize(text) filtered = self._apply_oov_policy(tokens) if not filtered: raise ValueError("ChatRuntime.chat() received no in-vocabulary tokens.") # ADR-0090 — unified-ingest path is flag-gated. Default (False) # preserves the historical probe-then-commit behavior; True # commits first so the gate and the walk see the same field. # ``committed`` is materialized eagerly on the unified path and # lazily on the stub path of the historical flow; the explicit # ``FieldState | None`` declaration documents that and silences # Pyright's reportPossiblyUnbound across the conditional flow. committed: FieldState | None = None if self.config.unified_ingest: committed = self._context.commit_ingest(filtered) committed = self._apply_drive_bias(committed) gate_query = committed.F else: probe_state = self._context.probe_ingest(filtered) gate_query = probe_state.F direct_hits = self._context.vault.recall(gate_query, top_k=3) direct_recall_energy_class = _recall_energy_class_from_hits(direct_hits) direct_best = max((h["score"] for h in direct_hits), default=0.0) gate_decision = default_gate.check( direct_best, vault=self._context.vault, query=gate_query, decomposer=default_decomposer, ) if gate_decision.fire: if not self.config.unified_ingest: committed = self._context.commit_ingest(filtered) assert committed is not None # set above on both flag paths empty_result = GenerationResult(tokens=(), final_state=committed, vault_hits=0) attempts: list[DispatchAttempt] = [] pack_result = self._maybe_pack_grounded_surface( text, gate_decision.source, attempts=attempts ) if pack_result is None: pack_surface = None pack_source_tag = "none" pack_semantic_domains: tuple[str, ...] = () final_attempts = [] final_attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_resident_lemmas")) final_attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="no_teaching_chains")) final_attempts.append(DispatchAttempt(source="partial", outcome="fell_through", reason="no_partial_match")) final_attempts.append(DispatchAttempt(source="oov", outcome="fell_through", reason="no_oov_learning_invitation")) final_attempts.append(DispatchAttempt(source="universal_disclosure", outcome="admitted", reason="fallback_to_universal_disclosure")) attempts = final_attempts selected_source = "universal_disclosure" else: pack_surface, pack_source_tag, pack_semantic_domains = pack_result planned = self._maybe_apply_discourse_planner( text, pack_source_tag ) if planned is not None: pack_surface, pack_source_tag = planned # ADR-0077 — planner-rendered surfaces are outside # the gloss DEFINITION/RECALL convivial-expansion # path; drop the carried semantic_domains so the # ``append_semantic_domain_clause`` knob is a no-op # over planner output. pack_semantic_domains = () selected_source = pack_source_tag dispatch_trace = DispatchTrace(attempts=tuple(attempts), selected=selected_source) self._context.finalize_turn( empty_result, tokens_in=tuple(filtered), input_versor=committed.F, dialogue_role="assert", metadata={ "unknown": True, "unknown_source": gate_decision.source, "grounding_source": pack_source_tag if pack_surface else "none", }, ) # ADR-0148 — post-finalize promotion scan (flag-gated, null-drop when False). if self.config.vault_promotion_enabled: self._context.vault.promote_eligible_entries(VaultPromotionPolicy()) discovery_intent_tag = None discovery_intent_subject: str | None = None stub_graph_atoms: tuple[str, ...] = () stub_graph_unconstrained = True if ( gate_decision.source == "empty_vault" and self.config.output_language == "en" ): from generate.intent_bridge import classify_intent_from_input _intent = classify_intent_from_input(text) self._last_intent = _intent # W-013 discovery_intent_tag = _intent.tag discovery_intent_subject = _intent.subject stub_articulation = ArticulationPlan( subject=_intent.subject or "", predicate="", object=None, surface="", output_language=self.config.output_language, frame_id="unknown_domain", ) stub_graph_atoms, stub_graph_unconstrained = self._graph_atom_context( text, stub_articulation, ) return self._checkpointed_response( self._stub_response( committed, tokens=tuple(filtered), pack_grounded_surface=pack_surface, grounded_source_tag=pack_source_tag, pack_semantic_domains=pack_semantic_domains, graph_atoms=stub_graph_atoms, graph_unconstrained=stub_graph_unconstrained, discovery_intent_tag=discovery_intent_tag, discovery_intent_subject=discovery_intent_subject, dispatch_trace=dispatch_trace, ) ) if self.config.unified_ingest: # ADR-0090 — commit + drive bias already ran before the gate # check; reuse the same field the gate decided against so the # walk navigates the manifold position the gate ratified. assert committed is not None # set in the unified-ingest branch above field_state = committed else: field_state = self._context.commit_ingest(filtered) field_state = self._apply_drive_bias(field_state) reference_blade = self._dialogue_reference() base_proposition = propose( field_state, None, self._context.vocab, self._frame_registry, output_lang=self.config.output_language, ) dialogue_role = _stable_dialogue_role( classify_dialogue_blade(base_proposition.relation, reference_blade), raw_text=text, tokens=tokens, ) proposition = propose_dialogue( field_state, self._context.vault, self._context.vocab, self._frame_registry, reference_blade, output_lang=self.config.output_language, ) articulation = realize(proposition, self._context.vocab, output_language=self.config.output_language) articulation = _prefer_prompt_anchor(articulation, filtered, output_language=self.config.output_language) self._context.record_dialogue(proposition) forward_region = None graph_atoms_main: tuple[str, ...] = () graph_unconstrained_main = True if self.config.output_language == "en": pre_gen_graph = build_graph_from_input(text, articulation) graph_atoms_main = atoms_for_graph_nodes(pre_gen_graph) if self.config.forward_graph_constraint: forward_region = build_graph_constraint(pre_gen_graph, self._context.vocab) graph_unconstrained_main = ( len(graph_atoms_main) == 0 or ( forward_region is not None and getattr(forward_region, "allowed_indices", None) is None ) ) # W-012 — catch InnerLoopExhaustion so the caller receives a # typed refusal ChatResponse instead of an unhandled exception. from generate.exhaustion import InnerLoopExhaustion as _ILE try: result = generate( field_state, self._context.vocab, self._context.persona, max_tokens=self.config.max_tokens if max_tokens is None else max_tokens, record_trajectory=True, vault=self._context.vault, recall_top_k=3 if self.config.allow_cross_language_recall else 0, output_lang=self.config.output_language, allow_cross_language_generation=self.config.allow_cross_language_generation, use_salience=self.config.use_salience, salience_top_k=self.config.salience_top_k, inhibition_threshold=self.config.inhibition_threshold, region=forward_region, inner_loop_admissibility=self.config.inner_loop_admissibility, admissibility_threshold=self.config.admissibility_threshold, admissibility_mode=self.config.admissibility_mode, admissibility_margin=self.config.admissibility_margin, stop_tokens=( frozenset(self.config.stop_tokens) if self.config.stop_tokens is not None else None ), ) except _ILE as _exhaustion_exc: self._context.finalize_turn( GenerationResult(tokens=(), final_state=field_state, vault_hits=0), tokens_in=tuple(filtered), input_versor=field_state.F, dialogue_role="assert", metadata={"exhaustion": True, "refusal_reason": _exhaustion_exc.reason.value}, ) stub = self._stub_response( field_state, tokens=tuple(filtered), ) return self._checkpointed_response( replace(stub, refusal_reason=_exhaustion_exc.reason.value) ) # --- Articulation fidelity: replace bare S-P-O join with intent-aware surface --- # Phase 2: pass proposition so the bridge grounds obj slots # from pack-resolved proposition slots (primary) rather than walk # tokens (supplemental backfill only). walk_tokens still participates # as a fallback when proposition.object_ is None/empty. # ADR-0088 Phase B (audit Finding 2, 2026-05-20) — compute # walk_tokens unconditionally so non-English packs can also # surface them via ``ChatResponse.recalled_words`` for the # pipeline's opt-in ``ground_graph`` step. English keeps # using them for ``articulate_with_intent`` grounding as # before. walk_tokens = tuple( tok for tok in (result.tokens or ()) if tok and tok.isalpha() ) if self.config.output_language == "en": intent_surface = articulate_with_intent( text, articulation, walk_tokens, proposition=proposition, ) if intent_surface: articulation = replace(articulation, surface=intent_surface) # --- end articulation fidelity --- reasoning_trajectory = _make_trajectory_from_result(result, self._context.turn) # ADR-0244 §2.2 — metric-exact operator-preservation identity score always # runs on the live versor final_state.F (scalar-L2 path excised). Live # *refusal* remains flag-gated via identity_wave_gate below. # ADR-0246 §3.7 — fuller admit surface, flag-gated + default-off. _admission_policy = ( AdmissionPolicy.placeholder_default() if self.config.identity_action_surface and self.config.identity_wave_gate else None ) identity_score = self._identity_check.check( reasoning_trajectory, self.identity_manifold, wave_field=result.final_state.F, admission_policy=_admission_policy, turn_id=self._context.turn, pack_id=self.identity_pack_id, ) # ADR-0246 §3.4/§3.5 — lawful-only session identity path (OBSERVE-ONLY, # same flags). Refused turns break; scope changes hard-break; the # ledger's session_admit is telemetry, never an egress decision # (epsilon_session is an uncertified placeholder; live activation # remains unauthorized). identity_path_ledger = None if _admission_policy is not None and identity_score.wave_mode_active: self._identity_path_ledger, _path_turn = advance_session_identity_path( self._identity_path_ledger, self.identity_manifold, result.final_state.F, _admission_policy, ) identity_path_ledger = self._identity_path_ledger flagged = identity_score.flagged cycle_cost = CycleCost( cycle_index=self._context.turn, attention_cost=float(result.candidates_used or 0), inhibition_cost=float(self.config.inhibition_threshold), digest_cost=0.0, trajectory_cost=float(len(result.trajectory or ())), ) self.exertion_meter.record(cycle_cost) fatigue = self.exertion_meter.fatigue(at_cycle=self._context.turn) self.character_profile = CharacterProfile.from_manifold( self.identity_manifold, drive_summaries={g.axis.name: g.magnitude * (1.0 - fatigue.value) for g in self.drive_gradients}, fatigue_index=fatigue.value, ) self._context.finalize_turn( result, tokens_in=tuple(filtered), dialogue_role=str(dialogue_role), ) # ADR-0148 — post-finalize promotion scan (flag-gated, null-drop when False). if self.config.vault_promotion_enabled: self._context.vault.promote_eligible_entries(VaultPromotionPolicy()) current_valence = _energy_scalar(getattr(result.final_state, "valence", None)) surface_ctx = self._build_surface_context(identity_score, current_valence) self._last_valence = current_valence surface = _terminate_surface(articulation.surface, role=dialogue_role, output_language=self.config.output_language) articulation = replace(articulation, surface=surface) sentence_plan: SentencePlan = SentenceAssembler().assemble( articulation, result.tokens, role=dialogue_role, context=surface_ctx, ) walk_surface = sentence_plan.surface vault_hits = int(result.vault_hits) is_grounded = walk_surface != _UNKNOWN_DOMAIN_SURFACE hedge_emitted = _surface_contains_hedge(walk_surface, self.identity_manifold) safety_ctx = SafetyContext( field_state=_FieldStateWithVersor( versor_condition=float(versor_condition(result.final_state.F)), ), last_refusal_was_typed=self._last_refusal_was_typed, identity_manifold_hash_before=self._identity_manifold_hash, identity_manifold_hash_after=_hash_identity_manifold(self.identity_manifold), ) safety_verdict = self.safety_check.check(safety_ctx, self.safety_pack) ethics_ctx = EthicsContext( alignment_score=float(getattr(identity_score, "alignment", 0.0)), hedge_threshold_soft=float( self.identity_manifold.surface_preferences.hedge_threshold_soft ), hedge_emitted=hedge_emitted, grounded_in_evidence=is_grounded, disclosure_emitted=not is_grounded, ) ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack) refusal_surface = build_refusal_surface( safety_verdict, ethics_verdict, self.ethics_pack, ) # ADR-0244 §2.2 — supplement the identity gate now that the safety/ethics # verdicts exist: intersect the turn's violated boundaries with the # manifold's committed boundary_ids, and fold a fail-closed geometric # IdentityGateRefusal into the typed refusal surface. Flag-gated OFF by # default → skipped → byte-identical to the pre-ADR-0244 path. # ``refusal_emitted`` is derived from ``refusal_surface`` after this # block so the (surface is not None) ⇔ emitted invariant is preserved. if self.config.identity_wave_gate: _violated_boundaries: frozenset[str] = ( frozenset(getattr(safety_verdict, "violated_boundaries", ()) or ()) | frozenset(getattr(ethics_verdict, "violated_commitments", ()) or ()) ) _boundary_breach = ( _violated_boundaries & self.identity_manifold.boundary_ids ) if _boundary_breach: identity_score = replace( identity_score, boundary_violations=_boundary_breach, flagged=True, ) flagged = True if refusal_surface is None and IdentityCheck.would_violate(identity_score): refusal_surface = TYPED_REFUSAL_PREFIX + "identity:wave_gate" refusal_emitted = refusal_surface is not None hedge_injected = False warm_grounding_source: str | None = None warm_pack_subject: str | None = None warm_pack_intent_tag: Any = None warm_pack_semantic_domains: tuple[str, ...] = () attempts: list[DispatchAttempt] = [] selected_source = "vault" if refusal_emitted: response_surface = refusal_surface self._last_refusal_was_typed = True for src in ("pack", "teaching", "partial", "oov", "universal_disclosure"): attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="refusal_emitted")) selected_source = "none" else: response_surface = walk_surface warm_pack_result = self._maybe_pack_grounded_surface( text, "warm", allow_warm=True, attempts=attempts ) if warm_pack_result is None: from generate.intent import IntentTag from generate.intent_bridge import classify_intent_from_input _wintent = classify_intent_from_input(text) # Discovery-signal preservation on warm path: when CAUSE / # VERIFICATION lacks both a teaching chain and a cross-pack # chain, the cold path emits the unknown-domain disclosure. # The warm path must match — fabricating a vault-grounded # walk fragment ("Work infer.") would mask the very gap # the discovery layer is meant to surface. final_attempts = [] final_attempts.append(DispatchAttempt(source="pack", outcome="fell_through", reason="no_pack_resident_lemmas")) final_attempts.append(DispatchAttempt(source="teaching", outcome="fell_through", reason="no_teaching_chains")) final_attempts.append(DispatchAttempt(source="partial", outcome="fell_through", reason="no_partial_match")) final_attempts.append(DispatchAttempt(source="oov", outcome="fell_through", reason="no_oov_learning_invitation")) if _wintent.tag in (IntentTag.CAUSE, IntentTag.VERIFICATION): response_surface = _UNKNOWN_DOMAIN_SURFACE articulation = replace(articulation, surface=_UNKNOWN_DOMAIN_SURFACE) warm_grounding_source = "none" final_attempts.append(DispatchAttempt(source="universal_disclosure", outcome="admitted", reason="cause_verification_warm_fallback")) selected_source = "universal_disclosure" else: final_attempts.append(DispatchAttempt(source="universal_disclosure", outcome="skipped", reason="used_walk_surface")) selected_source = "vault" attempts = final_attempts elif warm_pack_result is not None: warm_pack_surface, warm_grounding_source, warm_pack_semantic_domains = warm_pack_result if self.config.thread_anaphora and warm_grounding_source in {"pack", "teaching"}: from chat.anaphora import thread_anaphora_prefix from generate.intent_bridge import classify_intent_from_input _wintent = classify_intent_from_input(text) warm_pack_intent_tag = _wintent.tag warm_pack_subject = _wintent.subject if warm_pack_subject and warm_pack_intent_tag is not None: prefix = thread_anaphora_prefix( self.thread_context, warm_pack_subject, warm_pack_intent_tag.name.lower(), warm_grounding_source, ) if prefix is not None: warm_pack_surface = prefix + warm_pack_surface response_surface = warm_pack_surface articulation = replace(articulation, surface=warm_pack_surface) # Step 5 — discourse planner. Opt-in; engages only on # pack/teaching-grounded turns where the response mode # asks for more than a single-sentence brief. When the # planner returns a multi-move plan, replace the warm # surface with the deterministic multi-clause rendering. # BRIEF mode always collapses to a single ANCHOR move so # the flag-off path stays byte-identical to the existing # composer. planned = self._maybe_apply_discourse_planner( text, warm_grounding_source or "" ) if planned is not None: planned_surface, planned_source = planned response_surface = planned_surface articulation = replace(articulation, surface=planned_surface) warm_grounding_source = planned_source # ADR-0077 — planner-rendered surfaces are outside # the gloss DEFINITION/RECALL convivial-expansion # path; drop the carried semantic_domains so the # ``append_semantic_domain_clause`` knob is a no-op # over planner output. warm_pack_semantic_domains = () selected_source = warm_grounding_source if should_inject_hedge(ethics_verdict, self.ethics_pack): hedge_prefix = build_hedge_prefix(self.identity_manifold) before = response_surface response_surface = inject_hedge(response_surface, hedge_prefix) hedge_injected = response_surface != before dispatch_trace = DispatchTrace(attempts=tuple(attempts), selected=selected_source) # ADR-0075 (C1) — realizer slot-type guard (main path). Runs # AFTER all composer / planner / hedge transformations and # BEFORE register decoration so a single seam covers every # articulation path. On rejection: surface is replaced with # the bounded disclosure string, grounding_source forced to # ``"none"``, and walk_surface preserves the rejected # candidate so the manifold-walk evidence is overwritten only # in the rejection branch (the contract says illegal # articulation evidence is the relevant telemetry). # Same ADR-0257 exemption as the stub path: quoted deduction # templates are not slot-composed articulations. guard_verdict_main = ( _GUARD_QUOTED_EXEMPT if warm_grounding_source == "deduction" else _check_realizer_surface( response_surface, pos_lookup=self._pos_by_surface.get, ) ) realizer_guard_status_main = guard_verdict_main.status realizer_guard_rule_main = guard_verdict_main.rule_id if guard_verdict_main.status == "rejected": walk_surface = response_surface response_surface = _GUARD_DISCLOSURE_SURFACE warm_grounding_source = "none" main_grounding_source = warm_grounding_source or "vault" recall_energy_class_main = ( direct_recall_energy_class if main_grounding_source == "vault" else None ) if recall_energy_class_main: from generate.realizer import energy_modulated_surface try: ec = EnergyClass(recall_energy_class_main) except ValueError: pass else: response_surface = energy_modulated_surface(response_surface, ec) articulation = replace(articulation, surface=response_surface) # ADR-0077 (R6) — register layering separation (main path). See # the stub-path equivalent for full semantics: the canonical # surface is captured pre-substantive so the cognition pipeline # can hash it for ``trace_hash``, preserving register # invariance under R6's stronger consumer set. Substantive # transforms are skipped on ungrounded turns so the bounded # disclosure stays sacrosanct under terse's drop_articles. register_canonical_surface_main = response_surface if main_grounding_source == "none": substantive_surface_main = response_surface else: substantive_surface_main = apply_substantive_register( response_surface, self.register_pack, semantic_domains=warm_pack_semantic_domains, ) response_surface = substantive_surface_main # ADR-0071 (R4) — seeded discourse-marker decoration runs AFTER # substantive register transforms and is the last step before # TurnEvent is sealed. Applies uniformly to every grounding # path (vault / pack / teaching / planner / hedge-prefixed). # No-op for registers with empty marker buckets (UNREGISTERED / # default_neutral_v1 / terse_v1). Pre-decoration surface is # preserved separately so the cognition pipeline can hash the # truth-path surface and trace_hash stays invariant under # register (ADR-0069 inv C, strengthened by ADR-0077). pre_decoration_surface_main = response_surface decoration_main = decorate_surface( response_surface, self.register_pack, turn_idx=len(self.turn_log), ) response_surface = decoration_main.surface register_id_main = ( "" if self.register_pack.is_unregistered() else self.register_pack.register_id ) # ADR-0073d — anchor-lens telemetry (main path). See stub-path # comment above for semantics. anchor_lens_id_main = ( "" if self.anchor_lens.is_unanchored() else self.anchor_lens.lens_id ) anchor_lens_mode_label_main = _extract_anchor_lens_mode_label( pre_decoration_surface_main, anchor_lens_id_main, ) atom_equivalence_main = self._composer_graph_atom_equivalence( grounding_source=main_grounding_source, composer_atoms=warm_pack_semantic_domains, graph_atoms=graph_atoms_main, graph_unconstrained=graph_unconstrained_main, ) verdicts_bundle = TurnVerdicts( identity_score=identity_score, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, refusal_emitted=refusal_emitted, hedge_injected=hedge_injected, ) main_epistemic = epistemic_state_for_grounding_source(main_grounding_source) main_epistemic_state = main_epistemic.value # ADR-0206 — Response Governance Bridge seam (cognition path). # govern_response is STRICT-only (scaffold), so shape_surface is the # identity transform and response_surface is returned verbatim — # byte-identical to the pre-bridge path. This is live wiring, not # dead code: the response surface now flows through the policy # consumer, and the ONLY thing keeping it strict is the STRICT # return value (proven by the live-wiring test). The risk-reward # widening pathway and the math-serving seam are deferred to their # own PRs (ADR-0206 §5); wrong==0 is untouched here. main_reach_policy = govern_response(epistemic_state=main_epistemic) main_reach_level = main_reach_policy.level.value response_surface = shape_surface( main_reach_policy, committed_surface=response_surface, decode_state=main_epistemic, ) main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value main_normative_detail = normative_detail_from_verdicts(verdicts_bundle) turn_event = TurnEvent( turn=self._context.turn - 1, input_tokens=tuple(filtered), surface=response_surface, walk_surface=walk_surface, articulation_surface=articulation.surface, dialogue_role=str(dialogue_role), identity_score=identity_score, cycle_cost_total=cycle_cost.total, vault_hits=vault_hits, versor_condition=versor_condition(result.final_state.F), flagged=flagged, elaboration=sentence_plan.elaboration, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, verdicts=verdicts_bundle, grounding_source=main_grounding_source, register_id=register_id_main, register_variant_id=decoration_main.variant_id, anchor_lens_id=anchor_lens_id_main, anchor_lens_mode_label=anchor_lens_mode_label_main, realizer_guard_status=realizer_guard_status_main, realizer_guard_rule=realizer_guard_rule_main, register_canonical_surface=register_canonical_surface_main, composer_graph_atom_status=atom_equivalence_main.status, composer_atom_set_hash=atom_equivalence_main.composer_atom_set_hash, graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash, composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count, epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, normative_detail=main_normative_detail, reach_level=main_reach_level, identity_path=identity_path_ledger, ) self.turn_log.append(turn_event) self._emit_turn_event(turn_event) self._push_thread_summary( turn_event=turn_event, intent_tag=warm_pack_intent_tag, intent_subject=warm_pack_subject or articulation.subject, grounding_source=main_grounding_source, surface=response_surface, ) return self._checkpointed_response( ChatResponse( surface=response_surface, proposition=proposition, articulation=articulation, articulation_surface=articulation.surface, dialogue_role=dialogue_role, versor_condition=versor_condition(result.final_state.F), output_language=self.config.output_language, frame_pack=self.config.frame_pack, walk_surface=walk_surface, salience_top_k=result.salience_top_k, candidates_used=result.candidates_used, vault_hits=vault_hits, identity_score=identity_score, character_profile=self.character_profile, flagged=flagged, recall_energy_class=recall_energy_class_main, admissibility_trace=result.admissibility_trace, region_was_unconstrained=result.region_was_unconstrained, safety_verdict=safety_verdict, ethics_verdict=ethics_verdict, verdicts=verdicts_bundle, grounding_source=main_grounding_source, pre_decoration_surface=pre_decoration_surface_main, register_id=register_id_main, register_variant_id=decoration_main.variant_id, anchor_lens_id=anchor_lens_id_main, anchor_lens_mode_label=anchor_lens_mode_label_main, realizer_guard_status=realizer_guard_status_main, realizer_guard_rule=realizer_guard_rule_main, register_canonical_surface=register_canonical_surface_main, composer_graph_atom_status=atom_equivalence_main.status, composer_atom_set_hash=atom_equivalence_main.composer_atom_set_hash, graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash, composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count, recalled_words=walk_tokens, epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, normative_detail=main_normative_detail, reach_level=main_reach_level, refusal_reason=refusal_surface if refusal_emitted else "", dispatch_trace=dispatch_trace, ) ) def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse: return self._stub_response(field_state) def respond(self, text: str, max_tokens: int | None = None) -> str: """Return only the user-facing surface string for *text*. Convenience wrapper around :meth:`chat` for callers that need the raw surface without ChatResponse provenance — REPLs, simple scripts, and the existing test_language_pack_runtime suite. For audit / telemetry / verdict access, call :meth:`chat`. """ return self.chat(text, max_tokens=max_tokens).surface async def achat(self, text: str, max_tokens: int | None = None) -> ChatResponse: """Async-compatible convenience wrapper around :meth:`chat`. This is a thin async surface; the underlying call is still synchronous CPU-bound work (versor walk, vault recall, surface composition). Use this only for integration with asyncio-based callers that need an awaitable. No real off-thread execution is performed — if true non-blocking concurrency is required, wrap calls in :func:`asyncio.to_thread` at the call site. """ return self.chat(text, max_tokens=max_tokens) async def arespond(self, text: str, max_tokens: int | None = None) -> str: """Async-compatible convenience wrapper around :meth:`respond`. Same caveats as :meth:`achat` — wrapper, not true async. """ return self.respond(text, max_tokens=max_tokens) def correct(self, text: str, target_turn: int = -1, max_tokens: int | None = None) -> ChatResponse: tokens = self._tokenize(text) filtered = self._apply_oov_policy(tokens) if not filtered: raise ValueError("correct() received no in-vocabulary tokens.") correction_state = inject(filtered, self._context.vocab) correction_result = self._correction_pass.apply( self._context.graph, correction_state.F, from_turn=target_turn, ) self._context.apply_corrected_outputs(correction_result.records) self._emit_correction_event(correction_result, target_turn=target_turn) regen_tokens = self._context.last_input_tokens if not regen_tokens: return self._stub_response(correction_state) return self.chat(" ".join(regen_tokens), max_tokens=max_tokens) def _emit_correction_event( self, correction_result, *, target_turn: int, ) -> None: """ADR-0059 — emit one JSONL correction event to the telemetry sink. Any staged turn event is flushed FIRST (audit-ledger R7, I6). A durable stream's consumers may rely on order, and a correction event for turn *N* must appear after turn *N*'s own event. Ordering this deliberately rather than relying on ``correct()``'s call sequence is the point: today ``correct`` emits here and only then calls :meth:`chat`, so the two happen to land in the right order — a fact about one call site, not an invariant. """ self._flush_staged_turn_event() sink = self._telemetry_sink if sink is None: return line = format_correction_event_jsonl( correction_result, target_turn=target_turn, identity_pack_id=self.identity_pack_id, safety_pack_id=self.safety_pack.pack_id, ethics_pack_id=self.ethics_pack_id, ) sink.emit(line)