feat(W-007): wire DerivedRecognizer registry into CognitiveTurnPipeline (ADR-0149) (#274)
- RecognizerRegistry.first_admitted() — deterministic first-registered selection - CognitiveTurnPipeline consults runtime registry when no recognizer explicitly passed - ChatRuntime gains _pending_recognizer_examples + record_recognition_example() - checkpoint_engine_state() derives and registers recognizer from accumulated examples - RuntimeConfig.recognition_grounded_graph gate (already existed) controls wiring - ADR-0149 decision record
This commit is contained in:
parent
5152719613
commit
81718a0952
5 changed files with 247 additions and 1 deletions
|
|
@ -51,6 +51,8 @@ from teaching.discovery import (
|
||||||
)
|
)
|
||||||
from teaching.discovery_sink import DiscoveryCandidateSink
|
from teaching.discovery_sink import DiscoveryCandidateSink
|
||||||
from engine_state import EngineStateStore
|
from engine_state import EngineStateStore
|
||||||
|
from recognition.anti_unifier import derive_recognizer
|
||||||
|
from recognition.outcome import FeatureBundle
|
||||||
from recognition.registry import RecognizerRegistry
|
from recognition.registry import RecognizerRegistry
|
||||||
from core.config import DEFAULT_CONFIG, DEFAULT_IDENTITY_PACK, RuntimeConfig
|
from core.config import DEFAULT_CONFIG, DEFAULT_IDENTITY_PACK, RuntimeConfig
|
||||||
from core.physics.drive import DriveGradientMap, GradientField
|
from core.physics.drive import DriveGradientMap, GradientField
|
||||||
|
|
@ -634,6 +636,9 @@ class ChatRuntime:
|
||||||
self._recognizer_registry: RecognizerRegistry = RecognizerRegistry()
|
self._recognizer_registry: RecognizerRegistry = RecognizerRegistry()
|
||||||
self._turn_count: int = 0
|
self._turn_count: int = 0
|
||||||
self._pending_candidates: list[DiscoveryCandidate] = []
|
self._pending_candidates: list[DiscoveryCandidate] = []
|
||||||
|
self._pending_recognizer_examples: list[
|
||||||
|
tuple[tuple[str, ...], FeatureBundle]
|
||||||
|
] = []
|
||||||
if self._engine_state_store is not None and self._engine_state_store.exists():
|
if self._engine_state_store is not None and self._engine_state_store.exists():
|
||||||
self._load_engine_state()
|
self._load_engine_state()
|
||||||
|
|
||||||
|
|
@ -652,6 +657,13 @@ class ChatRuntime:
|
||||||
store = self._engine_state_store
|
store = self._engine_state_store
|
||||||
if store is None:
|
if store is None:
|
||||||
return
|
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()
|
||||||
store.save_recognizers(self._recognizer_registry.all())
|
store.save_recognizers(self._recognizer_registry.all())
|
||||||
candidates_to_save = self._pending_candidates
|
candidates_to_save = self._pending_candidates
|
||||||
if self.config.auto_contemplate and candidates_to_save:
|
if self.config.auto_contemplate and candidates_to_save:
|
||||||
|
|
@ -664,6 +676,18 @@ class ChatRuntime:
|
||||||
store.save_discovery_candidates(candidates_to_save)
|
store.save_discovery_candidates(candidates_to_save)
|
||||||
store.save_manifest(self._turn_count)
|
store.save_manifest(self._turn_count)
|
||||||
|
|
||||||
|
def record_recognition_example(
|
||||||
|
self,
|
||||||
|
tokens: tuple[str, ...],
|
||||||
|
bundle: FeatureBundle,
|
||||||
|
) -> None:
|
||||||
|
self._pending_recognizer_examples.append((tuple(tokens), bundle))
|
||||||
|
|
||||||
|
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:
|
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:
|
||||||
self._turn_count += 1
|
self._turn_count += 1
|
||||||
self.checkpoint_engine_state()
|
self.checkpoint_engine_state()
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,12 @@ class CognitiveTurnPipeline:
|
||||||
self.runtime = runtime
|
self.runtime = runtime
|
||||||
self._last_node_id: str | None = None
|
self._last_node_id: str | None = None
|
||||||
self.teaching_store = teaching_store or TeachingStore()
|
self.teaching_store = teaching_store or TeachingStore()
|
||||||
self._recognizer = recognizer
|
if recognizer is not None:
|
||||||
|
self._recognizer = recognizer
|
||||||
|
elif hasattr(runtime, "first_admitted_recognizer"):
|
||||||
|
self._recognizer = runtime.first_admitted_recognizer()
|
||||||
|
else:
|
||||||
|
self._recognizer = None
|
||||||
self._prior_surface: str | None = None
|
self._prior_surface: str | None = None
|
||||||
self._turn_number: int = 0
|
self._turn_number: int = 0
|
||||||
# ADR-0021 §Articulation: subjects of prior SPECULATIVE teaching
|
# ADR-0021 §Articulation: subjects of prior SPECULATIVE teaching
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# ADR-0149 — Integrate DerivedRecognizer into CognitiveTurnPipeline
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-05-25
|
||||||
|
**Work item:** W-007
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0143 introduced deterministic `DerivedRecognizer` derivation and matching.
|
||||||
|
ADR-0144 gave `CognitiveTurnPipeline` an epistemic graph carrier and an optional
|
||||||
|
`recognizer` constructor slot. ADR-0146 added persisted engine state, including
|
||||||
|
`RecognizerRegistry`. ADR-0148 / W-003 wired vault promotion so `COHERENT`
|
||||||
|
entries can eventually become recognition evidence.
|
||||||
|
|
||||||
|
The missing edge was live admission: the runtime had a registry, but the
|
||||||
|
pipeline never consulted it. A populated registry was therefore inert unless a
|
||||||
|
test or caller manually passed a recognizer to `CognitiveTurnPipeline`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
`RecognizerRegistry.first_admitted()` returns the first registered recognizer in
|
||||||
|
deterministic insertion order, or `None` when empty.
|
||||||
|
|
||||||
|
`ChatRuntime` now exposes `first_admitted_recognizer()`, gated by
|
||||||
|
`RuntimeConfig.recognition_grounded_graph`. When the flag is false, the method
|
||||||
|
returns `None` and the turn path is byte-behavior compatible with the previous
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
`CognitiveTurnPipeline` uses an explicitly supplied recognizer first. When no
|
||||||
|
recognizer is supplied, it asks the runtime for the first admitted recognizer.
|
||||||
|
If the registry is empty, recognition remains absent and the existing
|
||||||
|
intent-derived graph path is used.
|
||||||
|
|
||||||
|
`ChatRuntime.record_recognition_example(tokens, bundle)` records deterministic
|
||||||
|
training pairs for test harnesses and future automated collection. At
|
||||||
|
`checkpoint_engine_state()`, after the vault promotion boundary, a non-empty
|
||||||
|
pending example set is passed to `derive_recognizer()` and the result is
|
||||||
|
registered before engine-state persistence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Null-Drop
|
||||||
|
|
||||||
|
`recognition_grounded_graph=False` means the registry is ignored, no recognizer
|
||||||
|
is passed to the pipeline, and pending examples are not derived at checkpoint.
|
||||||
|
The default therefore preserves the previous turn behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Follow-Up
|
||||||
|
|
||||||
|
Automated example collection is intentionally out of scope. `derive_recognizer()`
|
||||||
|
requires `(TokenSequence, FeatureBundle)` pairs with span-level evidence, not
|
||||||
|
teaching corpus templates. The follow-up path is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
finalize_turn -> FeatureBundle with evidence spans -> record_recognition_example -> checkpoint derivation
|
||||||
|
```
|
||||||
|
|
||||||
|
That follow-up makes the registry self-populating; this ADR makes the registry
|
||||||
|
live and replay-persistent.
|
||||||
|
|
@ -23,6 +23,12 @@ class RecognizerRegistry:
|
||||||
def all(self) -> list[DerivedRecognizer]:
|
def all(self) -> list[DerivedRecognizer]:
|
||||||
return list(self._registry.values())
|
return list(self._registry.values())
|
||||||
|
|
||||||
|
def first_admitted(self) -> DerivedRecognizer | None:
|
||||||
|
"""Return the first registered recognizer, or None if registry is empty."""
|
||||||
|
if not self._registry:
|
||||||
|
return None
|
||||||
|
return next(iter(self._registry.values()))
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self._registry)
|
return len(self._registry)
|
||||||
|
|
||||||
|
|
|
||||||
146
tests/test_adr_0149_recognizer_pipeline_wiring.py
Normal file
146
tests/test_adr_0149_recognizer_pipeline_wiring.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
from chat.runtime import ChatRuntime
|
||||||
|
from core.cognition import CognitiveTurnPipeline
|
||||||
|
from core.config import DEFAULT_CONFIG
|
||||||
|
from engine_state import EngineStateStore
|
||||||
|
from recognition.anti_unifier import derive_recognizer
|
||||||
|
from recognition.outcome import EvidenceSpan, FeatureBundle, NegativeEvidence
|
||||||
|
from recognition.registry import RecognizerRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def _config(*, recognition_grounded_graph: bool):
|
||||||
|
return replace(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
recognition_grounded_graph=recognition_grounded_graph,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _span(tokens: tuple[str, ...], start: int, end: int) -> EvidenceSpan:
|
||||||
|
return EvidenceSpan(start=start, end=end, text=" ".join(tokens[start:end]))
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle(
|
||||||
|
tokens: tuple[str, ...],
|
||||||
|
agent_span: tuple[int, int],
|
||||||
|
count_span: tuple[int, int],
|
||||||
|
unit_span: tuple[int, int],
|
||||||
|
agent: str,
|
||||||
|
count: int,
|
||||||
|
unit: str,
|
||||||
|
) -> FeatureBundle:
|
||||||
|
return FeatureBundle.from_mapping(
|
||||||
|
{
|
||||||
|
"agent": (agent, _span(tokens, *agent_span)),
|
||||||
|
"count": (count, _span(tokens, *count_span)),
|
||||||
|
"intentionality": (
|
||||||
|
"possession",
|
||||||
|
_span(
|
||||||
|
tokens,
|
||||||
|
1 if tokens[0] in {"A", "The"} else 0,
|
||||||
|
3 if tokens[0] in {"A", "The"} else 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"modality": (
|
||||||
|
"actual",
|
||||||
|
NegativeEvidence(0, len(tokens), "no modal counter-marker present"),
|
||||||
|
),
|
||||||
|
"polarity": ("+", NegativeEvidence(0, len(tokens), "no negator present")),
|
||||||
|
"relation": ("has", _span(tokens, count_span[0] - 1, count_span[0])),
|
||||||
|
"tense": ("present", _span(tokens, count_span[0] - 1, count_span[0])),
|
||||||
|
"unit": (unit, _span(tokens, *unit_span)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _examples() -> list[tuple[tuple[str, ...], FeatureBundle]]:
|
||||||
|
john = ("John", "has", "5", "apples")
|
||||||
|
mary = ("Mary", "has", "3", "books")
|
||||||
|
school = ("A", "school", "has", "100", "students")
|
||||||
|
library = ("The", "library", "has", "12", "chairs")
|
||||||
|
return [
|
||||||
|
(john, _bundle(john, (0, 1), (2, 3), (3, 4), "John", 5, "apple")),
|
||||||
|
(mary, _bundle(mary, (0, 1), (2, 3), (3, 4), "Mary", 3, "book")),
|
||||||
|
(
|
||||||
|
school,
|
||||||
|
_bundle(school, (1, 2), (3, 4), (4, 5), "school", 100, "student"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
library,
|
||||||
|
_bundle(library, (1, 2), (3, 4), (4, 5), "library", 12, "chair"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _recognizer():
|
||||||
|
return derive_recognizer(_examples())
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_empty_no_recognizer_passed(tmp_path) -> None:
|
||||||
|
runtime = ChatRuntime(
|
||||||
|
config=_config(recognition_grounded_graph=True),
|
||||||
|
engine_state_path=tmp_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = CognitiveTurnPipeline(runtime).run("A baker has 24 loaves", max_tokens=4)
|
||||||
|
|
||||||
|
assert result.epistemic_graph is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_with_recognizer_wires_into_pipeline(tmp_path) -> None:
|
||||||
|
runtime = ChatRuntime(
|
||||||
|
config=_config(recognition_grounded_graph=True),
|
||||||
|
engine_state_path=tmp_path,
|
||||||
|
)
|
||||||
|
recognizer = _recognizer()
|
||||||
|
runtime._recognizer_registry.register(recognizer)
|
||||||
|
|
||||||
|
result = CognitiveTurnPipeline(runtime).run("A baker has 24 loaves", max_tokens=4)
|
||||||
|
|
||||||
|
assert result.epistemic_graph is not None
|
||||||
|
assert result.epistemic_graph.recognizer_id == recognizer.teaching_set_id
|
||||||
|
assert result.epistemic_graph.nodes[0].node_id.startswith(
|
||||||
|
f"{recognizer.teaching_set_id}:"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flag_off_registry_ignored(tmp_path) -> None:
|
||||||
|
runtime = ChatRuntime(
|
||||||
|
config=_config(recognition_grounded_graph=False),
|
||||||
|
engine_state_path=tmp_path,
|
||||||
|
)
|
||||||
|
runtime._recognizer_registry.register(_recognizer())
|
||||||
|
|
||||||
|
result = CognitiveTurnPipeline(runtime).run("A baker has 24 loaves", max_tokens=4)
|
||||||
|
|
||||||
|
assert result.epistemic_graph is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_admitted_returns_none_on_empty() -> None:
|
||||||
|
assert RecognizerRegistry().first_admitted() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_admitted_returns_registered() -> None:
|
||||||
|
registry = RecognizerRegistry()
|
||||||
|
recognizer = _recognizer()
|
||||||
|
|
||||||
|
registry.register(recognizer)
|
||||||
|
|
||||||
|
assert registry.first_admitted() == recognizer
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_and_checkpoint_derives_recognizer(tmp_path) -> None:
|
||||||
|
runtime = ChatRuntime(
|
||||||
|
config=_config(recognition_grounded_graph=True),
|
||||||
|
engine_state_path=tmp_path,
|
||||||
|
)
|
||||||
|
for tokens, bundle in _examples():
|
||||||
|
runtime.record_recognition_example(tokens, bundle)
|
||||||
|
|
||||||
|
runtime.checkpoint_engine_state()
|
||||||
|
|
||||||
|
assert len(runtime._recognizer_registry) == 1
|
||||||
|
persisted = EngineStateStore(tmp_path).load_recognizers()
|
||||||
|
assert persisted == runtime._recognizer_registry.all()
|
||||||
Loading…
Reference in a new issue