diff --git a/provider/copilotprovider/copilot.go b/provider/copilotprovider/copilot.go index cf5741cd..68996dd2 100644 --- a/provider/copilotprovider/copilot.go +++ b/provider/copilotprovider/copilot.go @@ -668,10 +668,23 @@ func (p *provider) assistantMessageUpdate(event copilot.SessionEvent, data *copi ContentHeader: message.ContentHeader{RawRepresentation: event}, }} } else { - update.Contents = []message.Content{&message.TextContent{ + textContent := &message.TextContent{ ContentHeader: message.ContentHeader{RawRepresentation: event}, Text: data.Content, - }} + } + // Surface native model citations (enabled via AgentConfig.EnableCitations) + // as CitationAnnotations, mirroring the OpenAI chat/Responses providers. + if data.Citations != nil { + for _, source := range data.Citations.Sources { + textContent.Annotations = append(textContent.Annotations, &message.CitationAnnotation{ + FileID: derefString(source.Path), + Title: derefString(source.Title), + URL: derefString(source.URL), + RawRepresentation: source, + }) + } + } + update.Contents = []message.Content{textContent} if data.ReasoningText != nil { update.Contents = append(update.Contents, &message.TextReasoningContent{ ContentHeader: message.ContentHeader{RawRepresentation: event}, @@ -837,6 +850,13 @@ func int64Value(value *int64) int64 { return *value } +func derefString(value *string) string { + if value == nil { + return "" + } + return *value +} + func firstNonNilString(values ...*string) string { for _, value := range values { if value != nil { diff --git a/provider/copilotprovider/copilot_test.go b/provider/copilotprovider/copilot_test.go index c3486b39..c076e91e 100644 --- a/provider/copilotprovider/copilot_test.go +++ b/provider/copilotprovider/copilot_test.go @@ -1264,3 +1264,43 @@ func assertStringSlice(t *testing.T, got any, want []string, name string) { } } } + +func TestConvertToAgentResponseUpdate_AssistantMessageSurfacesCitations(t *testing.T) { + runtime := newFakeRuntime(t, + sessionEvent("assistant.message", map[string]any{ + "messageId": "msg-cite", + "content": "The sky is blue.", + "citations": map[string]any{ + "sources": []any{ + map[string]any{ + "id": "s1", + "provider": "openai", + "title": "Sky facts", + "url": "https://example.com/sky", + }, + }, + "spans": []any{}, + }, + }), + idleEvent(), + ) + agent := copilotprovider.NewAgent(runtime.client(), copilotprovider.AgentConfig{}) + + response, err := runText(t, agent, "why is the sky blue?", agentpkg.Stream(false)) + if err != nil { + t.Fatalf("RunText: %v", err) + } + text := firstContent[*message.TextContent](t, response) + var citation *message.CitationAnnotation + for _, ann := range text.Annotations { + if c, ok := ann.(*message.CitationAnnotation); ok { + citation = c + } + } + if citation == nil { + t.Fatalf("expected a CitationAnnotation on the assistant text, got %#v", text.Annotations) + } + if citation.Title != "Sky facts" || citation.URL != "https://example.com/sky" { + t.Errorf("citation = %#v, want Title=%q URL=%q", citation, "Sky facts", "https://example.com/sky") + } +}