From f5d46fca3b9f462818aa9e41817416fd941e7ba7 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 12:04:29 +0100
Subject: [PATCH 1/7] Add adapters to allow SK agents to be exposed as AIAgent
---
dotnet/Directory.Packages.props | 3 +-
.../GettingStartedWithAgents.csproj | 16 +-
dotnet/src/Agents/A2A/A2AAgentExtensions.cs | 29 +++
.../Abstractions/AIAgent/AIAgentAdapter.cs | 128 +++++++++++++
.../AIAgent/AIAgentThreadAdapter.cs | 38 ++++
.../AIAgent/ChatMessageExtensions.cs | 65 +++++++
.../Agents/Abstractions/AgentExtensions.cs | 34 ++++
.../Abstractions/Agents.Abstractions.csproj | 1 +
.../Agents/AzureAI/AzureAIAgentExtensions.cs | 29 +++
.../Extensions/BedrockAgentExtensions.cs | 19 ++
.../Copilot/CopilotStudioAgentExtensions.cs | 29 +++
.../Core/ChatCompletionAgentExtensions.cs | 30 +++
.../OpenAI/OpenAIAssistantAgentExtensions.cs | 29 +++
.../OpenAI/OpenAIResponseAgentExtensions.cs | 29 +++
.../UnitTests/A2A/A2AAgentExtensionsTests.cs | 145 +++++++++++++++
.../UnitTests/AIAgent/AIAgentAdapterTests.cs | 174 ++++++++++++++++++
.../AIAgent/AIAgentThreadAdapterTests.cs | 166 +++++++++++++++++
.../Agents/UnitTests/AgentExtensionsTests.cs | 153 +++++++++++++++
.../Agents/UnitTests/Agents.UnitTests.csproj | 16 +-
.../AzureAI/AzureAIAgentExtensionsTests.cs | 145 +++++++++++++++
.../BedrockAgentExtensionsTests.cs | 127 +++++++++++++
.../CopilotStudioAgentExtensionsTests.cs | 133 +++++++++++++
.../ChatCompletionAgentExtensionsTests.cs | 158 ++++++++++++++++
.../OpenAIAssistantAgentExtensionsTests.cs | 149 +++++++++++++++
.../OpenAIResponseAgentExtensionsTests.cs | 132 +++++++++++++
.../AIAgentAdapterTests.cs | 44 +++++
.../AzureAIAgentAdapterTests.cs | 5 +
.../BedrockAgentAdapterTests.cs | 17 ++
.../ChatCompletionAgentAdapterTests.cs | 7 +
.../OpenAIAssistantAgentAdapterTests.cs | 5 +
.../OpenAIResponseAgentAdapterTests.cs | 7 +
.../AgentFixture.cs | 3 +
.../AzureAIAgentFixture.cs | 3 +
.../BedrockAgentFixture.cs | 3 +
.../ChatCompletionAgentFixture.cs | 3 +
.../OpenAIAssistantAgentFixture.cs | 3 +
.../OpenAIResponseAgentFixture.cs | 3 +
37 files changed, 2063 insertions(+), 17 deletions(-)
create mode 100644 dotnet/src/Agents/A2A/A2AAgentExtensions.cs
create mode 100644 dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
create mode 100644 dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs
create mode 100644 dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
create mode 100644 dotnet/src/Agents/Abstractions/AgentExtensions.cs
create mode 100644 dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs
create mode 100644 dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs
create mode 100644 dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs
create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs
create mode 100644 dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs
create mode 100644 dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index a3b283b557eb..d2669d83f5e7 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -49,6 +49,7 @@
+
@@ -101,7 +102,7 @@
-
+
diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
index 1519070d304a..90f2f6225a91 100644
--- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
+++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
@@ -18,16 +18,16 @@
-
-
+
+
-
-
-
-
+
+
+
+
-
-
+
+
diff --git a/dotnet/src/Agents/A2A/A2AAgentExtensions.cs b/dotnet/src/Agents/A2A/A2AAgentExtensions.cs
new file mode 100644
index 000000000000..ad07a120e18a
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AAgentExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class A2AAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this A2AAgent a2aAgent)
+ => a2aAgent.AsAIAgent(
+ () => new A2AAgentThread(a2aAgent.Client),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new A2AAgentThread(a2aAgent.Client) : new A2AAgentThread(a2aAgent.Client, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as A2AAgentThread)?.Id));
+}
diff --git a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
new file mode 100644
index 000000000000..c7fa8db4ab8c
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using MAAI = Microsoft.Agents.AI;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+[Experimental("SKEXP0110")]
+internal sealed class AIAgentAdapter : MAAI.AIAgent
+{
+ private readonly Func _threadFactory;
+ private readonly Func _threadDeserializationFactory;
+ private readonly Func _threadSerializer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// A factory method to create the required type to use with the agent.
+ /// A factory method to deserialize the required type.
+ /// A method to serialize the type.
+ public AIAgentAdapter(
+ Agent semanticKernelAgent,
+ Func threadFactory,
+ Func threadDeserializationFactory,
+ Func threadSerializer)
+ {
+ Throw.IfNull(semanticKernelAgent);
+ Throw.IfNull(threadFactory);
+ Throw.IfNull(threadDeserializationFactory);
+ Throw.IfNull(threadSerializer);
+
+ this.InnerAgent = semanticKernelAgent;
+ this._threadFactory = threadFactory;
+ this._threadDeserializationFactory = threadDeserializationFactory;
+ this._threadSerializer = threadSerializer;
+ }
+
+ ///
+ /// Gets the underlying Semantic Kernel Agent Framework .
+ ///
+ public Agent InnerAgent { get; }
+
+ ///
+ public override MAAI.AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ => new AIAgentThreadAdapter(this._threadDeserializationFactory(serializedThread, jsonSerializerOptions), this._threadSerializer);
+
+ ///
+ public override MAAI.AgentThread GetNewThread() => new AIAgentThreadAdapter(this._threadFactory(), this._threadSerializer);
+
+ ///
+ public override async Task RunAsync(IEnumerable messages, MAAI.AgentThread? thread = null, MAAI.AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ thread ??= this.GetNewThread();
+ if (thread is not AIAgentThreadAdapter typedThread)
+ {
+ throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
+ }
+
+ List responseMessages = [];
+ var invokeOptions = new AgentInvokeOptions()
+ {
+ OnIntermediateMessage = (msg) =>
+ {
+ responseMessages.Add(msg.ToChatMessage());
+ return Task.CompletedTask;
+ }
+ };
+
+ AgentResponseItem? lastResponseItem = null;
+ ChatMessage? lastResponseMessage = null;
+ await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, invokeOptions, cancellationToken).ConfigureAwait(false))
+ {
+ lastResponseItem = responseItem;
+ lastResponseMessage = responseItem.Message.ToChatMessage();
+ responseMessages.Add(lastResponseMessage);
+ }
+
+ return new MAAI.AgentRunResponse(responseMessages)
+ {
+ AgentId = this.InnerAgent.Id,
+ RawRepresentation = lastResponseItem,
+ AdditionalProperties = lastResponseMessage?.AdditionalProperties,
+ CreatedAt = lastResponseMessage?.CreatedAt,
+ };
+ }
+
+ ///
+ public override async IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ MAAI.AgentThread? thread = null,
+ MAAI.AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ thread ??= this.GetNewThread();
+ if (thread is not AIAgentThreadAdapter typedThread)
+ {
+ throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
+ }
+
+ await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, cancellationToken: cancellationToken).ConfigureAwait(false))
+ {
+ var chatMessage = responseItem.Message.ToChatMessage();
+
+ yield return new MAAI.AgentRunResponseUpdate
+ {
+ AgentId = this.InnerAgent.Id,
+ RawRepresentation = responseItem,
+ AdditionalProperties = chatMessage.AdditionalProperties,
+ MessageId = chatMessage.MessageId,
+ Role = chatMessage.Role,
+ CreatedAt = chatMessage.CreatedAt,
+ Contents = chatMessage.Contents
+ };
+ }
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs
new file mode 100644
index 000000000000..31d7174a6b62
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+[Experimental("SKEXP0110")]
+internal sealed class AIAgentThreadAdapter : MAAI.AgentThread
+{
+ private readonly Func _threadSerializer;
+
+ internal AIAgentThreadAdapter(AgentThread thread, Func threadSerializer)
+ {
+ Throw.IfNull(thread);
+ Throw.IfNull(threadSerializer);
+
+ this.InnerThread = thread;
+ this._threadSerializer = threadSerializer;
+ }
+
+ ///
+ /// Gets the underlying Semantic Kernel Agent Framework .
+ ///
+ public AgentThread InnerThread { get; }
+
+ ///
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ => this._threadSerializer(this.InnerThread, jsonSerializerOptions);
+
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ => base.GetService(serviceType, serviceKey)
+ ?? (serviceType == typeof(AgentThread) && serviceKey is null
+ ? this.InnerThread
+ : null);
+}
diff --git a/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs b/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
new file mode 100644
index 000000000000..3e55ddebace2
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using MEAI = Microsoft.Extensions.AI;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
+internal static class ChatMessageExtensions
+{
+ /// Converts a to a .
+ internal static ChatMessageContent ToChatMessageContent(this ChatMessage message, ChatResponse? response = null)
+ {
+ ChatMessageContent result = new()
+ {
+ ModelId = response?.ModelId,
+ AuthorName = message.AuthorName,
+ InnerContent = response?.RawRepresentation ?? message.RawRepresentation,
+ Metadata = new AdditionalPropertiesDictionary(message.AdditionalProperties ?? []) { ["Usage"] = response?.Usage },
+ Role = new AuthorRole(message.Role.Value),
+ };
+
+ foreach (AIContent content in message.Contents)
+ {
+ KernelContent? resultContent = content switch
+ {
+ MEAI.TextContent tc => new TextContent(tc.Text),
+ DataContent dc when dc.HasTopLevelMediaType("image") => new ImageContent(dc.Uri),
+ UriContent uc when uc.HasTopLevelMediaType("image") => new ImageContent(uc.Uri),
+ DataContent dc when dc.HasTopLevelMediaType("audio") => new AudioContent(dc.Uri),
+ UriContent uc when uc.HasTopLevelMediaType("audio") => new AudioContent(uc.Uri),
+ DataContent dc => new BinaryContent(dc.Uri),
+ UriContent uc => new BinaryContent(uc.Uri),
+ MEAI.FunctionCallContent fcc => new FunctionCallContent(
+ functionName: fcc.Name,
+ id: fcc.CallId,
+ arguments: fcc.Arguments is not null ? new(fcc.Arguments) : null),
+ MEAI.FunctionResultContent frc => new FunctionResultContent(
+ functionName: GetFunctionCallContent(frc.CallId)?.Name,
+ callId: frc.CallId,
+ result: frc.Result),
+ _ => null
+ };
+
+ if (resultContent is not null)
+ {
+ resultContent.Metadata = content.AdditionalProperties;
+ resultContent.InnerContent = content.RawRepresentation;
+ resultContent.ModelId = response?.ModelId;
+ result.Items.Add(resultContent);
+ }
+ }
+
+ return result;
+
+ MEAI.FunctionCallContent? GetFunctionCallContent(string callId)
+ => response?.Messages
+ .Select(m => m.Contents
+ .FirstOrDefault(c => c is MEAI.FunctionCallContent fcc && fcc.CallId == callId) as MEAI.FunctionCallContent)
+ .FirstOrDefault(fcc => fcc is not null);
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentExtensions.cs b/dotnet/src/Agents/Abstractions/AgentExtensions.cs
new file mode 100644
index 000000000000..cfa9f6579fee
--- /dev/null
+++ b/dotnet/src/Agents/Abstractions/AgentExtensions.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class AgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// A factory method to create the required type to use with the agent.
+ /// A factory method to deserialize the required type.
+ /// A method to serialize the type.
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(
+ this Agent semanticKernelAgent,
+ Func threadFactory,
+ Func threadDeserializationFactory,
+ Func threadSerializer)
+ => new AIAgentAdapter(
+ semanticKernelAgent,
+ threadFactory,
+ threadDeserializationFactory,
+ threadSerializer);
+}
diff --git a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
index a0fedaa605ec..d052813952e2 100644
--- a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
+++ b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
@@ -27,6 +27,7 @@
+
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs
new file mode 100644
index 000000000000..255128159d26
--- /dev/null
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents.AzureAI;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class AzureAIAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this AzureAIAgent azureAIAgent)
+ => azureAIAgent.AsAIAgent(
+ () => new AzureAIAgentThread(azureAIAgent.Client),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new AzureAIAgentThread(azureAIAgent.Client) : new AzureAIAgentThread(azureAIAgent.Client, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as AzureAIAgentThread)?.Id));
+}
diff --git a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs
index eee1efe21bcb..9e3cae27edfa 100644
--- a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs
+++ b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs
@@ -1,10 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Amazon.BedrockAgent;
using Amazon.BedrockAgent.Model;
+using Microsoft.Agents.AI;
namespace Microsoft.SemanticKernel.Agents.Bedrock;
@@ -13,6 +16,22 @@ namespace Microsoft.SemanticKernel.Agents.Bedrock;
///
public static class BedrockAgentExtensions
{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static AIAgent AsAIAgent(this BedrockAgent bedrockAgent)
+ => bedrockAgent.AsAIAgent(
+ () => new BedrockAgentThread(bedrockAgent.RuntimeClient),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new BedrockAgentThread(bedrockAgent.RuntimeClient) : new BedrockAgentThread(bedrockAgent.RuntimeClient, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as BedrockAgentThread)?.Id));
+
///
/// Creates an agent.
///
diff --git a/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs b/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs
new file mode 100644
index 000000000000..fedb67b5fe40
--- /dev/null
+++ b/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents.Copilot;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class CopilotStudioAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this CopilotStudioAgent copilotStudioAgent)
+ => copilotStudioAgent.AsAIAgent(
+ () => new CopilotStudioAgentThread(copilotStudioAgent.Client),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new CopilotStudioAgentThread(copilotStudioAgent.Client) : new CopilotStudioAgentThread(copilotStudioAgent.Client, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as CopilotStudioAgentThread)?.Id));
+}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs b/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs
new file mode 100644
index 000000000000..d95f4a89b445
--- /dev/null
+++ b/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using Microsoft.SemanticKernel.ChatCompletion;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Exposes a Semantic Kernel as a Microsoft Agent Framework .
+///
+public static class ChatCompletionAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this ChatCompletionAgent chatCompletionAgent)
+ => chatCompletionAgent.AsAIAgent(
+ () => new ChatHistoryAgentThread(),
+ (json, options) =>
+ {
+ var chatHistory = JsonSerializer.Deserialize(json);
+ return chatHistory is null ? new ChatHistoryAgentThread() : new ChatHistoryAgentThread(chatHistory);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as ChatHistoryAgentThread)?.ChatHistory));
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs
new file mode 100644
index 000000000000..6f8881ca2b50
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class OpenAIAssistantAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this OpenAIAssistantAgent assistantAgent)
+ => assistantAgent.AsAIAgent(
+ () => new OpenAIAssistantAgentThread(assistantAgent.Client),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new OpenAIAssistantAgentThread(assistantAgent.Client) : new OpenAIAssistantAgentThread(assistantAgent.Client, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as OpenAIAssistantAgentThread)?.Id));
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs
new file mode 100644
index 000000000000..90184975bbed
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using MAAI = Microsoft.Agents.AI;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+///
+public static class OpenAIResponseAgentExtensions
+{
+ ///
+ /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework .
+ ///
+ /// The Semantic Kernel to expose as a Microsoft Agent Framework .
+ /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework
+ [Experimental("SKEXP0110")]
+ public static MAAI.AIAgent AsAIAgent(this OpenAIResponseAgent responseAgent)
+ => responseAgent.AsAIAgent(
+ () => new OpenAIResponseAgentThread(responseAgent.Client),
+ (json, options) =>
+ {
+ var agentId = JsonSerializer.Deserialize(json);
+ return agentId is null ? new OpenAIResponseAgentThread(responseAgent.Client) : new OpenAIResponseAgentThread(responseAgent.Client, agentId);
+ },
+ (thread, options) => JsonSerializer.SerializeToElement((thread as OpenAIResponseAgentThread)?.Id));
+}
diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs
new file mode 100644
index 000000000000..3be6f5cfb8e6
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs
@@ -0,0 +1,145 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using System.Text.Json;
+using A2A;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.A2A;
+
+public sealed class A2AAgentExtensionsTests
+{
+ [Fact]
+ public void AsAIAgent_WithValidA2AAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+
+ // Act
+ var result = a2aAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullA2AAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ A2AAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+
+ // Act
+ var result = a2aAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(a2aAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+
+ // Act
+ var result = a2aAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = a2aAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+ var threadId = "test-agent-id";
+ var jsonElement = JsonSerializer.SerializeToElement(threadId);
+
+ // Act
+ var result = a2aAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ Assert.Equal(threadId, threadAdapter.InnerThread.Id);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ using var httpClient = new HttpClient();
+ var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient);
+ var agentCard = new AgentCard();
+ var a2aAgent = new A2AAgent(a2aClient, agentCard);
+ var expectedThreadId = "test-thread-id";
+ var a2aThread = new A2AAgentThread(a2aClient, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = a2aAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs
new file mode 100644
index 000000000000..7ce8daad4e1e
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.AIAgent;
+
+public sealed class AIAgentAdapterTests
+{
+ [Fact]
+ public void Constructor_InitializesProperties()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var adapter = new AIAgentAdapter(agentMock.Object, ThreadFactory, ThreadDeserializationFactory, ThreadSerializer);
+
+ // Assert
+ Assert.Equal(agentMock.Object, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void Constructor_WithNullSemanticKernelAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => new AIAgentAdapter(null!, ThreadFactory, ThreadDeserializationFactory, ThreadSerializer));
+ }
+
+ [Fact]
+ public void Constructor_WithNullThreadFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => new AIAgentAdapter(agentMock.Object, null!, ThreadDeserializationFactory, ThreadSerializer));
+ }
+
+ [Fact]
+ public void Constructor_WithNullThreadDeserializationFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => new AIAgentAdapter(agentMock.Object, ThreadFactory, null!, ThreadSerializer));
+ }
+
+ [Fact]
+ public void Constructor_WithNullThreadSerializer_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+
+ // Act & Assert
+ Assert.Throws(() => new AIAgentAdapter(agentMock.Object, ThreadFactory, ThreadDeserializationFactory, null!));
+ }
+
+ [Fact]
+ public void DeserializeThread_ReturnsAIAgentThreadAdapter()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => expectedThread;
+ var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, ThreadDeserializationFactory, ThreadSerializer);
+ var json = JsonDocument.Parse("{}").RootElement;
+
+ // Act
+ var result = adapter.DeserializeThread(json);
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void GetNewThread_ReturnsAIAgentThreadAdapter()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+ var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, (e, o) => expectedThread, ThreadSerializer);
+
+ // Act
+ var result = adapter.GetNewThread();
+
+ // Assert
+ Assert.IsType(result);
+ Assert.Equal(expectedThread, ((AIAgentThreadAdapter)result).InnerThread);
+ }
+
+ [Fact]
+ public void DeserializeThread_CallsDeserializationFactory()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ var factoryCallCount = 0;
+
+ AgentThread DeserializationFactory(JsonElement e, JsonSerializerOptions? o)
+ {
+ factoryCallCount++;
+ return expectedThread;
+ }
+
+ var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, DeserializationFactory, (t, o) => default);
+ var json = JsonDocument.Parse("{}").RootElement;
+
+ // Act
+ var result = adapter.DeserializeThread(json);
+
+ // Assert
+ Assert.Equal(1, factoryCallCount);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void GetNewThread_CallsThreadFactory()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ var factoryCallCount = 0;
+
+ AgentThread ThreadFactory()
+ {
+ factoryCallCount++;
+ return expectedThread;
+ }
+
+ var adapter = new AIAgentAdapter(agentMock.Object, ThreadFactory, (e, o) => expectedThread, (t, o) => default);
+
+ // Act
+ var result = adapter.GetNewThread();
+
+ // Assert
+ Assert.Equal(1, factoryCallCount);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void InnerAgent_Property_ReturnsCorrectValue()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var adapter = new AIAgentAdapter(agentMock.Object, () => Mock.Of(), (e, o) => Mock.Of(), (t, o) => default);
+
+ // Act
+ var result = adapter.InnerAgent;
+
+ // Assert
+ Assert.Same(agentMock.Object, result);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs
new file mode 100644
index 000000000000..5305a48290ee
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs
@@ -0,0 +1,166 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.AIAgent;
+
+public sealed class AIAgentThreadAdapterTests
+{
+ [Fact]
+ public void Constructor_InitializesProperties()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer);
+
+ // Assert
+ Assert.Equal(threadMock.Object, adapter.InnerThread);
+ }
+
+ [Fact]
+ public void Serialize_CallsThreadSerializer()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var serializerCallCount = 0;
+ var expectedJsonElement = JsonDocument.Parse("{\"test\": \"value\"}").RootElement;
+
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o)
+ {
+ serializerCallCount++;
+ Assert.Same(threadMock.Object, t);
+ return expectedJsonElement;
+ }
+
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer);
+
+ // Act
+ var result = adapter.Serialize();
+
+ // Assert
+ Assert.Equal(1, serializerCallCount);
+ Assert.Equal(expectedJsonElement.ToString(), result.ToString());
+ }
+
+ [Fact]
+ public void Serialize_WithJsonSerializerOptions_PassesOptionsToSerializer()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var expectedOptions = new JsonSerializerOptions();
+ JsonSerializerOptions? capturedOptions = null;
+
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o)
+ {
+ capturedOptions = o;
+ return default;
+ }
+
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer);
+
+ // Act
+ adapter.Serialize(expectedOptions);
+
+ // Assert
+ Assert.Same(expectedOptions, capturedOptions);
+ }
+
+ [Fact]
+ public void GetService_WithAgentThreadType_ReturnsInnerThread()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default);
+
+ // Act
+ var result = adapter.GetService(typeof(AgentThread));
+
+ // Assert
+ Assert.Same(threadMock.Object, result);
+ }
+
+ [Fact]
+ public void GetService_WithAgentThreadTypeAndServiceKey_ReturnsNull()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default);
+ var serviceKey = new object();
+
+ // Act
+ var result = adapter.GetService(typeof(AgentThread), serviceKey);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetService_WithNonAgentThreadType_ReturnsNull()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default);
+
+ // Act
+ var result = adapter.GetService(typeof(string));
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetService_WithNullType_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default);
+
+ // Act & Assert
+ Assert.Throws(() => adapter.GetService(null!));
+ }
+
+ [Fact]
+ public void Serialize_WithNullOptions_CallsSerializerWithNull()
+ {
+ // Arrange
+ var threadMock = new Mock();
+ JsonSerializerOptions? capturedOptions = new();
+
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o)
+ {
+ capturedOptions = o;
+ return default;
+ }
+
+ var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer);
+
+ // Act
+ adapter.Serialize(null);
+
+ // Assert
+ Assert.Null(capturedOptions);
+ }
+
+ [Fact]
+ public void Constructor_WithNullThread_ThrowsArgumentNullException()
+ {
+ // Arrange & Act
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+ Assert.Throws(() => new AIAgentThreadAdapter(null!, ThreadSerializer));
+ }
+
+ [Fact]
+ public void Constructor_WithNullSerializer_ThrowsArgumentNullException()
+ {
+ // Arrange & Act
+ var threadMock = new Mock();
+ Assert.Throws(() => new AIAgentThreadAdapter(threadMock.Object, null!));
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs
new file mode 100644
index 000000000000..1263de16ea27
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests;
+
+public sealed class AgentExtensionsTests
+{
+ [Fact]
+ public void AsAIAgent_WithValidParameters_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullSemanticKernelAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Agent nullAgent = null!;
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer));
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullThreadFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => agentMock.Object.AsAIAgent(null!, ThreadDeserializationFactory, ThreadSerializer));
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullThreadDeserializationFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act & Assert
+ Assert.Throws(() => agentMock.Object.AsAIAgent(ThreadFactory, null!, ThreadSerializer));
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullThreadSerializer_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+
+ // Act & Assert
+ Assert.Throws(() => agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, null!));
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ AgentThread ThreadFactory() => Mock.Of();
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of();
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer);
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(agentMock.Object, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithValidFactories_CreatesWorkingAdapter()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ var factoryCallCount = 0;
+
+ AgentThread ThreadFactory()
+ {
+ factoryCallCount++;
+ return expectedThread;
+ }
+
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => expectedThread;
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer);
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.Equal(1, factoryCallCount);
+ Assert.IsType(thread);
+ Assert.Same(expectedThread, ((AIAgentThreadAdapter)thread).InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithDeserializationFactory_CreatesWorkingAdapter()
+ {
+ // Arrange
+ var agentMock = new Mock();
+ var expectedThread = Mock.Of();
+ var deserializationCallCount = 0;
+
+ AgentThread ThreadFactory() => Mock.Of();
+
+ AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o)
+ {
+ deserializationCallCount++;
+ return expectedThread;
+ }
+
+ JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default;
+
+ // Act
+ var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer);
+ var json = JsonDocument.Parse("{}").RootElement;
+ var thread = result.DeserializeThread(json);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.Equal(1, deserializationCallCount);
+ Assert.IsType(thread);
+ Assert.Same(expectedThread, ((AIAgentThreadAdapter)thread).InnerThread);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index f4a98b16a9cc..da3c9326460c 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -14,16 +14,16 @@
-
-
+
+
-
-
-
-
+
+
+
+
-
-
+
+
diff --git a/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs
new file mode 100644
index 000000000000..13f36012f9fa
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs
@@ -0,0 +1,145 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel.Primitives;
+using System.Text.Json;
+using Azure.AI.Agents.Persistent;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.AzureAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.AzureAI;
+
+public sealed class AzureAIAgentExtensionsTests
+{
+ private static readonly JsonSerializerOptions s_jsonModelConvererOptions = new() { Converters = { new JsonModelConverter() } };
+ private static readonly PersistentAgent s_agentMetadata = JsonSerializer.Deserialize(
+ """
+ {
+ "id": "1",
+ "description": "A test agent",
+ "name": "TestAgent"
+ }
+ """, s_jsonModelConvererOptions)!;
+
+ [Fact]
+ public void AsAIAgent_WithValidAzureAIAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+
+ // Act
+ var result = azureAIAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullAzureAIAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ AzureAIAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+
+ // Act
+ var result = azureAIAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(azureAIAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+
+ // Act
+ var result = azureAIAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = azureAIAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+
+ var threadId = "test-thread-id";
+ var jsonElement = JsonSerializer.SerializeToElement(threadId);
+
+ // Act
+ var result = azureAIAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ Assert.Equal(threadId, threadAdapter.InnerThread.Id);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object);
+
+ var expectedThreadId = "test-thread-id";
+ var azureAIThread = new AzureAIAgentThread(clientMock.Object, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = azureAIAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
index e48faaa2b797..f0dc1e7eedcc 100644
--- a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
@@ -1,9 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Text.Json;
using System.Threading.Tasks;
+using A2A;
using Amazon.BedrockAgent;
using Amazon.BedrockAgent.Model;
using Amazon.BedrockAgentRuntime;
+using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Bedrock;
using Moq;
using Xunit;
@@ -30,6 +34,124 @@ public class BedrockAgentExtensionsTests
Instruction = "Instruction must have at least 40 characters",
};
+ [Fact]
+ public void AsAIAgent_WithValidBedrockAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Act
+ var result = bedrockAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullBedrockAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ BedrockAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Act
+ var result = bedrockAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(bedrockAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Act
+ var result = bedrockAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = bedrockAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+ var agentId = "test-agent-id";
+ var jsonElement = JsonSerializer.SerializeToElement(agentId);
+
+ // Act
+ var result = bedrockAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+ var expectedThreadId = "test-thread-id";
+ var bedrockThread = new BedrockAgentThread(mockRuntimeClient.Object, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = bedrockAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+
///
/// Verify the creation of the agent and the preparation of the agent.
/// The status of the agent should be checked 3 times based on the setup.
@@ -240,6 +362,11 @@ public async Task VerifyEnableUserInputActionGroupAsync()
}
});
+ mockClient.Setup(x => x.PrepareAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new PrepareAgentResponse { AgentId = this._agentModel.AgentId, AgentStatus = AgentStatus.PREPARING });
+
return (mockClient, mockRuntimeClient);
}
diff --git a/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
new file mode 100644
index 000000000000..79e2a9fe768d
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using A2A;
+using Microsoft.Agents.CopilotStudio.Client;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Copilot;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Copilot;
+
+public sealed class CopilotStudioAgentExtensionsTests
+{
+ [Fact]
+ public void AsAIAgent_WithValidCopilotStudioAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+
+ // Act
+ var result = copilotStudioAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullCopilotStudioAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ CopilotStudioAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+
+ // Act
+ var result = copilotStudioAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(copilotStudioAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+
+ // Act
+ var result = copilotStudioAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = copilotStudioAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+ var agentId = "test-agent-id";
+ var jsonElement = JsonSerializer.SerializeToElement(agentId);
+
+ // Act
+ var result = copilotStudioAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ var clientMock = new Mock(null, null, null, null);
+ var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object);
+ var expectedThreadId = "test-thread-id";
+ var copilotStudioThread = new CopilotStudioAgentThread(clientMock.Object, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = copilotStudioAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs
new file mode 100644
index 000000000000..70eaf1c536be
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core;
+
+public sealed class ChatCompletionAgentExtensionsTests
+{
+ [Fact]
+ public void AsAIAgent_WithValidChatCompletionAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+
+ // Act
+ var result = chatCompletionAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullChatCompletionAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ ChatCompletionAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+
+ // Act
+ var result = chatCompletionAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(chatCompletionAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+
+ // Act
+ var result = chatCompletionAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullChatHistory_CreatesNewThread()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+ var jsonElement = JsonSerializer.SerializeToElement((ChatHistory?)null);
+
+ // Act
+ var result = chatCompletionAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidChatHistory_CreatesThreadWithHistory()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+
+ var chatHistory = new ChatHistory();
+ chatHistory.AddSystemMessage("System message");
+ chatHistory.AddUserMessage("User message");
+ var jsonElement = JsonSerializer.SerializeToElement(chatHistory);
+
+ // Act
+ var result = chatCompletionAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ var chatHistoryThread = (ChatHistoryAgentThread)threadAdapter.InnerThread;
+ Assert.Equal(2, chatHistoryThread.ChatHistory.Count);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesChatHistory()
+ {
+ // Arrange
+ var chatCompletionAgent = new ChatCompletionAgent()
+ {
+ Name = "TestAgent",
+ Instructions = "Test instructions"
+ };
+
+ var chatHistory = new ChatHistory();
+ chatHistory.AddSystemMessage("System message");
+ chatHistory.AddUserMessage("User message");
+ var chatHistoryThread = new ChatHistoryAgentThread(chatHistory);
+ var jsonElement = JsonSerializer.SerializeToElement(chatHistory);
+
+ var result = chatCompletionAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.Array, serializedElement.ValueKind);
+ var deserializedChatHistory = JsonSerializer.Deserialize(serializedElement.GetRawText());
+ Assert.NotNull(deserializedChatHistory);
+ Assert.Equal(2, deserializedChatHistory.Count);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs
new file mode 100644
index 000000000000..74ff9523346c
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel.Primitives;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Moq;
+using OpenAI.Assistants;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+public sealed class OpenAIAssistantAgentExtensionsTests
+{
+ private static readonly Assistant s_assistantDefinition = ModelReaderWriter.Read(BinaryData.FromString(
+ """
+ {
+ "id": "asst_abc123",
+ "object": "assistant",
+ "created_at": 1698984975,
+ "name": "TestAssistant",
+ "description": "A test assistant",
+ "model": "gpt-4",
+ "instructions": "Test instructions",
+ "tools": [],
+ "metadata": {}
+ }
+ """))!;
+
+ [Fact]
+ public void AsAIAgent_WithValidOpenAIAssistantAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+
+ // Act
+ var result = assistantAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullOpenAIAssistantAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ OpenAIAssistantAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+
+ // Act
+ var result = assistantAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(assistantAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+
+ // Act
+ var result = assistantAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = assistantAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+ var threadId = "test-thread-id";
+ var jsonElement = JsonSerializer.SerializeToElement(threadId);
+
+ // Act
+ var result = assistantAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ Assert.Equal(threadId, threadAdapter.InnerThread.Id);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object);
+ var expectedThreadId = "test-thread-id";
+ var assistantThread = new OpenAIAssistantAgentThread(clientMock.Object, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = assistantAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs
new file mode 100644
index 000000000000..878b07c8b8b1
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using OpenAI.Responses;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+public sealed class OpenAIResponseAgentExtensionsTests
+{
+ [Fact]
+ public void AsAIAgent_WithValidOpenAIResponseAgent_ReturnsAIAgentAdapter()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+
+ // Act
+ var result = responseAgent.AsAIAgent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void AsAIAgent_WithNullOpenAIResponseAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ OpenAIResponseAgent nullAgent = null!;
+
+ // Act & Assert
+ Assert.Throws(() => nullAgent.AsAIAgent());
+ }
+
+ [Fact]
+ public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+
+ // Act
+ var result = responseAgent.AsAIAgent();
+
+ // Assert
+ var adapter = Assert.IsType(result);
+ Assert.Same(responseAgent, adapter.InnerAgent);
+ }
+
+ [Fact]
+ public void AsAIAgent_CreatesWorkingThreadFactory()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+
+ // Act
+ var result = responseAgent.AsAIAgent();
+ var thread = result.GetNewThread();
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+ var jsonElement = JsonSerializer.SerializeToElement((string?)null);
+
+ // Act
+ var result = responseAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+ var threadId = "test-agent-id";
+ var jsonElement = JsonSerializer.SerializeToElement(threadId);
+
+ // Act
+ var result = responseAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Assert
+ Assert.NotNull(thread);
+ Assert.IsType(thread);
+ var threadAdapter = (AIAgentThreadAdapter)thread;
+ Assert.IsType(threadAdapter.InnerThread);
+ Assert.Equal(threadId, threadAdapter.InnerThread.Id);
+ }
+
+ [Fact]
+ public void AsAIAgent_ThreadSerializer_SerializesThreadId()
+ {
+ // Arrange
+ var responseClient = new OpenAIResponseClient("model", "apikey");
+ var responseAgent = new OpenAIResponseAgent(responseClient);
+ var expectedThreadId = "test-thread-id";
+ var responseThread = new OpenAIResponseAgentThread(responseClient, expectedThreadId);
+ var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId);
+
+ var result = responseAgent.AsAIAgent();
+ var thread = result.DeserializeThread(jsonElement);
+
+ // Act
+ var serializedElement = thread.Serialize();
+
+ // Assert
+ Assert.Equal(JsonValueKind.String, serializedElement.ValueKind);
+ Assert.Equal(expectedThreadId, serializedElement.GetString());
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs
new file mode 100644
index 000000000000..8d510d800e26
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public abstract class AIAgentAdapterTests(Func createAgentFixture) : IAsyncLifetime
+{
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private AgentFixture _agentFixture;
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+ protected AgentFixture Fixture => this._agentFixture;
+
+ [Fact]
+ public virtual async Task ConvertAndRunAgentAsync()
+ {
+ var aiagent = this.Fixture.AIAgent;
+ var thread = aiagent.GetNewThread();
+
+ var result = await aiagent.RunAsync("What is the capital of France?", thread);
+ Assert.Contains("Paris", result.Text, StringComparison.OrdinalIgnoreCase);
+
+ var serialisedTreadJsonElement = thread.Serialize();
+
+ var deserializedThread = aiagent.DeserializeThread(serialisedTreadJsonElement);
+
+ var secondResult = await aiagent.RunAsync("And Austria?", deserializedThread);
+ Assert.Contains("Vienna", secondResult.Text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public Task InitializeAsync()
+ {
+ this._agentFixture = createAgentFixture();
+ return this._agentFixture.InitializeAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return this._agentFixture.DisposeAsync();
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs
new file mode 100644
index 000000000000..e78df12447b2
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs
@@ -0,0 +1,5 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public class AzureAIAgentAdapterTests() : AIAgentAdapterTests(() => new AzureAIAgentFixture());
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs
new file mode 100644
index 000000000000..7a32afad292e
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public class BedrockAgentAdapterTests() : AIAgentAdapterTests(() => new BedrockAgentFixture())
+{
+ private const string ManualVerificationSkipReason = "This test is for manual verification.";
+
+ [Fact(Skip = ManualVerificationSkipReason)]
+ public override Task ConvertAndRunAgentAsync()
+ {
+ return base.ConvertAndRunAgentAsync();
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs
new file mode 100644
index 000000000000..cb8cf145500d
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public class ChatCompletionAgentAdapterTests() : AIAgentAdapterTests(() => new ChatCompletionAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs
new file mode 100644
index 000000000000..a393d773ab8f
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs
@@ -0,0 +1,5 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public class OpenAIAssistantAgentAdapterTests() : AIAgentAdapterTests(() => new OpenAIAssistantAgentFixture());
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs
new file mode 100644
index 000000000000..d3ed57b47587
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance;
+
+public class OpenAIResponseAgentAdapterTests() : AIAgentAdapterTests(() => new OpenAIResponseAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
index 89ea56fa4e27..fea7e5859e7c 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs
@@ -4,6 +4,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Xunit;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -15,6 +16,8 @@ public abstract class AgentFixture : IAsyncLifetime
{
public abstract Agent Agent { get; }
+ public abstract MAAI.AIAgent AIAgent { get; }
+
public abstract AgentThread AgentThread { get; }
public abstract AgentThread CreatedAgentThread { get; }
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
index 29c96bc5e371..958858bcd2cb 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
@@ -10,6 +10,7 @@
using Microsoft.SemanticKernel.Agents.AzureAI;
using Microsoft.SemanticKernel.ChatCompletion;
using SemanticKernel.IntegrationTests.TestSettings;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -34,6 +35,8 @@ public class AzureAIAgentFixture : AgentFixture
public override Agent Agent => this._agent!;
+ public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent();
+
public override AgentThread AgentThread => this._thread!;
public override AgentThread CreatedAgentThread => this._createdThread!;
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs
index d9ba2a333003..e3e738856f22 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs
@@ -15,6 +15,7 @@
using Microsoft.SemanticKernel.ChatCompletion;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -39,6 +40,8 @@ public sealed class BedrockAgentFixture : AgentFixture, IAsyncDisposable
public override Microsoft.SemanticKernel.Agents.Agent Agent => this._agent!;
+ public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent();
+
public override AgentThread AgentThread => this._thread!;
public override AgentThread CreatedAgentThread => this._createdThread!;
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
index f866de16fbf6..fbb0766b86ff 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs
@@ -7,6 +7,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using SemanticKernel.IntegrationTests.TestSettings;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -28,6 +29,8 @@ public class ChatCompletionAgentFixture : AgentFixture
public override Agent Agent => this._agent!;
+ public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent();
+
public override AgentThread AgentThread => this._thread!;
public override AgentThread CreatedAgentThread => this._createdThread!;
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
index fbcbda581a9c..053743ab3f78 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
@@ -11,6 +11,7 @@
using Microsoft.SemanticKernel.ChatCompletion;
using OpenAI.Assistants;
using SemanticKernel.IntegrationTests.TestSettings;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -38,6 +39,8 @@ public class OpenAIAssistantAgentFixture : AgentFixture
public override Agent Agent => this._agent!;
+ public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent();
+
public override AgentThread AgentThread => this._thread!;
public override AgentThread CreatedAgentThread => this._createdThread!;
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs
index 5a14cd765374..fcc799315508 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs
@@ -9,6 +9,7 @@
using OpenAI;
using OpenAI.Responses;
using SemanticKernel.IntegrationTests.TestSettings;
+using MAAI = Microsoft.Agents.AI;
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
@@ -35,6 +36,8 @@ public class OpenAIResponseAgentFixture : AgentFixture
public override Agent Agent => this._agent!;
+ public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent();
+
public override AgentThread AgentThread => this._thread!;
public override AgentThread CreatedAgentThread => this._createdThread!;
From 4279c17eb1e75b27271c12e5ecabf9418fd05457 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 12:09:59 +0100
Subject: [PATCH 2/7] Fix usings ordering
---
dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
index c7fa8db4ab8c..46129e2d3955 100644
--- a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
+++ b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs
@@ -2,14 +2,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Threading.Tasks;
-using System.Text.Json;
-using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
using MAAI = Microsoft.Agents.AI;
-using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel.Agents;
From 5e6d734d3f92acde2f1dca2ba34d0671b7f7741c Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 12:15:41 +0100
Subject: [PATCH 3/7] Remove unnecessary usings.
---
.../Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs | 1 -
.../UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs | 1 -
2 files changed, 2 deletions(-)
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
index f0dc1e7eedcc..ecd26d58e84f 100644
--- a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
@@ -3,7 +3,6 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
-using A2A;
using Amazon.BedrockAgent;
using Amazon.BedrockAgent.Model;
using Amazon.BedrockAgentRuntime;
diff --git a/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
index 79e2a9fe768d..e831d362dab3 100644
--- a/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs
@@ -2,7 +2,6 @@
using System;
using System.Text.Json;
-using A2A;
using Microsoft.Agents.CopilotStudio.Client;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Copilot;
From 9834fe978e0588e97f76dff289cc40c8d77d11f3 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 12:24:14 +0100
Subject: [PATCH 4/7] Suppress NU5104 for Agents.Abstractions.
---
dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
index d052813952e2..81f13a14926b 100644
--- a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
+++ b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
@@ -5,7 +5,7 @@
Microsoft.SemanticKernel.Agents.Abstractions
Microsoft.SemanticKernel.Agents
net8.0;netstandard2.0
- $(NoWarn)
+ $(NoWarn);NU5104
false
From dd0e1e40eef5bb30dca90ca1e1e0cef76386b342 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 15:28:47 +0100
Subject: [PATCH 5/7] Move ChatMessageExtensions to internal utilities and add
tests for it.
---
.../AIAgent/ChatMessageExtensions.cs | 65 ---
.../Abstractions/Agents.Abstractions.csproj | 1 +
.../meai/Extensions}/ChatMessageExtensions.cs | 9 +-
.../meai/MEAIUtilities.props | 5 +
.../SemanticKernel.Abstractions.csproj | 1 +
.../Extensions/ChatMessageExtensionsTests.cs | 516 ++++++++++++++++++
6 files changed, 530 insertions(+), 67 deletions(-)
delete mode 100644 dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
rename dotnet/src/{SemanticKernel.Abstractions/AI/ChatClient => InternalUtilities/meai/Extensions}/ChatMessageExtensions.cs (87%)
create mode 100644 dotnet/src/InternalUtilities/meai/MEAIUtilities.props
create mode 100644 dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs
diff --git a/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs b/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
deleted file mode 100644
index 3e55ddebace2..000000000000
--- a/dotnet/src/Agents/Abstractions/AIAgent/ChatMessageExtensions.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Linq;
-using Microsoft.Extensions.AI;
-using Microsoft.SemanticKernel.ChatCompletion;
-using MEAI = Microsoft.Extensions.AI;
-
-namespace Microsoft.SemanticKernel.Agents;
-
-#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
-
-internal static class ChatMessageExtensions
-{
- /// Converts a to a .
- internal static ChatMessageContent ToChatMessageContent(this ChatMessage message, ChatResponse? response = null)
- {
- ChatMessageContent result = new()
- {
- ModelId = response?.ModelId,
- AuthorName = message.AuthorName,
- InnerContent = response?.RawRepresentation ?? message.RawRepresentation,
- Metadata = new AdditionalPropertiesDictionary(message.AdditionalProperties ?? []) { ["Usage"] = response?.Usage },
- Role = new AuthorRole(message.Role.Value),
- };
-
- foreach (AIContent content in message.Contents)
- {
- KernelContent? resultContent = content switch
- {
- MEAI.TextContent tc => new TextContent(tc.Text),
- DataContent dc when dc.HasTopLevelMediaType("image") => new ImageContent(dc.Uri),
- UriContent uc when uc.HasTopLevelMediaType("image") => new ImageContent(uc.Uri),
- DataContent dc when dc.HasTopLevelMediaType("audio") => new AudioContent(dc.Uri),
- UriContent uc when uc.HasTopLevelMediaType("audio") => new AudioContent(uc.Uri),
- DataContent dc => new BinaryContent(dc.Uri),
- UriContent uc => new BinaryContent(uc.Uri),
- MEAI.FunctionCallContent fcc => new FunctionCallContent(
- functionName: fcc.Name,
- id: fcc.CallId,
- arguments: fcc.Arguments is not null ? new(fcc.Arguments) : null),
- MEAI.FunctionResultContent frc => new FunctionResultContent(
- functionName: GetFunctionCallContent(frc.CallId)?.Name,
- callId: frc.CallId,
- result: frc.Result),
- _ => null
- };
-
- if (resultContent is not null)
- {
- resultContent.Metadata = content.AdditionalProperties;
- resultContent.InnerContent = content.RawRepresentation;
- resultContent.ModelId = response?.ModelId;
- result.Items.Add(resultContent);
- }
- }
-
- return result;
-
- MEAI.FunctionCallContent? GetFunctionCallContent(string callId)
- => response?.Messages
- .Select(m => m.Contents
- .FirstOrDefault(c => c is MEAI.FunctionCallContent fcc && fcc.CallId == callId) as MEAI.FunctionCallContent)
- .FirstOrDefault(fcc => fcc is not null);
- }
-}
diff --git a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
index 81f13a14926b..1d24d51cbf02 100644
--- a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
+++ b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
@@ -10,6 +10,7 @@
+
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
similarity index 87%
rename from dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs
rename to dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
index f9198c46c4c4..9db2d0d49b80 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs
+++ b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
@@ -1,11 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
-namespace Microsoft.SemanticKernel.ChatCompletion;
+namespace Microsoft.Extensions.AI;
+[ExcludeFromCodeCoverage]
internal static class ChatMessageExtensions
{
/// Converts a to a .
@@ -22,6 +25,7 @@ internal static ChatMessageContent ToChatMessageContent(this ChatMessage message
foreach (AIContent content in message.Contents)
{
+#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
KernelContent? resultContent = content switch
{
Microsoft.Extensions.AI.TextContent tc => new Microsoft.SemanticKernel.TextContent(tc.Text),
@@ -41,6 +45,7 @@ internal static ChatMessageContent ToChatMessageContent(this ChatMessage message
result: frc.Result),
_ => null
};
+#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (resultContent is not null)
{
diff --git a/dotnet/src/InternalUtilities/meai/MEAIUtilities.props b/dotnet/src/InternalUtilities/meai/MEAIUtilities.props
new file mode 100644
index 000000000000..57f8ec064371
--- /dev/null
+++ b/dotnet/src/InternalUtilities/meai/MEAIUtilities.props
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj
index a048b020e8c4..d69f2d0cc9b5 100644
--- a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj
+++ b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj
@@ -15,6 +15,7 @@
+
diff --git a/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs
new file mode 100644
index 000000000000..982b4122d06e
--- /dev/null
+++ b/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs
@@ -0,0 +1,516 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+
+namespace SemanticKernel.UnitTests.Extensions;
+
+///
+/// Unit tests for class.
+///
+public sealed class ChatMessageExtensionsTests
+{
+ [Fact]
+ public void ToChatMessageContentWithTextContentReturnsCorrectChatMessageContent()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.User, "Hello, world!");
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(AuthorRole.User, result.Role);
+ Assert.Single(result.Items);
+ var textContent = Assert.IsType(result.Items[0]);
+ Assert.Equal("Hello, world!", textContent.Text);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithAuthorNameSetsAuthorName()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.Assistant, "Response")
+ {
+ AuthorName = "TestAssistant"
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("TestAssistant", result.AuthorName);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithResponseSetsModelIdFromResponse()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.Assistant, "Response");
+ var response = new ChatResponse(new[] { chatMessage })
+ {
+ ModelId = "gpt-4"
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent(response);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("gpt-4", result.ModelId);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithImageDataContentCreatesImageContent()
+ {
+ // Arrange
+ var imageUri = new Uri("data:image/png;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new DataContent(imageUri, "image/png")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var imageContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(imageUri.OriginalString, imageContent.DataUri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithImageUriContentCreatesImageContent()
+ {
+ // Arrange
+ var imageUri = new Uri("https://example.com/image.jpg");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new UriContent(imageUri, "image/jpeg")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var imageContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(imageUri, imageContent.Uri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithAudioDataContentCreatesAudioContent()
+ {
+ // Arrange
+ var audioUri = new Uri("data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new DataContent(audioUri, "audio/mpeg")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var audioContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(audioUri.OriginalString, audioContent.DataUri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithAudioUriContentCreatesAudioContent()
+ {
+ // Arrange
+ var audioUri = new Uri("http://example.com/audio.wav");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new UriContent(audioUri, "audio/wav")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var audioContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(audioUri, audioContent.Uri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithBinaryDataContentCreatesBinaryContent()
+ {
+ // Arrange
+ var dataUri = new Uri("data:application/octet-stream;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new DataContent(dataUri, "application/octet-stream")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var binaryContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(dataUri.OriginalString, binaryContent.DataUri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithBinaryUriContentCreatesBinaryContent()
+ {
+ // Arrange
+ var dataUri = new Uri("https://example.com/data.pdf");
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new UriContent(dataUri, "application/pdf")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var binaryContent = Assert.IsType(result.Items[0]);
+ Assert.Equal(dataUri, binaryContent.Uri);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithFunctionCallContentCreatesFunctionCallContent()
+ {
+ // Arrange
+ var arguments = new Dictionary { { "param1", "value1" } };
+ var chatMessage = new ChatMessage(ChatRole.Assistant, [
+ new Microsoft.Extensions.AI.FunctionCallContent("call-123", "MyFunction", arguments)
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var functionCall = Assert.IsType(result.Items[0]);
+ Assert.Equal("call-123", functionCall.Id);
+ Assert.Equal("MyFunction", functionCall.FunctionName);
+ Assert.NotNull(functionCall.Arguments);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithFunctionResultContentCreatesFunctionResultContent()
+ {
+ // Arrange
+ var functionCallMessage = new ChatMessage(ChatRole.Assistant, [
+ new Microsoft.Extensions.AI.FunctionCallContent("call-123", "MyFunction")
+ ]);
+ var resultMessage = new ChatMessage(ChatRole.Tool, [
+ new Microsoft.Extensions.AI.FunctionResultContent("call-123", "result value")
+ ]);
+ var response = new ChatResponse(new[] { functionCallMessage, resultMessage });
+
+ // Act
+ var result = resultMessage.ToChatMessageContent(response);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var functionResult = Assert.IsType(result.Items[0]);
+ Assert.Equal("call-123", functionResult.CallId);
+ Assert.Equal("MyFunction", functionResult.FunctionName);
+ Assert.Equal("result value", functionResult.Result);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithMultipleContentItemsCreatesMultipleItems()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new Microsoft.Extensions.AI.TextContent("Hello"),
+ new Microsoft.Extensions.AI.TextContent("World")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Items.Count);
+ Assert.All(result.Items, item => Assert.IsType(item));
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithAdditionalPropertiesSetsMetadata()
+ {
+ // Arrange
+ var additionalProps = new AdditionalPropertiesDictionary
+ {
+ { "customKey", "customValue" }
+ };
+ var chatMessage = new ChatMessage(ChatRole.User, "Test")
+ {
+ AdditionalProperties = additionalProps
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotNull(result.Metadata);
+ Assert.True(result.Metadata.ContainsKey("customKey"));
+ Assert.Equal("customValue", result.Metadata["customKey"]);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithUsageInResponseSetsUsageInMetadata()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.Assistant, "Response");
+ var usage = new UsageDetails
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20
+ };
+ var response = new ChatResponse(new[] { chatMessage })
+ {
+ Usage = usage
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent(response);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotNull(result.Metadata);
+ Assert.True(result.Metadata.ContainsKey("Usage"));
+ Assert.Same(usage, result.Metadata["Usage"]);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithRawRepresentationSetsInnerContent()
+ {
+ // Arrange
+ var rawObject = new { test = "value" };
+ var chatMessage = new ChatMessage(ChatRole.User, "Test")
+ {
+ RawRepresentation = rawObject
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(rawObject, result.InnerContent);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithResponseRawRepresentationUsesResponseRawRepresentation()
+ {
+ // Arrange
+ var messageRaw = new { message = "value" };
+ var responseRaw = new { response = "value" };
+ var chatMessage = new ChatMessage(ChatRole.User, "Test")
+ {
+ RawRepresentation = messageRaw
+ };
+ var response = new ChatResponse(new[] { chatMessage })
+ {
+ RawRepresentation = responseRaw
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent(response);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(responseRaw, result.InnerContent);
+ }
+
+ [Fact]
+ public void ToChatMessageContentSetsContentMetadataFromAIContent()
+ {
+ // Arrange
+ var contentProps = new AdditionalPropertiesDictionary { { "contentKey", "contentValue" } };
+ var contentRaw = new { content = "raw" };
+ var textContent = new Microsoft.Extensions.AI.TextContent("Hello")
+ {
+ AdditionalProperties = contentProps,
+ RawRepresentation = contentRaw
+ };
+ var chatMessage = new ChatMessage(ChatRole.User, [textContent]);
+ var response = new ChatResponse(new[] { chatMessage })
+ {
+ ModelId = "gpt-4"
+ };
+
+ // Act
+ var result = chatMessage.ToChatMessageContent(response);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var resultContent = result.Items[0];
+ Assert.NotNull(resultContent.Metadata);
+ Assert.Equal("contentValue", resultContent.Metadata["contentKey"]);
+ Assert.Same(contentRaw, resultContent.InnerContent);
+ Assert.Equal("gpt-4", resultContent.ModelId);
+ }
+
+ [Fact]
+ public void ToChatHistoryWithEmptyCollectionReturnsEmptyChatHistory()
+ {
+ // Arrange
+ var messages = Array.Empty();
+
+ // Act
+ var result = messages.ToChatHistory();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ToChatHistoryWithMultipleMessagesReturnsCorrectChatHistory()
+ {
+ // Arrange
+ var messages = new[]
+ {
+ new ChatMessage(ChatRole.System, "You are a helpful assistant."),
+ new ChatMessage(ChatRole.User, "Hello!"),
+ new ChatMessage(ChatRole.Assistant, "Hi there! How can I help you?")
+ };
+
+ // Act
+ var result = messages.ToChatHistory();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(3, result.Count);
+ Assert.Equal(AuthorRole.System, result[0].Role);
+ Assert.Equal(AuthorRole.User, result[1].Role);
+ Assert.Equal(AuthorRole.Assistant, result[2].Role);
+ }
+
+ [Fact]
+ public void ToChatHistoryPreservesMessageOrder()
+ {
+ // Arrange
+ var messages = new[]
+ {
+ new ChatMessage(ChatRole.User, "First"),
+ new ChatMessage(ChatRole.Assistant, "Second"),
+ new ChatMessage(ChatRole.User, "Third")
+ };
+
+ // Act
+ var result = messages.ToChatHistory();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(3, result.Count);
+ Assert.Equal("First", ((Microsoft.SemanticKernel.TextContent)result[0].Items[0]).Text);
+ Assert.Equal("Second", ((Microsoft.SemanticKernel.TextContent)result[1].Items[0]).Text);
+ Assert.Equal("Third", ((Microsoft.SemanticKernel.TextContent)result[2].Items[0]).Text);
+ }
+
+ [Fact]
+ public void ToChatHistoryWithComplexMessagesConvertsAllContent()
+ {
+ // Arrange
+ var imageUri = new Uri("https://example.com/image.png");
+ var messages = new[]
+ {
+ new ChatMessage(ChatRole.User, [
+ new Microsoft.Extensions.AI.TextContent("Look at this image:"),
+ new UriContent(imageUri, "image/png")
+ ])
+ };
+
+ // Act
+ var result = messages.ToChatHistory();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Items.Count);
+ Assert.IsType(result[0].Items[0]);
+ Assert.IsType(result[0].Items[1]);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithNullFunctionCallIdDoesNotThrow()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.Tool, [
+ new Microsoft.Extensions.AI.FunctionResultContent("call-456", "result")
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ var functionResult = Assert.IsType(result.Items[0]);
+ Assert.Null(functionResult.FunctionName);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithSystemRoleMapsToSystemRole()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.System, "You are helpful.");
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(AuthorRole.System, result.Role);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithToolRoleMapsToToolRole()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.Tool, "Tool result");
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(AuthorRole.Tool, result.Role);
+ }
+
+ [Fact]
+ public void ToChatMessageContentWithUnknownContentTypeSkipsContent()
+ {
+ // Arrange
+ var chatMessage = new ChatMessage(ChatRole.User, [
+ new Microsoft.Extensions.AI.TextContent("Valid text"),
+ new CustomAIContent() // Unknown content type
+ ]);
+
+ // Act
+ var result = chatMessage.ToChatMessageContent();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Items); // Only the text content should be included
+ Assert.IsType(result.Items[0]);
+ }
+
+ ///
+ /// Custom AI content type for testing unknown content handling
+ ///
+ private sealed class CustomAIContent : AIContent
+ {
+ }
+}
From ea938db3e94642df6f6bc61eaacfe71a6094b2f4 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 15:32:18 +0100
Subject: [PATCH 6/7] Fix formatting
---
.../InternalUtilities/meai/Extensions/ChatMessageExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
index 9db2d0d49b80..b82bfb61f577 100644
--- a/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
+++ b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs
@@ -8,7 +8,7 @@
namespace Microsoft.Extensions.AI;
-[ExcludeFromCodeCoverage]
+[ExcludeFromCodeCoverage]
internal static class ChatMessageExtensions
{
/// Converts a to a .
From 5032d2859f0ceab8cd8614b1d8db316a6d0a2a34 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 10 Oct 2025 15:39:23 +0100
Subject: [PATCH 7/7] Remove unused using clause.
---
.../AI/ChatClient/KernelFunctionInvokingChatClient.cs | 1 -
.../src/SemanticKernel.Abstractions/Functions/FunctionResult.cs | 1 -
2 files changed, 2 deletions(-)
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index 74a8c24fbe11..5757ad9fb966 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -7,7 +7,6 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.ChatCompletion;
namespace Microsoft.Extensions.AI;
diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs
index 4454c2c0b493..4bc58f6c7156 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs
@@ -5,7 +5,6 @@
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.AI;
-using Microsoft.SemanticKernel.ChatCompletion;
namespace Microsoft.SemanticKernel;