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
|
# ``attach_discovery_sink``. Candidates are *evidence*, never
|
||||||
# mutate the corpus or runtime state.
|
# mutate the corpus or runtime state.
|
||||||
self._discovery_sink: DiscoveryCandidateSink | None = None
|
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._correction_pass = CorrectionPass()
|
||||||
self._last_valence: float = 0.0
|
self._last_valence: float = 0.0
|
||||||
|
|
||||||
|
|
@ -463,6 +470,22 @@ class ChatRuntime:
|
||||||
"""
|
"""
|
||||||
self._discovery_sink = sink
|
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(
|
def _emit_discovery_candidates(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
@ -480,6 +503,12 @@ class ChatRuntime:
|
||||||
intent_subject,
|
intent_subject,
|
||||||
grounding_source=grounding_source,
|
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:
|
for candidate in candidates:
|
||||||
sink.emit(format_candidate_jsonl(candidate))
|
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