diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs
new file mode 100644
index 000000000000..e4775f4555d1
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.ComponentModel;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace GettingStarted;
+
+///
+/// Demonstrate creation of and
+/// eliciting its response to three explicit user messages.
+///
+public class Step08_AgentAsKernelFunction(ITestOutputHelper output) : BaseAgentsTest(output)
+{
+ protected override bool ForceOpenAI { get; } = true;
+
+ [Fact]
+ public async Task SalesAssistantAgentAsync()
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ kernel.Plugins.AddFromType();
+ kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output));
+
+ // Define the agent
+ ChatCompletionAgent agent =
+ new()
+ {
+ Name = "SalesAssistant",
+ Instructions = "You are a sales assistant. Place orders for items the user requests.",
+ Kernel = kernel,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+
+ // Invoke the agent and display the responses
+ var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "Place an order for a black boot."));
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ this.WriteAgentChatMessage(responseItem);
+ }
+ }
+
+ [Fact]
+ public async Task RefundAgentAsync()
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ kernel.Plugins.AddFromType();
+ kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output));
+
+ // Define the agent
+ ChatCompletionAgent agent =
+ new()
+ {
+ Name = "RefundAgent",
+ Instructions = "You are a refund agent. Help the user with refunds.",
+ Kernel = kernel,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+
+ // Invoke the agent and display the responses
+ var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "I want a refund for a black boot."));
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ this.WriteAgentChatMessage(responseItem);
+ }
+ }
+
+ [Fact]
+ public async Task MultipleAgentsAsync()
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin",
+ [
+ AgentKernelFunctionFactory.CreateFromAgent(this.CreateSalesAssistant()),
+ AgentKernelFunctionFactory.CreateFromAgent(this.CreateRefundAgent())
+ ]);
+ kernel.Plugins.Add(agentPlugin);
+ kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output));
+
+ // Define the agent
+ ChatCompletionAgent agent =
+ new()
+ {
+ Name = "ShoppingAssistant",
+ Instructions = "You are a sales assistant. Delegate to the provided agents to help the user with placing orders and requesting refunds.",
+ Kernel = kernel,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+
+ // Invoke the agent and display the responses
+ string[] messages =
+ [
+ "Place an order for a black boot.",
+ "Now I want a refund for the black boot."
+ ];
+
+ AgentThread? agentThread = null;
+ foreach (var message in messages)
+ {
+ var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, message), agentThread);
+ await foreach (var responseItem in responseItems)
+ {
+ agentThread = responseItem.Thread;
+ this.WriteAgentChatMessage(responseItem.Message);
+ }
+ }
+ }
+
+ #region private
+ private ChatCompletionAgent CreateSalesAssistant()
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ kernel.Plugins.AddFromType();
+ kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output));
+
+ // Define the agent
+ return new()
+ {
+ Name = "SalesAssistant",
+ Instructions = "You are a sales assistant. Place orders for items the user requests.",
+ Description = "Agent to invoke to place orders for items the user requests.",
+ Kernel = kernel,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+
+ private ChatCompletionAgent CreateRefundAgent()
+ {
+ Kernel kernel = this.CreateKernelWithChatCompletion();
+ kernel.Plugins.AddFromType();
+ kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output));
+
+ // Define the agent
+ return new()
+ {
+ Name = "RefundAgent",
+ Instructions = "You are a refund agent. Help the user with refunds.",
+ Description = "Agent to invoke to execute a refund an item on behalf of the user.",
+ Kernel = kernel,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+ #endregion
+}
+
+public sealed class OrderPlugin
+{
+ [KernelFunction, Description("Place an order for the specified item.")]
+ public string PlaceOrder([Description("The name of the item to be ordered.")] string itemName) => "success";
+}
+
+public sealed class RefundPlugin
+{
+ [KernelFunction, Description("Execute a refund for the specified item.")]
+ public string ExecuteRefund(string itemName) => "success";
+}
+
+public sealed class AutoFunctionInvocationFilter(ITestOutputHelper output) : IAutoFunctionInvocationFilter
+{
+ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next)
+ {
+ output.WriteLine($"Invoke: {context.Function.Name}");
+
+ await next(context);
+ }
+}
diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs
new file mode 100644
index 000000000000..5c45d2a8d41f
--- /dev/null
+++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Agents.Extensions;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// Provides factory methods for creating implementations of backed by an .
+///
+[Experimental("SKEXP0110")]
+public static class AgentKernelFunctionFactory
+{
+ ///
+ /// Creates a that will invoke the provided Agent.
+ ///
+ /// The to be represented via the created .
+ /// The name to use for the function. If null, it will default to the agent name.
+ /// The description to use for the function. If null, it will default to agent description.
+ /// Optional parameter descriptions. If null, it will default to query and additional instructions parameters.
+ /// The to use for logging. If null, no logging will be performed.
+ /// The created for invoking the .
+ [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+ [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+ public static KernelFunction CreateFromAgent(
+ Agent agent,
+ string? functionName = null,
+ string? description = null,
+ IEnumerable? parameters = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Verify.NotNull(agent);
+
+ async Task InvokeAgentAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken)
+ {
+ arguments.TryGetValue("query", out var query);
+ var queryString = query?.ToString() ?? string.Empty;
+
+ AgentInvokeOptions? options = null;
+
+ if (arguments.TryGetValue("instructions", out var instructions) && instructions is not null)
+ {
+ options = new()
+ {
+ AdditionalInstructions = instructions?.ToString() ?? string.Empty
+ };
+ }
+
+ var response = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, queryString), null, options, cancellationToken);
+ var responseItems = await response.ToArrayAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ var chatMessages = responseItems.Select(i => i.Message).ToArray();
+ return new FunctionResult(function, chatMessages, kernel.Culture);
+ }
+
+ KernelFunctionFromMethodOptions options = new()
+ {
+ FunctionName = functionName ?? agent.GetName(),
+ Description = description ?? agent.Description,
+ Parameters = parameters ?? GetDefaultKernelParameterMetadata(),
+ ReturnParameter = new() { ParameterType = typeof(FunctionResult) },
+ };
+
+ return KernelFunctionFactory.CreateFromMethod(
+ InvokeAgentAsync,
+ options);
+ }
+
+ #region private
+ [RequiresUnreferencedCode("Uses reflection for generating JSON schema for method parameters and return type, making it incompatible with AOT scenarios.")]
+ [RequiresDynamicCode("Uses reflection for generating JSON schema for method parameters and return type, making it incompatible with AOT scenarios.")]
+ private static IEnumerable GetDefaultKernelParameterMetadata()
+ {
+ return s_kernelParameterMetadata ??= [
+ new KernelParameterMetadata("query") { Description = "Available information that will guide in performing this operation.", ParameterType = typeof(string), IsRequired = true },
+ new KernelParameterMetadata("instructions") { Description = "Additional instructions for the agent.", ParameterType = typeof(string), IsRequired = true },
+ ];
+ }
+
+ private static IEnumerable? s_kernelParameterMetadata;
+ #endregion
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs
new file mode 100644
index 000000000000..7bbefbb8652e
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs
@@ -0,0 +1,209 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core.Functions;
+
+///
+/// Unit testing of .
+///
+public class AgentKernelFunctionFactoryTests
+{
+ ///
+ /// Verify calling AgentKernelFunctionFactory.CreateFromAgent.
+ ///
+ [Fact]
+ public void VerifyCreateFromAgent()
+ {
+ // Arrange
+ var agent = new MockAgent()
+ {
+ Name = "MyAgent",
+ Description = "Description for MyAgent"
+ };
+
+ // Act
+ var function = AgentKernelFunctionFactory.CreateFromAgent(agent);
+
+ // Assert
+ Assert.NotNull(function);
+ Assert.Equal(agent.Name, function.Name);
+ Assert.Equal(agent.Description, function.Description);
+ }
+
+ ///
+ /// Verify calling AgentKernelFunctionFactory.CreateFromAgent with overrides.
+ ///
+ [Fact]
+ public void VerifyCreateFromAgentWithOverrides()
+ {
+ // Arrange
+ var agent = new MockAgent()
+ {
+ Name = "MyAgent",
+ Description = "Description for MyAgent"
+ };
+
+ // Act
+ var function = AgentKernelFunctionFactory.CreateFromAgent(
+ agent,
+ "MyAgentFunction",
+ "Description for MyAgentFunction"
+ );
+
+ // Assert
+ Assert.NotNull(function);
+ Assert.Equal("MyAgentFunction", function.Name);
+ Assert.Equal("Description for MyAgentFunction", function.Description);
+ }
+
+ ///
+ /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent.
+ ///
+ [Fact]
+ public async Task VerifyInvokeAgentAsKernelFunctionAsync()
+ {
+ // Arrange
+ var agent = new MockAgent()
+ {
+ Name = "MyAgent",
+ Description = "Description for MyAgent"
+ };
+ var function = AgentKernelFunctionFactory.CreateFromAgent(agent);
+
+ // Act
+ var arguments = new KernelArguments
+ {
+ { "query", "Mock query" }
+ };
+ var result = await function.InvokeAsync(new(), arguments);
+
+ // Assert
+ Assert.NotNull(result);
+ var items = result.GetValue>();
+ Assert.NotNull(items);
+ Assert.NotEmpty(items);
+ Assert.Equal("Response to: 'Mock query' with instructions: ''", items.First().ToString());
+ }
+
+ ///
+ /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent.
+ ///
+ [Fact]
+ public async Task VerifyInvokeAgentAsKernelFunctionWithNoQueryAsync()
+ {
+ // Arrange
+ var agent = new MockAgent()
+ {
+ Name = "MyAgent",
+ Description = "Description for MyAgent"
+ };
+ var function = AgentKernelFunctionFactory.CreateFromAgent(agent);
+
+ // Act
+ var result = await function.InvokeAsync(new());
+
+ // Assert
+ Assert.NotNull(result);
+ var items = result.GetValue>();
+ Assert.NotNull(items);
+ Assert.NotEmpty(items);
+ Assert.Equal("Response to: '' with instructions: ''", items.First().ToString());
+ }
+
+ ///
+ /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent.
+ ///
+ [Fact]
+ public async Task VerifyInvokeAgentAsKernelFunctionWithInstructionsAsync()
+ {
+ // Arrange
+ var agent = new MockAgent()
+ {
+ Name = "MyAgent",
+ Description = "Description for MyAgent"
+ };
+ var function = AgentKernelFunctionFactory.CreateFromAgent(agent);
+
+ // Act
+ var arguments = new KernelArguments
+ {
+ { "query", "Mock query" },
+ { "instructions", "Mock instructions" }
+ };
+ var result = await function.InvokeAsync(new(), arguments);
+
+ // Assert
+ Assert.NotNull(result);
+ var items = result.GetValue>();
+ Assert.NotNull(items);
+ Assert.NotEmpty(items);
+ Assert.Equal("Response to: 'Mock query' with instructions: 'Mock instructions'", items.First().ToString());
+ }
+
+ ///
+ /// Mock implementation of .
+ ///
+ private sealed class MockAgent : Agent
+ {
+ public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var agentThread = thread ?? new MockAgentThread();
+ foreach (var message in messages)
+ {
+ await Task.Delay(100, cancellationToken);
+ yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, $"Response to: '{message.Content}' with instructions: '{options?.AdditionalInstructions}'"), agentThread);
+ }
+ }
+
+ public override IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected internal override Task CreateChannelAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected internal override IEnumerable GetChannelKeys()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected internal override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// Mock implementation of
+ ///
+ private sealed class MockAgentThread : AgentThread
+ {
+ protected override Task CreateInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult("mock_thread_id");
+ }
+
+ protected override Task DeleteInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs
index a9f3a79874ef..97cb426c307d 100644
--- a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs
+++ b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs
@@ -494,6 +494,12 @@ public static string ProcessFunctionResult(object functionResult)
return chatMessageContent.ToString();
}
+ // Same optimization but for a enumerable of ChatMessageContent
+ if (functionResult is IEnumerable chatMessageContents)
+ {
+ return string.Join(",", chatMessageContents.Select(c => c.ToString()));
+ }
+
return JsonSerializer.Serialize(functionResult, s_functionResultSerializerOptions);
}