feat(runtime): read-only proposal-review sub-pass in idle_tick (IT-b)
core/config.py: review_pending_proposals: bool = False (opt-in, mirrors consolidate_determinations). chat/runtime.py: IdleTickResult gains an additive optional proposal_review field; idle_tick runs a READ-ONLY sub-pass (after consolidation, gated) that surfaces core.proposal_review.idle_summary(). It does NOT set did_work / checkpoint / mutate / ratify / mount / modify readers; a reporter exception is CAPTURED (safe=False, errors=('proposal_review_failed:<type>',)), never propagated.
docs/runtime_contracts.md documents the sub-pass + field (required for runtime-surface changes). Contract tests: default-off -> proposal_review None (existing shape preserved); enabled -> summary surfaced; exception captured not propagated; read-only review never checkpoints; other passes unperturbed. 91-test smoke green incl. architectural invariants + both existing idle contract suites. Existing idle_tick remains the only idle_tick; NOT L10 always-on.
This commit is contained in:
parent
65c8587207
commit
32385d954c
4 changed files with 120 additions and 1 deletions
|
|
@ -38,6 +38,7 @@ from core.epistemic_state import (
|
||||||
epistemic_state_for_grounding_source,
|
epistemic_state_for_grounding_source,
|
||||||
normative_detail_from_verdicts,
|
normative_detail_from_verdicts,
|
||||||
)
|
)
|
||||||
|
from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
|
||||||
from core.response_governance import govern_response, shape_surface
|
from core.response_governance import govern_response, shape_surface
|
||||||
from chat.telemetry import (
|
from chat.telemetry import (
|
||||||
TurnEventSink,
|
TurnEventSink,
|
||||||
|
|
@ -534,7 +535,9 @@ class IdleTickResult:
|
||||||
The proposal pass is PROPOSAL-ONLY learning, never ratification (HITL ratifies). The
|
The proposal pass is PROPOSAL-ONLY learning, never ratification (HITL ratifies). The
|
||||||
Step D consolidation pass is SESSION-memory learning: ``facts_consolidated`` derived
|
Step D consolidation pass is SESSION-memory learning: ``facts_consolidated`` derived
|
||||||
facts were written back into the held self so the next ``determine`` reaches them
|
facts were written back into the held self so the next ``determine`` reaches them
|
||||||
directly — still never corpus mutation, never a proposal.
|
directly — still never corpus mutation, never a proposal. The proposal-review sub-pass
|
||||||
|
(``proposal_review``) is READ-ONLY: it surfaces pending comprehension-failure proposals for
|
||||||
|
review and mutates nothing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
candidates_contemplated: int
|
candidates_contemplated: int
|
||||||
|
|
@ -543,6 +546,9 @@ class IdleTickResult:
|
||||||
#: Step D — derived facts consolidated into the held self this tick (0 unless
|
#: Step D — derived facts consolidated into the held self this tick (0 unless
|
||||||
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
|
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
|
||||||
facts_consolidated: int = 0
|
facts_consolidated: int = 0
|
||||||
|
#: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``).
|
||||||
|
#: Surfaces pending comprehension-failure proposals for review; mutates nothing.
|
||||||
|
proposal_review: ProposalReviewIdleSummary | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChatRuntime:
|
class ChatRuntime:
|
||||||
|
|
@ -919,6 +925,21 @@ class ChatRuntime:
|
||||||
facts_consolidated = consolidate_once(self._context).consolidated
|
facts_consolidated = consolidate_once(self._context).consolidated
|
||||||
did_work = True
|
did_work = True
|
||||||
|
|
||||||
|
# 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure
|
||||||
|
# proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an
|
||||||
|
# artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies
|
||||||
|
# or mounts. A reporter failure is CAPTURED, not propagated, so it cannot corrupt the
|
||||||
|
# tick. Gated off by default.
|
||||||
|
proposal_review = None
|
||||||
|
if self.config.review_pending_proposals:
|
||||||
|
try:
|
||||||
|
proposal_review = idle_summary()
|
||||||
|
except Exception as exc: # noqa: BLE001 — a reporter failure must not corrupt the tick
|
||||||
|
proposal_review = ProposalReviewIdleSummary(
|
||||||
|
safe=False, total=0, review_needed=0, malformed=0, by_family=(),
|
||||||
|
errors=(f"proposal_review_failed:{type(exc).__name__}",),
|
||||||
|
)
|
||||||
|
|
||||||
# Persist the advanced state once (backlog + lineage +, with
|
# Persist the advanced state once (backlog + lineage +, with
|
||||||
# persist_session_state, the consolidated facts). Skipped on a no-op tick so an
|
# persist_session_state, the consolidated facts). Skipped on a no-op tick so an
|
||||||
# idle engine with nothing to learn does not churn the checkpoint.
|
# idle engine with nothing to learn does not churn the checkpoint.
|
||||||
|
|
@ -929,6 +950,7 @@ class ChatRuntime:
|
||||||
proposals_created=created,
|
proposals_created=created,
|
||||||
pending_proposals=self._count_pending_proposals(),
|
pending_proposals=self._count_pending_proposals(),
|
||||||
facts_consolidated=facts_consolidated,
|
facts_consolidated=facts_consolidated,
|
||||||
|
proposal_review=proposal_review,
|
||||||
)
|
)
|
||||||
|
|
||||||
def record_recognition_example(
|
def record_recognition_example(
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,13 @@ class RuntimeConfig:
|
||||||
# same _SUBSUMPTION_SUBSET_FACT_BUDGET; converges (a saturated tick is a no-op).
|
# same _SUBSUMPTION_SUBSET_FACT_BUDGET; converges (a saturated tick is a no-op).
|
||||||
consolidate_determinations: bool = False
|
consolidate_determinations: bool = False
|
||||||
|
|
||||||
|
# IT (proposal-review surface) — when on, idle_tick runs a READ-ONLY sub-pass that scans
|
||||||
|
# the comprehension-failure proposal sink and surfaces a summary in
|
||||||
|
# IdleTickResult.proposal_review. It NEVER mutates an artifact, sets did_work, checkpoints,
|
||||||
|
# ratifies, mounts, or modifies readers; a reporter failure is captured, not propagated.
|
||||||
|
# OFF by default — idle ticks don't pay for the scan unless a deployment wants the surface.
|
||||||
|
review_pending_proposals: bool = False
|
||||||
|
|
||||||
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
|
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
|
||||||
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
|
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
|
||||||
# IF the predicate-class has earned the SERVE license on the ratified, committed
|
# IF the predicate-class has earned the SERVE license on the ratified, committed
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,26 @@ Contract:
|
||||||
`algebra/versor.py` keeps closure. Off by default; the falsification lane is
|
`algebra/versor.py` keeps closure. Off by default; the falsification lane is
|
||||||
`evals.determination_closure`.
|
`evals.determination_closure`.
|
||||||
|
|
||||||
|
### Idle proposal review (read-only)
|
||||||
|
|
||||||
|
When `review_pending_proposals` is enabled, `ChatRuntime.idle_tick` runs a **read-only**
|
||||||
|
sub-pass (after the consolidation pass) that scans the comprehension-failure proposal sink
|
||||||
|
(`teaching/proposals/comprehension_failures/`, the contemplation pass's proposal-only
|
||||||
|
artifacts) and surfaces a summary in `IdleTickResult.proposal_review` (`safe`, `total`,
|
||||||
|
`review_needed`, `malformed`, `by_family`, `errors`).
|
||||||
|
|
||||||
|
Contract:
|
||||||
|
|
||||||
|
- **Read-only, no mutation.** The sub-pass scans, reports, and dry-checks; it writes, moves,
|
||||||
|
and deletes nothing. It does **not** set `did_work`, so it never triggers a checkpoint, and
|
||||||
|
it never advances learning, ratifies, mounts, or modifies a reader.
|
||||||
|
- **Failure-isolated.** A reporter exception is **captured, not propagated** — surfaced as
|
||||||
|
`proposal_review.safe == False` with `errors == ("proposal_review_failed:<type>",)` — so a
|
||||||
|
malformed sink or filesystem error can never corrupt the idle tick's state or return.
|
||||||
|
- **Default off, additive.** Disabled, `proposal_review` is `None` and `IdleTickResult` is
|
||||||
|
unchanged for existing callers. This is **not** a second idle loop and **not** the L10
|
||||||
|
always-on heartbeat — it only surfaces existing review obligations.
|
||||||
|
|
||||||
### Estimation surface (Step E — ESTIMATION)
|
### Estimation surface (Step E — ESTIMATION)
|
||||||
|
|
||||||
When `estimation_enabled` and a turn is a **converse query** DETERMINE refused (told
|
When `estimation_enabled` and a turn is a **converse query** DETERMINE refused (told
|
||||||
|
|
|
||||||
70
tests/test_idle_proposal_review.py
Normal file
70
tests/test_idle_proposal_review.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Contract tests for the read-only proposal-review sub-pass of idle_tick (IT-b).
|
||||||
|
|
||||||
|
Pins the runtime contract: default-off preserves the existing IdleTickResult shape/behavior;
|
||||||
|
enabled surfaces a summary; a reporter exception is CAPTURED (safe=False) and never corrupts
|
||||||
|
the tick; the read-only sub-pass never sets did_work / checkpoints / mutates / creates proposals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import chat.runtime as rtmod
|
||||||
|
from chat.runtime import ChatRuntime
|
||||||
|
from core.config import RuntimeConfig
|
||||||
|
from core.proposal_review.summary import ProposalReviewIdleSummary
|
||||||
|
|
||||||
|
_OK = ProposalReviewIdleSummary(safe=True, total=0, review_needed=0, malformed=0, by_family=())
|
||||||
|
|
||||||
|
|
||||||
|
def _rt(tmp_path: Path, *, review: bool = False) -> ChatRuntime:
|
||||||
|
return ChatRuntime(
|
||||||
|
config=RuntimeConfig(review_pending_proposals=review), engine_state_path=tmp_path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_off_proposal_review_is_none(tmp_path: Path) -> None:
|
||||||
|
result = _rt(tmp_path).idle_tick()
|
||||||
|
assert result.proposal_review is None # additive field absent for existing callers
|
||||||
|
assert result.candidates_contemplated == 0 and result.proposals_created == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_enabled_surfaces_the_summary(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
sentinel = ProposalReviewIdleSummary(
|
||||||
|
safe=True, total=3, review_needed=3, malformed=0, by_family=(("missing_total_count", 3),)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(rtmod, "idle_summary", lambda *a, **k: sentinel)
|
||||||
|
assert _rt(tmp_path, review=True).idle_tick().proposal_review == sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_reporter_exception_is_captured_not_propagated(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
def _boom(*a, **k):
|
||||||
|
raise ValueError("sink exploded")
|
||||||
|
|
||||||
|
monkeypatch.setattr(rtmod, "idle_summary", _boom)
|
||||||
|
result = _rt(tmp_path, review=True).idle_tick() # must NOT raise
|
||||||
|
assert result.proposal_review is not None
|
||||||
|
assert result.proposal_review.safe is False
|
||||||
|
assert result.proposal_review.errors == ("proposal_review_failed:ValueError",)
|
||||||
|
assert result.candidates_contemplated == 0 and result.proposals_created == 0 # tick intact
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_subpass_does_not_checkpoint_or_set_did_work(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
rt = _rt(tmp_path, review=True)
|
||||||
|
calls: list[int] = []
|
||||||
|
monkeypatch.setattr(rt, "checkpoint_engine_state", lambda: calls.append(1))
|
||||||
|
monkeypatch.setattr(rtmod, "idle_summary", lambda *a, **k: _OK)
|
||||||
|
result = rt.idle_tick()
|
||||||
|
assert result.proposal_review is not None # the review ran
|
||||||
|
assert calls == [] # ...but a read-only review never checkpoints (no did_work)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enabled_does_not_perturb_other_passes(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(rtmod, "idle_summary", lambda *a, **k: _OK)
|
||||||
|
on = _rt(tmp_path, review=True).idle_tick()
|
||||||
|
off = _rt(tmp_path).idle_tick()
|
||||||
|
assert (on.candidates_contemplated, on.proposals_created, on.facts_consolidated) == (
|
||||||
|
off.candidates_contemplated,
|
||||||
|
off.proposals_created,
|
||||||
|
off.facts_consolidated,
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue