feat(adr-0056): wire contemplation into live turn path (opt-in)
ChatRuntime.attach_contemplation(enabled=True) flips an opt-in flag; when on, each emitted DiscoveryCandidate runs through teaching.contemplation.contemplate before the sink writes the JSONL line. Default off ⇒ Phase B raw output preserved byte- identical. Trust boundary - Contemplation is read-only over pack + corpus. - Without an attached discovery sink the flag is inert (no hidden work — emission requires an observable destination). - Active teaching corpus on disk byte-identical pre/post. Lanes: smoke 67 / runtime 19 / cognition 121 / contemplation- wiring 6 — all green. Cognition eval unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f78def7f3a
commit
db6ce08589
2 changed files with 190 additions and 0 deletions
|
|
@ -418,6 +418,13 @@ class ChatRuntime:
|
|||
# ``attach_discovery_sink``. Candidates are *evidence*, never
|
||||
# mutate the corpus or runtime state.
|
||||
self._discovery_sink: DiscoveryCandidateSink | None = None
|
||||
# ADR-0056 Phase C1 — opt-in contemplation pass that enriches
|
||||
# each emitted DiscoveryCandidate with polarity / claim_domain /
|
||||
# evidence / sub_questions before the sink writes the JSONL
|
||||
# line. Default False preserves prior behavior (Phase B raw
|
||||
# candidates). Toggling on does NOT mutate the corpus; the
|
||||
# loop is read-only over pack + corpus + (optional) vault.
|
||||
self._contemplate_discoveries: bool = False
|
||||
self._correction_pass = CorrectionPass()
|
||||
self._last_valence: float = 0.0
|
||||
|
||||
|
|
@ -463,6 +470,22 @@ class ChatRuntime:
|
|||
"""
|
||||
self._discovery_sink = sink
|
||||
|
||||
def attach_contemplation(self, *, enabled: bool = True) -> None:
|
||||
"""ADR-0056 Phase C1 — opt-in inline contemplation.
|
||||
|
||||
When enabled, each emitted ``DiscoveryCandidate`` is passed
|
||||
through ``teaching.contemplation.contemplate`` before the
|
||||
sink writes the JSONL line. The sink therefore receives an
|
||||
*enriched* candidate (polarity / claim_domain / evidence /
|
||||
sub_questions populated) instead of the Phase B raw record.
|
||||
|
||||
Read-only over pack + corpus. No corpus mutation, no clock-
|
||||
time read, no LLM step. Requires ``attach_discovery_sink``
|
||||
to have been called first — without a sink there is nowhere
|
||||
to emit, so contemplation would do hidden work.
|
||||
"""
|
||||
self._contemplate_discoveries = bool(enabled)
|
||||
|
||||
def _emit_discovery_candidates(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -480,6 +503,12 @@ class ChatRuntime:
|
|||
intent_subject,
|
||||
grounding_source=grounding_source,
|
||||
)
|
||||
if self._contemplate_discoveries and candidates:
|
||||
# Local import — keeps the contemplation module out of
|
||||
# the runtime hot-path import graph for callers that
|
||||
# never opt in.
|
||||
from teaching.contemplation import contemplate
|
||||
candidates = tuple(contemplate(c) for c in candidates)
|
||||
for candidate in candidates:
|
||||
sink.emit(format_candidate_jsonl(candidate))
|
||||
|
||||
|
|
|
|||
161
tests/test_contemplation_wiring.py
Normal file
161
tests/test_contemplation_wiring.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""ADR-0056 Phase C1 — runtime wiring of the contemplation pass.
|
||||
|
||||
Pinned contracts:
|
||||
|
||||
- ``attach_contemplation`` is opt-in; default off ⇒ Phase B JSONL
|
||||
output is byte-identical to pre-C1 wiring.
|
||||
- When enabled, each emitted candidate is replaced by its
|
||||
contemplated form (Phase C1 fields populated).
|
||||
- Wiring does NOT mutate the active teaching corpus on disk.
|
||||
- Toggling off restores Phase B raw output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
from chat.teaching_grounding import _CORPUS_PATH
|
||||
from generate.intent import IntentTag
|
||||
from teaching.discovery_sink import DiscoveryBufferSink
|
||||
|
||||
|
||||
CORPUS_BYTES_BEFORE = _CORPUS_PATH.read_bytes() if _CORPUS_PATH.exists() else b""
|
||||
|
||||
|
||||
def _find_pack_lemma_without_active_chain() -> tuple[str, IntentTag]:
|
||||
"""Mirror of helper in tests/test_discovery_candidates.py."""
|
||||
from chat.pack_grounding import _pack_index
|
||||
from chat.teaching_grounding import _corpus_index
|
||||
|
||||
pack = _pack_index()
|
||||
corpus = _corpus_index()
|
||||
for lemma in sorted(pack.keys()):
|
||||
if (lemma, "cause") not in corpus:
|
||||
return lemma, IntentTag.CAUSE
|
||||
if (lemma, "verification") not in corpus:
|
||||
return lemma, IntentTag.VERIFICATION
|
||||
pytest.skip("No pack lemma without an active corpus chain")
|
||||
|
||||
|
||||
def _send_prompt_expecting_emission(rt: ChatRuntime, sink: DiscoveryBufferSink) -> dict:
|
||||
lemma, intent_tag = _find_pack_lemma_without_active_chain()
|
||||
if intent_tag is IntentTag.CAUSE:
|
||||
prompt = f"Why does {lemma} matter?"
|
||||
else:
|
||||
prompt = f"Does {lemma} require evidence?"
|
||||
rt.chat(prompt)
|
||||
assert len(sink.lines) == 1, sink.lines
|
||||
return json.loads(sink.lines[0])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default behaviour: contemplation OFF
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_default_does_not_contemplate():
|
||||
"""No call to attach_contemplation ⇒ no C1 fields in the JSONL."""
|
||||
rt = ChatRuntime()
|
||||
sink = DiscoveryBufferSink()
|
||||
rt.attach_discovery_sink(sink)
|
||||
payload = _send_prompt_expecting_emission(rt, sink)
|
||||
|
||||
# Phase B keys only. No C1 fields.
|
||||
assert "polarity" not in payload
|
||||
assert "claim_domain" not in payload
|
||||
assert "evidence" not in payload
|
||||
assert "sub_questions" not in payload
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Opt-in: contemplation ON
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_attach_contemplation_enriches_emitted_candidates():
|
||||
rt = ChatRuntime()
|
||||
sink = DiscoveryBufferSink()
|
||||
rt.attach_discovery_sink(sink)
|
||||
rt.attach_contemplation(enabled=True)
|
||||
payload = _send_prompt_expecting_emission(rt, sink)
|
||||
|
||||
# C1 fields present.
|
||||
assert "polarity" in payload
|
||||
assert "claim_domain" in payload
|
||||
assert "evidence" in payload
|
||||
assert "sub_questions" in payload
|
||||
assert payload["polarity"] in ("affirms", "falsifies", "undetermined")
|
||||
assert payload["claim_domain"] in ("factual", "relational", "evaluative")
|
||||
|
||||
|
||||
def test_attach_contemplation_then_disable_restores_raw():
|
||||
rt = ChatRuntime()
|
||||
sink = DiscoveryBufferSink()
|
||||
rt.attach_discovery_sink(sink)
|
||||
rt.attach_contemplation(enabled=True)
|
||||
rt.attach_contemplation(enabled=False)
|
||||
payload = _send_prompt_expecting_emission(rt, sink)
|
||||
assert "polarity" not in payload
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trust boundary: contemplation never mutates the corpus
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_inline_contemplation_does_not_mutate_corpus():
|
||||
rt = ChatRuntime()
|
||||
sink = DiscoveryBufferSink()
|
||||
rt.attach_discovery_sink(sink)
|
||||
rt.attach_contemplation(enabled=True)
|
||||
_ = _send_prompt_expecting_emission(rt, sink)
|
||||
after = _CORPUS_PATH.read_bytes() if _CORPUS_PATH.exists() else b""
|
||||
assert after == CORPUS_BYTES_BEFORE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Determinism: same prompt twice ⇒ identical contemplated payload
|
||||
# (modulo Phase B fields which are already deterministic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_contemplated_emission_deterministic_across_calls():
|
||||
rt1 = ChatRuntime()
|
||||
sink1 = DiscoveryBufferSink()
|
||||
rt1.attach_discovery_sink(sink1)
|
||||
rt1.attach_contemplation(enabled=True)
|
||||
p1 = _send_prompt_expecting_emission(rt1, sink1)
|
||||
|
||||
rt2 = ChatRuntime()
|
||||
sink2 = DiscoveryBufferSink()
|
||||
rt2.attach_discovery_sink(sink2)
|
||||
rt2.attach_contemplation(enabled=True)
|
||||
p2 = _send_prompt_expecting_emission(rt2, sink2)
|
||||
|
||||
# Phase B fields + C1 fields must match. Source trace hash is
|
||||
# content-derived so it is stable across fresh runtimes given
|
||||
# identical prompts.
|
||||
assert p1 == p2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# No sink ⇒ no contemplation (would be hidden work)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_no_sink_no_contemplation_work():
|
||||
"""attach_contemplation alone does nothing without a sink — the
|
||||
work would be unobservable."""
|
||||
rt = ChatRuntime()
|
||||
rt.attach_contemplation(enabled=True)
|
||||
# No sink attached; sending a fall-through prompt must not raise
|
||||
# and must produce no candidate emission.
|
||||
lemma, intent_tag = _find_pack_lemma_without_active_chain()
|
||||
if intent_tag is IntentTag.CAUSE:
|
||||
prompt = f"Why does {lemma} matter?"
|
||||
else:
|
||||
prompt = f"Does {lemma} require evidence?"
|
||||
rt.chat(prompt) # no exception
|
||||
Loading…
Reference in a new issue