.NET: Prevent streamed updates loss when resuming streaming (#2516)

* prevent stremed updates loss when resuming streaming with non-agent managed store or/and context provider

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* throw not supported exception instead invalid operation

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* use conversation id to check if chat history is managed by agent service or not

* extract background responses tests into a separate file

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2025-11-28 08:11:30 -08:00
committed by GitHub
Unverified
parent e769256976
commit 67de7bcd93
3 changed files with 673 additions and 493 deletions
@@ -204,6 +204,8 @@ public sealed partial class ChatClientAgent : AIAgent
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> inputMessagesForChatClient, IList<ChatMessage>? aiContextProviderMessages) =
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
ValidateStreamResumptionAllowed(chatOptions?.ContinuationToken, safeThread);
var chatClient = this.ChatClient;
chatClient = ApplyRunOptionsTransformations(options, chatClient);
@@ -621,6 +623,12 @@ public sealed partial class ChatClientAgent : AIAgent
{
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
}
if (chatOptions?.ContinuationToken is not null && typedThread.ConversationId is null && typedThread.MessageStore is null)
{
throw new InvalidOperationException("Continuation tokens are not allowed to be used for initial runs.");
}
List<ChatMessage> inputMessagesForChatClient = [];
IList<ChatMessage>? aiContextProviderMessages = null;
@@ -731,6 +739,28 @@ public sealed partial class ChatClientAgent : AIAgent
return Task.CompletedTask;
}
private static void ValidateStreamResumptionAllowed(ResponseContinuationToken? continuationToken, ChatClientAgentThread safeThread)
{
if (continuationToken is null)
{
return;
}
// Streaming resumption is only supported with chat history managed by the agent service because, currently, there's no good solution
// to collect updates received in failed runs and pass them to the last successful run so it can store them to the message store.
if (safeThread.ConversationId is null)
{
throw new NotSupportedException("Streaming resumption is only supported when chat history is stored and managed by the underlying AI service.");
}
// Similarly, streaming resumption is not supported when a context provider is used because, currently, there's no good solution
// to collect updates received in failed runs and pass them to the last successful run so it can notify the context provider of the updates.
if (safeThread.AIContextProvider is not null)
{
throw new NotSupportedException("Using context provider with streaming resumption is not supported.");
}
}
private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent";
#endregion
}