diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs index 72cbde5a6bd..8d3de412b6b 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,11 @@ 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. 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); var updates = this._hostAgent.RunStreamingAsync(chatMessages, session, options, cancellationToken); @@ -148,20 +151,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); } @@ -179,10 +182,7 @@ 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 options = CreateRunOptions(context, allowBackgroundResponses); + var options = CreateRunOptions(context); AgentResponse response; try @@ -233,13 +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. - /// - /// The value to assign to . Defaults to , which leaves it unset. - /// - /// - /// The run options to invoke the agent with, or when there is nothing to forward. - /// - private static AgentRunOptions? CreateRunOptions(RequestContext context, bool? allowBackgroundResponses = null) + /// The run options to invoke the agent with. + private static AgentRunOptions CreateRunOptions(RequestContext context) { AdditionalPropertiesDictionary? additionalProperties = context.Metadata is { Count: > 0 } ? context.Metadata.ToAdditionalProperties() @@ -252,14 +247,8 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue (additionalProperties ??= [])[ConfigurationPropertyKey] = configuration; } - if (allowBackgroundResponses is null && additionalProperties is null) - { - return null; - } - return new AgentRunOptions { - AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = additionalProperties }; } @@ -294,7 +283,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 +332,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 +375,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..95519b3255d 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,46 @@ 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 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 should be wrapped in an AgentTask. + /// + /// An async delegate that decides whether a new-message 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 +73,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 +86,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 +94,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..48999255b2e 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); } /// @@ -146,7 +148,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 +252,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 +274,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 +290,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 +308,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 +594,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 +603,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 +700,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 +732,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 +784,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 +794,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 +833,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 +871,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 +919,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 +942,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 +955,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 +975,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 +988,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 +1023,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 +1065,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 +1108,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 +1158,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 +1201,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 +1247,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 +1293,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 +1347,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 +1371,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 +1406,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 +1437,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 @@ -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); } /// @@ -2072,7 +2076,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 +2086,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 +2100,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 +2110,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); @@ -2128,17 +2132,20 @@ await handler.ExecuteAsync( } /// - /// Verifies that the agent run mode is applied on the continuation/task-update path, - /// not just the new message path. + /// Verifies that the ReturnTaskWhen delegate is not invoked when updating an existing task. /// [Fact] - public async Task ExecuteAsync_OnContinuation_RunModeIsAppliedAsync() + public async Task ExecuteAsync_OnContinuation_DoesNotInvokeDynamicModeCallbackAsync() { // Arrange - AgentRunOptions? capturedOptions = null; + bool callbackInvoked = false; A2AAgentHandler handler = CreateHandler( - CreateAgentMock(options => capturedOptions = options), - runMode: AgentRunMode.AllowBackgroundIfSupported); + CreateAgentMock(_ => { }), + runMode: AgentRunMode.ReturnTaskWhen((_, _) => + { + callbackInvoked = true; + return ValueTask.FromResult(false); + })); // Act await InvokeExecuteAsync(handler, new RequestContext @@ -2147,13 +2154,11 @@ public async Task ExecuteAsync_OnContinuation_RunModeIsAppliedAsync() 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); + Assert.False(callbackInvoked); } /// @@ -2463,7 +2468,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..b9c5e5920ac 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,18 @@ 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); } /// @@ -77,7 +76,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 +107,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 +119,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 +144,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));