From 3ee7461b5956d5ac0290ac54a9479ad1cd277882 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:04:06 +0100 Subject: [PATCH] Fix createdAt merging for agent run updates (#1161) --- .../AgentRunResponseExtensions.cs | 9 +- .../AgentRunResponseUpdateExtensionsTests.cs | 92 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs index 87ce3ea495..c5ffd07837 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs @@ -266,7 +266,7 @@ public static class AgentRunResponseExtensions if (isNewMessage) { - message = new ChatMessage(ChatRole.Assistant, []); + message = new(ChatRole.Assistant, []); response.Messages.Add(message); } else @@ -283,6 +283,11 @@ public static class AgentRunResponseExtensions message.AuthorName = update.AuthorName; } + if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt)) + { + message.CreatedAt = update.CreatedAt; + } + if (update.Role is ChatRole role) { message.Role = role; @@ -323,7 +328,7 @@ public static class AgentRunResponseExtensions response.ResponseId = update.ResponseId; } - if (update.CreatedAt is not null) + if (response.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > response.CreatedAt)) { response.CreatedAt = update.CreatedAt; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs index 95888bffb7..a653cf80f5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs @@ -207,6 +207,98 @@ public class AgentRunResponseUpdateExtensionsTests Assert.Equal("Hello, world!", Assert.IsType(Assert.Single(Assert.Single(response.Messages).Contents)).Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToAgentRunResponse_AlternativeTimestampsAsync(bool useAsync) + { + DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); + DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero); + DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero); + DateTimeOffset unixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + AgentRunResponseUpdate[] updates = + [ + + // Start with an early timestamp + new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, + + // Unix epoch (as "null") should not overwrite + new(null, "b") { CreatedAt = unixEpoch }, + + // Newer timestamp should overwrite + new(null, "c") { CreatedAt = middle }, + + // Older timestamp should not overwrite + new(null, "d") { CreatedAt = early }, + + // Even newer timestamp should overwrite + new(null, "e") { CreatedAt = late }, + + // Unix epoch should not overwrite again + new(null, "f") { CreatedAt = unixEpoch }, + + // null should not overwrite + new(null, "g") { CreatedAt = null }, + ]; + + AgentRunResponse response = useAsync ? + updates.ToAgentRunResponse() : + await YieldAsync(updates).ToAgentRunResponseAsync(); + Assert.Single(response.Messages); + + Assert.Equal("abcdefg", response.Messages[0].Text); + Assert.Equal(ChatRole.Tool, response.Messages[0].Role); + Assert.Equal(late, response.Messages[0].CreatedAt); + Assert.Equal(late, response.CreatedAt); + } + + public static IEnumerable ToAgentRunResponse_TimestampFolding_MemberData() + { + // Base test cases + var testCases = new (string? timestamp1, string? timestamp2, string? expectedTimestamp)[] + { + (null, null, null), + ("2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z"), + (null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + ("2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z"), + ("1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + }; + + // Yield each test case twice, once for useAsync = false and once for useAsync = true + foreach (var (timestamp1, timestamp2, expectedTimestamp) in testCases) + { + yield return new object?[] { false, timestamp1, timestamp2, expectedTimestamp }; + yield return new object?[] { true, timestamp1, timestamp2, expectedTimestamp }; + } + } + + [Theory] + [MemberData(nameof(ToAgentRunResponse_TimestampFolding_MemberData))] + public async Task ToAgentRunResponse_TimestampFoldingAsync(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp) + { + DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null; + DateTimeOffset? second = timestamp2 is not null ? DateTimeOffset.Parse(timestamp2) : null; + DateTimeOffset? expected = expectedTimestamp is not null ? DateTimeOffset.Parse(expectedTimestamp) : null; + + AgentRunResponseUpdate[] updates = + [ + new(ChatRole.Assistant, "a") { CreatedAt = first }, + new(null, "b") { CreatedAt = second }, + ]; + + AgentRunResponse response = useAsync ? + updates.ToAgentRunResponse() : + await YieldAsync(updates).ToAgentRunResponseAsync(); + + Assert.Single(response.Messages); + Assert.Equal("ab", response.Messages[0].Text); + Assert.Equal(expected, response.Messages[0].CreatedAt); + Assert.Equal(expected, response.CreatedAt); + } + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) { foreach (AgentRunResponseUpdate update in updates)