From b07de7017dec33c405ddb04ba4541ca42a7ef0a9 Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Fri, 17 Oct 2025 22:34:52 +0200 Subject: [PATCH 1/4] Add unit test --- ...entChatCompletionServiceConversionTests.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs index 8ef515b416b1..d7d3529be02a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs @@ -567,7 +567,7 @@ public async Task GetStreamingChatMessageContentsAsyncWithNullExecutionSettingsD } [Fact] - public async Task GetChatMessageContentsAsyncWithMutatingPrepareChatHistoryPreservesChatHistoryMutations() + public async Task GetChatMessageContentsAsyncWithToolUseShouldOrderHistoryAccordingToToolExecution() { // Arrange var originalChatHistory = new ChatHistory(); @@ -647,6 +647,74 @@ public async Task GetStreamingChatMessageContentsAsyncWithMutatingPrepareChatHis Assert.Equal("Original message", originalChatHistory[1].Content); } + [Fact] + public async Task GetStreamingChatMessageContentsAsyncShouldMaintainFunctionCallOrdering() + { + // Arrange + var chatHistory = new ChatHistory(); + + using var chatClient = new TestChatClient + { + CompleteStreamingAsyncDelegate = (messages, options, cancellationToken) => + { + // Simulate calls coming first, then results + return new[] + { + new ChatResponseUpdate(ChatRole.Assistant, [new Microsoft.Extensions.AI.FunctionCallContent("call-1", "Func1", null)]), + new ChatResponseUpdate(ChatRole.Assistant, [new Microsoft.Extensions.AI.FunctionCallContent("call-2", "Func2", null)]), + new ChatResponseUpdate(ChatRole.Tool, [new Microsoft.Extensions.AI.FunctionResultContent("call-1", "result1")]), + new ChatResponseUpdate(ChatRole.Tool, [new Microsoft.Extensions.AI.FunctionResultContent("call-2", "result2")]) + }.ToAsyncEnumerable(); + } + }; + + var service = chatClient.AsChatCompletionService(); + + // Act + var results = new List(); + await foreach (var update in service.GetStreamingChatMessageContentsAsync(chatHistory)) + { + results.Add(update); + } + + // Assert + Assert.Collection(chatHistory, + // Function calls + call1 => + { + Assert.Equal(AuthorRole.Assistant, call1.Role); + var callContent = Assert.Single(call1.Items); + var functionCall = Assert.IsType(callContent); + Assert.Equal("call-1", functionCall.Id); + Assert.Equal("Func1", functionCall.FunctionName); + }, + result1 => + { + Assert.Equal(AuthorRole.Tool, result1.Role); + var resultContent = Assert.Single(result1.Items); + var functionResult = Assert.IsType(resultContent); + Assert.Equal("call-1", functionResult.CallId); + Assert.Equal("result1", functionResult.Result); + }, + call2 => + { + Assert.Equal(AuthorRole.Assistant, call2.Role); + var callContent = Assert.Single(call2.Items); + var functionCall = Assert.IsType(callContent); + Assert.Equal("call-2", functionCall.Id); + Assert.Equal("Func2", functionCall.FunctionName); + }, + // Second function result added to history + result2 => + { + Assert.Equal(AuthorRole.Tool, result2.Role); + var resultContent = Assert.Single(result2.Items); + var functionResult = Assert.IsType(resultContent); + Assert.Equal("call-2", functionResult.CallId); + Assert.Equal("result2", functionResult.Result); + }); + } + /// /// Test implementation of PromptExecutionSettings that overrides PrepareChatHistoryToRequestAsync. /// From eb11d0353e453105db90dc6c5cd73b36cc6b169d Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Fri, 17 Oct 2025 23:32:21 +0200 Subject: [PATCH 2/4] Reverted changed testname out of scope (AI creep) --- .../ChatClientChatCompletionServiceConversionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs index d7d3529be02a..35910c476390 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs @@ -567,7 +567,7 @@ public async Task GetStreamingChatMessageContentsAsyncWithNullExecutionSettingsD } [Fact] - public async Task GetChatMessageContentsAsyncWithToolUseShouldOrderHistoryAccordingToToolExecution() + public async Task GetChatMessageContentsAsyncWithMutatingPrepareChatHistoryPreservesChatHistoryMutations() { // Arrange var originalChatHistory = new ChatHistory(); From 850a47f1ea8414a6555aad0051ff32c068b2347e Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Fri, 17 Oct 2025 23:36:45 +0200 Subject: [PATCH 3/4] Simplified test --- .../ChatClientChatCompletionServiceConversionTests.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs index 35910c476390..2bc7f4e67eab 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs @@ -671,15 +671,10 @@ public async Task GetStreamingChatMessageContentsAsyncShouldMaintainFunctionCall var service = chatClient.AsChatCompletionService(); // Act - var results = new List(); - await foreach (var update in service.GetStreamingChatMessageContentsAsync(chatHistory)) - { - results.Add(update); - } + await service.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); // Assert Assert.Collection(chatHistory, - // Function calls call1 => { Assert.Equal(AuthorRole.Assistant, call1.Role); @@ -704,7 +699,6 @@ public async Task GetStreamingChatMessageContentsAsyncShouldMaintainFunctionCall Assert.Equal("call-2", functionCall.Id); Assert.Equal("Func2", functionCall.FunctionName); }, - // Second function result added to history result2 => { Assert.Equal(AuthorRole.Tool, result2.Role); From 7f0eef2d9cf2b09b780c09d69be92c6b372c3a2a Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Tue, 21 Oct 2025 22:39:37 +0200 Subject: [PATCH 4/4] Resolve merge conflicts --- .../ChatClientChatCompletionService.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs index f447c38b0c99..1aa413918415 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs @@ -91,6 +91,9 @@ public async IAsyncEnumerable GetStreamingChatMessa ChatRole? role = null; + List functionCalls = []; + Dictionary functionResults = []; + await foreach (var update in this._chatClient.GetStreamingResponseAsync( chatHistory.ToChatMessageList(), executionSettings.ToChatOptions(kernel), @@ -103,17 +106,27 @@ public async IAsyncEnumerable GetStreamingChatMessa { if (fcc is Microsoft.Extensions.AI.FunctionCallContent functionCallContent) { - chatHistory.Add(new ChatMessage(ChatRole.Assistant, [functionCallContent]).ToChatMessageContent()); + functionCalls.Add(functionCallContent); continue; } if (fcc is Microsoft.Extensions.AI.FunctionResultContent functionResultContent) { - chatHistory.Add(new ChatMessage(ChatRole.Tool, [functionResultContent]).ToChatMessageContent()); + functionResults[functionResultContent.CallId] = functionResultContent; } } yield return update.ToStreamingChatMessageContent(); } + + // Tool result messages must be added immediately after the corresponding tool call to preserve correct conversation order. + foreach (var functionCallContent in functionCalls) + { + chatHistory.Add(new ChatMessage(ChatRole.Assistant, [functionCallContent]).ToChatMessageContent()); + if (functionResults.TryGetValue(functionCallContent.CallId, out var functionResultContent)) + { + chatHistory.Add(new ChatMessage(ChatRole.Tool, [functionResultContent]).ToChatMessageContent()); + } + } } }