diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
index 42f92be1b1..96a8856dea 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
@@ -84,9 +84,13 @@ public sealed class A2AAgent : AIAgent
}
else
{
- var a2aMessage = CreateA2AMessage(typedThread, messages);
+ MessageSendParams sendParams = new()
+ {
+ Message = CreateA2AMessage(typedThread, messages),
+ Metadata = options?.AdditionalProperties?.ToA2AMetadata()
+ };
- a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
+ a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
@@ -154,9 +158,13 @@ public sealed class A2AAgent : AIAgent
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
- var a2aMessage = CreateA2AMessage(typedThread, messages);
+ MessageSendParams sendParams = new()
+ {
+ Message = CreateA2AMessage(typedThread, messages),
+ Metadata = options?.AdditionalProperties?.ToA2AMetadata()
+ };
- a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
+ a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
@@ -198,10 +206,10 @@ public sealed class A2AAgent : AIAgent
protected override string? IdCore => this._id;
///
- public override string? Name => this._name ?? base.Name;
+ public override string? Name => this._name;
///
- public override string? Description => this._description ?? base.Description;
+ public override string? Description => this._description;
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs
index c0dedbd541..3c81c6abe8 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs
@@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions
///
/// Converts a dictionary of metadata to an .
///
+ ///
+ /// This method can be replaced by the one from A2A SDK once it is public.
+ ///
/// The metadata dictionary to convert.
/// The converted , or null if the input is null or empty.
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary? metadata)
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs
new file mode 100644
index 0000000000..a3340d2ca8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Agents.AI;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Extension methods for AdditionalPropertiesDictionary.
+///
+internal static class AdditionalPropertiesDictionaryExtensions
+{
+ ///
+ /// Converts an to a dictionary of values suitable for A2A metadata.
+ ///
+ ///
+ /// This method can be replaced by the one from A2A SDK once it is available.
+ ///
+ /// The additional properties dictionary to convert, or null.
+ /// A dictionary of JSON elements representing the metadata, or null if the input is null or empty.
+ internal static Dictionary? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
+ {
+ if (additionalProperties is not { Count: > 0 })
+ {
+ return null;
+ }
+
+ var metadata = new Dictionary();
+
+ foreach (var kvp in additionalProperties)
+ {
+ if (kvp.Value is JsonElement)
+ {
+ metadata[kvp.Key] = (JsonElement)kvp.Value!;
+ continue;
+ }
+
+ metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
+ }
+
+ return metadata;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
index c54af66bb8..499d724b1a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
@@ -43,10 +43,14 @@ public static class AIAgentExtensions
{
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
+ var options = messageSendParams.Metadata is not { Count: > 0 }
+ ? null
+ : new AgentRunOptions { AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
var response = await hostAgent.RunAsync(
messageSendParams.ToChatMessages(),
thread: thread,
+ options: options,
cancellationToken: cancellationToken).ConfigureAwait(false);
await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
@@ -56,7 +60,8 @@ public static class AIAgentExtensions
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
ContextId = contextId,
Role = MessageRole.Agent,
- Parts = parts
+ Parts = parts,
+ Metadata = response.AdditionalProperties?.ToA2AMetadata()
};
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs
new file mode 100644
index 0000000000..010264bb65
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
+
+///
+/// Extension methods for A2A metadata dictionary.
+///
+internal static class A2AMetadataExtensions
+{
+ ///
+ /// Converts a dictionary of metadata to an .
+ ///
+ ///
+ /// This method can be replaced by the one from A2A SDK once it is public.
+ ///
+ /// The metadata dictionary to convert.
+ /// The converted , or null if the input is null or empty.
+ internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary? metadata)
+ {
+ if (metadata is not { Count: > 0 })
+ {
+ return null;
+ }
+
+ var additionalProperties = new AdditionalPropertiesDictionary();
+ foreach (var kvp in metadata)
+ {
+ additionalProperties[kvp.Key] = kvp.Value;
+ }
+ return additionalProperties;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs
new file mode 100644
index 0000000000..d46ef72d1f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using A2A;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
+
+///
+/// Extension methods for AdditionalPropertiesDictionary.
+///
+internal static class AdditionalPropertiesDictionaryExtensions
+{
+ ///
+ /// Converts an to a dictionary of values suitable for A2A metadata.
+ ///
+ ///
+ /// This method can be replaced by the one from A2A SDK once it is available.
+ ///
+ /// The additional properties dictionary to convert, or null.
+ /// A dictionary of JSON elements representing the metadata, or null if the input is null or empty.
+ internal static Dictionary? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
+ {
+ if (additionalProperties is not { Count: > 0 })
+ {
+ return null;
+ }
+
+ var metadata = new Dictionary();
+
+ foreach (var kvp in additionalProperties)
+ {
+ if (kvp.Value is JsonElement)
+ {
+ metadata[kvp.Key] = (JsonElement)kvp.Value!;
+ continue;
+ }
+
+ metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
+ }
+
+ return metadata;
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
index 0b491fb303..236ae7b332 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
@@ -832,6 +832,174 @@ public sealed class A2AAgentTests : IDisposable
});
}
+ [Fact]
+ public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdditionalPropertiesAsync()
+ {
+ // Arrange
+ this._handler.ResponseToReturn = new AgentMessage
+ {
+ MessageId = "response-123",
+ Role = MessageRole.Agent,
+ Parts = [new TextPart { Text = "Response with metadata" }],
+ Metadata = new Dictionary
+ {
+ { "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
+ { "responseCount", JsonSerializer.SerializeToElement(99) }
+ }
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ // Act
+ var result = await this._agent.RunAsync(inputMessages);
+
+ // Assert
+ Assert.NotNull(result.AdditionalProperties);
+ Assert.NotNull(result.AdditionalProperties["responseKey1"]);
+ Assert.Equal("responseValue1", ((JsonElement)result.AdditionalProperties["responseKey1"]!).GetString());
+ Assert.NotNull(result.AdditionalProperties["responseCount"]);
+ Assert.Equal(99, ((JsonElement)result.AdditionalProperties["responseCount"]!).GetInt32());
+ }
+
+ [Fact]
+ public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
+ {
+ // Arrange
+ this._handler.ResponseToReturn = new AgentMessage
+ {
+ MessageId = "response-123",
+ Role = MessageRole.Agent,
+ Parts = [new TextPart { Text = "Response" }]
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ var options = new AgentRunOptions
+ {
+ AdditionalProperties = new()
+ {
+ { "key1", "value1" },
+ { "key2", 42 },
+ { "key3", true }
+ }
+ };
+
+ // Act
+ await this._agent.RunAsync(inputMessages, null, options);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedMessageSendParams);
+ Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
+ Assert.Equal("value1", this._handler.CapturedMessageSendParams.Metadata["key1"].GetString());
+ Assert.Equal(42, this._handler.CapturedMessageSendParams.Metadata["key2"].GetInt32());
+ Assert.True(this._handler.CapturedMessageSendParams.Metadata["key3"].GetBoolean());
+ }
+
+ [Fact]
+ public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
+ {
+ // Arrange
+ this._handler.ResponseToReturn = new AgentMessage
+ {
+ MessageId = "response-123",
+ Role = MessageRole.Agent,
+ Parts = [new TextPart { Text = "Response" }]
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ var options = new AgentRunOptions
+ {
+ AdditionalProperties = null
+ };
+
+ // Act
+ await this._agent.RunAsync(inputMessages, null, options);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedMessageSendParams);
+ Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
+ }
+
+ [Fact]
+ public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
+ {
+ // Arrange
+ this._handler.StreamingResponseToReturn = new AgentMessage
+ {
+ MessageId = "stream-123",
+ Role = MessageRole.Agent,
+ Parts = [new TextPart { Text = "Streaming response" }]
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test streaming message")
+ };
+
+ var options = new AgentRunOptions
+ {
+ AdditionalProperties = new()
+ {
+ { "streamKey1", "streamValue1" },
+ { "streamKey2", 100 },
+ { "streamKey3", false }
+ }
+ };
+
+ // Act
+ await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
+ {
+ }
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedMessageSendParams);
+ Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
+ Assert.Equal("streamValue1", this._handler.CapturedMessageSendParams.Metadata["streamKey1"].GetString());
+ Assert.Equal(100, this._handler.CapturedMessageSendParams.Metadata["streamKey2"].GetInt32());
+ Assert.False(this._handler.CapturedMessageSendParams.Metadata["streamKey3"].GetBoolean());
+ }
+
+ [Fact]
+ public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
+ {
+ // Arrange
+ this._handler.StreamingResponseToReturn = new AgentMessage
+ {
+ MessageId = "stream-123",
+ Role = MessageRole.Agent,
+ Parts = [new TextPart { Text = "Streaming response" }]
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test streaming message")
+ };
+
+ var options = new AgentRunOptions
+ {
+ AdditionalProperties = null
+ };
+
+ // Act
+ await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
+ {
+ }
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedMessageSendParams);
+ Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
+ }
+
[Fact]
public async Task RunAsync_WithInvalidThreadType_ThrowsInvalidOperationExceptionAsync()
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs
new file mode 100644
index 0000000000..4972b8857f
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.A2A.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public sealed class AdditionalPropertiesDictionaryExtensionsTests
+{
+ [Fact]
+ public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary? additionalProperties = null;
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = [];
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "stringKey", "stringValue" }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("stringKey"));
+ Assert.Equal("stringValue", result["stringKey"].GetString());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "numberKey", 42 }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("numberKey"));
+ Assert.Equal(42, result["numberKey"].GetInt32());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "booleanKey", true }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("booleanKey"));
+ Assert.True(result["booleanKey"].GetBoolean());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "stringKey", "stringValue" },
+ { "numberKey", 42 },
+ { "booleanKey", true }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(3, result.Count);
+
+ Assert.True(result.ContainsKey("stringKey"));
+ Assert.Equal("stringValue", result["stringKey"].GetString());
+
+ Assert.True(result.ContainsKey("numberKey"));
+ Assert.Equal(42, result["numberKey"].GetInt32());
+
+ Assert.True(result.ContainsKey("booleanKey"));
+ Assert.True(result["booleanKey"].GetBoolean());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ int[] arrayValue = [1, 2, 3];
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "arrayKey", arrayValue }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("arrayKey"));
+ Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind);
+ Assert.Equal(3, result["arrayKey"].GetArrayLength());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "nullKey", null! }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("nullKey"));
+ Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 });
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "jsonElementKey", jsonElement }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("jsonElementKey"));
+ Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind);
+ Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString());
+ Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32());
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs
new file mode 100644
index 0000000000..0d5b895974
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs
@@ -0,0 +1,218 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using A2A;
+using Microsoft.Extensions.AI;
+using Moq;
+using Moq.Protected;
+
+namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public sealed class AIAgentExtensionsTests
+{
+ ///
+ /// Verifies that when messageSendParams.Metadata is null, the options passed to RunAsync are null.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenMetadataIsNull_PassesNullOptionsToRunAsync()
+ {
+ // Arrange
+ AgentRunOptions? capturedOptions = null;
+ ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+
+ // Act
+ await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Metadata = null
+ });
+
+ // Assert
+ Assert.Null(capturedOptions);
+ }
+
+ ///
+ /// Verifies that when messageSendParams.Metadata has values, the options.AdditionalProperties contains the converted values.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenMetadataHasValues_PassesOptionsWithAdditionalPropertiesToRunAsync()
+ {
+ // Arrange
+ AgentRunOptions? capturedOptions = null;
+ ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+
+ // Act
+ await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Metadata = new Dictionary
+ {
+ ["key1"] = JsonSerializer.SerializeToElement("value1"),
+ ["key2"] = JsonSerializer.SerializeToElement(42)
+ }
+ });
+
+ // Assert
+ Assert.NotNull(capturedOptions);
+ Assert.NotNull(capturedOptions.AdditionalProperties);
+ Assert.Equal(2, capturedOptions.AdditionalProperties.Count);
+ Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key1"));
+ Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key2"));
+ }
+
+ ///
+ /// Verifies that when messageSendParams.Metadata is an empty dictionary, the options passed to RunAsync is null
+ /// because the ToAdditionalProperties extension method returns null for empty dictionaries.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenMetadataIsEmptyDictionary_PassesNullOptionsToRunAsync()
+ {
+ // Arrange
+ AgentRunOptions? capturedOptions = null;
+ ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+
+ // Act
+ await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Metadata = []
+ });
+
+ // Assert
+ Assert.Null(capturedOptions);
+ }
+
+ ///
+ /// Verifies that when the agent response has AdditionalProperties, the returned AgentMessage.Metadata contains the converted values.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessageWithMetadataAsync()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProps = new()
+ {
+ ["responseKey1"] = "responseValue1",
+ ["responseKey2"] = 123
+ };
+ AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
+ {
+ AdditionalProperties = additionalProps
+ };
+ ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+
+ // Act
+ A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
+ });
+
+ // Assert
+ AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.NotNull(agentMessage.Metadata);
+ Assert.Equal(2, agentMessage.Metadata.Count);
+ Assert.True(agentMessage.Metadata.ContainsKey("responseKey1"));
+ Assert.True(agentMessage.Metadata.ContainsKey("responseKey2"));
+ Assert.Equal("responseValue1", agentMessage.Metadata["responseKey1"].GetString());
+ Assert.Equal(123, agentMessage.Metadata["responseKey2"].GetInt32());
+ }
+
+ ///
+ /// Verifies that when the agent response has null AdditionalProperties, the returned AgentMessage.Metadata is null.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenResponseHasNullAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
+ {
+ // Arrange
+ AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
+ {
+ AdditionalProperties = null
+ };
+ ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+
+ // Act
+ A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
+ });
+
+ // Assert
+ AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Null(agentMessage.Metadata);
+ }
+
+ ///
+ /// Verifies that when the agent response has empty AdditionalProperties, the returned AgentMessage.Metadata is null.
+ ///
+ [Fact]
+ public async Task MapA2A_WhenResponseHasEmptyAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
+ {
+ // Arrange
+ AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
+ {
+ AdditionalProperties = []
+ };
+ ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+
+ // Act
+ A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ {
+ Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
+ });
+
+ // Assert
+ AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Null(agentMessage.Metadata);
+ }
+
+ private static Mock CreateAgentMock(Action optionsCallback)
+ {
+ Mock agentMock = new() { CallBase = true };
+ agentMock.SetupGet(x => x.Name).Returns("TestAgent");
+ agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
+ agentMock
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .Callback, AgentThread?, AgentRunOptions?, CancellationToken>(
+ (_, _, options, _) => optionsCallback(options))
+ .ReturnsAsync(new AgentRunResponse([new ChatMessage(ChatRole.Assistant, "Test response")]));
+
+ return agentMock;
+ }
+
+ private static Mock CreateAgentMockWithResponse(AgentRunResponse response)
+ {
+ Mock agentMock = new() { CallBase = true };
+ agentMock.SetupGet(x => x.Name).Returns("TestAgent");
+ agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
+ agentMock
+ .Protected()
+ .Setup>("RunCoreAsync",
+ ItExpr.IsAny>(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(response);
+
+ return agentMock;
+ }
+
+ private static async Task InvokeOnMessageReceivedAsync(ITaskManager taskManager, MessageSendParams messageSendParams)
+ {
+ Func>? handler = taskManager.OnMessageReceived;
+ Assert.NotNull(handler);
+ return await handler.Invoke(messageSendParams, CancellationToken.None);
+ }
+
+ private sealed class TestAgentThread : AgentThread;
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs
new file mode 100644
index 0000000000..e0c8c4e96b
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs
@@ -0,0 +1,187 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Agents.AI.Hosting.A2A.Converters;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
+
+///
+/// Unit tests for the class.
+///
+public sealed class AdditionalPropertiesDictionaryExtensionsTests
+{
+ [Fact]
+ public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary? additionalProperties = null;
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = [];
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "stringKey", "stringValue" }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("stringKey"));
+ Assert.Equal("stringValue", result["stringKey"].GetString());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "numberKey", 42 }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("numberKey"));
+ Assert.Equal(42, result["numberKey"].GetInt32());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "booleanKey", true }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("booleanKey"));
+ Assert.True(result["booleanKey"].GetBoolean());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "stringKey", "stringValue" },
+ { "numberKey", 42 },
+ { "booleanKey", true }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(3, result.Count);
+
+ Assert.True(result.ContainsKey("stringKey"));
+ Assert.Equal("stringValue", result["stringKey"].GetString());
+
+ Assert.True(result.ContainsKey("numberKey"));
+ Assert.Equal(42, result["numberKey"].GetInt32());
+
+ Assert.True(result.ContainsKey("booleanKey"));
+ Assert.True(result["booleanKey"].GetBoolean());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ int[] arrayValue = [1, 2, 3];
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "arrayKey", arrayValue }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("arrayKey"));
+ Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind);
+ Assert.Equal(3, result["arrayKey"].GetArrayLength());
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement()
+ {
+ // Arrange
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "nullKey", null! }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("nullKey"));
+ Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind);
+ }
+
+ [Fact]
+ public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement()
+ {
+ // Arrange
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 });
+ AdditionalPropertiesDictionary additionalProperties = new()
+ {
+ { "jsonElementKey", jsonElement }
+ };
+
+ // Act
+ Dictionary? result = additionalProperties.ToA2AMetadata();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.True(result.ContainsKey("jsonElementKey"));
+ Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind);
+ Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString());
+ Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32());
+ }
+}