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.
///