From 2bec859e6280686a7e5103ff54b100ca97bcea45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:27:53 +0000 Subject: [PATCH] [dotnet-port-api] Port message source attribution helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/context.go | 7 +--- agent/history.go | 7 +--- agent/middleware.go | 4 +- docs/dotnet-go-sdk-feature-comparison.md | 2 +- message/message.go | 31 ++++++++++++++ message/message_test.go | 52 ++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 16 deletions(-) diff --git a/agent/context.go b/agent/context.go index 40c3baa5..5d8c144e 100644 --- a/agent/context.go +++ b/agent/context.go @@ -135,12 +135,7 @@ func (p *defaultContextProvider) Invoking(ctx context.Context, invoking Invoking if len(providedMessages) > 0 { source := message.Source{Type: SourceTypeContextProvider, ID: p.config.SourceID} for i, msg := range providedMessages { - if msg == nil || msg.Source == source { - continue - } - marked := msg.Clone() - marked.Source = source - providedMessages[i] = marked + providedMessages[i] = msg.WithSource(source) } outMessages = append(outMessages, providedMessages...) } diff --git a/agent/history.go b/agent/history.go index 348175eb..ef92fa03 100644 --- a/agent/history.go +++ b/agent/history.go @@ -109,12 +109,7 @@ func (p *defaultHistoryProvider) Invoking(ctx context.Context, invoking Invoking source := message.Source{Type: SourceTypeHistoryProvider, ID: p.config.SourceID} for i, msg := range providedMessages { - if msg == nil || msg.Source == source { - continue - } - marked := msg.Clone() - marked.Source = source - providedMessages[i] = marked + providedMessages[i] = msg.WithSource(source) } outMessages := append(providedMessages, invoking.Messages...) diff --git a/agent/middleware.go b/agent/middleware.go index 029c00f7..53de38e9 100644 --- a/agent/middleware.go +++ b/agent/middleware.go @@ -62,9 +62,7 @@ func (mr middlewareRunner) Run(ctx context.Context, messages []*message.Message, if msg == nil || msg.Source != (message.Source{}) { continue } - marked := msg.Clone() - marked.Source = message.Source{Type: SourceTypeMiddleware} - outMessages[i] = marked + outMessages[i] = msg.WithSource(message.Source{Type: SourceTypeMiddleware}) } return mr.next(ctx, outMessages, opts...) } diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 22f52a52..c2ffee5d 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -26,7 +26,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Feature area | .NET SDK | Go SDK | Status | Misalignment | | --- | --- | --- | --- | --- | | Core agent abstraction | `AIAgent`, `DelegatingAIAgent`, `AgentRunOptions`, `AgentResponse`, `AgentResponseUpdate`, current run context, metadata, typed structured responses. | `agent.Agent`, `agent.ProviderConfig`, `agent.Config`, `agent.Option`, `Response`, `ResponseUpdate`, `Run`, `RunText`, `RunMessage`, `ResponseStream`. | Aligned | .NET exposes extension-method adapters around `Microsoft.Extensions.AI`; Go uses a provider `RunFunc` contract and package-level option wrappers. | -| Agent identity and metadata | `AIAgentMetadata`, agent ID/name/description, source attribution extensions. | Agent ID/name/description, provider name, response author stamping. | Partial | Go does not expose the same request source attribution helpers as .NET. | +| Agent identity and metadata | `AIAgentMetadata`, agent ID/name/description, source attribution extensions. | Agent ID/name/description, provider name, response author stamping, `message.Message` source attribution helpers. | Aligned | API shape differs: .NET exposes extension methods around `Microsoft.Extensions.AI`; Go exposes an explicit `message.Source` field plus helper methods. | | Sessions | `AgentSession`, `AgentSessionStateBag`, session serialization helpers, provider session state. | `agent.Session`, marshal/unmarshal hooks, provider session hooks, local/service ID support. | Aligned | .NET has a richer typed state bag and extension helpers; Go stores provider/session values through its own session abstraction. | | Chat history | `ChatHistoryProvider`, `InMemoryChatHistoryProvider`, per-service-call persistence, reducer triggers. | `HistoryProvider`, default in-memory history for local sessions, third-party storage example. | Partial | Go has the core lifecycle but fewer built-in storage providers and no first-class reducer trigger options on the history provider. | | Context providers and memory injection | `AIContextProvider`, `MessageAIContextProvider`, provider invoking/invoked lifecycle. | `agent.ContextProvider`, `(*agent.ContextProvider).Middleware`, before/after lifecycle. | Aligned | .NET context providers are integrated with `Microsoft.Extensions.AI`; Go providers directly transform `message.Message` slices and options. | diff --git a/message/message.go b/message/message.go index aad9ca73..8f2962ec 100644 --- a/message/message.go +++ b/message/message.go @@ -69,6 +69,37 @@ func (m *Message) Usage() UsageDetails { return m.Contents.Usage() } +// SourceType returns the message source type. +// +// When no explicit source type is set, [SourceTypeExternal] is returned. +func (m *Message) SourceType() SourceType { + if m == nil { + return SourceTypeExternal + } + return m.Source.Type +} + +// SourceID returns the message source identifier. +func (m *Message) SourceID() string { + if m == nil { + return "" + } + return m.Source.ID +} + +// WithSource returns the message tagged with the provided source. +// +// If the message already has the requested source, the original message is +// returned. Otherwise, a cloned message is returned with the updated source. +func (m *Message) WithSource(source Source) *Message { + if m == nil || m.Source == source { + return m + } + v := m.Clone() + v.Source = source + return v +} + // Clone creates a shallow copy of the message. func (m *Message) Clone() *Message { if m == nil { diff --git a/message/message_test.go b/message/message_test.go index 83c33c0c..d0a770ae 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -26,3 +26,55 @@ func TestMessage_Clone_ClonesAdditionalProperties(t *testing.T) { t.Fatalf("expected original additional properties to remain unchanged, got %v", original.AdditionalProperties["k"]) } } + +func TestMessage_SourceType_DefaultsToExternal(t *testing.T) { + msg := message.NewText("hello") + + if got := msg.SourceType(); got != message.SourceTypeExternal { + t.Fatalf("SourceType() = %q, want %q", got, message.SourceTypeExternal) + } +} + +func TestMessage_SourceTypeAndSourceID_ReturnExplicitSource(t *testing.T) { + msg := message.NewText("hello") + msg.Source = message.Source{Type: message.SourceType("context-provider"), ID: "ctx"} + + if got := msg.SourceType(); got != message.SourceType("context-provider") { + t.Fatalf("SourceType() = %q, want %q", got, message.SourceType("context-provider")) + } + if got := msg.SourceID(); got != "ctx" { + t.Fatalf("SourceID() = %q, want %q", got, "ctx") + } +} + +func TestMessage_WithSource_ClonesWhenSourceChanges(t *testing.T) { + original := message.NewText("hello") + original.AdditionalProperties = map[string]any{"k": "v"} + + got := original.WithSource(message.Source{Type: message.SourceType("history-provider"), ID: "history"}) + if got == nil { + t.Fatal("expected sourced message") + } + if got == original { + t.Fatal("expected WithSource to clone when source changes") + } + if got.Source != (message.Source{Type: message.SourceType("history-provider"), ID: "history"}) { + t.Fatalf("WithSource source = %#v", got.Source) + } + if got.AdditionalProperties["k"] != "v" { + t.Fatalf("expected cloned additional properties, got %v", got.AdditionalProperties["k"]) + } + if original.Source != (message.Source{}) { + t.Fatalf("expected original source to remain unchanged, got %#v", original.Source) + } +} + +func TestMessage_WithSource_ReturnsOriginalWhenUnchanged(t *testing.T) { + original := message.NewText("hello") + original.Source = message.Source{Type: message.SourceType("context-provider"), ID: "ctx"} + + got := original.WithSource(message.Source{Type: message.SourceType("context-provider"), ID: "ctx"}) + if got != original { + t.Fatal("expected WithSource to return original message when source is unchanged") + } +}