|
| 1 | +# Citations |
| 2 | + |
| 3 | +Citations link spans of an assistant response back to the sources that support them. Turn on `enableCitations` when you create or resume a session, then read the `citations` payload on `assistant.message` events to render footnotes, source lists, or inline links. |
| 4 | + |
| 5 | +> [!WARNING] |
| 6 | +> Citations are experimental. The option name, event payload, and provider coverage can change in a future release. |
| 7 | +
|
| 8 | +## How citations work |
| 9 | + |
| 10 | +Citations are produced by the model provider, not by the SDK. The flow has three parts: |
| 11 | + |
| 12 | +1. Your application supplies citable material, such as a document attachment or a tool result that carries source content. |
| 13 | +1. The runtime marks that material as citable on the wire when `enableCitations` is on. For Anthropic models, file attachments are sent as `document` blocks with citations enabled. |
| 14 | +1. The model returns citation metadata, and the runtime normalizes it into a provider-agnostic `citations` object on the final `assistant.message` event. |
| 15 | + |
| 16 | +Provider support is limited. The `provider` field on each source records where the citation came from: |
| 17 | + |
| 18 | +| Provider value | Meaning | |
| 19 | +|---|---| |
| 20 | +| `anthropic` | Citation produced by an Anthropic (Claude) model response | |
| 21 | +| `openai` | Citation produced by an OpenAI model response | |
| 22 | +| `client` | Citation synthesized by the runtime from tool output | |
| 23 | + |
| 24 | +> [!NOTE] |
| 25 | +> Turning on `enableCitations` does not guarantee that a response contains citations. Models emit them only when the response is grounded in citable source material. Always treat the `citations` field as optional. |
| 26 | +
|
| 27 | +## Enable citations on a session |
| 28 | + |
| 29 | +Set the option on session create, and set it again on resume if you want citations after a restart. |
| 30 | + |
| 31 | +<details open> |
| 32 | +<summary><strong>TypeScript</strong></summary> |
| 33 | + |
| 34 | +<!-- docs-validate: skip --> |
| 35 | + |
| 36 | +```typescript |
| 37 | +const session = await client.createSession({ |
| 38 | + onPermissionRequest: approveAll, |
| 39 | + enableCitations: true, |
| 40 | +}); |
| 41 | + |
| 42 | +const resumed = await client.resumeSession(session.sessionId, { |
| 43 | + onPermissionRequest: approveAll, |
| 44 | + enableCitations: true, |
| 45 | +}); |
| 46 | +``` |
| 47 | + |
| 48 | +</details> |
| 49 | +<details> |
| 50 | +<summary><strong>Python</strong></summary> |
| 51 | + |
| 52 | +<!-- docs-validate: skip --> |
| 53 | + |
| 54 | +```python |
| 55 | +session = await client.create_session( |
| 56 | + on_permission_request=PermissionHandler.approve_all, |
| 57 | + enable_citations=True, |
| 58 | +) |
| 59 | + |
| 60 | +resumed = await client.resume_session( |
| 61 | + session.session_id, |
| 62 | + on_permission_request=PermissionHandler.approve_all, |
| 63 | + enable_citations=True, |
| 64 | +) |
| 65 | +``` |
| 66 | + |
| 67 | +</details> |
| 68 | +<details> |
| 69 | +<summary><strong>Go</strong></summary> |
| 70 | + |
| 71 | +<!-- docs-validate: skip --> |
| 72 | + |
| 73 | +```go |
| 74 | +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ |
| 75 | + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, |
| 76 | + EnableCitations: copilot.Bool(true), |
| 77 | +}) |
| 78 | + |
| 79 | +resumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{ |
| 80 | + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, |
| 81 | + EnableCitations: copilot.Bool(true), |
| 82 | +}) |
| 83 | +``` |
| 84 | + |
| 85 | +</details> |
| 86 | +<details> |
| 87 | +<summary><strong>.NET</strong></summary> |
| 88 | + |
| 89 | +<!-- docs-validate: skip --> |
| 90 | + |
| 91 | +```csharp |
| 92 | +var session = await client.CreateSessionAsync(new SessionConfig |
| 93 | +{ |
| 94 | + OnPermissionRequest = PermissionHandler.ApproveAll, |
| 95 | + EnableCitations = true, |
| 96 | +}); |
| 97 | + |
| 98 | +var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig |
| 99 | +{ |
| 100 | + OnPermissionRequest = PermissionHandler.ApproveAll, |
| 101 | + EnableCitations = true, |
| 102 | +}); |
| 103 | +``` |
| 104 | + |
| 105 | +</details> |
| 106 | +<details> |
| 107 | +<summary><strong>Java</strong></summary> |
| 108 | + |
| 109 | +<!-- docs-validate: skip --> |
| 110 | + |
| 111 | +```java |
| 112 | +CopilotSession session = client |
| 113 | + .createSession(new SessionConfig() |
| 114 | + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) |
| 115 | + .setEnableCitations(true)) |
| 116 | + .get(); |
| 117 | + |
| 118 | +CopilotSession resumed = client |
| 119 | + .resumeSession(session.getSessionId(), new ResumeSessionConfig() |
| 120 | + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) |
| 121 | + .setEnableCitations(true)) |
| 122 | + .get(); |
| 123 | +``` |
| 124 | + |
| 125 | +</details> |
| 126 | +<details> |
| 127 | +<summary><strong>Rust</strong></summary> |
| 128 | + |
| 129 | +<!-- docs-validate: skip --> |
| 130 | + |
| 131 | +```rust |
| 132 | +let session = client |
| 133 | + .create_session( |
| 134 | + SessionConfig::new() |
| 135 | + .approve_all_permissions() |
| 136 | + .with_enable_citations(true), |
| 137 | + ) |
| 138 | + .await?; |
| 139 | + |
| 140 | +let resumed = client |
| 141 | + .resume_session( |
| 142 | + ResumeSessionConfig::new(session.id().clone()) |
| 143 | + .approve_all_permissions() |
| 144 | + .with_enable_citations(true), |
| 145 | + ) |
| 146 | + .await?; |
| 147 | +``` |
| 148 | + |
| 149 | +</details> |
| 150 | + |
| 151 | +## Read citations from assistant messages |
| 152 | + |
| 153 | +Citations arrive on the final `assistant.message` event, not on `assistant.message_delta` events. Wait for the final message before you render source markers. |
| 154 | + |
| 155 | +<details open> |
| 156 | +<summary><strong>TypeScript</strong></summary> |
| 157 | + |
| 158 | +<!-- docs-validate: skip --> |
| 159 | + |
| 160 | +```typescript |
| 161 | +session.on((event) => { |
| 162 | + if (event.type !== "assistant.message" || !event.data.citations) { |
| 163 | + return; |
| 164 | + } |
| 165 | + |
| 166 | + const { sources, spans } = event.data.citations; |
| 167 | + const sourceById = new Map(sources.map((source) => [source.id, source])); |
| 168 | + |
| 169 | + for (const span of spans) { |
| 170 | + const quoted = event.data.content.slice(span.startIndex, span.endIndex); |
| 171 | + for (const reference of span.references) { |
| 172 | + const source = sourceById.get(reference.sourceId); |
| 173 | + console.log(`"${quoted}" — ${source?.title ?? source?.url ?? source?.path}`); |
| 174 | + } |
| 175 | + } |
| 176 | +}); |
| 177 | +``` |
| 178 | + |
| 179 | +</details> |
| 180 | +<details> |
| 181 | +<summary><strong>Python</strong></summary> |
| 182 | + |
| 183 | +<!-- docs-validate: skip --> |
| 184 | + |
| 185 | +```python |
| 186 | +from copilot.session_events import SessionEventType |
| 187 | + |
| 188 | +def handle(event): |
| 189 | + if event.type != SessionEventType.ASSISTANT_MESSAGE or not event.data.citations: |
| 190 | + return |
| 191 | + |
| 192 | + sources = {source.id: source for source in event.data.citations.sources} |
| 193 | + |
| 194 | + for span in event.data.citations.spans: |
| 195 | + quoted = event.data.content[span.start_index : span.end_index] |
| 196 | + for reference in span.references: |
| 197 | + source = sources[reference.source_id] |
| 198 | + print(f'"{quoted}" — {source.title or source.url or source.path}') |
| 199 | + |
| 200 | +session.on(handle) |
| 201 | +``` |
| 202 | + |
| 203 | +</details> |
| 204 | +<details> |
| 205 | +<summary><strong>Go</strong></summary> |
| 206 | + |
| 207 | +<!-- docs-validate: skip --> |
| 208 | + |
| 209 | +```go |
| 210 | +session.On(func(event copilot.SessionEvent) { |
| 211 | + d, ok := event.Data.(*copilot.AssistantMessageData) |
| 212 | + if !ok || d.Citations == nil { |
| 213 | + return |
| 214 | + } |
| 215 | + |
| 216 | + sources := map[string]copilot.CitationSource{} |
| 217 | + for _, source := range d.Citations.Sources { |
| 218 | + sources[source.ID] = source |
| 219 | + } |
| 220 | + |
| 221 | + for _, span := range d.Citations.Spans { |
| 222 | + quoted := d.Content[span.StartIndex:span.EndIndex] |
| 223 | + for _, reference := range span.References { |
| 224 | + source := sources[reference.SourceID] |
| 225 | + fmt.Printf("%q — %s\n", quoted, source.ID) |
| 226 | + } |
| 227 | + }}) |
| 228 | +``` |
| 229 | + |
| 230 | +</details> |
| 231 | +<details> |
| 232 | +<summary><strong>.NET</strong></summary> |
| 233 | + |
| 234 | +<!-- docs-validate: skip --> |
| 235 | + |
| 236 | +```csharp |
| 237 | +session.On<SessionEvent>(evt => |
| 238 | +{ |
| 239 | + if (evt is not AssistantMessageEvent message || message.Data.Citations is null) |
| 240 | + { |
| 241 | + return; |
| 242 | + } |
| 243 | + |
| 244 | + var sources = message.Data.Citations.Sources.ToDictionary(source => source.Id); |
| 245 | + |
| 246 | + foreach (var span in message.Data.Citations.Spans) |
| 247 | + { |
| 248 | + var quoted = message.Data.Content[(int)span.StartIndex..(int)span.EndIndex]; |
| 249 | + foreach (var reference in span.References) |
| 250 | + { |
| 251 | + var source = sources[reference.SourceId]; |
| 252 | + Console.WriteLine($"\"{quoted}\" — {source.Title ?? source.Url ?? source.Path}"); |
| 253 | + } |
| 254 | + } |
| 255 | +}); |
| 256 | +``` |
| 257 | + |
| 258 | +</details> |
| 259 | +<details> |
| 260 | +<summary><strong>Java</strong></summary> |
| 261 | + |
| 262 | +<!-- docs-validate: skip --> |
| 263 | + |
| 264 | +```java |
| 265 | +session.on(AssistantMessageEvent.class, event -> { |
| 266 | + Citations citations = event.getData().citations(); |
| 267 | + if (citations == null) { |
| 268 | + return; |
| 269 | + } |
| 270 | + |
| 271 | + Map<String, CitationSource> sources = citations.sources().stream() |
| 272 | + .collect(Collectors.toMap(CitationSource::id, source -> source)); |
| 273 | + |
| 274 | + for (CitationSpan span : citations.spans()) { |
| 275 | + String quoted = event.getData().content() |
| 276 | + .substring(span.startIndex().intValue(), span.endIndex().intValue()); |
| 277 | + for (CitationReference reference : span.references()) { |
| 278 | + CitationSource source = sources.get(reference.sourceId()); |
| 279 | + System.out.printf("\"%s\" — %s%n", quoted, source.title()); |
| 280 | + } |
| 281 | + } |
| 282 | +}); |
| 283 | +``` |
| 284 | + |
| 285 | +</details> |
| 286 | +<details> |
| 287 | +<summary><strong>Rust</strong></summary> |
| 288 | + |
| 289 | +<!-- docs-validate: skip --> |
| 290 | + |
| 291 | +```rust |
| 292 | +let mut events = session.subscribe(); |
| 293 | + |
| 294 | +while let Ok(event) = events.recv().await { |
| 295 | + if event.event_type != "assistant.message" { |
| 296 | + continue; |
| 297 | + } |
| 298 | + |
| 299 | + let Some(citations) = event.data.get("citations") else { |
| 300 | + continue; |
| 301 | + }; |
| 302 | + |
| 303 | + println!("{citations}"); |
| 304 | +} |
| 305 | +``` |
| 306 | + |
| 307 | +</details> |
| 308 | + |
| 309 | +## Citation payload reference |
| 310 | + |
| 311 | +The `citations` object separates deduplicated sources from the spans that reference them, so a source cited five times appears once in `sources`. |
| 312 | + |
| 313 | +| Type | Field | Description | |
| 314 | +|---|---|---| |
| 315 | +| `Citations` | `sources` | Deduplicated set of sources referenced by the citation spans | |
| 316 | +| `Citations` | `spans` | Spans of generated text annotated with their supporting sources | |
| 317 | +| `CitationSource` | `id` | Stable, turn-scoped identifier referenced by `CitationReference.sourceId` | |
| 318 | +| `CitationSource` | `provider` | System that produced the citation: `anthropic`, `openai`, or `client` | |
| 319 | +| `CitationSource` | `title?` | Human-readable title of the source | |
| 320 | +| `CitationSource` | `url?` | URL of the source, when it is a web resource | |
| 321 | +| `CitationSource` | `path?` | File path relative to the agent workspace root, when the source is a file | |
| 322 | +| `CitationSpan` | `startIndex` | Start offset in the final message content (UTF-16 code units, zero-based, inclusive) | |
| 323 | +| `CitationSpan` | `endIndex` | End offset in the final message content (UTF-16 code units, zero-based, exclusive) | |
| 324 | +| `CitationSpan` | `references` | The sources that support this span | |
| 325 | +| `CitationReference` | `sourceId` | Identifier of the `CitationSource` this reference points to | |
| 326 | +| `CitationReference` | `citedText?` | Exact text from the source that supports the span, when the model provides it | |
| 327 | +| `CitationReference` | `location?` | Location within the source that supports the span | |
| 328 | +| `CitationReference` | `providerMetadata?` | Provider-native correlation data, passed through opaquely | |
| 329 | + |
| 330 | +> [!TIP] |
| 331 | +> Span offsets are measured in UTF-16 code units against the final `content` string. In Python, Go, and Rust, convert offsets before slicing if the response contains characters outside the Basic Multilingual Plane, such as emoji. |
| 332 | +
|
| 333 | +### Citation locations |
| 334 | + |
| 335 | +`CitationReference.location` is a discriminated union keyed on `type`: |
| 336 | + |
| 337 | +| Location type | Fields | Use | |
| 338 | +|---|---|---| |
| 339 | +| `char` | `startIndex`, `endIndex` | Character range within the source text | |
| 340 | +| `page` | `startPage`, `endPage` | Page range within a paginated document | |
| 341 | +| `block` | `startBlock`, `endBlock` | Content-block range within a structured document | |
| 342 | + |
| 343 | +## Provide citable sources |
| 344 | + |
| 345 | +Citations need source material the model can attribute. There are two ways to supply it. |
| 346 | + |
| 347 | +### Attach documents to a message |
| 348 | + |
| 349 | +When citations are enabled and the session uses an Anthropic provider, file attachments are sent as `document` blocks with citations turned on, so the model can cite passages from them. |
| 350 | + |
| 351 | +<!-- docs-validate: skip --> |
| 352 | + |
| 353 | +```typescript |
| 354 | +await session.sendAndWait({ |
| 355 | + prompt: "Summarize the attached PDF and cite the passages you used.", |
| 356 | + attachments: [ |
| 357 | + { |
| 358 | + type: "blob", |
| 359 | + data: pdfBase64, |
| 360 | + displayName: "quarterly-report.pdf", |
| 361 | + mimeType: "application/pdf", |
| 362 | + }, |
| 363 | + ], |
| 364 | +}); |
| 365 | +``` |
| 366 | + |
| 367 | +See [Image input](./image-input.md) for the full attachment API. |
| 368 | + |
| 369 | +### Return citable sources from a tool |
| 370 | + |
| 371 | +Tool results carry an experimental `citableSources` array. Each entry supplies `content` that the model can cite, along with an `id` and optional `title`, `url`, and `path`. These sources are persisted with the tool result, so they survive session resume, and citations built from them are tagged with the `client` provider. |
| 372 | + |
| 373 | +## Limitations |
| 374 | + |
| 375 | +* Citations are experimental in every SDK and are not covered by compatibility guarantees. |
| 376 | +* Coverage depends on the model provider. A session configured for a provider without citation support emits no `citations` payload. |
| 377 | +* Citations are only present on the final `assistant.message` event, so streaming consumers cannot render them mid-response. |
| 378 | +* Public code and IP-duplication citations are not part of this surface. |
| 379 | + |
| 380 | +## Further reading |
| 381 | + |
| 382 | +* [Streaming events](./streaming-events.md): subscribe to session events and narrow event types |
| 383 | +* [Image input](./image-input.md): send documents and images as attachments |
| 384 | +* [Session persistence](./session-persistence.md): resume sessions and re-apply session options |
| 385 | +* [Compatibility](../troubleshooting/compatibility.md): SDK and CLI feature matrix |
0 commit comments