From 0b64a030641df832af5846e711c7c267bb16f9d7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 3 Nov 2025 17:42:07 +0000 Subject: [PATCH] alternative solution for the background responses --- .../Program.cs | 17 +- .../Program.cs | 10 +- .../AIAgent.cs | 218 +++++++++++++++++ .../AgentRunOptions.cs | 43 ---- .../AgentRunResponse.cs | 10 +- .../AgentRunResponseUpdate.cs | 4 +- .../DelegatingAIAgent.cs | 18 ++ .../OpenAIChatClientAgent.cs | 8 + .../OpenAIResponseClientAgent.cs | 8 + .../WorkflowHostAgent.cs | 10 + .../AnonymousDelegatingAIAgent.cs | 10 + .../ChatClient/ChatClientAgent.cs | 221 +++++++++++------- .../ChatClientAgentStructuredOutput.cs | 2 +- .../FunctionInvocationDelegatingAgent.cs | 6 + .../Microsoft.Agents.AI/OpenTelemetryAgent.cs | 12 + .../AIAgentTests.cs | 16 ++ .../AgentRunOptionsTests.cs | 58 ----- .../ChatClient/ChatClientAgentTests.cs | 203 ++++------------ .../Sample/10_Sequential_HostAsAgent.cs | 10 +- .../Sample/11_Concurrent_HostAsAgent.cs | 10 +- .../Sample/12_HandOff_HostAsAgent.cs | 10 +- 21 files changed, 512 insertions(+), 392 deletions(-) delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs index 456d968c34..1fa131ab87 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs @@ -16,13 +16,10 @@ .GetOpenAIResponseClient(deploymentName) .CreateAIAgent(); -// Enable background responses (only supported by OpenAI Responses at this time). -AgentRunOptions options = new() { AllowBackgroundResponses = true }; - AgentThread thread = agent.GetNewThread(); // Start the initial run. -AgentRunResponse response = await agent.RunAsync("Write a very long novel about otters in space.", thread, options); +AgentRunResponse response = await agent.RunBackgroundAsync("Write a very long novel about otters in space.", thread); // Poll until the response is complete. while (response.ContinuationToken is { } token) @@ -31,21 +28,17 @@ await Task.Delay(TimeSpan.FromSeconds(2)); // Continue with the token. - options.ContinuationToken = token; - - response = await agent.RunAsync(thread, options); + response = await agent.RunBackgroundAsync(thread, token); } // Display the result. Console.WriteLine(response.Text); -// Reset options and thread for streaming. -options = new() { AllowBackgroundResponses = true }; thread = agent.GetNewThread(); AgentRunResponseUpdate? lastReceivedUpdate = null; // Start streaming. -await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("Write a very long novel about otters in space.", thread, options)) +await foreach (AgentRunResponseUpdate update in agent.RunBackgroundStreamingAsync("Write a very long novel about otters in space.", thread)) { // Output each update. Console.Write(update.Text); @@ -61,9 +54,7 @@ } // Resume from interruption point. -options.ContinuationToken = lastReceivedUpdate?.ContinuationToken; - -await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread, options)) +await foreach (AgentRunResponseUpdate update in agent.RunBackgroundStreamingAsync(thread, lastReceivedUpdate?.ContinuationToken)) { // Output each update. Console.Write(update.Text); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Program.cs index f2a3bdf5c0..c79704c9c6 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Program.cs @@ -29,16 +29,13 @@ "Write complete chapters without asking for approval or feedback. Do not ask the user about tone, style, pace, or format preferences - just write the novel based on the request.", tools: [AIFunctionFactory.Create(ResearchSpaceFactsAsync), AIFunctionFactory.Create(GenerateCharacterProfilesAsync)]); -// Enable background responses (only supported by {Azure}OpenAI Responses at this time). -AgentRunOptions options = new() { AllowBackgroundResponses = true }; - AgentThread thread = agent.GetNewThread(); // Start the initial run. -AgentRunResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", thread, options); +AgentRunResponse response = await agent.RunBackgroundAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", thread); // Poll for background responses until complete. -while (response.ContinuationToken is not null) +while (response.ContinuationToken is { } token) { PersistAgentState(thread, response.ContinuationToken); @@ -46,8 +43,7 @@ RestoreAgentState(agent, out thread, out object? continuationToken); - options.ContinuationToken = continuationToken; - response = await agent.RunAsync(thread, options); + response = await agent.RunBackgroundAsync(thread, token); } Console.WriteLine(response.Text); diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs index 35aa866552..8661d345b6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs @@ -329,6 +329,224 @@ public abstract IAsyncEnumerable RunStreamingAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default); + /// + /// Runs the agent in background mode if supported; otherwise, runs synchronously with no message assuming that all required instructions are already provided to the agent or on the thread. + /// + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with any response messages generated during invocation. + /// + /// + /// The continuation token for getting the result of the agent response identified by this token. + /// The token can be obtained from the property of a previous call to + /// one of the RunBackgroundAsync method overloads and passed as an argument to this parameter on subsequent calls to this method. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + /// + /// This overload is useful when the agent has sufficient context from previous messages in the thread + /// or from its initial configuration to generate a meaningful response without additional input. + /// + public Task RunBackgroundAsync( + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + this.RunBackgroundAsync([], thread, continuationToken, options, cancellationToken); + + /// + /// Runs the agent in background mode if supported; otherwise, runs synchronously with a text message from the user. + /// + /// The user message to send to the agent. + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + /// is , empty, or contains only whitespace. + /// + /// The provided text will be wrapped in a with the role + /// before being sent to the agent in background mode. This is a convenience method for simple text-based interactions. + /// + public Task RunBackgroundAsync( + string message, + AgentThread thread, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNullOrWhitespace(message); + + return this.RunBackgroundAsync(new ChatMessage(ChatRole.User, message), thread, options, cancellationToken); + } + + /// + /// Runs the agent in background mode if supported; otherwise, runs synchronously with a single chat message. + /// + /// The chat message to send to the agent. + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + /// is . + public Task RunBackgroundAsync( + ChatMessage message, + AgentThread thread, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(message); + + return this.RunBackgroundAsync([message], thread, null, options, cancellationToken); + } + + /// + /// Runs the agent in background mode if supported; otherwise, runs synchronously with a collection of chat messages, providing the core invocation logic that all other overloads delegate to. + /// + /// The collection of messages to send to the agent for processing. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input messages and any response messages generated during invocation. + /// + /// + /// The continuation token for getting the result of the agent response identified by this token. + /// The token can be obtained from the property of a previous call to + /// one of the RunBackgroundAsync method overloads and passed as an argument to this parameter on subsequent calls to this method. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + /// + /// + /// This method provides background invocation support. The default implementation delegates to . + /// Implementations that support true background processing should override this method to provide optimized background execution. + /// + /// + /// The messages are processed in the order provided and become part of the conversation history. + /// The agent's response will also be added to if one is provided. + /// + /// + public virtual Task RunBackgroundAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + this.RunAsync(messages, thread, options, cancellationToken); + + /// + /// Runs the agent in background streaming mode if supported; otherwise, streams synchronously without providing new input messages, relying on existing context and instructions. + /// + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with any response messages generated during invocation. + /// + /// + /// The continuation token for resuming an agent response stream if interrupted identified by this token. + /// The token can be obtained from the property of a previous call to + /// one of the RunBackgroundStreamingAsync method overloads and passed as an argument to this parameter on subsequent calls to this method + /// to resume the stream from the point of interruption. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + public IAsyncEnumerable RunBackgroundStreamingAsync( + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + this.RunBackgroundStreamingAsync([], thread, continuationToken, options, cancellationToken); + + /// + /// Runs the agent in background streaming mode if supported; otherwise, streams synchronously with a text message from the user. + /// + /// The user message to send to the agent. + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + /// is , empty, or contains only whitespace. + /// + /// The provided text will be wrapped in a with the role. + /// Streaming invocation in background mode provides real-time updates as the agent generates its response. + /// + public IAsyncEnumerable RunBackgroundStreamingAsync( + string message, + AgentThread thread, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNullOrWhitespace(message); + + return this.RunBackgroundStreamingAsync(new ChatMessage(ChatRole.User, message), thread, options, cancellationToken); + } + + /// + /// Runs the agent in background streaming mode if supported; otherwise, streams synchronously with a single chat message. + /// + /// The chat message to send to the agent. + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + /// is . + public IAsyncEnumerable RunBackgroundStreamingAsync( + ChatMessage message, + AgentThread thread, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(message); + + return this.RunBackgroundStreamingAsync([message], thread, continuationToken: null, options, cancellationToken); + } + + /// + /// Runs the agent in background streaming mode if supported; otherwise, streams synchronously with a collection of chat messages, providing the core streaming invocation logic. + /// + /// The collection of messages to send to the agent for processing. + /// + /// The conversation thread to use for this invocation. + /// The thread will be updated with the input messages and any response updates generated during invocation. + /// + /// + /// The continuation token for resuming an agent response stream if interrupted identified by this token. + /// The token can be obtained from the property of a previous call to + /// one of the RunBackgroundStreamingAsync method overloads and passed as an argument to this parameter on subsequent calls to this method + /// to resume the stream from the point of interruption. + /// + /// Optional configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + /// + /// + /// This method provides background streaming support. The default implementation delegates to . + /// Implementations that support true background processing should override this method to provide optimized background streaming execution. + /// + /// + /// Each represents a portion of the complete response, allowing consumers + /// to display partial results, implement progressive loading, or provide immediate feedback to users. + /// + /// + public virtual IAsyncEnumerable RunBackgroundStreamingAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + this.RunStreamingAsync(messages, thread, options, cancellationToken); + /// /// Notifies the specified thread about new messages that have been added to the conversation. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs index c6a64915cf..cd10676547 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs @@ -30,48 +30,5 @@ public AgentRunOptions() public AgentRunOptions(AgentRunOptions options) { _ = Throw.IfNull(options); - this.ContinuationToken = options.ContinuationToken; - this.AllowBackgroundResponses = options.AllowBackgroundResponses; } - - /// - /// Gets or sets the continuation token for resuming and getting the result of the agent response identified by this token. - /// - /// - /// This property is used for background responses that can be activated via the - /// property if the implementation supports them. - /// Streamed background responses, such as those returned by default by - /// can be resumed if interrupted. This means that a continuation token obtained from the - /// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption. - /// Non-streamed background responses, such as those returned by , - /// can be polled for completion by obtaining the token from the property - /// and passing it via this property on subsequent calls to . - /// - public object? ContinuationToken { get; set; } - - /// - /// Gets or sets a value indicating whether the background responses are allowed. - /// - /// - /// - /// Background responses allow running long-running operations or tasks asynchronously in the background that can be resumed by streaming APIs - /// and polled for completion by non-streaming APIs. - /// - /// - /// When this property is set to true, non-streaming APIs may start a background operation and return an initial - /// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with - /// the continuation token to get the final result of the operation. - /// - /// - /// When this property is set to true, streaming APIs may also start a background operation and begin streaming - /// response updates until the operation is completed. If the streaming connection is interrupted, the - /// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API - /// to resume the stream from the point of interruption and continue receiving updates until the operation is completed. - /// - /// - /// This property only takes effect if the implementation it's used with supports background responses. - /// If the implementation does not support background responses, this property will be ignored. - /// - /// - public bool? AllowBackgroundResponses { get; set; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs index 2beb287918..b4cd06c518 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs @@ -165,14 +165,12 @@ public IList Messages /// /// /// implementations that support background responses will return - /// a continuation token if background responses are allowed in - /// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained, + /// a continuation token if background responses are requested and the result of the response + /// has not been obtained yet. If the response has completed and the result has been obtained, /// the token will be . /// - /// This property should be used in conjunction with to - /// continue to poll for the completion of the response. Pass this token to - /// on subsequent calls to - /// to poll for completion. + /// This property should be used in conjunction with to + /// continue to poll for the completion of the response. /// /// public object? ContinuationToken { get; set; } diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs index 954893dbcb..f0f90f3144 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs @@ -154,11 +154,11 @@ public IList Contents /// /// /// implementations that support background responses will return - /// a continuation token on each update if background responses are allowed in + /// a continuation token if background responses are requested on each update /// except for the last update, for which the token will be . /// /// This property should be used for stream resumption, where the continuation token of the latest received update should be - /// passed to on subsequent calls to + /// passed to on subsequent calls to /// to resume streaming from the point of interruption. /// /// diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs index 353c82c996..9b13ddc397 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs @@ -95,4 +95,22 @@ public override IAsyncEnumerable RunStreamingAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default) => this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken); + + /// + public override Task RunBackgroundAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + => this.InnerAgent.RunBackgroundAsync(messages, thread, continuationToken, options, cancellationToken); + + /// + public override IAsyncEnumerable RunBackgroundStreamingAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + => this.InnerAgent.RunBackgroundStreamingAsync(messages, thread, continuationToken, options, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs index b529e1151b..162ba9ee02 100644 --- a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs @@ -94,4 +94,12 @@ public sealed override Task RunAsync(IEnumerable public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => base.RunStreamingAsync(messages, thread, options, cancellationToken); + + /// + public sealed override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + base.RunBackgroundAsync(messages, thread, continuationToken, options, cancellationToken); + + /// + public sealed override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + base.RunBackgroundStreamingAsync(messages, thread, continuationToken, options, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs index 8c5603fb05..5c264aa550 100644 --- a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs @@ -112,4 +112,12 @@ public sealed override Task RunAsync(IEnumerable /// public sealed override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => base.RunStreamingAsync(messages, thread, options, cancellationToken); + + /// + public sealed override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + base.RunBackgroundAsync(messages, thread, continuationToken, options, cancellationToken); + + /// + public sealed override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + base.RunBackgroundStreamingAsync(messages, thread, continuationToken, options, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs index 98dc5903bf..f538c7a916 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs @@ -118,4 +118,14 @@ IAsyncEnumerable RunStreamingAsync( yield return update; } } + + public override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this.RunAsync(messages, thread, options, cancellationToken); + } + + public override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this.RunStreamingAsync(messages, thread, options, cancellationToken); + } } diff --git a/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs index 21fbfda639..aac90e1bea 100644 --- a/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/AnonymousDelegatingAIAgent.cs @@ -191,6 +191,16 @@ static async IAsyncEnumerable GetStreamingRunAsyncViaRun } } + public override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException("Background runs are not implemented in AnonymousDelegatingAIAgent."); + } + + public override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException("Background streaming runs are not implemented in AnonymousDelegatingAIAgent."); + } + /// Throws an exception if both of the specified delegates are . /// Both and are . internal static void ThrowIfBothDelegatesNull(object? runFunc, object? runStreamingFunc) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 46f893e531..6e232b624f 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -165,7 +165,7 @@ static AgentRunResponse CreateResponse(ChatResponse chatResponse) return new AgentRunResponse(chatResponse); } - return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken); + return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken: cancellationToken); } /// @@ -199,81 +199,10 @@ public override async IAsyncEnumerable RunStreamingAsync AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? messages.ToList(); - - (ChatClientAgentThread safeThread, ChatOptions? chatOptions, List inputMessagesForChatClient, IList? aiContextProviderMessages) = - await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false); - - var chatClient = this.ChatClient; - - chatClient = ApplyRunOptionsTransformations(options, chatClient); - - var loggingAgentName = this.GetLoggingAgentName(); - - this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType); - - List responseUpdates = []; - - IAsyncEnumerator responseUpdatesEnumerator; - - try - { - // Using the enumerator to ensure we consider the case where no updates are returned for notification. - responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForChatClient, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken); - } - catch (Exception ex) + await foreach (var update in this.RunStreamingCoreAsync(messages, thread, false, null, options, cancellationToken).ConfigureAwait(false)) { - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - throw; - } - - this._logger.LogAgentChatClientInvokedStreamingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType); - - bool hasUpdates; - try - { - // Ensure we start the streaming request - hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - throw; - } - - while (hasUpdates) - { - var update = responseUpdatesEnumerator.Current; - if (update is not null) - { - update.AuthorName ??= this.Name; - - responseUpdates.Add(update); - yield return new(update) { AgentId = this.Id }; - } - - try - { - hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - throw; - } + yield return update; } - - var chatResponse = responseUpdates.ToChatResponse(); - - // We can derive the type of supported thread from whether we have a conversation id, - // so let's update it and set the conversation id for the service thread case. - this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId); - - // To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request. - await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false); - - // Notify the AIContextProvider of all new messages. - await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); } /// @@ -332,6 +261,36 @@ public override AgentThread DeserializeThread(JsonElement serializedThread, Json aiContextProviderFactory); } + /// + public override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + static Task GetResponseAsync(IChatClient chatClient, List threadMessages, ChatOptions? chatOptions, CancellationToken ct) + { + return chatClient.GetResponseAsync(threadMessages, chatOptions, ct); + } + + static AgentRunResponse CreateResponse(ChatResponse chatResponse) + { + return new AgentRunResponse(chatResponse); + } + + return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, true, continuationToken, cancellationToken); + } + + /// + public override async IAsyncEnumerable RunBackgroundStreamingAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var update in this.RunStreamingCoreAsync(messages, thread, true, continuationToken, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } + #region Private private async Task RunCoreAsync( @@ -340,6 +299,8 @@ private async Task RunCoreAsync messages, AgentThread? thread = null, AgentRunOptions? options = null, + bool? allowBackgroundResponses = null, + object? continuationToken = null, CancellationToken cancellationToken = default) where TAgentRunResponse : AgentRunResponse where TChatClientResponse : ChatResponse @@ -347,7 +308,7 @@ private async Task RunCoreAsync ?? messages.ToList(); (ChatClientAgentThread safeThread, ChatOptions? chatOptions, List inputMessagesForChatClient, IList? aiContextProviderMessages) = - await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false); + await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, allowBackgroundResponses, continuationToken, cancellationToken).ConfigureAwait(false); var chatClient = this.ChatClient; @@ -394,6 +355,91 @@ private async Task RunCoreAsync RunStreamingCoreAsync( + IEnumerable messages, + AgentThread? thread = null, + bool? allowBackgroundResponses = null, + object? continuationToken = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? messages.ToList(); + + (ChatClientAgentThread safeThread, ChatOptions? chatOptions, List inputMessagesForChatClient, IList? aiContextProviderMessages) = + await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, allowBackgroundResponses, continuationToken, cancellationToken).ConfigureAwait(false); + + var chatClient = this.ChatClient; + + chatClient = ApplyRunOptionsTransformations(options, chatClient); + + var loggingAgentName = this.GetLoggingAgentName(); + + this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType); + + List responseUpdates = []; + + IAsyncEnumerator responseUpdatesEnumerator; + + try + { + // Using the enumerator to ensure we consider the case where no updates are returned for notification. + responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForChatClient, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken); + } + catch (Exception ex) + { + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + throw; + } + + this._logger.LogAgentChatClientInvokedStreamingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType); + + bool hasUpdates; + try + { + // Ensure we start the streaming request + hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + throw; + } + + while (hasUpdates) + { + var update = responseUpdatesEnumerator.Current; + if (update is not null) + { + update.AuthorName ??= this.Name; + + responseUpdates.Add(update); + yield return new(update) { AgentId = this.Id }; + } + + try + { + hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + throw; + } + } + + var chatResponse = responseUpdates.ToChatResponse(); + + // We can derive the type of supported thread from whether we have a conversation id, + // so let's update it and set the conversation id for the service thread case. + this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId); + + // To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request. + await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false); + + // Notify the AIContextProvider of all new messages. + await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); + } + /// /// Notify the when an agent run succeeded, if there is an . /// @@ -435,22 +481,24 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider /// agent's default chat options. Any unset properties in the run options will be filled using the agent's chat /// options. If both are , the method returns . /// Optional run options that may include specific chat configuration settings. + /// Whether to allow background responses. + /// The continuation token. /// A object representing the merged chat configuration, or if /// neither the run options nor the agent's chat options are available. - private ChatOptions? CreateConfiguredChatOptions(AgentRunOptions? runOptions) + private ChatOptions? CreateConfiguredChatOptions(AgentRunOptions? runOptions, bool? allowBackgroundResponses, object? continuationToken) { ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone(); // If no agent chat options were provided, return the request chat options as is. if (this._agentOptions?.ChatOptions is null) { - return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions); + return ApplyBackgroundResponsesProperties(requestChatOptions, allowBackgroundResponses, continuationToken); } // If no request chat options were provided, use the agent's chat options clone. if (requestChatOptions is null) { - return ApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions); + return ApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), allowBackgroundResponses, continuationToken); } // If both are present, we need to merge them. @@ -540,16 +588,15 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider } } - return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions); + return ApplyBackgroundResponsesProperties(requestChatOptions, allowBackgroundResponses, continuationToken); - static ChatOptions? ApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions) + static ChatOptions? ApplyBackgroundResponsesProperties(ChatOptions? chatOptions, bool? allowBackgroundResponses, object? continuationToken) { - // If any of the background response properties are set in the run options, we should apply both to the chat options. - if (agentRunOptions?.AllowBackgroundResponses is not null || agentRunOptions?.ContinuationToken is not null) + if (continuationToken is not null || allowBackgroundResponses is not null) { chatOptions ??= new ChatOptions(); - chatOptions.AllowBackgroundResponses = agentRunOptions.AllowBackgroundResponses; - chatOptions.ContinuationToken = agentRunOptions.ContinuationToken; + chatOptions.ContinuationToken = continuationToken; + chatOptions.AllowBackgroundResponses = allowBackgroundResponses; } return chatOptions; @@ -562,15 +609,19 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider /// The conversation thread to use or create. /// The input messages to use. /// Optional parameters for agent invocation. + /// Whether to allow background responses. + /// The continuation token for background responses. /// The to monitor for cancellation requests. The default is . /// A tuple containing the thread, chat options, and thread messages. private async Task<(ChatClientAgentThread AgentThread, ChatOptions? ChatOptions, List InputMessagesForChatClient, IList? AIContextProviderMessages)> PrepareThreadAndMessagesAsync( AgentThread? thread, IEnumerable inputMessages, AgentRunOptions? runOptions, + bool? allowBackgroundResponses, + object? continuationToken, CancellationToken cancellationToken) { - ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions); + ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions, allowBackgroundResponses, continuationToken); // Supplying a thread for background responses is required to prevent inconsistent experience // for callers if they forget to provide the thread for initial or follow-up runs. diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs index 913be969c6..d049ae789a 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs @@ -157,6 +157,6 @@ static ChatClientAgentRunResponse CreateResponse(ChatResponse chatResponse return new ChatClientAgentRunResponse(chatResponse); } - return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken); + return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken: cancellationToken); } } diff --git a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs index 7eefcebc55..6754fe08c1 100644 --- a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs @@ -27,6 +27,12 @@ public override Task RunAsync(IEnumerable message public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => this.InnerAgent.RunStreamingAsync(messages, thread, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken); + public override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + => this.InnerAgent.RunBackgroundAsync(messages, thread, continuationToken, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken); + + public override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + => this.InnerAgent.RunBackgroundStreamingAsync(messages, thread, continuationToken, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken); + // Decorate options to add the middleware function private AgentRunOptions? AgentRunOptionsWithFunctionMiddleware(AgentRunOptions? options) { diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs index 7cd3c27b70..f35890a288 100644 --- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs @@ -100,6 +100,18 @@ public override async IAsyncEnumerable RunStreamingAsync } } + /// + public override Task RunBackgroundAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotSupportedException("Clarify how to handle background runs here."); + } + + /// + public override IAsyncEnumerable RunBackgroundStreamingAsync(IEnumerable messages, AgentThread thread, object? continuationToken = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotSupportedException("Clarify how to handle background runs here."); + } + /// Augments the current activity created by the with agent-specific information. /// The that was current prior to the 's invocation. private void UpdateCurrentActivity(Activity? previousActivity) diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs index bfa14a89d4..4b644af17e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs @@ -382,6 +382,22 @@ public override IAsyncEnumerable RunStreamingAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public override Task RunBackgroundAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + public override IAsyncEnumerable RunBackgroundStreamingAsync( + IEnumerable messages, + AgentThread thread, + object? continuationToken = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); } private static async IAsyncEnumerable ToAsyncEnumerableAsync(IEnumerable values) diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs deleted file mode 100644 index 40901a4969..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Abstractions.UnitTests; - -/// -/// Unit tests for the class. -/// -public class AgentRunOptionsTests -{ - [Fact] - public void CloningConstructorCopiesProperties() - { - // Arrange - var options = new AgentRunOptions - { - ContinuationToken = new object(), - AllowBackgroundResponses = true - }; - - // Act - var clone = new AgentRunOptions(options); - - // Assert - Assert.NotNull(clone); - Assert.Same(options.ContinuationToken, clone.ContinuationToken); - Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses); - } - - [Fact] - public void CloningConstructorThrowsIfNull() => - // Act & Assert - Assert.Throws(() => new AgentRunOptions(null!)); - - [Fact] - public void JsonSerializationRoundtrips() - { - // Arrange - var options = new AgentRunOptions - { - ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), - AllowBackgroundResponses = true - }; - - // Act - string json = JsonSerializer.Serialize(options, AgentAbstractionsJsonUtilities.DefaultOptions); - - var deserialized = JsonSerializer.Deserialize(json, AgentAbstractionsJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken); - Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs index 862b9ef3b4..4f1e3df4b9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs @@ -2107,59 +2107,8 @@ public void GetNewThreadUsesAIContextProviderFactoryIfProvided() #region Background Responses Tests - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RunAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions) - { - // Arrange - object continuationToken = new(); - ChatOptions? capturedChatOptions = null; - Mock mockChatClient = new(); - mockChatClient - .Setup(c => c.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co) - .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null }); - - AgentRunOptions agentRunOptions; - - if (providePropsViaChatOptions) - { - ChatOptions chatOptions = new() - { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken - }; - - agentRunOptions = new ChatClientAgentRunOptions(chatOptions); - } - else - { - agentRunOptions = new AgentRunOptions() - { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken - }; - } - - ChatClientAgent agent = new(mockChatClient.Object); - - ChatClientAgentThread thread = new(); - - // Act - await agent.RunAsync(thread, options: agentRunOptions); - - // Assert - Assert.NotNull(capturedChatOptions); - Assert.True(capturedChatOptions.AllowBackgroundResponses); - Assert.Same(continuationToken, capturedChatOptions.ContinuationToken); - } - [Fact] - public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync() + public async Task RunBackgroundAsyncOverridesBackgroundResponsesPropertiesSpecifiedInChatOptionsAsync() { // Arrange object continuationToken1 = new(); @@ -2174,91 +2123,30 @@ public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOp .Callback, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co) .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null }); - ChatOptions chatOptions = new() - { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken1 - }; - - ChatClientAgentRunOptions agentRunOptions = new(chatOptions) - { - AllowBackgroundResponses = false, - ContinuationToken = continuationToken2 - }; - - ChatClientAgent agent = new(mockChatClient.Object); - - // Act - await agent.RunAsync(options: agentRunOptions); - - // Assert - Assert.NotNull(capturedChatOptions); - Assert.False(capturedChatOptions.AllowBackgroundResponses); - Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RunStreamingAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions) - { - // Arrange - ChatResponseUpdate[] returnUpdates = - [ - new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"), - new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?"), - ]; - - object continuationToken = new(); - ChatOptions? capturedChatOptions = null; - Mock mockChatClient = new(); - mockChatClient - .Setup(c => c.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co) - .Returns(ToAsyncEnumerableAsync(returnUpdates)); - - AgentRunOptions agentRunOptions; - - if (providePropsViaChatOptions) - { - ChatOptions chatOptions = new() - { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken - }; - - agentRunOptions = new ChatClientAgentRunOptions(chatOptions); - } - else + ChatClientAgentRunOptions agentRunOptions = new() { - agentRunOptions = new AgentRunOptions() + ChatOptions = new() { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken - }; - } + AllowBackgroundResponses = false, + ContinuationToken = continuationToken1 + } + }; ChatClientAgent agent = new(mockChatClient.Object); - ChatClientAgentThread thread = new(); + AgentThread thread = agent.GetNewThread(); // Act - await foreach (var _ in agent.RunStreamingAsync(thread, options: agentRunOptions)) - { - } + await agent.RunBackgroundAsync(thread, continuationToken2, options: agentRunOptions); // Assert Assert.NotNull(capturedChatOptions); - Assert.True(capturedChatOptions.AllowBackgroundResponses); - Assert.Same(continuationToken, capturedChatOptions.ContinuationToken); + Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken); } [Fact] - public async Task RunStreamingAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync() + public async Task RunBackgroundStreamingAsyncOverridesBackgroundResponsesPropertiesSpecifiedInChatOptionsAsync() { // Arrange ChatResponseUpdate[] returnUpdates = @@ -2278,33 +2166,32 @@ public async Task RunStreamingAsyncPrioritizesBackgroundResponsesPropertiesFromA .Callback, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co) .Returns(ToAsyncEnumerableAsync(returnUpdates)); - ChatOptions chatOptions = new() - { - AllowBackgroundResponses = true, - ContinuationToken = continuationToken1 - }; - - ChatClientAgentRunOptions agentRunOptions = new(chatOptions) + ChatClientAgentRunOptions agentRunOptions = new() { - AllowBackgroundResponses = false, - ContinuationToken = continuationToken2 + ChatOptions = new() + { + AllowBackgroundResponses = false, + ContinuationToken = continuationToken1 + } }; ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + // Act - await foreach (var _ in agent.RunStreamingAsync(options: agentRunOptions)) + await foreach (var _ in agent.RunBackgroundStreamingAsync(thread, continuationToken2, options: agentRunOptions)) { } // Assert Assert.NotNull(capturedChatOptions); - Assert.False(capturedChatOptions.AllowBackgroundResponses); + Assert.True(capturedChatOptions.AllowBackgroundResponses); Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken); } [Fact] - public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync() + public async Task RunBackgroundAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync() { // Arrange object continuationToken = new(); @@ -2322,14 +2209,14 @@ public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunR ChatClientAgentThread thread = new(); // Act - var response = await agent.RunAsync([new(ChatRole.User, "hi")], thread, options: runOptions); + var response = await agent.RunBackgroundAsync([new(ChatRole.User, "hi")], thread, options: runOptions); // Assert Assert.Same(continuationToken, response.ContinuationToken); } [Fact] - public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync() + public async Task RunBackgroundStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync() { // Arrange object token1 = new(); @@ -2353,7 +2240,7 @@ public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync( // Act var actualUpdates = new List(); - await foreach (var u in agent.RunStreamingAsync([new(ChatRole.User, "hi")], thread, options: new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true }))) + await foreach (var u in agent.RunBackgroundStreamingAsync([new(ChatRole.User, "hi")], thread, options: new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true }))) { actualUpdates.Add(u); } @@ -2365,19 +2252,19 @@ public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync( } [Fact] - public async Task RunAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync() + public async Task RunBackgroundAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync() { // Arrange Mock mockChatClient = new(); ChatClientAgent agent = new(mockChatClient.Object); - AgentRunOptions runOptions = new() { ContinuationToken = new() }; - IEnumerable inputMessages = [new ChatMessage(ChatRole.User, "test message")]; + AgentThread thread = agent.GetNewThread(); + // Act & Assert - await Assert.ThrowsAsync(() => agent.RunAsync(inputMessages, options: runOptions)); + await Assert.ThrowsAsync(() => agent.RunBackgroundAsync(inputMessages, thread, continuationToken: new object())); // Verify that the IChatClient was never called due to early validation mockChatClient.Verify( @@ -2389,21 +2276,21 @@ public async Task RunAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync() } [Fact] - public async Task RunStreamingAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync() + public async Task RunBackgroundStreamingAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync() { // Arrange Mock mockChatClient = new(); ChatClientAgent agent = new(mockChatClient.Object); - AgentRunOptions runOptions = new() { ContinuationToken = new() }; - IEnumerable inputMessages = [new ChatMessage(ChatRole.User, "test message")]; + AgentThread thread = agent.GetNewThread(); + // Act & Assert await Assert.ThrowsAsync(async () => { - await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions)) + await foreach (var update in agent.RunBackgroundStreamingAsync(inputMessages, thread, continuationToken: new object())) { // Should not reach here } @@ -2419,7 +2306,7 @@ await Assert.ThrowsAsync(async () => } [Fact] - public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync() + public async Task RunBackgroundAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync() { // Arrange List capturedMessages = []; @@ -2459,10 +2346,8 @@ public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync AIContextProvider = mockContextProvider.Object }; - AgentRunOptions runOptions = new() { ContinuationToken = new() }; - // Act - await agent.RunAsync([], thread, options: runOptions); + await agent.RunBackgroundAsync([], thread, new object()); // Assert @@ -2481,7 +2366,7 @@ public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync } [Fact] - public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync() + public async Task RunBackgroundStreamingAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync() { // Arrange List capturedMessages = []; @@ -2521,10 +2406,8 @@ public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationT AIContextProvider = mockContextProvider.Object }; - AgentRunOptions runOptions = new() { ContinuationToken = new() }; - // Act - await agent.RunStreamingAsync([], thread, options: runOptions).ToListAsync(); + await agent.RunBackgroundStreamingAsync([], thread, new object()).ToListAsync(); // Assert @@ -2543,19 +2426,17 @@ public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationT } [Fact] - public async Task RunAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync() + public async Task RunBackgroundAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync() { // Arrange Mock mockChatClient = new(); ChatClientAgent agent = new(mockChatClient.Object); - AgentRunOptions runOptions = new() { AllowBackgroundResponses = true }; - IEnumerable inputMessages = [new ChatMessage(ChatRole.User, "test message")]; // Act & Assert - await Assert.ThrowsAsync(() => agent.RunAsync(inputMessages, options: runOptions)); + await Assert.ThrowsAsync(() => agent.RunBackgroundAsync(inputMessages, thread: null!)); // Verify that the IChatClient was never called due to early validation mockChatClient.Verify( @@ -2567,21 +2448,19 @@ public async Task RunAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync() } [Fact] - public async Task RunStreamingAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync() + public async Task RunBackgroundStreamingAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync() { // Arrange Mock mockChatClient = new(); ChatClientAgent agent = new(mockChatClient.Object); - AgentRunOptions runOptions = new() { AllowBackgroundResponses = true }; - IEnumerable inputMessages = [new ChatMessage(ChatRole.User, "test message")]; // Act & Assert await Assert.ThrowsAsync(async () => { - await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions)) + await foreach (var update in agent.RunBackgroundStreamingAsync(inputMessages, thread: null!)) { // Should not reach here } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs index 4670f8b931..f4963e6e8c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs @@ -24,12 +24,12 @@ public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvi AgentThread thread = hostAgent.GetNewThread(); foreach (string input in inputs) { - AgentRunResponse response; - object? continuationToken = null; - do + AgentRunResponse response = await hostAgent.RunBackgroundAsync(input, thread); + + while (response.ContinuationToken is { } token) { - response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken }); - } while ((continuationToken = response.ContinuationToken) is { }); + response = await hostAgent.RunBackgroundAsync(thread, token); + } foreach (ChatMessage message in response.Messages) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs index ca2ba46405..b0b2c98776 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs @@ -36,12 +36,12 @@ public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvi AgentThread thread = hostAgent.GetNewThread(); foreach (string input in inputs) { - AgentRunResponse response; - object? continuationToken = null; - do + AgentRunResponse response = await hostAgent.RunBackgroundAsync(input, thread); + + while (response.ContinuationToken is { } token) { - response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken }); - } while ((continuationToken = response.ContinuationToken) is { }); + response = await hostAgent.RunBackgroundAsync(thread, token); + } foreach (ChatMessage message in response.Messages) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs index 8eb553b868..c59d03041d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs @@ -72,12 +72,12 @@ public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvi AgentThread thread = hostAgent.GetNewThread(); foreach (string input in inputs) { - AgentRunResponse response; - object? continuationToken = null; - do + AgentRunResponse response = await hostAgent.RunBackgroundAsync(input, thread); + + while (response.ContinuationToken is { } token) { - response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken }); - } while ((continuationToken = response.ContinuationToken) is { }); + response = await hostAgent.RunBackgroundAsync(thread, token); + } foreach (ChatMessage message in response.Messages) {