.NET: Add support for background responses (#1501)

* add support for background responses

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs

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

* fix broken link

* fix xml comments and background responses properties override funcitonity

* change ai model provider

* use Run{Streaming}Async overloads that don't require messages

* stop using m: prefix in cref attribute of <see/> element.

* reject input messages provided with continuation token + don't extract messages from message store and context provide if continuation token is provided

* use agent thread for background-responses sample

* require agent thread for background responses

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

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

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

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* remove CA1200

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs

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

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs

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

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs

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

* address pr review comments

* Update dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2025-10-22 17:43:57 +00:00
committed by GitHub
co-authored by Copilot Roger Barreto westey
parent 699149c260
commit 1bf520a7c2
17 changed files with 2499 additions and 39 deletions
@@ -10,9 +10,6 @@ namespace Microsoft.Agents.AI;
/// </summary>
/// <remarks>
/// <para>
/// This class currently has no options, but may be extended in the future to include additional configuration settings.
/// </para>
/// <para>
/// Implementations of <see cref="AIAgent"/> may provide subclasses of <see cref="AgentRunOptions"/> with additional options specific to that agent type.
/// </para>
/// </remarks>
@@ -33,5 +30,48 @@ public class AgentRunOptions
public AgentRunOptions(AgentRunOptions options)
{
_ = Throw.IfNull(options);
this.ContinuationToken = options.ContinuationToken;
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
}
/// <summary>
/// Gets or sets the continuation token for resuming and getting the result of the agent response identified by this token.
/// </summary>
/// <remarks>
/// This property is used for background responses that can be activated via the <see cref="AllowBackgroundResponses"/>
/// property if the <see cref="AIAgent"/> implementation supports them.
/// Streamed background responses, such as those returned by default by <see cref="AIAgent.RunStreamingAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// can be resumed if interrupted. This means that a continuation token obtained from the <see cref="AgentRunResponseUpdate.ContinuationToken"/>
/// 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 <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>,
/// can be polled for completion by obtaining the token from the <see cref="AgentRunResponse.ContinuationToken"/> property
/// and passing it via this property on subsequent calls to <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>.
/// </remarks>
public object? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the background responses are allowed.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public bool? AllowBackgroundResponses { get; set; }
}
@@ -74,6 +74,7 @@ public class AgentRunResponse
this.RawRepresentation = response;
this.ResponseId = response.ResponseId;
this.Usage = response.Usage;
this.ContinuationToken = response.ContinuationToken;
}
/// <summary>
@@ -159,6 +160,23 @@ public class AgentRunResponse
/// </value>
public string? ResponseId { get; set; }
/// <summary>
/// Gets or sets the continuation token for getting the result of a background agent response.
/// </summary>
/// <remarks>
/// <see cref="AIAgent"/> implementations that support background responses will return
/// a continuation token if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
/// 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 <see langword="null"/>.
/// <para>
/// This property should be used in conjunction with <see cref="AgentRunOptions.ContinuationToken"/> to
/// continue to poll for the completion of the response. Pass this token to
/// <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// to poll for completion.
/// </para>
/// </remarks>
public object? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the timestamp indicating when this response was created.
/// </summary>
@@ -234,7 +252,7 @@ public class AgentRunResponse
{
extra = new AgentRunResponseUpdate
{
AdditionalProperties = this.AdditionalProperties
AdditionalProperties = this.AdditionalProperties,
};
if (this.Usage is { } usage)
@@ -42,6 +42,7 @@ public static class AgentRunResponseExtensions
RawRepresentation = response,
ResponseId = response.ResponseId,
Usage = response.Usage,
ContinuationToken = response.ContinuationToken,
};
}
@@ -74,6 +75,7 @@ public static class AgentRunResponseExtensions
RawRepresentation = responseUpdate,
ResponseId = responseUpdate.ResponseId,
Role = responseUpdate.Role,
ContinuationToken = responseUpdate.ContinuationToken,
};
}
@@ -78,6 +78,7 @@ public class AgentRunResponseUpdate
this.RawRepresentation = chatResponseUpdate;
this.ResponseId = chatResponseUpdate.ResponseId;
this.Role = chatResponseUpdate.Role;
this.ContinuationToken = chatResponseUpdate.ContinuationToken;
}
/// <summary>Gets or sets the name of the author of the response update.</summary>
@@ -148,6 +149,21 @@ public class AgentRunResponseUpdate
/// <summary>Gets or sets a timestamp for the response update.</summary>
public DateTimeOffset? CreatedAt { get; set; }
/// <summary>
/// Gets or sets the continuation token for resuming the streamed agent response of which this update is a part.
/// </summary>
/// <remarks>
/// <see cref="AIAgent"/> implementations that support background responses will return
/// a continuation token on each update if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
/// except for the last update, for which the token will be <see langword="null"/>.
/// <para>
/// This property should be used for stream resumption, where the continuation token of the latest received update should be
/// passed to <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunStreamingAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// to resume streaming from the point of interruption.
/// </para>
/// </remarks>
public object? ContinuationToken { get; set; }
/// <inheritdoc/>
public override string ToString() => this.Text;
@@ -436,13 +436,13 @@ public sealed partial class ChatClientAgent : AIAgent
// If no agent chat options were provided, return the request chat options as is.
if (this._agentOptions?.ChatOptions is null)
{
return requestChatOptions;
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
}
// If no request chat options were provided, use the agent's chat options clone.
if (requestChatOptions is null)
{
return this._agentOptions?.ChatOptions.Clone();
return ApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
}
// If both are present, we need to merge them.
@@ -532,7 +532,20 @@ public sealed partial class ChatClientAgent : AIAgent
}
}
return requestChatOptions;
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
static ChatOptions? ApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
{
// 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)
{
chatOptions ??= new ChatOptions();
chatOptions.AllowBackgroundResponses = agentRunOptions.AllowBackgroundResponses;
chatOptions.ContinuationToken = agentRunOptions.ContinuationToken;
}
return chatOptions;
}
}
/// <summary>
@@ -551,50 +564,67 @@ public sealed partial class ChatClientAgent : AIAgent
{
ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions);
// 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.
if (chatOptions?.AllowBackgroundResponses is true && thread is null)
{
throw new InvalidOperationException("A thread must be provided when continuing a background response with a continuation token.");
}
thread ??= this.GetNewThread();
if (thread is not ChatClientAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
}
// Add any existing messages from the thread to the messages to be sent to the chat client.
List<ChatMessage> threadMessages = [];
if (typedThread.MessageStore is not null)
// Supplying messages when continuing a background response is not allowed.
if (chatOptions?.ContinuationToken is not null && inputMessages.Any())
{
threadMessages.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
}
List<ChatMessage> threadMessages = [];
// If we have an AIContextProvider, we should get context from it, and update our
// messages and options with the additional context.
if (typedThread.AIContextProvider is not null)
// Populate the thread messages only if we are not continuing an existing response as it's not allowed
if (chatOptions?.ContinuationToken is null)
{
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
var aiContext = await typedThread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is { Count: > 0 })
// Add any existing messages from the thread to the messages to be sent to the chat client.
if (typedThread.MessageStore is not null)
{
threadMessages.AddRange(aiContext.Messages);
threadMessages.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
}
if (aiContext.Tools is { Count: > 0 })
// If we have an AIContextProvider, we should get context from it, and update our
// messages and options with the additional context.
if (typedThread.AIContextProvider is not null)
{
chatOptions ??= new();
chatOptions.Tools ??= [];
foreach (AITool tool in aiContext.Tools)
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
var aiContext = await typedThread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is { Count: > 0 })
{
chatOptions.Tools.Add(tool);
threadMessages.AddRange(aiContext.Messages);
}
if (aiContext.Tools is { Count: > 0 })
{
chatOptions ??= new();
chatOptions.Tools ??= [];
foreach (AITool tool in aiContext.Tools)
{
chatOptions.Tools.Add(tool);
}
}
if (aiContext.Instructions is not null)
{
chatOptions ??= new();
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
}
}
if (aiContext.Instructions is not null)
{
chatOptions ??= new();
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
}
// Add the input messages to the end of thread messages.
threadMessages.AddRange(inputMessages);
}
// Add the input messages to the end of thread messages.
threadMessages.AddRange(inputMessages);
// If a user provided two different thread ids, via the thread object and options, we should throw
// since we don't know which one to use.
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && typedThread.ConversationId != chatOptions!.ConversationId)