Context
Today the agent's emit_claim tool takes body_markdown containing ${ref:N} placeholders plus a separate provenance: list[ProvenanceRef] array. Two paths run after the model returns:
- Placeholder gate (
agent_service/policy/structural.py): regex resolves every ${ref:N} against the provenance list. Catches syntactic mismatches (N out of bounds, no provenance entries, etc).
- Structural gate: per-
Number provenance entries are value-compared against a per-thread binding store populated from primitive tool outputs. Catches fabricated values.
Neither catches semantic reference drift: the model writes \"the wallet has \${ref:0} connections\" but provenance[0] is the wallet entity (community/edge/etc), and the actual connection-count number sits at provenance[3]. Both gates pass: ref:0 resolves syntactically, no value is fabricated. The user reads a wrong-pointer claim.
This failure mode has no clean fix in the post-hoc reconciliation shape. The 2025-2026 research surfaced one architectural alternative that side-steps it entirely.
What the field shipped
Anthropic Citations API (Jan 2025, Bedrock GA Jun 2025): citations are model-emitted as inline annotations during generation; the API server extracts the cited substring by character/page/chunk offset. There is no separate symbolic name to drift because the citation IS the offset into the source. cited_text is guaranteed to be a literal substring of the source document.
Limitations: text spans only, source must be a document block, incompatible with Structured Outputs (mutually exclusive with response_format: json_schema).
Adapting that model to our problem (citations of tool-output VALUES, not document substrings) means inline-emitting the binding-store lookup key at the position the value appears in prose, instead of the post-hoc ${ref:N} symbolic name. Roughly:
\"the wallet moved <cite metric=\\\"out_volume_sol\\\" value=\\\"50.0\\\">50 SOL</cite>\"
The verifier reads each <cite> tag, looks up metric in the binding store, value-compares — same gate, but the binding-store key is bound to the prose position, not to a separate symbol that can mis-resolve.
Tradeoffs:
- Closes the semantic-mis-pointing hole.
- Forces the model to emit XML/JSON inline, which Gemini-class models handle but at higher token cost.
- Schema-incompatible with strict structured-output modes (same constraint Anthropic hit).
Research-backed alternatives weighed and rejected
- Self-RAG reflection tokens (Asai 2024): requires fine-tuning into the model. Closed-model providers don't offer this.
- Chain-of-Verification draft→verify→revise (Meta 2023, ACL 2024 Findings): doubles inference cost, no structured artifact for downstream UIs, thin production adoption.
- VeriFastScore atomic decomposition (May 2025): eval-time tooling, not a user-facing gate.
Files to inspect when revisiting
agent_service/agent.py:80-96 — EmitClaimInput schema (the ${ref:N} contract).
agent_service/policy/structural.py — placeholder + value-compare gates.
agent_service/policy/binding_store.py — the lookup table the structural gate consults.
agent_service/prompts/system_v4.txt:59-77 — the prompt that teaches the model to use ${ref:N}.
Decision needed before scoping
Whether semantic reference drift fires often enough in real turns to justify the schema change. Concrete first step: add span attribute mcae.gate.structural.ref_drift_suspected that flags claims where every gate passed but the cited number's metric name does NOT match the prose's surrounding noun. Run for a week, count incidents, then decide.
Out of scope
- The general "claim verify citation" architecture review (this is one specific class of failure within an already-functioning gate stack; the rest of the verification pipeline is ahead of SOTA for our problem class per the May 2026 research).
- Anthropic Citations API integration directly (it's text-substring only, doesn't fit the tool-output-value case).
Context
Today the agent's
emit_claimtool takesbody_markdowncontaining${ref:N}placeholders plus a separateprovenance: list[ProvenanceRef]array. Two paths run after the model returns:agent_service/policy/structural.py): regex resolves every${ref:N}against the provenance list. Catches syntactic mismatches (N out of bounds, no provenance entries, etc).Numberprovenance entries are value-compared against a per-thread binding store populated from primitive tool outputs. Catches fabricated values.Neither catches semantic reference drift: the model writes
\"the wallet has \${ref:0} connections\"butprovenance[0]is the wallet entity (community/edge/etc), and the actual connection-count number sits atprovenance[3]. Both gates pass: ref:0 resolves syntactically, no value is fabricated. The user reads a wrong-pointer claim.This failure mode has no clean fix in the post-hoc reconciliation shape. The 2025-2026 research surfaced one architectural alternative that side-steps it entirely.
What the field shipped
Anthropic Citations API (Jan 2025, Bedrock GA Jun 2025): citations are model-emitted as inline annotations during generation; the API server extracts the cited substring by character/page/chunk offset. There is no separate symbolic name to drift because the citation IS the offset into the source.
cited_textis guaranteed to be a literal substring of the source document.Limitations: text spans only, source must be a
documentblock, incompatible with Structured Outputs (mutually exclusive withresponse_format: json_schema).Adapting that model to our problem (citations of tool-output VALUES, not document substrings) means inline-emitting the binding-store lookup key at the position the value appears in prose, instead of the post-hoc
${ref:N}symbolic name. Roughly:The verifier reads each
<cite>tag, looks upmetricin the binding store, value-compares — same gate, but the binding-store key is bound to the prose position, not to a separate symbol that can mis-resolve.Tradeoffs:
Research-backed alternatives weighed and rejected
Files to inspect when revisiting
agent_service/agent.py:80-96—EmitClaimInputschema (the${ref:N}contract).agent_service/policy/structural.py— placeholder + value-compare gates.agent_service/policy/binding_store.py— the lookup table the structural gate consults.agent_service/prompts/system_v4.txt:59-77— the prompt that teaches the model to use${ref:N}.Decision needed before scoping
Whether semantic reference drift fires often enough in real turns to justify the schema change. Concrete first step: add span attribute
mcae.gate.structural.ref_drift_suspectedthat flags claims where every gate passed but the cited number's metric name does NOT match the prose's surrounding noun. Run for a week, count incidents, then decide.Out of scope