Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions agent/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,7 @@ func (p *defaultContextProvider) Invoking(ctx context.Context, invoking Invoking
copy(outMessages, invoking.Messages)
source := message.Source{Type: SourceTypeContextProvider, ID: p.config.SourceID}
for _, msg := range providedMessages {
if msg == nil || msg.Source == source {
outMessages = append(outMessages, msg)
continue
}
marked := msg.Clone()
marked.Source = source
outMessages = append(outMessages, marked)
outMessages = append(outMessages, msg.WithSource(source))
}
}

Expand Down
8 changes: 1 addition & 7 deletions agent/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,7 @@ func (p *defaultHistoryProvider) Invoking(ctx context.Context, invoking Invoking
outMessages := make([]*message.Message, 0, len(providedMessages)+len(invoking.Messages))
source := message.Source{Type: SourceTypeHistoryProvider, ID: p.config.SourceID}
for _, msg := range providedMessages {
if msg == nil || msg.Source == source {
outMessages = append(outMessages, msg)
continue
}
marked := msg.Clone()
marked.Source = source
outMessages = append(outMessages, marked)
outMessages = append(outMessages, msg.WithSource(source))
}
outMessages = append(outMessages, invoking.Messages...)

Expand Down
4 changes: 1 addition & 3 deletions agent/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,11 @@ 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}
if !outMessagesCloned {
outMessages = slices.Clone(outMessages)
outMessagesCloned = true
}
outMessages[i] = marked
outMessages[i] = msg.WithSource(message.Source{Type: SourceTypeMiddleware})
}
return mr.next(ctx, outMessages, opts...)
}
Expand Down
2 changes: 1 addition & 1 deletion docs/dotnet-go-sdk-feature-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Intentional contract choices in this parity pass:
| 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`; response aggregation preserves provider `RawRepresentation`. | 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. |
Expand Down
31 changes: 31 additions & 0 deletions message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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, cloning its top-level map and
// slice containers while sharing their values and content objects.
func (m *Message) Clone() *Message {
Expand Down
52 changes: 52 additions & 0 deletions message/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,58 @@ func TestMessage_Clone_ClonesAdditionalProperties(t *testing.T) {
}
}

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")
}
}

func TestMessage_Clone_ClonesContentsSlice(t *testing.T) {
content := &message.TextContent{Text: "original"}
original := message.New(content)
Expand Down
Loading