Skip to content
20 changes: 14 additions & 6 deletions dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMes
}
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);
Expand Down Expand Up @@ -154,9 +158,13 @@ protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreami
// 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);

Expand Down Expand Up @@ -198,10 +206,10 @@ protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreami
protected override string? IdCore => this._id;

/// <inheritdoc/>
public override string? Name => this._name ?? base.Name;
public override string? Name => this._name;

/// <inheritdoc/>
public override string? Description => this._description ?? base.Description;
public override string? Description => this._description;

private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions
/// <summary>
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is public.
/// </remarks>
/// <param name="metadata">The metadata dictionary to convert.</param>
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for AdditionalPropertiesDictionary.
/// </summary>
internal static class AdditionalPropertiesDictionaryExtensions
{
/// <summary>
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is available.
/// </remarks>
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
{
if (additionalProperties is not { Count: > 0 })
{
return null;
}

var metadata = new Dictionary<string, JsonElement>();

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendPara
{
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);
Expand All @@ -56,7 +60,8 @@ async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendPara
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
ContextId = contextId,
Role = MessageRole.Agent,
Parts = parts
Parts = parts,
Metadata = response.AdditionalProperties?.ToA2AMetadata()
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for A2A metadata dictionary.
/// </summary>
internal static class A2AMetadataExtensions
{
/// <summary>
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is public.
/// </remarks>
/// <param name="metadata">The metadata dictionary to convert.</param>
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for AdditionalPropertiesDictionary.
/// </summary>
internal static class AdditionalPropertiesDictionaryExtensions
{
/// <summary>
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is available.
/// </remarks>
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
{
if (additionalProperties is not { Count: > 0 })
{
return null;
}

var metadata = new Dictionary<string, JsonElement>();

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;
}
}
168 changes: 168 additions & 0 deletions dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,174 @@ await Assert.ThrowsAsync<InvalidOperationException>(async () =>
});
}

[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<string, JsonElement>
{
{ "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
{ "responseCount", JsonSerializer.SerializeToElement(99) }
}
};

var inputMessages = new List<ChatMessage>
{
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<ChatMessage>
{
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<ChatMessage>
{
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<ChatMessage>
{
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<ChatMessage>
{
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()
{
Expand Down
Loading
Loading