Compare commits
1 commit
add-ask-ac
...
feat/W-018
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b88b5182 |
4 changed files with 140 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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