From c9c3c0c76ec8f8854cc228cfc6127fd0315607c4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 3 Sep 2026 10:21:26 +0100 Subject: [PATCH 1/5] backup --- .../A2AAgentHandler.cs | 59 +++++++------- .../A2AServerRegistrationOptions.cs | 2 +- .../A2AServerServiceCollectionExtensions.cs | 2 +- .../AgentRunMode.cs | 52 ++++++------- .../A2AAgentHandlerTests.cs | 78 +++++++++---------- .../A2AEndpointRouteBuilderExtensionsTests.cs | 2 +- ...AServerServiceCollectionExtensionsTests.cs | 4 +- .../AgentRunModeTests.cs | 64 +++++++-------- 8 files changed, 134 insertions(+), 129 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs index 72cbde5a6bd..3222bced095 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs @@ -33,7 +33,7 @@ internal sealed class A2AAgentHandler : IAgentHandler /// Initializes a new instance of the class. /// /// The hosted agent that provides the execution logic. - /// Controls whether the agent runs in background mode. + /// Controls which A2A artifact the agent response is returned as. public A2AAgentHandler( AIHostAgent hostAgent, AgentRunMode runMode) @@ -80,20 +80,20 @@ public async Task CancelAsync(RequestContext context, AgentEventQueue eventQueue /// The queue the response events are written to. /// /// to run the agent to completion before emitting a single completed task; - /// to emit task updates as they are produced. Ignored when the server disallows - /// background responses, because a message response is always aggregated. + /// to emit task updates as they are produced. Ignored when the server is configured + /// through to return a message, because a message response is always aggregated. /// /// A to cancel the operation. /// /// The response shape is decided by two independent inputs: /// /// - /// Whether the server allows background responses. This is configured per agent registration, for example: + /// Which A2A artifact the server returns. This is configured per agent registration, for example: /// /// builder.AddA2AServer(agent, (A2AServerRegistrationOptions options) => - /// options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported); + /// options.AgentRunMode = AgentRunMode.ReturnTask); /// - /// Use AgentRunMode.DisallowBackground to always respond with a message instead of a task. + /// Use AgentRunMode.ReturnMessage to always respond with a message instead of a task. /// /// /// Whether the client asked for an immediate response. In the A2A protocol this is the @@ -104,17 +104,20 @@ public async Task CancelAsync(RequestContext context, AgentEventQueue eventQueue /// The resulting combinations are: /// /// - /// Server allows background responses and ReturnImmediately = true: returns the initial task, then the - /// rest of the updates piece by piece. + /// Server is configured through to return a task and ReturnImmediately = true: + /// returns the initial task, then the rest of the updates piece by piece. /// /// - /// Server allows background responses and ReturnImmediately = false: returns a single completed task. + /// Server is configured through to return a task and ReturnImmediately = false: + /// returns a single completed task. /// /// - /// Server disallows background responses and ReturnImmediately = true: returns a message. + /// Server is configured through to return a message and ReturnImmediately = true: + /// returns a message. /// /// - /// Server disallows background responses and ReturnImmediately = false: returns a message. + /// Server is configured through to return a message and ReturnImmediately = false: + /// returns a message. /// /// /// @@ -133,11 +136,12 @@ private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue List chatMessages = context.Message is not null ? [context.Message.ToChatMessage()] : []; - var options = CreateRunOptions(context); - - // Decide whether to run in background based on user preferences and agent capabilities + // Decide which A2A artifact to return based on the configured run mode. Returning a task also allows + // background responses so that the streamed updates carry continuation tokens for stream resumption. var decisionContext = new A2ARunDecisionContext(context); - var returnTask = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false); + var returnTask = await this._runMode.ShouldReturnTaskAsync(decisionContext, cancellationToken).ConfigureAwait(false); + + var options = CreateRunOptions(context, returnTask); var updates = this._hostAgent.RunStreamingAsync(chatMessages, session, options, cancellationToken); @@ -148,20 +152,20 @@ private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId); if (aggregateTaskUpdates) { - // The server allows background responses, but the non-streaming client request has + // The server is configured through AgentRunMode to return a task, but the non-streaming client request has // ReturnImmediately disabled, so collect all updates and return a completed task. await AggregateTaskUpdatesAsync(updates, taskUpdater, eventQueue, cancellationToken).ConfigureAwait(false); } else { - // The server allows background responses and this is either a streaming request or a + // The server is configured through AgentRunMode to return a task and this is either a streaming request or a // non-streaming request with ReturnImmediately enabled, so emit task updates as they arrive. await StreamTaskUpdatesAsync(updates, taskUpdater, cancellationToken).ConfigureAwait(false); } } else { - // The server disallows background responses, so return one aggregated message regardless + // The server is configured through AgentRunMode to return a message, so return one aggregated message regardless // of the client request's ReturnImmediately value. await StreamMessageUpdatesAsync(contextId, updates, eventQueue, cancellationToken).ConfigureAwait(false); } @@ -180,9 +184,9 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue List chatMessages = ExtractChatMessagesFromTaskHistory(context.Task); var decisionContext = new A2ARunDecisionContext(context); - var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false); + var returnTask = await this._runMode.ShouldReturnTaskAsync(decisionContext, cancellationToken).ConfigureAwait(false); - var options = CreateRunOptions(context, allowBackgroundResponses); + var options = CreateRunOptions(context, returnTask); AgentResponse response; try @@ -234,12 +238,12 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue /// /// The A2A request context of the incoming request. /// - /// The value to assign to . Defaults to , which leaves it unset. + /// Whether to enable for the hosted agent run. /// /// /// The run options to invoke the agent with, or when there is nothing to forward. /// - private static AgentRunOptions? CreateRunOptions(RequestContext context, bool? allowBackgroundResponses = null) + private static AgentRunOptions? CreateRunOptions(RequestContext context, bool allowBackgroundResponses) { AdditionalPropertiesDictionary? additionalProperties = context.Metadata is { Count: > 0 } ? context.Metadata.ToAdditionalProperties() @@ -252,7 +256,7 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue (additionalProperties ??= [])[ConfigurationPropertyKey] = configuration; } - if (allowBackgroundResponses is null && additionalProperties is null) + if (!allowBackgroundResponses && additionalProperties is null) { return null; } @@ -294,7 +298,8 @@ private static List ExtractChatMessagesFromTaskHistory(AgentTask? a /// Emits a task and streams the agent updates into it as artifacts as they are produced. /// /// - /// Handles the case where the server allows background responses and the response is delivered incrementally: + /// Handles the case where the server is configured through to return a task and the + /// response is delivered incrementally: /// either a streaming (message/stream) request, or a non-streaming request with /// ReturnImmediately = true. In the latter case the caller receives the initial task immediately and /// obtains the remaining updates by polling the task. @@ -342,7 +347,8 @@ private static async Task StreamTaskUpdatesAsync(IAsyncEnumerable /// - /// Handles the case where the server allows background responses and a non-streaming client sent + /// Handles the case where the server is configured through to return a task and a + /// non-streaming client sent /// ReturnImmediately = false, meaning it wants the final result in the response rather than a task /// it has to poll. No task event is emitted until the agent stream finishes, because the server returns on the /// first task event; emitting early would hand the caller an in-progress task instead of a completed one. @@ -384,7 +390,8 @@ await eventQueue.AddArtifactAsync( /// Consumes the agent updates and emits the aggregated result as a single message. /// /// - /// Handles the case where the server disallows background responses, which applies regardless of the client's + /// Handles the case where the server is configured through to return a message, which + /// applies regardless of the client's /// ReturnImmediately value: a message is not a long-running entity, so there is nothing to return early /// or poll for and the full agent run is always aggregated into one message. An empty message is emitted when /// the agent produces no messages. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerRegistrationOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerRegistrationOptions.cs index 7bd30f9a7cd..2ef3f6be779 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerRegistrationOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerRegistrationOptions.cs @@ -16,7 +16,7 @@ public sealed class A2AServerRegistrationOptions /// Gets or sets the agent run mode that controls how the agent responds to A2A requests. /// /// - /// When , defaults to . + /// When , defaults to . /// public AgentRunMode? AgentRunMode { get; set; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs index 556a0d931a3..8e8240493c2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs @@ -186,7 +186,7 @@ private static A2AServer CreateA2AServer(IServiceProvider serviceProvider, AIAge if (agentHandler is null) { var agentSessionStore = serviceProvider.GetKeyedService(agent.Name); - var runMode = options?.AgentRunMode ?? AgentRunMode.DisallowBackground; + var runMode = options?.AgentRunMode ?? AgentRunMode.ReturnMessage; // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. if (agentSessionStore?.GetService() is null) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs index 3abb90afb6d..0e3c0329d62 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs @@ -10,7 +10,8 @@ namespace Microsoft.Agents.AI.Hosting.A2A; /// -/// Specifies how the A2A hosting layer determines whether to run in background or not. +/// Specifies which A2A protocol artifact the hosting layer returns for a run of an : +/// an AgentMessage or an AgentTask. /// [Experimental(DiagnosticIds.Experiments.AIResponseContinuations)] public sealed class AgentRunMode : IEquatable @@ -20,48 +21,45 @@ public sealed class AgentRunMode : IEquatable private const string DynamicValue = "dynamic"; private readonly string _value; - private readonly Func>? _runInBackground; + private readonly Func>? _returnTask; - private AgentRunMode(string value, Func>? runInBackground = null) + private AgentRunMode(string value, Func>? returnTask = null) { this._value = value; - this._runInBackground = runInBackground; + this._returnTask = returnTask; } /// - /// Disallows the background responses from the agent. Is equivalent to configuring as false. - /// In the A2A protocol terminology will make responses be returned as AgentMessage. + /// Returns the agent response as an AgentMessage. The updates produced by the agent are aggregated + /// into a single message. /// - public static AgentRunMode DisallowBackground => new(MessageValue); + public static AgentRunMode ReturnMessage => new(MessageValue); /// - /// Allows the background responses from the agent. Is equivalent to configuring as true. - /// In the A2A protocol terminology will make responses be returned as AgentTask if the agent supports background responses, and as AgentMessage otherwise. + /// Returns the agent response as an AgentTask, allowing the caller to track its lifecycle and to + /// receive the result incrementally. /// - public static AgentRunMode AllowBackgroundIfSupported => new(TaskValue); + public static AgentRunMode ReturnTask => new(TaskValue); /// - /// The agent run mode is decided by the supplied delegate. - /// The delegate receives an with the incoming - /// message and returns a boolean specifying whether to run the agent in background mode. - /// indicates that the agent should run in background mode and return an - /// AgentTask if the agent supports background mode; otherwise, it returns an AgentMessage - /// if the mode is not supported. indicates that the agent should run in - /// non-background mode and return an AgentMessage. + /// Defers the choice between an AgentMessage and an AgentTask to the supplied + /// delegate, which is invoked per request. The delegate receives an + /// describing the incoming request and returns + /// to return an AgentTask, or to return an AgentMessage. /// - /// - /// An async delegate that decides whether the response should be wrapped in an AgentTask. + /// + /// An async delegate that decides whether the response is returned as an AgentTask. /// - public static AgentRunMode AllowBackgroundWhen(Func> runInBackground) + public static AgentRunMode ReturnTaskWhen(Func> returnTask) { - ArgumentNullException.ThrowIfNull(runInBackground); - return new(DynamicValue, runInBackground); + ArgumentNullException.ThrowIfNull(returnTask); + return new(DynamicValue, returnTask); } /// /// Determines whether the agent response should be returned as an AgentTask. /// - internal ValueTask ShouldRunInBackgroundAsync(A2ARunDecisionContext context, CancellationToken cancellationToken) + internal ValueTask ShouldReturnTaskAsync(A2ARunDecisionContext context, CancellationToken cancellationToken) { if (string.Equals(this._value, MessageValue, StringComparison.OrdinalIgnoreCase)) { @@ -74,9 +72,9 @@ internal ValueTask ShouldRunInBackgroundAsync(A2ARunDecisionContext contex } // Dynamic: delegate to custom callback. - if (this._runInBackground is not null) + if (this._returnTask is not null) { - return this._runInBackground(context, cancellationToken); + return this._returnTask(context, cancellationToken); } // No delegate provided — fall back to "message" behavior. @@ -87,7 +85,7 @@ internal ValueTask ShouldRunInBackgroundAsync(A2ARunDecisionContext contex public bool Equals(AgentRunMode? other) => other is not null && string.Equals(this._value, other._value, StringComparison.OrdinalIgnoreCase) - && ReferenceEquals(this._runInBackground, other._runInBackground); + && ReferenceEquals(this._returnTask, other._returnTask); /// public override bool Equals(object? obj) => this.Equals(obj as AgentRunMode); @@ -95,7 +93,7 @@ other is not null /// public override int GetHashCode() => HashCode.Combine( StringComparer.OrdinalIgnoreCase.GetHashCode(this._value), - RuntimeHelpers.GetHashCode(this._runInBackground)); + RuntimeHelpers.GetHashCode(this._returnTask)); /// public override string ToString() => this._value; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs index 8ab2c239a31..c8049782116 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs @@ -146,7 +146,7 @@ public async Task ExecuteAsync_WhenConfigurationRequestsImmediateReturn_DoesNotS AgentRunOptions? capturedOptions = null; A2AAgentHandler handler = CreateHandler( CreateAgentMock(options => capturedOptions = options), - runMode: AgentRunMode.DisallowBackground); + runMode: AgentRunMode.ReturnMessage); // Act await InvokeExecuteAsync(handler, new RequestContext @@ -250,7 +250,7 @@ public async Task ExecuteAsync_DynamicMode_WithFalseCallback_ReturnsMessageAsync // Arrange A2AAgentHandler handler = CreateHandler( CreateAgentMock(_ => { }), - runMode: AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(false))); + runMode: AgentRunMode.ReturnTaskWhen((_, _) => ValueTask.FromResult(false))); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -272,7 +272,7 @@ public async Task ExecuteAsync_DynamicMode_WithTrueCallback_ReturnsTaskAsync() // Arrange A2AAgentHandler handler = CreateHandler( CreateAgentMock(_ => { }), - runMode: AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true))); + runMode: AgentRunMode.ReturnTaskWhen((_, _) => ValueTask.FromResult(true))); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -288,10 +288,10 @@ public async Task ExecuteAsync_DynamicMode_WithTrueCallback_ReturnsTaskAsync() #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. /// - /// Verifies that an immediate request emits the initial task and streams subsequent updates when background responses are allowed. + /// Verifies that an immediate request emits the initial task and streams subsequent updates in ReturnTask mode. /// [Fact] - public async Task ExecuteAsync_WhenBackgroundResponsesAllowedAndReturnImmediatelyTrue_StreamsTaskUpdatesAsync() + public async Task ExecuteAsync_WhenReturnTaskModeAndReturnImmediatelyTrue_StreamsTaskUpdatesAsync() { // Arrange AgentResponseUpdate[] updates = @@ -306,7 +306,7 @@ public async Task ExecuteAsync_WhenBackgroundResponsesAllowedAndReturnImmediatel ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -592,7 +592,7 @@ public async Task ExecuteAsync_WhenMessageIsNull_SucceedsWithEmptyMessagesAsync( } /// - /// Verifies that the dynamic AllowBackgroundWhen delegate receives the correct RequestContext. + /// Verifies that the dynamic ReturnTaskWhen delegate receives the correct RequestContext. /// [Fact] public async Task ExecuteAsync_DynamicMode_DelegateReceivesRequestContextAsync() @@ -601,7 +601,7 @@ public async Task ExecuteAsync_DynamicMode_DelegateReceivesRequestContextAsync() A2ARunDecisionContext? capturedContext = null; A2AAgentHandler handler = CreateHandler( CreateAgentMock(_ => { }), - runMode: AgentRunMode.AllowBackgroundWhen((ctx, _) => + runMode: AgentRunMode.ReturnTaskWhen((ctx, _) => { capturedContext = ctx; return ValueTask.FromResult(false); @@ -698,10 +698,10 @@ public async Task ExecuteAsync_Streaming_EnqueuesSingleAggregatedMessageAsync() } /// - /// Verifies that allowing background responses emits a task lifecycle in streaming mode. + /// Verifies that ReturnTask mode emits a task lifecycle in streaming mode. /// [Fact] - public async Task ExecuteAsync_Streaming_WhenBackgroundResponsesAllowed_StreamsTaskUpdatesAsync() + public async Task ExecuteAsync_Streaming_WhenReturnTaskMode_StreamsTaskUpdatesAsync() { // Arrange AgentResponseUpdate[] updates = @@ -730,7 +730,7 @@ public async Task ExecuteAsync_Streaming_WhenBackgroundResponsesAllowed_StreamsT ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -782,7 +782,7 @@ public async Task ExecuteAsync_Streaming_WhenBackgroundResponsesAllowed_StreamsT /// Verifies that a non-immediate request aggregates all updates into a completed task. /// [Fact] - public async Task ExecuteAsync_WhenBackgroundResponsesAllowedAndReturnImmediatelyFalse_ReturnsCompletedTaskAsync() + public async Task ExecuteAsync_WhenReturnTaskModeAndReturnImmediatelyFalse_ReturnsCompletedTaskAsync() { // Arrange AgentResponseUpdate[] updates = @@ -792,7 +792,7 @@ public async Task ExecuteAsync_WhenBackgroundResponsesAllowedAndReturnImmediatel ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -831,7 +831,7 @@ public async Task ExecuteAsync_WhenAggregatingTaskUpdates_PreservesArtifactMetad ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -869,7 +869,7 @@ public async Task ExecuteAsync_WhenAggregatedTaskEmissionIsCanceled_CancelsTaskA ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); var events = new EventCollector(); var eventQueue = new AgentEventQueue(); var readerTask = ReadEventsAsync(eventQueue, events); @@ -917,7 +917,7 @@ public async Task ExecuteAsync_WhenAggregatedTaskEmissionFails_FailsTaskAsync() ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsForThrowingExecuteAsync(handler, new RequestContext @@ -940,10 +940,10 @@ public async Task ExecuteAsync_WhenAggregatedTaskEmissionFails_FailsTaskAsync() } /// - /// Verifies that an immediate request returns one aggregated message when background responses are disabled. + /// Verifies that an immediate request returns one aggregated message in ReturnMessage mode. /// [Fact] - public async Task ExecuteAsync_WhenBackgroundResponsesDisallowedAndReturnImmediatelyTrue_ReturnsMessageAsync() + public async Task ExecuteAsync_WhenReturnMessageModeAndReturnImmediatelyTrue_ReturnsMessageAsync() { // Arrange AgentResponseUpdate[] updates = @@ -953,7 +953,7 @@ public async Task ExecuteAsync_WhenBackgroundResponsesDisallowedAndReturnImmedia ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.DisallowBackground); + runMode: AgentRunMode.ReturnMessage); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -973,10 +973,10 @@ public async Task ExecuteAsync_WhenBackgroundResponsesDisallowedAndReturnImmedia } /// - /// Verifies that a non-immediate request aggregates all updates into one message when background responses are disabled. + /// Verifies that a non-immediate request aggregates all updates into one message in ReturnMessage mode. /// [Fact] - public async Task ExecuteAsync_WhenBackgroundResponsesDisallowedAndReturnImmediatelyFalse_ReturnsMessageAsync() + public async Task ExecuteAsync_WhenReturnMessageModeAndReturnImmediatelyFalse_ReturnsMessageAsync() { // Arrange AgentResponseUpdate[] updates = @@ -986,7 +986,7 @@ public async Task ExecuteAsync_WhenBackgroundResponsesDisallowedAndReturnImmedia ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.DisallowBackground); + runMode: AgentRunMode.ReturnMessage); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1021,7 +1021,7 @@ public async Task ExecuteAsync_Streaming_WithoutMessageId_ContinuesCurrentArtifa ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1063,7 +1063,7 @@ public async Task ExecuteAsync_Streaming_WithoutMessageIds_StreamsSingleArtifact ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1106,7 +1106,7 @@ public async Task ExecuteAsync_Streaming_WithEmptyMessageIds_StreamsSingleArtifa ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1156,7 +1156,7 @@ public async Task ExecuteAsync_Streaming_WhenCancellationRequested_CancelsTaskAs ItExpr.IsAny(), ItExpr.IsAny()) .Returns(() => ToCancelingAsyncEnumerableAsync(cts)); - A2AAgentHandler handler = CreateHandler(agentMock, runMode: AgentRunMode.AllowBackgroundIfSupported); + A2AAgentHandler handler = CreateHandler(agentMock, runMode: AgentRunMode.ReturnTask); var events = new EventCollector(); var eventQueue = new AgentEventQueue(); var readerTask = ReadEventsAsync(eventQueue, events); @@ -1199,7 +1199,7 @@ public async Task ExecuteAsync_Streaming_WhenAgentThrows_FailsTaskAsync() CreateThrowingStreamingAgentMock( [new AgentResponseUpdate(ChatRole.Assistant, "chunk 1") { ResponseId = "r1", MessageId = "m1" }], new InvalidOperationException("Stream failed")), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsForThrowingExecuteAsync(handler, new RequestContext @@ -1245,7 +1245,7 @@ public async Task ExecuteAsync_Streaming_WhenMessageIdChanges_FinalizesPreviousA ]; A2AAgentHandler handler = CreateHandler( CreateThrowingStreamingAgentMock(updates, new InvalidOperationException("Stream failed")), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsForThrowingExecuteAsync(handler, new RequestContext @@ -1291,7 +1291,7 @@ public async Task ExecuteAsync_Streaming_WhenMessageIdReappears_UsesDistinctArti ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1345,7 +1345,7 @@ public async Task ExecuteAsync_Streaming_WhenAgentThrowsOperationCanceledWithout // Arrange A2AAgentHandler handler = CreateHandler( CreateThrowingStreamingAgentMock([], new OperationCanceledException("Agent gave up")), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsForThrowingExecuteAsync(handler, new RequestContext @@ -1369,7 +1369,7 @@ public async Task ExecuteAsync_Streaming_WithNoUpdates_CompletesTaskAsync() // Arrange A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock([]), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1404,7 +1404,7 @@ public async Task ExecuteAsync_Streaming_WithOnlyContentlessUpdates_CompletesTas ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -1435,7 +1435,7 @@ public async Task ExecuteAsync_Streaming_WhenContentlessUpdateChangesMessageId_F ]; A2AAgentHandler handler = CreateHandler( CreateStreamingAgentMock(updates), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act var events = await CollectEventsAsync(handler, new RequestContext @@ -2072,7 +2072,7 @@ public async Task Handler_WithNullSessionStore_SessionIsPersistedAcrossCallsAsyn } /// - /// Verifies that when the AllowBackgroundWhen delegate throws, the exception propagates + /// Verifies that when the ReturnTaskWhen delegate throws, the exception propagates /// and the agent is not invoked. /// [Fact] @@ -2082,7 +2082,7 @@ public async Task ExecuteAsync_DynamicMode_WhenCallbackThrows_PropagatesExceptio bool agentInvoked = false; A2AAgentHandler handler = CreateHandler( CreateAgentMock(_ => agentInvoked = true), - runMode: AgentRunMode.AllowBackgroundWhen((_, _) => + runMode: AgentRunMode.ReturnTaskWhen((_, _) => throw new InvalidOperationException("Callback failed"))); // Act & Assert @@ -2096,7 +2096,7 @@ await Assert.ThrowsAsync(() => } /// - /// Verifies that the CancellationToken is propagated to the AllowBackgroundWhen delegate. + /// Verifies that the CancellationToken is propagated to the ReturnTaskWhen delegate. /// [Fact] public async Task ExecuteAsync_DynamicMode_CancellationTokenIsPropagatedToCallbackAsync() @@ -2106,7 +2106,7 @@ public async Task ExecuteAsync_DynamicMode_CancellationTokenIsPropagatedToCallba using var cts = new CancellationTokenSource(); A2AAgentHandler handler = CreateHandler( CreateAgentMock(_ => { }), - runMode: AgentRunMode.AllowBackgroundWhen((_, ct) => + runMode: AgentRunMode.ReturnTaskWhen((_, ct) => { capturedToken = ct; return ValueTask.FromResult(false); @@ -2138,7 +2138,7 @@ public async Task ExecuteAsync_OnContinuation_RunModeIsAppliedAsync() AgentRunOptions? capturedOptions = null; A2AAgentHandler handler = CreateHandler( CreateAgentMock(options => capturedOptions = options), - runMode: AgentRunMode.AllowBackgroundIfSupported); + runMode: AgentRunMode.ReturnTask); // Act await InvokeExecuteAsync(handler, new RequestContext @@ -2463,7 +2463,7 @@ private static A2AAgentHandler CreateHandler( AgentRunMode? runMode = null, AgentSessionStore? agentSessionStore = null) { - runMode ??= AgentRunMode.DisallowBackground; + runMode ??= AgentRunMode.ReturnMessage; var hostAgent = new AIHostAgent( innerAgent: agentMock.Object, diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AEndpointRouteBuilderExtensionsTests.cs index df4669a014a..9d61932417e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AEndpointRouteBuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AEndpointRouteBuilderExtensionsTests.cs @@ -248,7 +248,7 @@ public void AddA2AServer_WithCustomOptions_Succeeds() IChatClient mockChatClient = new DummyChatClient(); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client"); - agentBuilder.AddA2AServer(options => options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported); + agentBuilder.AddA2AServer(options => options.AgentRunMode = AgentRunMode.ReturnTask); builder.Services.AddLogging(); using WebApplication app = builder.Build(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs index 40905bd6ecb..ce310dbec84 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs @@ -249,7 +249,7 @@ public async Task AddA2AServer_WithConfigureOptions_InvokesCallbackAsync() services.AddA2AServer(AgentName, options => { callbackInvoked = true; - options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported; + options.AgentRunMode = AgentRunMode.ReturnTask; }); // Assert - callback is invoked during resolution @@ -510,7 +510,7 @@ public async Task AddA2AServer_WithBackgroundResponsesAndNonImmediateRequest_Ret services.AddKeyedSingleton(AgentName, (_, _) => agentMock.Object); services.AddA2AServer( AgentName, - options => options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported); + options => options.AgentRunMode = AgentRunMode.ReturnTask); await using var provider = services.BuildServiceProvider(); var server = provider.GetRequiredKeyedService(AgentName); SendMessageRequest request = CreateTestSendMessageRequest(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs index cbe1254b81c..990fee439d2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs @@ -12,25 +12,25 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests; public sealed class AgentRunModeTests { /// - /// Verifies that AllowBackgroundWhen throws ArgumentNullException for null delegate. + /// Verifies that ReturnTaskWhen throws ArgumentNullException for null delegate. /// [Fact] - public void AllowBackgroundWhen_NullDelegate_ThrowsArgumentNullException() + public void ReturnTaskWhen_NullDelegate_ThrowsArgumentNullException() { // Arrange & Act & Assert Assert.Throws(() => - AgentRunMode.AllowBackgroundWhen(null!)); + AgentRunMode.ReturnTaskWhen(null!)); } /// - /// Verifies that DisallowBackground equals another DisallowBackground instance. + /// Verifies that ReturnMessage equals another ReturnMessage instance. /// [Fact] - public void Equals_DisallowBackground_AreEqual() + public void Equals_ReturnMessage_AreEqual() { // Arrange - var mode1 = AgentRunMode.DisallowBackground; - var mode2 = AgentRunMode.DisallowBackground; + var mode1 = AgentRunMode.ReturnMessage; + var mode2 = AgentRunMode.ReturnMessage; // Act & Assert Assert.True(mode1.Equals(mode2)); @@ -40,14 +40,14 @@ public void Equals_DisallowBackground_AreEqual() } /// - /// Verifies that AllowBackgroundIfSupported equals another AllowBackgroundIfSupported instance. + /// Verifies that ReturnTask equals another ReturnTask instance. /// [Fact] - public void Equals_AllowBackgroundIfSupported_AreEqual() + public void Equals_ReturnTask_AreEqual() { // Arrange - var mode1 = AgentRunMode.AllowBackgroundIfSupported; - var mode2 = AgentRunMode.AllowBackgroundIfSupported; + var mode1 = AgentRunMode.ReturnTask; + var mode2 = AgentRunMode.ReturnTask; // Act & Assert Assert.True(mode1.Equals(mode2)); @@ -55,19 +55,19 @@ public void Equals_AllowBackgroundIfSupported_AreEqual() } /// - /// Verifies that DisallowBackground and AllowBackgroundIfSupported are not equal. + /// Verifies that ReturnMessage and ReturnTask are not equal. /// [Fact] public void Equals_DifferentModes_AreNotEqual() { // Arrange - var disallow = AgentRunMode.DisallowBackground; - var allow = AgentRunMode.AllowBackgroundIfSupported; + var message = AgentRunMode.ReturnMessage; + var task = AgentRunMode.ReturnTask; // Act & Assert - Assert.False(disallow.Equals(allow)); - Assert.False(disallow == allow); - Assert.True(disallow != allow); + Assert.False(message.Equals(task)); + Assert.False(message == task); + Assert.True(message != task); } /// @@ -77,7 +77,7 @@ public void Equals_DifferentModes_AreNotEqual() public void Equals_Null_ReturnsFalse() { // Arrange - var mode = AgentRunMode.DisallowBackground; + var mode = AgentRunMode.ReturnMessage; // Act & Assert Assert.False(mode.Equals(null)); @@ -108,9 +108,9 @@ public void Equals_BothNull_AreEqual() public void ToString_ReturnsExpectedValues() { // Act & Assert - Assert.Equal("message", AgentRunMode.DisallowBackground.ToString()); - Assert.Equal("task", AgentRunMode.AllowBackgroundIfSupported.ToString()); - Assert.Equal("dynamic", AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true)).ToString()); + Assert.Equal("message", AgentRunMode.ReturnMessage.ToString()); + Assert.Equal("task", AgentRunMode.ReturnTask.ToString()); + Assert.Equal("dynamic", AgentRunMode.ReturnTaskWhen((_, _) => ValueTask.FromResult(true)).ToString()); } /// @@ -120,24 +120,24 @@ public void ToString_ReturnsExpectedValues() public void Equals_WithObjectParameter_WorksCorrectly() { // Arrange - var mode = AgentRunMode.DisallowBackground; + var mode = AgentRunMode.ReturnMessage; // Act & Assert - Assert.True(mode.Equals((object)AgentRunMode.DisallowBackground)); - Assert.False(mode.Equals((object)AgentRunMode.AllowBackgroundIfSupported)); + Assert.True(mode.Equals((object)AgentRunMode.ReturnMessage)); + Assert.False(mode.Equals((object)AgentRunMode.ReturnTask)); Assert.False(mode.Equals("not a run mode")); } /// - /// Verifies that two AllowBackgroundWhen instances with different delegates are not considered equal, + /// Verifies that two ReturnTaskWhen instances with different delegates are not considered equal, /// because equality includes delegate identity for dynamic modes. /// [Fact] - public void Equals_AllowBackgroundWhen_DifferentDelegates_AreNotEqual() + public void Equals_ReturnTaskWhen_DifferentDelegates_AreNotEqual() { // Arrange - var mode1 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true)); - var mode2 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(false)); + var mode1 = AgentRunMode.ReturnTaskWhen((_, _) => ValueTask.FromResult(true)); + var mode2 = AgentRunMode.ReturnTaskWhen((_, _) => ValueTask.FromResult(false)); // Act & Assert Assert.False(mode1.Equals(mode2)); @@ -145,15 +145,15 @@ public void Equals_AllowBackgroundWhen_DifferentDelegates_AreNotEqual() } /// - /// Verifies that two AllowBackgroundWhen instances with the same delegate are considered equal. + /// Verifies that two ReturnTaskWhen instances with the same delegate are considered equal. /// [Fact] - public void Equals_AllowBackgroundWhen_SameDelegate_AreEqual() + public void Equals_ReturnTaskWhen_SameDelegate_AreEqual() { // Arrange static ValueTask CallbackAsync(A2ARunDecisionContext _, CancellationToken __) => ValueTask.FromResult(true); - var mode1 = AgentRunMode.AllowBackgroundWhen(CallbackAsync); - var mode2 = AgentRunMode.AllowBackgroundWhen(CallbackAsync); + var mode1 = AgentRunMode.ReturnTaskWhen(CallbackAsync); + var mode2 = AgentRunMode.ReturnTaskWhen(CallbackAsync); // Act & Assert Assert.True(mode1.Equals(mode2)); From 31d517a9ba798d0175b4fd4d53fe66f48a6a7d26 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 3 Sep 2026 11:00:52 +0100 Subject: [PATCH 2/5] .NET: Always create A2A agent run options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a876988-01c0-4110-9517-77aba4bf41a8 --- .../A2AAgentHandler.cs | 11 ++--------- .../A2AAgentHandlerTests.cs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs index 3222bced095..10cb1b61148 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs @@ -240,10 +240,8 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue /// /// Whether to enable for the hosted agent run. /// - /// - /// The run options to invoke the agent with, or when there is nothing to forward. - /// - private static AgentRunOptions? CreateRunOptions(RequestContext context, bool allowBackgroundResponses) + /// The run options to invoke the agent with. + private static AgentRunOptions CreateRunOptions(RequestContext context, bool allowBackgroundResponses) { AdditionalPropertiesDictionary? additionalProperties = context.Metadata is { Count: > 0 } ? context.Metadata.ToAdditionalProperties() @@ -256,11 +254,6 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue (additionalProperties ??= [])[ConfigurationPropertyKey] = configuration; } - if (!allowBackgroundResponses && additionalProperties is null) - { - return null; - } - return new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs index c8049782116..df4e5e3330a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs @@ -24,10 +24,10 @@ public sealed class A2AAgentHandlerTests private const string ConfigurationPropertyKey = "a2a.configuration"; /// - /// Verifies that when there is no request data to forward, null options are passed to RunStreamingAsync. + /// Verifies that when there is no request data to forward, empty options are passed to RunStreamingAsync. /// [Fact] - public async Task ExecuteAsync_WhenMetadataIsNull_PassesNullOptionsToRunStreamingAsync() + public async Task ExecuteAsync_WhenMetadataIsNull_PassesEmptyOptionsToRunStreamingAsync() { // Arrange AgentRunOptions? capturedOptions = null; @@ -40,7 +40,9 @@ public async Task ExecuteAsync_WhenMetadataIsNull_PassesNullOptionsToRunStreamin }); // Assert - Assert.Null(capturedOptions); + Assert.NotNull(capturedOptions); + Assert.Null(capturedOptions.AllowBackgroundResponses); + Assert.Null(capturedOptions.AdditionalProperties); } /// @@ -1500,10 +1502,10 @@ public async Task ExecuteAsync_Streaming_WithMetadata_PassesOptionsWithAdditiona } /// - /// Verifies that streaming mode passes null options when metadata is null. + /// Verifies that streaming mode passes empty options when metadata is null. /// [Fact] - public async Task ExecuteAsync_Streaming_WithNullMetadata_PassesNullOptionsAsync() + public async Task ExecuteAsync_Streaming_WithNullMetadata_PassesEmptyOptionsAsync() { // Arrange AgentRunOptions? capturedOptions = null; @@ -1522,7 +1524,9 @@ public async Task ExecuteAsync_Streaming_WithNullMetadata_PassesNullOptionsAsync // Assert Assert.True(optionsCaptured); - Assert.Null(capturedOptions); + Assert.NotNull(capturedOptions); + Assert.Null(capturedOptions.AllowBackgroundResponses); + Assert.Null(capturedOptions.AdditionalProperties); } /// From f2c69a3c89c87122fa341c116c3628cffda5db7d Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 3 Sep 2026 11:48:59 +0100 Subject: [PATCH 3/5] .NET: Decouple A2A run mode from agent options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a876988-01c0-4110-9517-77aba4bf41a8 --- .../A2AAgentHandler.cs | 16 +++------- .../A2AAgentHandlerTests.cs | 29 ------------------- 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs index 10cb1b61148..8d3de412b6b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs @@ -136,12 +136,11 @@ private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue List chatMessages = context.Message is not null ? [context.Message.ToChatMessage()] : []; - // Decide which A2A artifact to return based on the configured run mode. Returning a task also allows - // background responses so that the streamed updates carry continuation tokens for stream resumption. + // Decide which A2A artifact to return based on the configured run mode. var decisionContext = new A2ARunDecisionContext(context); var returnTask = await this._runMode.ShouldReturnTaskAsync(decisionContext, cancellationToken).ConfigureAwait(false); - var options = CreateRunOptions(context, returnTask); + var options = CreateRunOptions(context); var updates = this._hostAgent.RunStreamingAsync(chatMessages, session, options, cancellationToken); @@ -183,10 +182,7 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue List chatMessages = ExtractChatMessagesFromTaskHistory(context.Task); - var decisionContext = new A2ARunDecisionContext(context); - var returnTask = await this._runMode.ShouldReturnTaskAsync(decisionContext, cancellationToken).ConfigureAwait(false); - - var options = CreateRunOptions(context, returnTask); + var options = CreateRunOptions(context); AgentResponse response; try @@ -237,11 +233,8 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue /// MessageSendParams.metadata and MessageSendParams.configuration to the hosted agent. /// /// The A2A request context of the incoming request. - /// - /// Whether to enable for the hosted agent run. - /// /// The run options to invoke the agent with. - private static AgentRunOptions CreateRunOptions(RequestContext context, bool allowBackgroundResponses) + private static AgentRunOptions CreateRunOptions(RequestContext context) { AdditionalPropertiesDictionary? additionalProperties = context.Metadata is { Count: > 0 } ? context.Metadata.ToAdditionalProperties() @@ -256,7 +249,6 @@ private static AgentRunOptions CreateRunOptions(RequestContext context, bool all return new AgentRunOptions { - AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = additionalProperties }; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs index df4e5e3330a..ef62da6f588 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs @@ -2131,35 +2131,6 @@ await handler.ExecuteAsync( Assert.Equal(cts.Token, capturedToken); } - /// - /// Verifies that the agent run mode is applied on the continuation/task-update path, - /// not just the new message path. - /// - [Fact] - public async Task ExecuteAsync_OnContinuation_RunModeIsAppliedAsync() - { - // Arrange - AgentRunOptions? capturedOptions = null; - A2AAgentHandler handler = CreateHandler( - CreateAgentMock(options => capturedOptions = options), - runMode: AgentRunMode.ReturnTask); - - // Act - await InvokeExecuteAsync(handler, new RequestContext - { - StreamingResponse = false, - TaskId = "task-1", - ContextId = "ctx-1", - Message = new Message { MessageId = "empty", Role = Role.User, Parts = [] }, - - Task = new AgentTask { Id = "task-1", ContextId = "ctx-1", History = [new Message { Role = Role.User, Parts = [new Part { Text = "Hello" }] }] } - }); - - // Assert - Assert.NotNull(capturedOptions); - Assert.True(capturedOptions.AllowBackgroundResponses); - } - /// /// Verifies that in the non-streaming endpoint path, SaveSessionAsync is called with /// CancellationToken.None even when RunStreamingAsync throws an exception. From 2021189dc2d2db756a168cae593607f41c015c46 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 3 Sep 2026 12:09:08 +0100 Subject: [PATCH 4/5] .NET: Clarify dynamic A2A run mode scope Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a876988-01c0-4110-9517-77aba4bf41a8 --- .../AgentRunMode.cs | 9 +++--- .../A2AAgentHandlerTests.cs | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs index 0e3c0329d62..95519b3255d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs @@ -43,12 +43,13 @@ private AgentRunMode(string value, Func /// Defers the choice between an AgentMessage and an AgentTask to the supplied - /// delegate, which is invoked per request. The delegate receives an - /// describing the incoming request and returns - /// to return an AgentTask, or to return an AgentMessage. + /// delegate, which is invoked for each new-message request. The delegate receives + /// an describing the incoming request and returns to + /// return an AgentTask, or to return an AgentMessage. Continuations of an + /// existing task remain task responses and do not invoke the delegate. /// /// - /// An async delegate that decides whether the response is returned as an AgentTask. + /// An async delegate that decides whether a new-message response is returned as an AgentTask. /// public static AgentRunMode ReturnTaskWhen(Func> returnTask) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs index ef62da6f588..48999255b2e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs @@ -2131,6 +2131,36 @@ await handler.ExecuteAsync( Assert.Equal(cts.Token, capturedToken); } + /// + /// Verifies that the ReturnTaskWhen delegate is not invoked when updating an existing task. + /// + [Fact] + public async Task ExecuteAsync_OnContinuation_DoesNotInvokeDynamicModeCallbackAsync() + { + // Arrange + bool callbackInvoked = false; + A2AAgentHandler handler = CreateHandler( + CreateAgentMock(_ => { }), + runMode: AgentRunMode.ReturnTaskWhen((_, _) => + { + callbackInvoked = true; + return ValueTask.FromResult(false); + })); + + // Act + await InvokeExecuteAsync(handler, new RequestContext + { + StreamingResponse = false, + TaskId = "task-1", + ContextId = "ctx-1", + Message = new Message { MessageId = "empty", Role = Role.User, Parts = [] }, + Task = new AgentTask { Id = "task-1", ContextId = "ctx-1", History = [new Message { Role = Role.User, Parts = [new Part { Text = "Hello" }] }] } + }); + + // Assert + Assert.False(callbackInvoked); + } + /// /// Verifies that in the non-streaming endpoint path, SaveSessionAsync is called with /// CancellationToken.None even when RunStreamingAsync throws an exception. From 09a340a0c41fd319941342a9c663e3381993552d Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 4 Sep 2026 14:30:22 +0100 Subject: [PATCH 5/5] .NET: Remove redundant run mode assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a876988-01c0-4110-9517-77aba4bf41a8 --- .../AgentRunModeTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs index 990fee439d2..b9c5e5920ac 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AgentRunModeTests.cs @@ -67,7 +67,6 @@ public void Equals_DifferentModes_AreNotEqual() // Act & Assert Assert.False(message.Equals(task)); Assert.False(message == task); - Assert.True(message != task); } ///