Compare commits

...

1 commit

4 changed files with 140 additions and 1 deletions

View file

@ -177,6 +177,13 @@ def _build_vault_probe(vault, vocab):
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
@ -646,7 +653,15 @@ class ChatRuntime:
if store is None:
return
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)
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:

View file

@ -269,6 +269,10 @@ class RuntimeConfig:
# Unlocks W-007 (DerivedRecognizer derivation from promoted COHERENT entries).
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_ETHICS_PACK: str = "default_general_ethics_v1"

View file

@ -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.

View 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