diff --git a/docs/features/README.md b/docs/features/README.md index ef9ae996d..9f3b937c9 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -16,6 +16,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Skills](./skills.md) | Load reusable prompt modules from directories | | [Plugin Directories](./plugin-directories.md) | Bundle skills, hooks, MCP servers, and agents as a single loadable plugin | | [Session limits](./session-limits.md) | Set an AI Credits budget for a session and observe budget events | +| [Citations](./citations.md) | Link assistant responses back to their supporting sources | | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota | diff --git a/docs/features/citations.md b/docs/features/citations.md new file mode 100644 index 000000000..b68ae292c --- /dev/null +++ b/docs/features/citations.md @@ -0,0 +1,443 @@ +# Citations + +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. + +> [!WARNING] +> Citations are experimental. The option name, event payload, and provider coverage can change in a future release. + +## How citations work + +Citations are produced by the model provider, not by the SDK. The flow has three parts: + +1. Your application supplies citable material, such as a document attachment or a tool result that carries source content. +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. +1. The model returns citation metadata, and the runtime normalizes it into a provider-agnostic `citations` object on the final `assistant.message` event. + +Provider support is limited. The `provider` field on each source records where the citation came from: + +| Provider value | Meaning | +|---|---| +| `anthropic` | Citation produced by an Anthropic (Claude) model response | +| `openai` | Citation produced by an OpenAI model response | +| `client` | Citation synthesized by the runtime from tool output | + +> [!NOTE] +> 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. + +## Enable citations on a session + +Set the option on session create, and set it again on resume if you want citations after a restart. + +
+TypeScript + + + +```typescript +const session = await client.createSession({ + onPermissionRequest: approveAll, + enableCitations: true, +}); + +const resumed = await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + enableCitations: true, +}); +``` + +
+
+Python + + + +```python +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + enable_citations=True, +) + +resumed = await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + enable_citations=True, +) +``` + +
+
+Go + + + +```go +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableCitations: copilot.Bool(true), +}) + +resumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableCitations: copilot.Bool(true), +}) +``` + +
+
+.NET + + + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + OnPermissionRequest = PermissionHandler.ApproveAll, + EnableCitations = true, +}); + +var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig +{ + OnPermissionRequest = PermissionHandler.ApproveAll, + EnableCitations = true, +}); +``` + +
+
+Java + + + +```java +CopilotSession session = client + .createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableCitations(true)) + .get(); + +CopilotSession resumed = client + .resumeSession(session.getSessionId(), new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableCitations(true)) + .get(); +``` + +
+
+Rust + + + +```rust +let session = client + .create_session( + SessionConfig::new() + .approve_all_permissions() + .with_enable_citations(true), + ) + .await?; + +let resumed = client + .resume_session( + ResumeSessionConfig::new(session.id().clone()) + .approve_all_permissions() + .with_enable_citations(true), + ) + .await?; +``` + +
+ +## Read citations from assistant messages + +Citations arrive on the final `assistant.message` event, not on `assistant.message_delta` events. Wait for the final message before you render source markers. + +
+TypeScript + + + +```typescript +session.on((event) => { + if (event.type !== "assistant.message" || !event.data.citations) { + return; + } + + const { sources, spans } = event.data.citations; + const sourceById = new Map(sources.map((source) => [source.id, source])); + + for (const span of spans) { + const quoted = event.data.content.slice(span.startIndex, span.endIndex); + for (const reference of span.references) { + const source = sourceById.get(reference.sourceId); + const label = source?.title ?? source?.url ?? source?.path ?? source?.id; + console.log(`"${quoted}" — ${label}`); + } + } +}); +``` + +
+
+Python + + + +```python +from copilot.session_events import SessionEventType + +def utf16_slice(text: str, start: int, end: int) -> str: + """Slice by UTF-16 code units, which is how span offsets are measured.""" + units = text.encode("utf-16-le") + return units[start * 2 : end * 2].decode("utf-16-le") + +def handle(event): + if event.type != SessionEventType.ASSISTANT_MESSAGE or not event.data.citations: + return + + sources = {source.id: source for source in event.data.citations.sources} + + for span in event.data.citations.spans: + quoted = utf16_slice(event.data.content, span.start_index, span.end_index) + for reference in span.references: + source = sources[reference.source_id] + label = source.title or source.url or source.path or source.id + print(f'"{quoted}" — {label}') + +session.on(handle) +``` + +
+
+Go + + + +```go +// import "unicode/utf16" + +session.On(func(event copilot.SessionEvent) { + d, ok := event.Data.(*copilot.AssistantMessageData) + if !ok || d.Citations == nil { + return + } + + sources := map[string]copilot.CitationSource{} + for _, source := range d.Citations.Sources { + sources[source.ID] = source + } + + // Span offsets are UTF-16 code units, so index the UTF-16 view of the content. + units := utf16.Encode([]rune(d.Content)) + + for _, span := range d.Citations.Spans { + quoted := string(utf16.Decode(units[span.StartIndex:span.EndIndex])) + for _, reference := range span.References { + source := sources[reference.SourceID] + label := source.ID + switch { + case source.Title != nil: + label = *source.Title + case source.URL != nil: + label = *source.URL + case source.Path != nil: + label = *source.Path + } + fmt.Printf("%q — %s\n", quoted, label) + } + } +}) +``` + +
+
+.NET + + + +```csharp +session.On(evt => +{ + if (evt is not AssistantMessageEvent message || message.Data.Citations is null) + { + return; + } + + var sources = message.Data.Citations.Sources.ToDictionary(source => source.Id); + + foreach (var span in message.Data.Citations.Spans) + { + var quoted = message.Data.Content[(int)span.StartIndex..(int)span.EndIndex]; + foreach (var reference in span.References) + { + var source = sources[reference.SourceId]; + var label = source.Title ?? source.Url ?? source.Path ?? source.Id; + Console.WriteLine($"\"{quoted}\" — {label}"); + } + } +}); +``` + +
+
+Java + + + +```java +session.on(AssistantMessageEvent.class, event -> { + Citations citations = event.getData().citations(); + if (citations == null) { + return; + } + + Map sources = citations.sources().stream() + .collect(Collectors.toMap(CitationSource::id, source -> source)); + + for (CitationSpan span : citations.spans()) { + String quoted = event.getData().content() + .substring(span.startIndex().intValue(), span.endIndex().intValue()); + for (CitationReference reference : span.references()) { + CitationSource source = sources.get(reference.sourceId()); + String label = source.title() != null ? source.title() + : source.url() != null ? source.url() + : source.path() != null ? source.path() + : source.id(); + System.out.printf("\"%s\" — %s%n", quoted, label); + } + } +}); +``` + +
+
+Rust + + + +```rust +use github_copilot_sdk::session_events::AssistantMessageData; +use std::collections::HashMap; + +let mut events = session.subscribe(); + +while let Ok(event) = events.recv().await { + if event.event_type != "assistant.message" { + continue; + } + + let Some(data) = event.typed_data::() else { + continue; + }; + let Some(citations) = data.citations.as_ref() else { + continue; + }; + + let sources: HashMap<&str, _> = citations + .sources + .iter() + .map(|source| (source.id.as_str(), source)) + .collect(); + + // Span offsets are UTF-16 code units, so index the UTF-16 view of the content. + let units: Vec = data.content.encode_utf16().collect(); + + for span in &citations.spans { + let quoted = String::from_utf16_lossy( + &units[span.start_index as usize..span.end_index as usize], + ); + for reference in &span.references { + let Some(source) = sources.get(reference.source_id.as_str()) else { + continue; + }; + let label = source + .title + .as_deref() + .or(source.url.as_deref()) + .or(source.path.as_deref()) + .unwrap_or(source.id.as_str()); + println!("\"{quoted}\" — {label}"); + } + } +} +``` + +
+ +## Citation payload reference + +The `citations` object separates deduplicated sources from the spans that reference them, so a source cited five times appears once in `sources`. + +| Type | Field | Description | +|---|---|---| +| `Citations` | `sources` | Deduplicated set of sources referenced by the citation spans | +| `Citations` | `spans` | Spans of generated text annotated with their supporting sources | +| `CitationSource` | `id` | Stable, turn-scoped identifier referenced by `CitationReference.sourceId` | +| `CitationSource` | `provider` | System that produced the citation: `anthropic`, `openai`, or `client` | +| `CitationSource` | `title?` | Human-readable title of the source | +| `CitationSource` | `url?` | URL of the source, when it is a web resource | +| `CitationSource` | `path?` | File path relative to the agent workspace root, when the source is a file | +| `CitationSpan` | `startIndex` | Start offset in the final message content (UTF-16 code units, zero-based, inclusive) | +| `CitationSpan` | `endIndex` | End offset in the final message content (UTF-16 code units, zero-based, exclusive) | +| `CitationSpan` | `references` | The sources that support this span | +| `CitationReference` | `sourceId` | Identifier of the `CitationSource` this reference points to | +| `CitationReference` | `citedText?` | Exact text from the source that supports the span, when the model provides it | +| `CitationReference` | `location?` | Location within the source that supports the span | +| `CitationReference` | `providerMetadata?` | Provider-native correlation data, passed through opaquely | + +> [!TIP] +> Span offsets are measured in UTF-16 code units against the final `content` string. TypeScript, Java, and .NET strings are already UTF-16, so you can slice them directly. Python strings are indexed by Unicode code point and Go and Rust strings are UTF-8, so convert the content to UTF-16 code units before slicing, as the examples above do. + +### Citation locations + +`CitationReference.location` is a discriminated union keyed on `type`: + +| Location type | Fields | Use | +|---|---|---| +| `char` | `startIndex`, `endIndex` | Character range within the source text | +| `page` | `startPage`, `endPage` | Page range within a paginated document | +| `block` | `startBlock`, `endBlock` | Content-block range within a structured document | + +## Provide citable sources + +Citations need source material the model can attribute. There are two ways to supply it. + +### Attach documents to a message + +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. + + + +```typescript +await session.sendAndWait({ + prompt: "Summarize the attached PDF and cite the passages you used.", + attachments: [ + { + type: "blob", + data: pdfBase64, + displayName: "quarterly-report.pdf", + mimeType: "application/pdf", + }, + ], +}); +``` + +See [Image input](./image-input.md) for the attachment API and the `file` and `blob` attachment shapes. + +### Return citable sources from a tool + +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. + +## Limitations + +* Citations are experimental in every SDK and are not covered by compatibility guarantees. +* Coverage depends on the model provider. A session configured for a provider without citation support emits no `citations` payload. +* Citations are only present on the final `assistant.message` event, so streaming consumers cannot render them mid-response. +* Public code and IP-duplication citations are not part of this surface. + +## Further reading + +* [Streaming events](./streaming-events.md): subscribe to session events and narrow event types +* [Image input](./image-input.md): attach files and in-memory blobs to a message +* [Session persistence](./session-persistence.md): resume sessions and re-apply session options +* [Compatibility](../troubleshooting/compatibility.md): SDK and CLI feature matrix