feat(W-018): autonomous inter-session contemplation at checkpoint (ADR-0150)
This commit is contained in:
parent
0b940674c0
commit
61b88b5182
4 changed files with 140 additions and 1 deletions
|
|
@ -177,6 +177,13 @@ def _build_vault_probe(vault, vocab):
|
||||||
return _probe
|
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:
|
def _energy_scalar(energy_obj) -> float:
|
||||||
if energy_obj is None:
|
if energy_obj is None:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
@ -646,7 +653,15 @@ class ChatRuntime:
|
||||||
if store is None:
|
if store is None:
|
||||||
return
|
return
|
||||||
store.save_recognizers(self._recognizer_registry.all())
|
store.save_recognizers(self._recognizer_registry.all())
|
||||||
store.save_discovery_candidates(self._pending_candidates)
|
candidates_to_save = self._pending_candidates
|
||||||
|
if self.config.auto_contemplate and candidates_to_save:
|
||||||
|
from teaching.contemplation import contemplate
|
||||||
|
vault_probe = _vault_probe_for_context(self._context) if self._context else None
|
||||||
|
candidates_to_save = [
|
||||||
|
contemplate(c, vault_probe=vault_probe)
|
||||||
|
for c in candidates_to_save
|
||||||
|
]
|
||||||
|
store.save_discovery_candidates(candidates_to_save)
|
||||||
store.save_manifest(self._turn_count)
|
store.save_manifest(self._turn_count)
|
||||||
|
|
||||||
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:
|
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,10 @@ class RuntimeConfig:
|
||||||
# Unlocks W-007 (DerivedRecognizer derivation from promoted COHERENT entries).
|
# Unlocks W-007 (DerivedRecognizer derivation from promoted COHERENT entries).
|
||||||
vault_promotion_enabled: bool = False
|
vault_promotion_enabled: bool = False
|
||||||
|
|
||||||
|
# ADR-0150 — run contemplation on pending discovery candidates at checkpoint.
|
||||||
|
# Activates ADR-0056 Phase C1. Null-drop when False.
|
||||||
|
auto_contemplate: bool = False
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
|
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
|
||||||
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"
|
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# ADR-0150 — Autonomous Inter-Session Contemplation
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
Date: 2026-05-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
ADR-0056 Phase C1 shipped `contemplate()` as a pure function that enriches
|
||||||
|
DiscoveryCandidate with polarity, evidence, claim_domain, and sub_questions.
|
||||||
|
It ran inline (opt-in via attach_contemplation) or via CLI batch. Neither path
|
||||||
|
ran at session boundaries. Engine state (ADR-0146) persists discovery candidates
|
||||||
|
to disk, but stored candidates were unenriched (raw Phase B output).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Run `contemplate()` on pending session candidates at `checkpoint_engine_state()`
|
||||||
|
before persisting to `engine_state/discovery_candidates.jsonl`. Enriched
|
||||||
|
candidates (polarity/evidence/claim_domain populated) are stored instead of
|
||||||
|
raw ones.
|
||||||
|
|
||||||
|
Flag: `RuntimeConfig.auto_contemplate = False` (null-drop default).
|
||||||
|
|
||||||
|
## Trust boundary
|
||||||
|
`contemplate()` is read-only w.r.t. corpus, pack, and vault per ADR-0056.
|
||||||
|
It enriches the in-memory candidate struct only. Nothing is written to any
|
||||||
|
shared store during enrichment.
|
||||||
|
|
||||||
|
## Why checkpoint, not inline
|
||||||
|
Fresh candidates are produced during the turn and accumulated in
|
||||||
|
`_pending_candidates`. Contemplation at checkpoint runs after the session
|
||||||
|
completes, not on the hot turn path. This avoids blocking turn latency.
|
||||||
|
|
||||||
|
## Unlocks
|
||||||
|
W-017: auto-proposal pipeline can filter enriched candidates (polarity,
|
||||||
|
evidence) to generate TeachingChainProposals.
|
||||||
87
tests/test_adr_0150_autonomous_contemplation.py
Normal file
87
tests/test_adr_0150_autonomous_contemplation.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from core.config import RuntimeConfig
|
||||||
|
from chat.runtime import ChatRuntime
|
||||||
|
from engine_state import EngineStateStore
|
||||||
|
from teaching.discovery import DiscoveryCandidate, EvidencePointer, SubQuestion
|
||||||
|
from chat.pack_grounding import _pack_index
|
||||||
|
from chat.teaching_grounding import _corpus_index
|
||||||
|
from teaching.contemplation import contemplate
|
||||||
|
|
||||||
|
def _raw_candidate() -> DiscoveryCandidate:
|
||||||
|
return DiscoveryCandidate(
|
||||||
|
candidate_id="cand-1",
|
||||||
|
proposed_chain={"subject": "light", "intent": "verification", "connective": None, "object": None},
|
||||||
|
trigger="would_have_grounded",
|
||||||
|
source_turn_trace="trace-1",
|
||||||
|
pack_consistent=True,
|
||||||
|
boundary_clean=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_auto_contemplate_off_stores_raw_candidates(tmp_path: Path) -> None:
|
||||||
|
config = RuntimeConfig(auto_contemplate=False)
|
||||||
|
runtime = ChatRuntime(config=config, engine_state_path=tmp_path)
|
||||||
|
|
||||||
|
cand = _raw_candidate()
|
||||||
|
runtime._pending_candidates.append(cand)
|
||||||
|
runtime.checkpoint_engine_state()
|
||||||
|
|
||||||
|
store = EngineStateStore(tmp_path)
|
||||||
|
loaded = store.load_discovery_candidates()
|
||||||
|
assert len(loaded) == 1
|
||||||
|
assert loaded[0].polarity == "undetermined"
|
||||||
|
assert len(loaded[0].evidence) == 0
|
||||||
|
|
||||||
|
def test_auto_contemplate_enriches_at_checkpoint(tmp_path: Path) -> None:
|
||||||
|
config = RuntimeConfig(auto_contemplate=True)
|
||||||
|
runtime = ChatRuntime(config=config, engine_state_path=tmp_path)
|
||||||
|
|
||||||
|
cand = _raw_candidate()
|
||||||
|
runtime._pending_candidates.append(cand)
|
||||||
|
runtime.checkpoint_engine_state()
|
||||||
|
|
||||||
|
# Clear pending candidates and load from disk
|
||||||
|
runtime._pending_candidates = []
|
||||||
|
runtime._load_engine_state()
|
||||||
|
|
||||||
|
assert len(runtime._pending_candidates) == 1
|
||||||
|
loaded_cand = runtime._pending_candidates[0]
|
||||||
|
|
||||||
|
# Verify loaded candidate has claim_domain and evidence populated (not defaults)
|
||||||
|
assert loaded_cand.claim_domain == "factual"
|
||||||
|
assert len(loaded_cand.evidence) > 0
|
||||||
|
# The default polarity is undetermined, but evidence is populated
|
||||||
|
assert any(e.source == "pack" and e.ref == "light" for e in loaded_cand.evidence)
|
||||||
|
|
||||||
|
def test_enriched_candidates_survive_jsonl_round_trip(tmp_path: Path) -> None:
|
||||||
|
cand = _raw_candidate()
|
||||||
|
enriched = contemplate(cand)
|
||||||
|
|
||||||
|
store = EngineStateStore(tmp_path)
|
||||||
|
store.save_discovery_candidates([enriched])
|
||||||
|
|
||||||
|
loaded = store.load_discovery_candidates()
|
||||||
|
assert len(loaded) == 1
|
||||||
|
loaded_cand = loaded[0]
|
||||||
|
|
||||||
|
assert loaded_cand.polarity == enriched.polarity
|
||||||
|
assert loaded_cand.claim_domain == enriched.claim_domain
|
||||||
|
assert loaded_cand.evidence == enriched.evidence
|
||||||
|
assert loaded_cand.sub_questions == enriched.sub_questions
|
||||||
|
assert loaded_cand.contemplation_depth == enriched.contemplation_depth
|
||||||
|
assert loaded_cand.recursion_overflow == enriched.recursion_overflow
|
||||||
|
|
||||||
|
def test_contemplate_does_not_write_corpus_or_pack() -> None:
|
||||||
|
corpus_before = dict(_corpus_index())
|
||||||
|
pack_before = dict(_pack_index())
|
||||||
|
|
||||||
|
cand = _raw_candidate()
|
||||||
|
_ = contemplate(cand)
|
||||||
|
|
||||||
|
corpus_after = dict(_corpus_index())
|
||||||
|
pack_after = dict(_pack_index())
|
||||||
|
|
||||||
|
assert corpus_before == corpus_after
|
||||||
|
assert pack_before == pack_after
|
||||||
Loading…
Reference in a new issue