Citation Architecture
Citations are the contract that lets an answer be traced back to the objects it drew from: a retrieved chunk, a web result, a tag, a note, or any other resource the agent can surface. This page describes the four layers that implement that contract, how state flows between them, and what a component must do to make its objects citable. For the surrounding execution model, see Agents SDK Architecture.
The four layers
A citation crosses four layers on its way from a tool result to a rendered reference. Each layer has one job, and they communicate through exactly two channels: the prompt (into the LLM) and the CitationStore (around the LLM).
- Presentation — tool sources and context sources show the model an object together with its citation key: the
source_urlfield on the payload. The key is the only handle the model is expected to cite with. - Registration — the same producer registers the object in the
CitationStoreunder that key, along with everything the downstream layers need: the evidence URL to render, and the context the citation contributes to the next turn. - Instruction — the system prompt tells the model how to write a citation: the wire format. This is the citation strategy, and it is independent of what kind of object is being cited.
- Resolution — after generation, the
CitationProcessorparses the response with the active strategy's pattern, looks each cited key up in the same store, and turns matches into structured evidences and next-turn context on the outgoing message.
The store is the state-transfer mechanism: producers write to it during tool execution, the processor reads from it after generation. The LLM only ever handles the citation key as an opaque string — it never sees the store, and the processor never trusts anything the model wrote beyond using it as a lookup key. A citation that resolves came from a registered object; one that does not resolve produces no evidence.
Registration: the CitationStore
The CitationStore is a request-scoped singleton, injected wherever it is needed through the dependency system (CitationStoreFactory is registered with __singleton__ = True, so every component in one agent run shares the same instance).
Each entry is a RegisteredHit — both views of one citable object:
| Field | Meaning |
|---|---|
citation_key | The string the model cites with. Must be exactly what the presentation layer shows as source_url. |
context_hit | The LLM-facing view: the payload fields the model saw. |
search_hit | The source-facing view: the raw object from the producing system. |
evidence_url | The URL a rendered evidence points at. Internal objects use a frontend retrieval URL, external objects their own URL. |
context_contribution | What a citation of this object adds to the next turn: internal document ids, or custom context items for external objects. |
index_type, index_id | Provenance of the object, when it came from an index. |
The store also indexes hits by short_id and by document id (get_hit_by_document_id), so resolution and follow-up tooling can recover a hit from identifiers other than the full key.
Instruction: strategies are object-independent
The citation strategy defines the wire format — how the model communicates a citation to the resolution layer:
| Strategy | Wire format |
|---|---|
inline_url | [[N]](source_url "text extract") |
short_id | [[short_id]] |
deferred | No inline citations; resolution inserts them after generation |
sup_numeric | <sup>N</sup> |
A strategy is a property of the conversation, not of an object category. Whatever kind of object is being cited, the model communicates it the same way: by its citation key, in the active strategy's format. This is deliberate — it means the default citation instructions are written against the abstract citable object, and a new citable category does not add new instruction text. If your object presents a source_url and is registered under it, the existing instructions already cover it.
Which resources the instruction covers is selected per agent through citation_instruction_source_configuration.citable_resources. Two kinds of fragment exist:
- Evidence fragments teach the
[[N]](source_url "extract")form for a class of citable source.search_resultscovers documents returned by search tools;contextcovers documents provided in the conversation context (for example, the documents the user is currently viewing). Enablecontextwhenever an agent answers from injected documents, so the model cites them as resolvable evidence rather than a plain link. - Reference-link fragments are for objects that are referenced rather than cited as evidence — resources with their own link form, like tags (
[tag name](/tags/{tag ID})).
Resolution: the CitationProcessor
After the model finishes, the CitationProcessor (enabled per agent via citation_processor_configuration) rewrites the final message:
- It scans the text with the active strategy's pattern.
- Each matched citation key is looked up in the store — verbatim for
inline_url, through the short-id map forshort_id. - A match becomes a
ChatMessageEvidence(pointing at the hit'sevidence_url) and merges the hit'scontext_contributioninto the message's context parts, so the cited objects are available to the next turn. - A miss produces nothing: the citation is left in the text as written and no evidence is attached.
Step 4 is why the consistency contract below matters. The processor cannot repair a citation whose key was never registered or never shown.
Making an object citable
A component that surfaces a new kind of object — a tool source returning external records, a context source injecting attached items — makes those objects citable by doing two things with the same key:
from zav.agents_sdk.adapters.agent_state.citation import (
CitationContextContribution,
CitationStore,
)
from zav.agents_sdk.domain.agent_dependency import AgentDependencyFactory
class MyToolsSourceFactory(AgentDependencyFactory):
@classmethod
def create(cls, citation_store: CitationStore) -> "MyToolsSource":
return MyToolsSource(citation_store=citation_store)
class MyToolsSource:
def __init__(self, citation_store: CitationStore):
self.__citation_store = citation_store
def my_tool(self) -> dict:
record = self.__fetch_record()
source_url = record["url"]
# 1. Register the object under its citation key.
self.__citation_store.register_hit(
citation_key=source_url,
context_hit={"title": record["title"], "source_url": source_url},
search_hit=record,
evidence_url=source_url,
context_contribution=CitationContextContribution(
custom_items=[...], # or document_ids={...} for internal objects
),
)
# 2. Present the same key in the payload the model reads.
return {"title": record["title"], "source_url": source_url}
For a search or context source, enable the matching evidence fragment (search_results or context) in citable_resources so the model is told to cite it in the [[N]](source_url "extract") form. Add a reference-link fragment only when the object should be referenced with its own link form instead of appearing as an evidence.
The consistency contract
Every citable surface must keep three statements true at once:
- Present ⇒ register. Any
source_url(or other key-shaped identifier) shown to the model must be registered in the store. An unregistered key that looks citable will be cited — and will silently fail to resolve. - Register ⇒ present. A registered hit whose key is never shown cannot be cited correctly; the model will improvise a handle (a bare id, a guessed path) that misses the store.
- Instruct only what is presented. Instruction text must reference only fields the payloads actually carry, under every combination of enabled sources and strategies.
When a surface cannot satisfy the contract — content that is deliberately not citable — it should not expose key-shaped identifiers at all, so the model is never tempted to cite them.