AG-UI · Guides

Citations

@threadplane/ag-ui can copy citations from AG-UI state onto chat messages.

The bridge is deliberately simple: put citations under state.citations, keyed by message id. When a STATE_SNAPSHOT or STATE_DELTA arrives, the adapter merges matching citations onto messages.

State shape

Use this shape from your AG-UI backend:

{
  citations: {
    "assistant-message-id": [
      {
        id: "doc-1",
        title: "Refund policy",
        url: "https://example.com/refunds",
        snippet: "Refunds are available within 30 days."
      }
    ]
  }
}

The key must match the assistant message id emitted by TEXT_MESSAGE_START or REASONING_MESSAGE_START.

{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' }
{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Refunds are available.' }
{
  type: 'STATE_SNAPSHOT',
  snapshot: {
    citations: {
      m1: [
        {
          id: 'refund-policy',
          title: 'Refund policy',
          url: 'https://example.com/refunds',
          snippet: 'Refunds are available within 30 days.',
        },
      ],
    },
  },
}

The message becomes:

{
  id: 'm1',
  role: 'assistant',
  content: 'Refunds are available.',
  citations: [
    {
      id: 'refund-policy',
      index: 1,
      title: 'Refund policy',
      url: 'https://example.com/refunds',
      snippet: 'Refunds are available within 30 days.',
    },
  ],
}

Accepted citation fields

The bridge normalizes a few common field names:

Citation fieldAccepted input
idid, refId, or generated c1, c2, ...
indexindex or array position starting at 1
titletitle or name
urlurl, href, or source
snippetsnippet, content, or excerpt
extraextra object

String entries are also accepted:

{
  citations: {
    m1: ['https://example.com/refunds']
  }
}

That becomes:

{ id: 'c1', index: 1, url: 'https://example.com/refunds' }

When citations update

Citations are merged after:

  • STATE_SNAPSHOT
  • STATE_DELTA

They are not merged after plain text events. If your backend streams the final answer first and citations later, send a state event after citation data is available.

If your backend sends citations before the matching message exists, send another state event after the message is created or use MESSAGES_SNAPSHOT with messages that already include citations.

Manual bridge

bridgeCitationsState() is exported for advanced adapters or custom reducers.

import { bridgeCitationsState } from '@threadplane/ag-ui';
 
const nextMessages = bridgeCitationsState(
  { state: threadState },
  currentMessages,
);

Most apps should not need this directly. The built-in AG-UI reducer already calls it for state snapshots and deltas. The bridgeCitationsState(thread, messages) helper merges thread-level citation state onto the message list; import it from @threadplane/ag-ui when you assemble messages yourself outside the standard adapter pipeline.

Gotchas

Citation matching is by message id, not by order. Stable message ids matter.

The bridge returns messages unchanged when state.citations is missing, not an object, or the entry for a message is empty.

The bridge normalizes citation shape; it does not fetch metadata, validate URLs, or deduplicate sources across messages.