Both optional capabilities read what earlier questions retrieved and cited from the capability's state, so a host that carries only the message history hands every run an empty record. Compaction then replaced the earlier evidence with receipts and retained nothing, and the loss was invisible: the citations the host already displayed were still there. It now refuses when it finds evidence from an earlier question and no record of what that question cited. `state_carried` reaches the optional capabilities through discovery, so the refusal distinguishes a host that never carries state from a question that simply cited nothing. The documentation taught the pattern that breaks: the compose example is now stateful and the requirement is stated where each capability is introduced. The app's browser storage was doing exactly this, keeping only the fields the UI reads. It now persists the whole namespace map, so the citation policy's violations survive a reload as well as the evidence record.
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Any, cast
|
|
|
|
from pydantic_ai import RunContext
|
|
|
|
from haiku.rag.capabilities._base import RAGCapabilityBase
|
|
from haiku.rag.capabilities.ledger import CapabilityEvidenceRecord
|
|
from haiku.rag.store.models.citation import Citation
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiscoveredEvidence:
|
|
"""One evidence capability's records, as another capability found them.
|
|
|
|
Read-only and rebuilt per request: whoever discovers these merges them into a
|
|
view and persists nothing about evidence itself.
|
|
"""
|
|
|
|
capability: str
|
|
record: CapabilityEvidenceRecord
|
|
citations: Mapping[str, Citation]
|
|
tool_names: frozenset[str]
|
|
cite_available: bool
|
|
state_carried: bool
|
|
|
|
|
|
def discover_evidence(ctx: RunContext[Any]) -> list[DiscoveredEvidence]:
|
|
"""Read what each evidence capability recorded, without writing anything.
|
|
|
|
Discovery runs one way through the run's capability registry, so no capability
|
|
holds a reference to another, and a host running one, both, or neither needs no
|
|
wiring change. The registry holds the per-run instances, which are the ones
|
|
carrying state; the registered objects never do. That includes a deferred
|
|
capability the model has not loaded, whose record is simply empty.
|
|
"""
|
|
discovered = [
|
|
DiscoveredEvidence(
|
|
capability=capability.state_namespace,
|
|
record=cast(CapabilityEvidenceRecord, cast(Any, capability.state).evidence),
|
|
citations=cast(Any, capability.state).citation_index,
|
|
tool_names=frozenset(capability.evidence_tool_names()),
|
|
cite_available=capability.cite_available,
|
|
state_carried=capability.state_carried,
|
|
)
|
|
for capability in ctx.capabilities.values()
|
|
if isinstance(capability, RAGCapabilityBase)
|
|
]
|
|
return sorted(discovered, key=lambda evidence: evidence.capability)
|
|
|
|
|
|
def question_in_progress(evidence: list[DiscoveredEvidence]) -> int:
|
|
"""The identity every evidence capability agrees this question has.
|
|
|
|
They all derive it from the same history, so they agree; taking the maximum
|
|
rather than a first entry keeps the result independent of ordering.
|
|
"""
|
|
return max((found.record.question or 0 for found in evidence), default=0)
|
|
|
|
|
|
__all__ = [
|
|
"DiscoveredEvidence",
|
|
"discover_evidence",
|
|
"question_in_progress",
|
|
]
|