From 117fc52a03411f2fa0426e3f80b2a6aabb7195ee Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 28 Jul 2026 14:27:22 +0100 Subject: [PATCH] Forward A2A MessageSendParams.Configuration in the A2A adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The A2A hosting layer now forwards the caller-supplied SendMessageConfiguration from RequestContext.Configuration into AgentRunOptions.AdditionalProperties under the key 'a2a.configuration'. This covers all three handler paths: non-streaming, streaming, and task continuation. The server-configured AgentRunMode remains authoritative for AllowBackgroundResponses — the caller's ReturnImmediately is forwarded but does not override the server decision. Closes microsoft/agent-framework#5869 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ee820de-3e34-493b-a19c-1db6bc04871d --- .../A2AAgentHandler.cs | 54 ++++++-- .../A2AAgentHandlerTests.cs | 122 ++++++++++++++++++ 2 files changed, 167 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs index 2113d273e0d..2971dc69646 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs @@ -20,6 +20,12 @@ namespace Microsoft.Agents.AI.Hosting.A2A; [Experimental(DiagnosticIds.Experiments.AIResponseContinuations)] internal sealed class A2AAgentHandler : IAgentHandler { + /// + /// The key under which the caller supplied + /// MessageSendParams.configuration is forwarded to the hosted agent. + /// + private const string ConfigurationPropertyKey = "a2a.configuration"; + private readonly AIHostAgent _hostAgent; private readonly AgentRunMode _runMode; @@ -84,9 +90,7 @@ private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue var decisionContext = new A2ARunDecisionContext(context); var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false); - var options = context.Metadata is not { Count: > 0 } - ? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses } - : new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = context.Metadata.ToAdditionalProperties() }; + var options = CreateRunOptions(context, allowBackgroundResponses); AgentResponse response; try @@ -137,9 +141,7 @@ private async Task HandleNewMessageStreamingAsync(RequestContext context, AgentE List chatMessages = context.Message is not null ? [context.Message.ToChatMessage()] : []; - var options = context.Metadata is { Count: > 0 } - ? new AgentRunOptions { AdditionalProperties = context.Metadata.ToAdditionalProperties() } - : null; + var options = CreateRunOptions(context); try { @@ -165,9 +167,7 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue var decisionContext = new A2ARunDecisionContext(context); var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false); - var options = context.Metadata is not { Count: > 0 } - ? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses } - : new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = context.Metadata.ToAdditionalProperties() }; + var options = CreateRunOptions(context, allowBackgroundResponses); AgentResponse response; try @@ -213,6 +213,42 @@ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue } } + /// + /// Creates the for a run, forwarding the caller supplied A2A + /// 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) + { + AdditionalPropertiesDictionary? additionalProperties = context.Metadata is { Count: > 0 } + ? context.Metadata.ToAdditionalProperties() + : null; + + // Forward the whole configuration object under a well-known key so that agents can observe + // the caller's requested configuration, including fields added to the A2A protocol in the future. + if (context.Configuration is { } configuration) + { + (additionalProperties ??= [])[ConfigurationPropertyKey] = configuration; + } + + if (allowBackgroundResponses is null && additionalProperties is null) + { + return null; + } + + return new AgentRunOptions + { + AllowBackgroundResponses = allowBackgroundResponses, + AdditionalProperties = additionalProperties + }; + } + private static Message CreateMessageFromResponse(string contextId, AgentResponse response) => new() { 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 b54b3d7db77..127e50b7a69 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs @@ -18,6 +18,11 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests; /// public sealed class A2AAgentHandlerTests { + /// + /// The key the handler forwards the A2A configuration under. + /// + private const string ConfigurationPropertyKey = "a2a.configuration"; + /// /// Verifies that when metadata is null, the options passed to RunAsync have /// AllowBackgroundResponses disabled and no AdditionalProperties. @@ -72,6 +77,93 @@ public async Task ExecuteAsync_WhenMetadataIsNonEmpty_PassesOptionsWithAdditiona Assert.Equal("value1", capturedOptions.AdditionalProperties["key1"]?.ToString()); } + /// + /// Verifies that when the caller supplies a MessageSendParams.configuration, it is forwarded to the + /// agent through . + /// + [Fact] + public async Task ExecuteAsync_WhenConfigurationIsProvided_ForwardsConfigurationToRunAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + A2AAgentHandler handler = CreateHandler(CreateAgentMock(options => capturedOptions = options)); + SendMessageConfiguration configuration = new() + { + AcceptedOutputModes = ["text/plain", "image/png"], + HistoryLength = 10 + }; + + // Act + await InvokeExecuteAsync(handler, new RequestContext + { + TaskId = "", ContextId = "ctx", StreamingResponse = false, + Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [new Part { Text = "Hello" }] }, + Configuration = configuration + }); + + // Assert + Assert.NotNull(capturedOptions); + Assert.NotNull(capturedOptions.AdditionalProperties); + Assert.Same(configuration, Assert.Single(capturedOptions.AdditionalProperties).Value); + Assert.Equal(ConfigurationPropertyKey, Assert.Single(capturedOptions.AdditionalProperties).Key); + } + + /// + /// Verifies that the caller supplied configuration and metadata are both forwarded to the agent. + /// + [Fact] + public async Task ExecuteAsync_WhenConfigurationAndMetadataAreProvided_ForwardsBothToRunAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + A2AAgentHandler handler = CreateHandler(CreateAgentMock(options => capturedOptions = options)); + SendMessageConfiguration configuration = new() { HistoryLength = 5 }; + + // Act + await InvokeExecuteAsync(handler, new RequestContext + { + TaskId = "", ContextId = "ctx", StreamingResponse = false, + Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [new Part { Text = "Hello" }] }, + Metadata = new Dictionary + { + ["key1"] = JsonSerializer.SerializeToElement("value1") + }, + Configuration = configuration + }); + + // Assert + Assert.NotNull(capturedOptions); + Assert.NotNull(capturedOptions.AdditionalProperties); + Assert.Equal(2, capturedOptions.AdditionalProperties.Count); + Assert.Equal("value1", capturedOptions.AdditionalProperties["key1"]?.ToString()); + Assert.Same(configuration, capturedOptions.AdditionalProperties[ConfigurationPropertyKey]); + } + + /// + /// Verifies that the caller supplied configuration does not override the run mode configured on the server. + /// + [Fact] + public async Task ExecuteAsync_WhenConfigurationRequestsImmediateReturn_DoesNotOverrideRunModeAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + A2AAgentHandler handler = CreateHandler( + CreateAgentMock(options => capturedOptions = options), + runMode: AgentRunMode.DisallowBackground); + + // Act + await InvokeExecuteAsync(handler, new RequestContext + { + TaskId = "", ContextId = "ctx", StreamingResponse = false, + Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [new Part { Text = "Hello" }] }, + Configuration = new SendMessageConfiguration { ReturnImmediately = true } + }); + + // Assert + Assert.NotNull(capturedOptions); + Assert.False(capturedOptions.AllowBackgroundResponses); + } + /// /// Verifies that when the agent response has AdditionalProperties, the returned Message.Metadata contains the converted values. /// @@ -672,6 +764,36 @@ public async Task ExecuteAsync_Streaming_WithNullMetadata_PassesNullOptionsAsync Assert.Null(capturedOptions); } + /// + /// Verifies that in streaming mode, when only a configuration is present, options carrying the + /// configuration are passed to RunStreamingAsync. + /// + [Fact] + public async Task ExecuteAsync_Streaming_WithConfiguration_PassesOptionsWithConfigurationAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + A2AAgentHandler handler = CreateHandler(CreateStreamingAgentMockWithOptionsCapture( + options => capturedOptions = options)); + SendMessageConfiguration configuration = new() { AcceptedOutputModes = ["text/plain"] }; + + // Act + await InvokeExecuteAsync(handler, new RequestContext + { + StreamingResponse = true, + TaskId = "", + ContextId = "ctx", + Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [new Part { Text = "Hello" }] }, + Configuration = configuration + }); + + // Assert + Assert.NotNull(capturedOptions); + Assert.Null(capturedOptions.AllowBackgroundResponses); + Assert.NotNull(capturedOptions.AdditionalProperties); + Assert.Same(configuration, capturedOptions.AdditionalProperties[ConfigurationPropertyKey]); + } + /// /// Verifies that in streaming mode, ReferenceTaskIds throws NotSupportedException. ///