Surface Anthropic text citation annotations on the streaming path - #681
Conversation
There was a problem hiding this comment.
Pull request overview
Ensures Anthropic streaming responses surface the same citation metadata as the non-streaming path by emitting citation annotations derived from accumulated text blocks when a streaming content block completes.
Changes:
- Emit citation annotations on the streaming path when a
TextBlockstops (without duplicating already-streamed text). - Factor citation-to-
message.CitationAnnotationconversion into a sharedcitationAnnotationshelper used by both streaming and non-streaming paths. - Add a new SSE-based streaming test to validate citation annotations are present in streamed
TextContent.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| provider/anthropicprovider/agent.go | Adds citation annotation emission on streaming content_block_stop and refactors citation conversion into a helper shared with non-streaming. |
| provider/anthropicprovider/agent_test.go | Adds a streaming SSE test asserting citations become CitationAnnotation updates during streaming collection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| contents = append(contents, &message.TextContent{ | ||
| ContentHeader: message.ContentHeader{ | ||
| Annotations: annotations, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Good catch — fixed in e996d12: the streamed citation-only TextContent now sets ContentHeader.RawRepresentation to the accumulated anthropic.TextBlock, matching buildBlock/buildDelta for consistent inspection of the underlying block.
This comment has been minimized.
This comment has been minimized.
9bc7407 to
e59d1eb
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The streaming loop dropped all citation annotations: content_block_stop only handled tool_use blocks and buildDelta had no citations_delta case, so streamed text lost the CitationAnnotation values the non-streaming buildBlock produces. Citations are only available on the accumulated text block, so emit an annotations-only TextContent (empty Text, to avoid duplicating the already-streamed text) when the block stops. This aligns the streaming path with the non-streaming path and with the .NET/Python SDKs, where streamed responses carry the same citation annotations as non-streamed ones.
e996d12 to
001d2e8
Compare
Parity Review —
|
What
The Anthropic streaming path dropped all text citation annotations. In
provider/anthropicprovider/agent.gothe streaming loop'scontent_block_stophandler only convertedtool_useblocks, andbuildDeltahas nocitations_deltacase, so streamed text arrived without themessage.CitationAnnotationvalues that the non-streamingbuildBlockproduces fromTextBlock.Citations.This change surfaces those citations on the streaming path. Citations are only populated on the accumulated text block (the SDK appends each
citations_deltatocb.Citations), so when a text block stops we inspectaccumulated.Content[index]; if it is aTextBlockwith citations we emit an annotations-onlyTextContent. The text itself was already streamed viatext_delta, soTextis left empty to avoid duplicating it. The citation-to-CitationAnnotationmapping is extracted into a smallcitationAnnotationshelper shared by both paths.Why
Streamed and non-streamed responses should carry identical citation metadata. This matches the non-streaming behavior already covered by
TestTextCitationsBecomeAnnotations, and aligns with the .NET and Python Agent Framework SDKs, where citation annotations are attached to streamed content updates as well as final messages.Tests
Adds
TestStreamingTextCitationsBecomeAnnotationsin the canonicalagent_test.go, using the existing httptest SSE harness:message_start-> textcontent_block_start->text_delta->citations_delta(aweb_search_result_locationwith cited_text/title/url) ->content_block_stop->message_delta/message_stop. Run withStream(true), it asserts the collectedTextContentcarries a*message.CitationAnnotationwith the expected Snippet/Title/URL. The test fails before the fix (no annotation) and passes after.go build ./...,go vet ./provider/anthropicprovider/..., andgo test ./provider/anthropicprovider/...all pass.