Contracts: Citation Grammar & Agent Output Schemas
The exact contracts the grounding validator and agents are built against, pinned before coding the agent layer.
The exact contracts the grounding validator and agents are built against. These are not "nice to have later" — they are what
ai/30andai/31execute. Pin them before coding the agent layer.
Citation grammar
Every grounded claim references evidence by id, inline, in this exact form:
[OBS-<digits>] a canonical observation e.g. [OBS-50412]
[DOC-<digits>] a RAG document e.g. [DOC-9931]
[EVT-<digits>] a derived event e.g. [EVT-2741]- Regex the validator uses:
/\[(OBS|DOC|EVT)-(\d+)\]/g. - A "claim" = a sentence asserting something about the world. Each claim must carry ≥1 citation.
- Multiple ids allowed:
… within the corridor [OBS-50412][OBS-50418]. - The validator resolves each
(kind, id)against the tenant's Postgres (OBS/EVT) or Mongo/documents(DOC); any dangling or out-of-tenant id → reject + regenerate once → else return a "could not ground" state. (ai/31)
Agent output JSON schema (analyst answer)
{
"answer_markdown": "string", // prose with inline [OBS-…]/[DOC-…]/[EVT-…] citations
"citations": [ // every id used, extracted, for UI provenance links
{ "kind": "OBS|DOC|EVT", "id": 50412, "quote": "string?" }
],
"claims": [ // optional structured decomposition for validation/eval
{ "text": "string", "citations": [ { "kind": "OBS|DOC|EVT", "id": 50412 } ], "confidence": 0.0 }
],
"tool_calls_made": ["query_observations", "search_corpus"],
"withheld": [ { "reason": "interval withheld — n too small", "metric": "loiter_pctile" } ],
"no_grounding": false // true when nothing in scope supports an answer
}Proposal schema (Ontology / Adjudicator / Watch — the propose-only writes)
{
"proposal_type": "case_update|report_section|source_mapping|entity_flag|adjudication",
"summary": "string", // human-readable, shown in the inbox
"patch": { }, // the change to apply on accept (typed per proposal_type)
"grounding": { "reasoning": "string", "observations": [50412, 50418] },
"confidence": 0.0,
"gate": "Q8|Q9|Q10", // which human-commit gate applies
"auto_appliable": false // true only for internal+reversible above threshold (Q10)
}After-Action / report draft schema
{
"title": "string",
"sections": [ { "heading": "string", "body_markdown": "string", "citations": [ { "kind": "OBS|DOC|EVT", "id": 50412 } ] } ],
"timeline": [ { "at": "ISO8601", "text": "string", "citations": [ { "kind": "EVT", "id": 2741 } ] } ],
"base_rates": [ { "metric": "string", "value": 0, "sample_n": 0, "interval": "string|null" } ],
"status": "DRAFT" // never PUBLISHED from an agent (Q8 human signs)
}Validator contract (pseudocode)
The validator must traverse every text-bearing field across all agent output schemas — not just answer_markdown. A dangling/cross-tenant citation buried in a report section, a timeline entry, or a proposal's grounding would otherwise ship unvalidated and break the Q8 grounding guarantee precisely on the customer-facing artifact.
// Collect every string that can carry citations, across all schema shapes.
function citationBearingText(out): string[] {
const t = [];
if (out.answer_markdown) t.push(out.answer_markdown);
for (const s of out.sections ?? []) t.push(s.body_markdown); // report/AAR sections
for (const e of out.timeline ?? []) t.push(e.text); // timeline entries
for (const c of out.claims ?? []) t.push(c.text); // decomposed claims
return t.filter(Boolean);
}
// Structured citation arrays (already {kind,id}) must ALSO resolve.
function structuredCitations(out): {kind:string,id:number}[] {
const c = [...(out.citations ?? [])];
for (const cl of out.claims ?? []) c.push(...(cl.citations ?? []));
// proposals: grounding.observations are OBS ids
for (const o of out.grounding?.observations ?? []) c.push({ kind: 'OBS', id: o });
return c;
}
function validate(out, scope): { ok: boolean, dangling: string[] } {
const inline = citationBearingText(out)
.flatMap(txt => [...txt.matchAll(/\[(OBS|DOC|EVT)-(\d+)\]/g)]
.map(m => ({ kind: m[1], id: +m[2] })));
const all = [...inline, ...structuredCitations(out)];
const dangling = all.filter(c => !resolves(c.kind, c.id, scope.tid)); // tenant-scoped resolve
// also enforce: every claim/section carrying assertions has ≥1 citation;
// no agent output may carry status: "PUBLISHED" (Q8 — a human signs).
return { ok: dangling.length === 0, dangling: dangling.map(c => `${c.kind}-${c.id}`) };
}Gaps to close
- Finalize each
patchshape perproposal_type. - Decide whether
claims[]decomposition is required (stronger eval) or optional (cheaper).
Note:
sections[].citations/timeline[].citations(the structured arrays in the report schema) use the same{kind,id}shape asclaims[].citations— bare numbers are never valid because50412alone can't be disambiguated across OBS/DOC/EVT.