diff --git a/agent/response.go b/agent/response.go index 0a74bedd..a2dee4be 100644 --- a/agent/response.go +++ b/agent/response.go @@ -146,6 +146,10 @@ func (resp *Response) ToUpdates() []*ResponseUpdate { if createdAt.IsZero() { createdAt = resp.CreatedAt } + role := msg.Role + if role == "" { + role = message.RoleAssistant + } updates = append(updates, &ResponseUpdate{ RawRepresentation: msg.RawRepresentation, AdditionalProperties: msg.AdditionalProperties, @@ -154,7 +158,7 @@ func (resp *Response) ToUpdates() []*ResponseUpdate { ResponseID: resp.ID, FinishReason: resp.FinishReason, AuthorName: msg.AuthorName, - Role: msg.Role, + Role: role, CreatedAt: createdAt, Contents: msg.Contents, }) @@ -179,25 +183,28 @@ func (resp *Response) Update(update *ResponseUpdate) { if update == nil { return } - msg := resp.targetMessage(update) - // Some members on ResponseUpdate map to members of Message. - // Incorporate those into the latest message; in cases where the message - // stores a single value, prefer the latest update's value over anything - // stored in the message. - msg.AuthorName = cmp.Or(update.AuthorName, msg.AuthorName) - msg.Role = cmp.Or(update.Role, msg.Role) - msg.ID = cmp.Or(update.MessageID, msg.ID) - if !isValidCreatedAt(msg.CreatedAt) && isValidCreatedAt(update.CreatedAt) { - msg.CreatedAt = update.CreatedAt - } - msg.Contents = append(msg.Contents, update.Contents...) - if update.AdditionalProperties != nil { - if msg.AdditionalProperties == nil { - msg.AdditionalProperties = make(map[string]any) + // A response-level metadata update must not create an empty message. + if update.MessageID != "" || update.AuthorName != "" || update.Role != "" || len(update.Contents) > 0 || update.RawRepresentation != nil { + msg := resp.targetMessage(update) + // Some members on ResponseUpdate map to members of Message. + // Incorporate those into the latest message; in cases where the message + // stores a single value, prefer the latest update's value over anything + // stored in the message. + msg.AuthorName = cmp.Or(update.AuthorName, msg.AuthorName) + msg.Role = cmp.Or(update.Role, msg.Role) + msg.ID = cmp.Or(update.MessageID, msg.ID) + if !isValidCreatedAt(msg.CreatedAt) && isValidCreatedAt(update.CreatedAt) { + msg.CreatedAt = update.CreatedAt + } + msg.Contents = append(msg.Contents, update.Contents...) + if update.AdditionalProperties != nil { + if msg.AdditionalProperties == nil { + msg.AdditionalProperties = make(map[string]any) + } + maps.Copy(msg.AdditionalProperties, update.AdditionalProperties) } - maps.Copy(msg.AdditionalProperties, update.AdditionalProperties) + msg.RawRepresentation = appendRawRepresentation(msg.RawRepresentation, update.RawRepresentation) } - msg.RawRepresentation = appendRawRepresentation(msg.RawRepresentation, update.RawRepresentation) // Other members on a ResponseUpdate map to members of the response. // Update the response object with those, preferring the values from later updates. diff --git a/agent/response_test.go b/agent/response_test.go index 7d45feee..cc5342ce 100644 --- a/agent/response_test.go +++ b/agent/response_test.go @@ -963,6 +963,86 @@ func TestResponse_ToUpdates_WithAdditionalPropertiesOnlyProducesSingleUpdate(t * } } +func TestResponse_ToUpdates_RoundTripDoesNotCreateMessageForResponseMetadata(t *testing.T) { + tests := []struct { + name string + response *agent.Response + wantToken string + wantProperty any + }{ + { + name: "continuation token", + response: &agent.Response{ContinuationToken: "token-123"}, + wantToken: "token-123", + }, + { + name: "additional properties", + response: &agent.Response{AdditionalProperties: map[string]any{"key": "value"}}, + wantProperty: "value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var collected agent.Response + for _, update := range tt.response.ToUpdates() { + collected.Update(update) + } + + if len(collected.Messages) != 0 { + t.Fatalf("expected no messages after round trip, got %d", len(collected.Messages)) + } + if collected.ContinuationToken != tt.wantToken { + t.Errorf("continuation token = %q, want %q", collected.ContinuationToken, tt.wantToken) + } + if got := collected.AdditionalProperties["key"]; got != tt.wantProperty { + t.Errorf("additional property = %v, want %v", got, tt.wantProperty) + } + }) + } +} + +func TestResponse_ToUpdates_RoundTripPreservesMetadataOnlyMessages(t *testing.T) { + createdAt := time.Date(2026, time.September, 20, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + message *message.Message + }{ + { + name: "additional properties", + message: &message.Message{ + AdditionalProperties: map[string]any{"key": "value"}, + }, + }, + { + name: "created at", + message: &message.Message{ + CreatedAt: createdAt, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &agent.Response{Messages: []*message.Message{tt.message}} + var collected agent.Response + for _, update := range original.ToUpdates() { + collected.Update(update) + } + + if len(collected.Messages) != 1 { + t.Fatalf("message count = %d, want 1", len(collected.Messages)) + } + if got := collected.Messages[0].AdditionalProperties["key"]; got != tt.message.AdditionalProperties["key"] { + t.Errorf("additional property = %v, want %v", got, tt.message.AdditionalProperties["key"]) + } + if got := collected.Messages[0].CreatedAt; !got.Equal(tt.message.CreatedAt) { + t.Errorf("created at = %v, want %v", got, tt.message.CreatedAt) + } + }) + } +} + func TestResponse_String(t *testing.T) { msg := func(texts ...string) *message.Message { var contents message.Contents