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.
2.4 KiB
Citation policy capability
CitationPolicyCapability requires every answer to declare what grounds it. Citing is
always available and always recorded without it, but nothing makes the model do it.
Register it alongside an evidence capability:
from pydantic_ai import Agent
from haiku.rag.capabilities.policy import create_capability as citation_policy
from haiku.rag.capabilities.rag import create_capability as rag
agent = Agent(
"openai:gpt-5",
capabilities=[rag(db_path="my.lancedb"), citation_policy()],
)
It exposes no tools and takes no configuration. Exactly one policy capability makes the decision, however many evidence capabilities are registered, so two of them cannot each demand a citation for one answer.
The host must carry the capability state between runs, alongside the message history. Enforcement reads what the conversation has already cited, so without it a follow-up about evidence cited earlier goes unenforced. See Compose an agent for the shape.
Declaring nothing is a valid answer
A model that finds nothing relevant calls the cite tool with an empty list. That records the answer as ungrounded, which is distinct from an answer that declared nothing at all (missing). The distinction is what makes a declaration requirable without forcing the model to invent grounding.
What happens when a question ends undeclared
The model is asked once to record what grounded the answer it already gave. It is not
asked to change the answer. If the cite tool is no longer available by then, or the
question finishes undeclared anyway, the question is recorded in
CitationPolicyState.violations under the "citation_policy" state key. Pointing a
model at a tool that is gone costs it retries, so the capability records the failure
instead.
Which answers are enforced
Every answer in a conversation that has something to declare: either this question retrieved evidence, or the conversation has already cited something, which stays available to later answers. A follow-up about evidence cited earlier is enforced even though it searched nothing, which is the case the capability exists for.
Once anything has been cited, later turns are enforced too, a greeting included. The model satisfies the policy by citing an empty list, at the cost of one extra request. A conversation with neither a current-question evidence outcome nor any earlier citation is not enforced.