ADR-0066 P3.1 + P3.2. Conversation now reads as a thread: turns
carry structured summaries of their predecessors and (optionally)
prefix new pack/teaching surfaces with deterministic backreferences.
P3.1 — chat/thread_context.py.
TurnSummary(turn_index, intent_tag_name, subject, grounding_source,
chain_id, corpus_id) — frozen, structured-fields-only.
ThreadContext — bounded FIFO (default MAX_THREAD_TURNS=8) with
snapshot(), recent_for_subject(), recent_subjects(), clear().
recent_for_subject() excludes ungrounded tiers (oov/partial/none)
by default — those are not strong-enough anchors.
ChatRuntime.thread_context is owned at construction.
_push_thread_summary runs at end-of-turn on BOTH stub and walk
paths. Teaching-grounded turns carry chain_id + corpus_id so
downstream composers (P3.2) can detect same-chain reference.
Cold-start intent classification now runs unconditionally (was:
gated on sink attachment) so thread context captures subject
regardless of sink state.
P3.2 — chat/anaphora.py.
thread_anaphora_prefix(ctx, subject, intent_name, source) returns
a deterministic prefix when:
- current turn is pack/teaching tier
- a prior pack/teaching turn on the same subject exists
- the prior intent differs from the current intent
Format (structural-fields-only — no prose):
"(Recalling turn N: chain <chain_id>.) " # prior was teaching
"(Recalling turn N: <subject> grounded pack.) " # prior was pack
Opt-in via RuntimeConfig.thread_anaphora=False. Default off keeps
every existing surface byte-identical.
Live verification (with thread_anaphora=True + seeded context):
> What is light? # following a "Why does light exist?" teaching turn
[pack] (Recalling turn 0: chain cause_light_reveals_truth.)
light — pack-grounded (en_core_cognition_v1): cognition.illumination;
logos.core; perception.clarity. No session evidence yet.
32 new tests passed. Curated lanes green. Cognition eval
byte-identical to pre-ADR baseline.
94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""chat/anaphora.py — Phase 3.2: deterministic thread anaphora prefix.
|
|
|
|
When the current turn's subject lemma appeared in a recent *grounded*
|
|
turn (pack or teaching tier), the anaphora composer prepends a
|
|
deterministic backreference to the current surface. Conversation
|
|
reads as a thread instead of a sequence of independent surfaces:
|
|
|
|
Turn 0 > Why does light exist?
|
|
[teaching] light — teaching-grounded (cognition_chains_v1):
|
|
cognition.illumination; logos.core. light reveals truth ...
|
|
Turn 1 > What is light?
|
|
[pack] (Recalling turn 0: chain cause_light_reveals_truth.)
|
|
light — pack-grounded (en_core_cognition_v1): cognition.illumination;
|
|
logos.core; perception.clarity. No session evidence yet.
|
|
|
|
The prefix is **strictly deterministic**:
|
|
|
|
- Same thread state + same current turn → byte-identical prefix.
|
|
- No prose generation; the prefix references the prior turn by
|
|
*turn index* and *chain id* (or grounding tier), nothing else.
|
|
- The composer only fires when *both* the prior turn AND the
|
|
current turn are pack/teaching tier; weaker tiers (vault /
|
|
partial / oov / none) do not anchor.
|
|
- Same-intent revisits (asking the same question twice) do not
|
|
fire — the prior turn IS the current turn modulo session vault
|
|
drift, prefixing it is redundant.
|
|
- Opt-in via :attr:`core.config.RuntimeConfig.thread_anaphora`.
|
|
Default ``False`` preserves every pre-P3.2 surface byte-identically.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from chat.thread_context import ThreadContext
|
|
|
|
|
|
# Grounding tiers strong enough to anchor an anaphora reference.
|
|
_ANCHOR_TIERS: frozenset[str] = frozenset({"teaching", "pack"})
|
|
|
|
|
|
def thread_anaphora_prefix(
|
|
thread_context: ThreadContext,
|
|
current_subject: str,
|
|
current_intent_tag_name: str,
|
|
current_grounding_source: str,
|
|
) -> str | None:
|
|
"""Return a deterministic anaphora prefix, or ``None``.
|
|
|
|
Engagement conditions (ALL must hold):
|
|
|
|
1. ``current_subject`` is non-empty.
|
|
2. ``current_grounding_source`` is in ``{"pack", "teaching"}``
|
|
— weaker tiers cannot host a meaningful backreference.
|
|
3. A prior turn on the same subject exists in
|
|
``thread_context`` AND was itself pack/teaching grounded.
|
|
4. The prior turn's intent differs from the current turn's
|
|
intent (a same-intent revisit is the same surface modulo
|
|
vault drift; prefixing would be redundant).
|
|
|
|
The prefix format references the prior turn by structured fields
|
|
only — never by surface text, never by re-derived prose. Two
|
|
shapes exist:
|
|
|
|
- Prior was teaching-grounded → ``"(Recalling turn N: chain
|
|
<chain_id>.) "``
|
|
- Prior was pack-grounded → ``"(Recalling turn N: <subject>
|
|
grounded pack.) "``
|
|
|
|
Returns ``None`` when any engagement condition fails. Callers
|
|
then emit the unprefixed surface byte-identically.
|
|
"""
|
|
if not current_subject or not isinstance(current_subject, str):
|
|
return None
|
|
key = current_subject.strip().lower()
|
|
if not key:
|
|
return None
|
|
if current_grounding_source not in _ANCHOR_TIERS:
|
|
return None
|
|
prior = thread_context.recent_for_subject(key)
|
|
if prior is None:
|
|
return None
|
|
if prior.grounding_source not in _ANCHOR_TIERS:
|
|
return None
|
|
if prior.intent_tag_name and prior.intent_tag_name == current_intent_tag_name:
|
|
return None
|
|
|
|
if prior.grounding_source == "teaching" and prior.chain_id:
|
|
return f"(Recalling turn {prior.turn_index}: chain {prior.chain_id}.) "
|
|
return (
|
|
f"(Recalling turn {prior.turn_index}: {key} grounded "
|
|
f"{prior.grounding_source}.) "
|
|
)
|
|
|
|
|
|
__all__ = ["thread_anaphora_prefix"]
|