From 79f74e79a3d8a0089c2bb98bf6e02bfe48995c56 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Mon, 21 Sep 2026 11:45:29 +0530 Subject: [PATCH] Scope Response.Update AdditionalProperties by MessageID Response.Update copied an update's AdditionalProperties into BOTH the target message and the response, so message-scoped provider metadata leaked to the response level and response-scoped metadata leaked onto the current message. Scope them to the message when the update carries a MessageID and to the response otherwise, matching .NET's ChatResponseExtensions.ProcessUpdate (which Response.Update ports). --- agent/response.go | 8 ++++++-- agent/response_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/agent/response.go b/agent/response.go index 0a74bedd..827828ba 100644 --- a/agent/response.go +++ b/agent/response.go @@ -191,7 +191,11 @@ func (resp *Response) Update(update *ResponseUpdate) { msg.CreatedAt = update.CreatedAt } msg.Contents = append(msg.Contents, update.Contents...) - if update.AdditionalProperties != nil { + // AdditionalProperties are scoped to the message when the update carries a + // MessageID, otherwise to the response (below), matching .NET's + // ChatResponseExtensions.ProcessUpdate. Copying to both would duplicate and + // mis-attribute provider metadata. + if update.AdditionalProperties != nil && update.MessageID != "" { if msg.AdditionalProperties == nil { msg.AdditionalProperties = make(map[string]any) } @@ -213,7 +217,7 @@ func (resp *Response) Update(update *ResponseUpdate) { if !isValidCreatedAt(resp.CreatedAt) && isValidCreatedAt(update.CreatedAt) { resp.CreatedAt = update.CreatedAt } - if update.AdditionalProperties != nil { + if update.AdditionalProperties != nil && update.MessageID == "" { if resp.AdditionalProperties == nil { resp.AdditionalProperties = make(map[string]any) } diff --git a/agent/response_test.go b/agent/response_test.go index 7d45feee..8cb7461a 100644 --- a/agent/response_test.go +++ b/agent/response_test.go @@ -455,6 +455,38 @@ func TestResponse_CreatedAt(t *testing.T) { } } +// AdditionalProperties are scoped to the message when the update carries a +// MessageID and to the response otherwise, matching .NET's ProcessUpdate. They +// must not land on both. +func TestResponse_Update_AdditionalPropertiesScopedByMessageID(t *testing.T) { + // With a MessageID -> message only. + withID := &agent.Response{} + withID.Update(&agent.ResponseUpdate{ + MessageID: "msg1", + AdditionalProperties: map[string]any{"k": "v"}, + Contents: message.Contents{&message.TextContent{Text: "hi"}}, + }) + if withID.Messages[0].AdditionalProperties["k"] != "v" { + t.Errorf("message AdditionalProperties = %v, want k=v", withID.Messages[0].AdditionalProperties) + } + if len(withID.AdditionalProperties) != 0 { + t.Errorf("response AdditionalProperties = %v, want empty (scoped to message)", withID.AdditionalProperties) + } + + // Without a MessageID -> response only. + noID := &agent.Response{} + noID.Update(&agent.ResponseUpdate{ + AdditionalProperties: map[string]any{"k": "v"}, + Contents: message.Contents{&message.TextContent{Text: "hi"}}, + }) + if noID.AdditionalProperties["k"] != "v" { + t.Errorf("response AdditionalProperties = %v, want k=v", noID.AdditionalProperties) + } + if len(noID.Messages[0].AdditionalProperties) != 0 { + t.Errorf("message AdditionalProperties = %v, want empty (scoped to response)", noID.Messages[0].AdditionalProperties) + } +} + func TestResponse_Update_AdditionalProperties(t *testing.T) { resp := &agent.Response{}