From 91d27555ee3724ecb0450b86c9bf568f1ac1bfcb Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 16 May 2026 05:08:27 +0800 Subject: [PATCH 1/2] fix: preserve AG-UI session history --- .../ChatClient/ChatClientAgent.cs | 17 ++++++-- .../AGUIChatClientTests.cs | 43 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 1133e10a8a0..b768fb6a9ff 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -38,6 +38,8 @@ namespace Microsoft.Agents.AI; /// public sealed partial class ChatClientAgent : AIAgent { + private const string AGUIProviderName = "ag-ui"; + private readonly ChatClientAgentOptions? _agentOptions; private readonly HashSet _aiContextProviderStateKeys; private readonly AIAgentMetadata _agentMetadata; @@ -815,7 +817,7 @@ internal void UpdateSessionConversationId(ChatClientAgentSession session, string if (!string.IsNullOrWhiteSpace(responseConversationId)) { - if (this._agentOptions?.ChatHistoryProvider is not null) + if (this.ConversationIdIndicatesServiceManagedHistory && this._agentOptions?.ChatHistoryProvider is not null) { // The agent has a ChatHistoryProvider configured, but the service returned a conversation id, // meaning the service manages chat history server-side. Both cannot be used simultaneously. @@ -929,6 +931,10 @@ private bool RequiresPerServiceCallChatHistoryPersistence } } + // AG-UI uses ConversationId as a thread id, not as a signal that the service stores model history. + private bool ConversationIdIndicatesServiceManagedHistory => + !string.Equals(this._agentMetadata.ProviderName, AGUIProviderName, StringComparison.Ordinal); + /// /// Ensures that contains the resolved session. /// @@ -976,12 +982,17 @@ private void WarnOnMissingPerServiceCallChatHistoryPersistingChatClient() private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions) { - ChatHistoryProvider? provider = chatOptions?.ConversationId is null ? this.ChatHistoryProvider : null; + ChatHistoryProvider? provider = + chatOptions?.ConversationId is null || !this.ConversationIdIndicatesServiceManagedHistory + ? this.ChatHistoryProvider + : null; // If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead. if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true) { - if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true && string.IsNullOrWhiteSpace(chatOptions?.ConversationId) is false) + if (this.ConversationIdIndicatesServiceManagedHistory && + this._agentOptions?.ThrowOnChatHistoryProviderConflict is true && + string.IsNullOrWhiteSpace(chatOptions?.ConversationId) is false) { throw new InvalidOperationException( $"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The current {nameof(ChatClientAgentSession)} has a {nameof(ChatClientAgentSession.ConversationId)} indicating server-side chat history management, but an override {nameof(this.ChatHistoryProvider)} was provided via {nameof(AgentRunOptions.AdditionalProperties)}."); diff --git a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs index ede2c07d377..d5890bb5f20 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs @@ -243,6 +243,46 @@ public async Task RunStreamingAsync_ReturnsStreamingUpdates_AfterCompletionAsync Assert.Contains(updates, u => u.Text == "Hello"); } + [Fact] + public async Task RunStreamingAsync_WithSession_SendsFullHistoryAfterThreadIdIsSetAsync() + { + // Arrange + var captureHandler = new StateCapturingTestDelegatingHandler(); + captureHandler.AddResponse( + [ + new RunStartedEvent { ThreadId = "thread1", RunId = "run1" }, + new TextMessageStartEvent { MessageId = "msg1", Role = AGUIRoles.Assistant }, + new TextMessageContentEvent { MessageId = "msg1", Delta = "First response" }, + new TextMessageEndEvent { MessageId = "msg1" }, + new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" } + ]); + captureHandler.AddResponse( + [ + new RunStartedEvent { ThreadId = "thread1", RunId = "run2" }, + new TextMessageStartEvent { MessageId = "msg2", Role = AGUIRoles.Assistant }, + new TextMessageContentEvent { MessageId = "msg2", Delta = "Second response" }, + new TextMessageEndEvent { MessageId = "msg2" }, + new RunFinishedEvent { ThreadId = "thread1", RunId = "run2" } + ]); + using HttpClient httpClient = new(captureHandler); + + var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options); + AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []); + AgentSession session = await agent.CreateSessionAsync(); + + // Act + await foreach (var _ in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "First")], session)) + { + } + + await foreach (var _ in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "Second")], session)) + { + } + + // Assert + Assert.Equal([1, 3], captureHandler.CapturedMessageCounts); + } + [Fact] public async Task DeserializeSession_WithValidState_ReturnsChatClientAgentSessionAsync() { @@ -1686,10 +1726,12 @@ private static HttpResponseMessage CreateResponse(BaseEvent[] events) internal sealed class StateCapturingTestDelegatingHandler : DelegatingHandler { private readonly Queue>> _responseFactories = new(); + private readonly List _capturedMessageCounts = []; public bool RequestWasMade { get; private set; } public JsonElement? CapturedState { get; private set; } public int CapturedMessageCount { get; private set; } + public IReadOnlyList CapturedMessageCounts => this._capturedMessageCounts; public void AddResponse(BaseEvent[] events) { @@ -1714,6 +1756,7 @@ protected override async Task SendAsync(HttpRequestMessage this.CapturedState = input.State; } this.CapturedMessageCount = input.Messages.Count(); + this._capturedMessageCounts.Add(this.CapturedMessageCount); } if (this._responseFactories.Count == 0) From ef63435284c62121721202dfb438fc5b7c6bca28 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 16 May 2026 16:44:30 +0800 Subject: [PATCH 2/2] refactor: use static AG-UI provider check --- .../Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index b768fb6a9ff..ff6d27aa7c2 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -817,7 +817,7 @@ internal void UpdateSessionConversationId(ChatClientAgentSession session, string if (!string.IsNullOrWhiteSpace(responseConversationId)) { - if (this.ConversationIdIndicatesServiceManagedHistory && this._agentOptions?.ChatHistoryProvider is not null) + if (!IsAGUIProviderName(this._agentMetadata.ProviderName) && this._agentOptions?.ChatHistoryProvider is not null) { // The agent has a ChatHistoryProvider configured, but the service returned a conversation id, // meaning the service manages chat history server-side. Both cannot be used simultaneously. @@ -931,9 +931,8 @@ private bool RequiresPerServiceCallChatHistoryPersistence } } - // AG-UI uses ConversationId as a thread id, not as a signal that the service stores model history. - private bool ConversationIdIndicatesServiceManagedHistory => - !string.Equals(this._agentMetadata.ProviderName, AGUIProviderName, StringComparison.Ordinal); + private static bool IsAGUIProviderName(string? providerName) => + string.Equals(providerName, AGUIProviderName, StringComparison.Ordinal); /// /// Ensures that contains the resolved session. @@ -983,14 +982,14 @@ private void WarnOnMissingPerServiceCallChatHistoryPersistingChatClient() private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions) { ChatHistoryProvider? provider = - chatOptions?.ConversationId is null || !this.ConversationIdIndicatesServiceManagedHistory + chatOptions?.ConversationId is null || IsAGUIProviderName(this._agentMetadata.ProviderName) ? this.ChatHistoryProvider : null; // If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead. if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true) { - if (this.ConversationIdIndicatesServiceManagedHistory && + if (!IsAGUIProviderName(this._agentMetadata.ProviderName) && this._agentOptions?.ThrowOnChatHistoryProviderConflict is true && string.IsNullOrWhiteSpace(chatOptions?.ConversationId) is false) {