Skip to content

Commit 1554af4

Browse files
patnikoCopilot
andcommitted
docs: add citations guide
Add docs/features/citations.md covering the experimental citations surface: enabling `enableCitations` on session create/resume in all six SDKs, reading the `citations` payload from `assistant.message` events, the payload/type reference, how to supply citable material via document attachments or tool `citableSources`, and current limitations. Link the new guide from the features index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c184f59-0104-4a89-a350-4096597a89b3
1 parent e787639 commit 1554af4

2 files changed

Lines changed: 386 additions & 0 deletions

File tree

docs/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
1616
| [Skills](./skills.md) | Load reusable prompt modules from directories |
1717
| [Plugin Directories](./plugin-directories.md) | Bundle skills, hooks, MCP servers, and agents as a single loadable plugin |
1818
| [Session limits](./session-limits.md) | Set an AI Credits budget for a session and observe budget events |
19+
| [Citations](./citations.md) | Link assistant responses back to their supporting sources (experimental) |
1920
| [Image Input](./image-input.md) | Send images to sessions as attachments |
2021
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
2122
| [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota |

docs/features/citations.md

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
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

Comments
 (0)