diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 1133e10a8a..ff6d27aa7c 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 (!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. @@ -929,6 +931,9 @@ private bool RequiresPerServiceCallChatHistoryPersistence } } + private static bool IsAGUIProviderName(string? providerName) => + string.Equals(providerName, AGUIProviderName, StringComparison.Ordinal); + /// /// Ensures that contains the resolved session. /// @@ -976,12 +981,17 @@ private void WarnOnMissingPerServiceCallChatHistoryPersistingChatClient() private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions) { - ChatHistoryProvider? provider = chatOptions?.ConversationId is null ? this.ChatHistoryProvider : null; + ChatHistoryProvider? provider = + 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._agentOptions?.ThrowOnChatHistoryProviderConflict is true && string.IsNullOrWhiteSpace(chatOptions?.ConversationId) is false) + if (!IsAGUIProviderName(this._agentMetadata.ProviderName) && + 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 ede2c07d37..d5890bb5f2 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)