Semantic Kernel: .NET Multi-Agent Systems in C#
Source: mortalapps.com- Semantic Kernel's Agent Framework enables .NET developers to orchestrate specialized, stateful AI agents natively in C#.
- Eliminates the architectural complexity of bridging Python-centric agent frameworks into enterprise .NET and ASP.NET Core environments.
- Provides native integration with Azure SDKs, OpenTelemetry, and Entra ID for secure, passwordless authentication.
- Delivers a production-ready multi-agent pipeline using type-safe selection and termination strategies.
Why This Matters
Enterprise engineering teams building on Microsoft Azure often face a dilemma: adopt Python-centric frameworks like LangGraph, or build custom orchestration from scratch. Implementing a semantic kernel c# multi agent system solves this by providing a native, enterprise-grade framework built directly on .NET. This approach was invented to bridge the gap between advanced LLM orchestration patterns and the robust, type-safe ecosystem of C# and ASP.NET Core.
If you ignore this native integration and attempt to bridge Python microservices into a .NET enterprise architecture, you introduce significant production risks. These include complex cross-language serialization overhead, fragmented logging that violates standard OpenTelemetry patterns, and security vulnerabilities arising from managing separate authentication mechanisms. By using Semantic Kernel, you can leverage Microsoft.Extensions.DependencyInjection, native logging (ILogger), and Azure SDK standards like DefaultAzureCredential for passwordless Entra ID authentication.
Choose Semantic Kernel's multi-agent framework when building complex, multi-step workflows within an existing .NET ecosystem, especially in highly regulated industries (such as finance or healthcare) where Azure compliance, strict RBAC, and static typing are non-negotiable. Avoid it only if your team is entirely standardized on Python or if you require highly experimental, research-grade agent architectures that have not yet been ported to the .NET SDK.
Core Concepts
To build robust multi-agent systems using Semantic Kernel in .NET, you must understand its core architectural abstractions. These components map directly to standard enterprise design patterns.
- Kernel: The central hub of Semantic Kernel. It manages the lifecycle of registered AI services, native plugins, prompt templates, and dependency injection configurations.
- KernelAgent: The base abstract class for all agents. It encapsulates a specific system persona, instruction set, and execution settings.
- ChatCompletionAgent: A concrete implementation of
KernelAgentdesigned for standard chat models. It processes conversation history and generates responses using registered plugins. - AgentGroupChat: The orchestration boundary where multiple agents interact. It maintains the shared conversation state and coordinates turn-taking.
- KernelPlugin: A collection of native C# methods decorated with
[KernelFunction]. These are exposed to agents as tools for auto-function calling. - SelectionStrategy: A deterministic or LLM-driven rule that determines which agent in the
AgentGroupChatshould take the next turn. - TerminationStrategy: A rule engine that evaluates the conversation history to decide when the multi-agent loop must stop executing.
| Component | Primary Responsibility | Lifetime Scope |
|---|---|---|
Kernel |
Service resolution and plugin management | Transient / Scoped |
ChatCompletionAgent |
Persona execution and tool invocation | Singleton / Scoped |
AgentGroupChat |
Thread state and turn coordination | Transient (Per-request) |
SelectionStrategy |
Routing execution flow | Singleton |
How It Works
The lifecycle of a Semantic Kernel multi-agent execution pipeline follows a structured, deterministic flow from initialization to termination.
Phase 1: Dependency Injection and Initialization
Execution begins when the host application (such as an ASP.NET Core Web API) resolves the Kernel and agent dependencies. The Kernel is configured with an Azure OpenAI chat completion service. Authentication is established using DefaultAzureCredential, which retrieves an Entra ID token at runtime, eliminating the need for static API keys. Native C# plugins are registered directly into the dependency injection container and injected into the Kernel instance.
Phase 2: Agent and Chat Construction
The application instantiates specialized ChatCompletionAgent objects. Each agent is assigned a unique system prompt defining its role, along with a reference to the shared Kernel containing the registered plugins. These agents are then registered inside an AgentGroupChat instance. The AgentGroupChat is configured with a custom SelectionStrategy and a TerminationStrategy to govern the execution loop.
Phase 3: The Execution Loop
When a user submits a request, the AgentGroupChat processes the input through the following loop:
- Selection: The
SelectionStrategyevaluates the currentChatHistory. It selects the appropriate agent based on predefined rules (e.g., a round-robin sequence or an LLM-based router). - Execution: The selected agent receives the chat history. If the agent requires external data, it invokes the registered
KernelPluginfunctions using automatic function calling. - Response Generation: The agent appends its final response to the shared
ChatHistory. - Termination Evaluation: The
TerminationStrategyinspects the latest message. If a termination condition is met (such as a specific keyword or maximum turn limit), the loop exits. Otherwise, the loop returns to step 1.
Phase 4: Failure Handling
If an agent or tool execution fails, Semantic Kernel triggers standard .NET exception handling. If an LLM call fails due to rate limits, the underlying HttpClient pipeline executes configured Polly retry policies. If a tool throws an exception, the error message is fed back into the agent's context, allowing it to attempt self-correction in the next turn.
Architecture
The architecture of a .NET multi-agent system built with Semantic Kernel centers on the AgentGroupChat as the runtime orchestrator. At the base layer, the .NET Host manages the lifecycle of the application and registers the Kernel and TokenCredential within the standard dependency injection container.
The Kernel acts as the execution engine, holding references to the Azure OpenAI chat completion service and the registered KernelPlugins. Above the Kernel layer sit the specialized ChatCompletionAgent instances. Each agent is a logical boundary wrapping a specific system prompt, a reference to the shared Kernel, and its own execution settings.
When execution begins, the client sends a request to the ASP.NET Core controller. The controller pushes this message into the AgentGroupChat. The AgentGroupChat contains a shared ChatHistory state. The flow of execution is governed by two key components: the SelectionStrategy and the TerminationStrategy.
The SelectionStrategy reads the ChatHistory and determines which agent should execute the next turn. The chosen agent processes the history, executes any required tool calls against the native C# plugins, and appends its response back to the ChatHistory. After each turn, the TerminationStrategy inspects the latest message. If the termination condition is met, execution halts, and the final state is returned through the controller to the client.
Native Plugin Architecture and Dependency Injection
In enterprise .NET development, maintaining clean separation of concerns is paramount. Semantic Kernel achieves this by allowing you to write native C# classes as plugins. These classes do not require any dependency on the Semantic Kernel SDK other than the Microsoft.SemanticKernel.Abstractions package. You decorate your methods with [KernelFunction] and [Description] attributes. The description is critical; it is the metadata the LLM uses to perform semantic routing and function calling.
To integrate these plugins into your application's lifecycle, you should register them with the .NET dependency injection (DI) container. This allows your plugins to resolve their own dependencies, such as database contexts, HTTP clients, or configuration options, using standard constructor injection. When building the Kernel, you retrieve these plugins from the service provider and add them to the kernel's plugin collection. This ensures that your AI tools respect the same lifetime scopes (e.g., Scoped, Transient) as the rest of your enterprise application.
Entra ID and Passwordless Azure OpenAI Integration
Hardcoded API keys are a primary source of security vulnerabilities in cloud deployments. In Azure compliance environments, you must use passwordless authentication via Microsoft Entra ID. Semantic Kernel supports this natively by accepting a TokenCredential from the Azure.Identity library when configuring the Azure OpenAI service.
By utilizing DefaultAzureCredential, your application automatically selects the best authentication method based on its environment. Locally, it can use your Azure CLI or Visual Studio credentials. In production, it seamlessly transitions to using a User-Assigned Managed Identity associated with your Azure App Service, Azure Container Apps, or Azure Kubernetes Service (AKS) cluster. This approach ensures that your agentic system adheres to the principle of least privilege, as you can assign fine-grained Azure Cognitive Services User roles to the managed identity without managing secrets.
Multi-Agent Orchestration with AgentGroupChat
Orchestrating multiple agents requires a robust mechanism to manage turn-taking and state. Semantic Kernel's AgentGroupChat provides a structured environment for this. The orchestration is governed by two abstract classes: SelectionStrategy and TerminationStrategy.
A custom SelectionStrategy allows you to define the routing logic. While you can use an LLM to decide the next speaker, a deterministic C# implementation is often preferred in enterprise scenarios to reduce latency and cost. For example, you can write a strategy that inspects the last message for specific metadata or routes the conversation based on a predefined state machine.
The TerminationStrategy prevents infinite loops, which are a common failure mode in multi-agent systems. Your custom termination strategy can evaluate the content of the latest message (e.g., looking for a "[COMPLETE]" token), check the total number of turns, or inspect the metadata of the execution context to decide when to halt the chat and return the results to the caller.
State Management and Thread Persistence
In production, agent conversations are rarely short-lived or single-turn. You must persist the state of the AgentGroupChat across asynchronous requests. Because AgentGroupChat exposes its history as a collection of ChatMessageContent objects, you can serialize and store this state in an enterprise database like Azure Cosmos DB, SQL Server, or Redis.
When a user resumes a conversation, you retrieve the serialized chat history from your database, instantiate a new AgentGroupChat with the required agents, and pre-populate the chat history using the AddChatMessages method. This stateless API design allows your application to scale horizontally across multiple container instances, as the execution state is not tied to the memory of a single server.
OpenTelemetry and Enterprise Observability
Semantic Kernel is instrumented using .NET's native System.Diagnostics.DiagnosticSource activity API, which integrates seamlessly with OpenTelemetry. This allows you to capture detailed traces of the entire multi-agent execution pipeline without polluting your business logic with logging statements.
By configuring an OpenTelemetry exporter (such as Azure Monitor Application Insights or Jaeger), you can track the latency of individual LLM calls, monitor the execution time of native C# plugins, and trace the flow of messages between different agents. These traces capture critical metadata, including token usage, model names, and function call parameters, enabling precise cost attribution and performance bottleneck analysis in production.
Code Example
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
namespace SemanticKernelAgentsDemo
{
// 1. Define a native C# plugin with proper descriptions for the LLM
public class OrderValidationPlugin
{
[KernelFunction, Description("Validates if an order ID exists and is in a pending state.")]
public async Task<string> ValidateOrderAsync([Description("The unique order identifier.")] string orderId)
{
// Simulate database lookup. In production, inject your DbContext here.
await Task.Delay(100);
if (orderId.StartsWith("ORD-99"))
{
return "VALID: Order is pending and eligible for processing.";
}
return "INVALID: Order not found or already processed.";
}
}
// 2. Custom Termination Strategy to stop execution when a specific token is detected
public class TokenTerminationStrategy : TerminationStrategy
{
private readonly string _terminationToken;
public TokenTerminationStrategy(string terminationToken, int maxTurns)
{
_terminationToken = terminationToken;
AutomaticReset = true;
MaxIterations = maxTurns;
}
protected override Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> history)
{
if (history.Count == 0) return Task.FromResult(false);
string lastMessage = history[history.Count - 1].Content ?? string.Empty;
bool shouldTerminate = lastMessage.Contains(_terminationToken, StringComparison.OrdinalIgnoreCase);
return Task.FromResult(shouldTerminate);
}
}
class Program
{
static async Task Main(string[] args)
{
// Retrieve configuration from environment variables
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? "gpt-4o";
// Set up dependency injection and logging
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
services.AddTransient<OrderValidationPlugin>();
var serviceProvider = services.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
// Initialize the Kernel with Azure OpenAI and DefaultAzureCredential (Entra ID)
var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
credential: new DefaultAzureCredential()
);
kernelBuilder.Services.AddSingleton(loggerFactory);
var kernel = kernelBuilder.Build();
// Register the native plugin
kernel.Plugins.AddFromType<OrderValidationPlugin>();
// Define Agent 1: The Validator
var validatorAgent = new ChatCompletionAgent
{
Name = "ValidatorAgent",
Instructions = "You are an order validation assistant. Use the OrderValidationPlugin to verify the status of the order. If the order is invalid, state the reason and output [TERMINATE]. If valid, pass the validation details to the processor.",
Kernel = kernel
};
// Define Agent 2: The Processor
var processorAgent = new ChatCompletionAgent
{
Name = "ProcessorAgent",
Instructions = "You are a processing assistant. Once the ValidatorAgent confirms the order is valid, generate a processing confirmation message and append [TERMINATE] to your response.",
Kernel = kernel
};
// Configure the Group Chat with a custom termination strategy
var terminationStrategy = new TokenTerminationStrategy("[TERMINATE]", maxTurns: 5);
var chat = new AgentGroupChat(validatorAgent, processorAgent)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = terminationStrategy
}
};
// Add user input to start the conversation
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "Please process order ORD-99123."));
Console.WriteLine("Starting multi-agent execution loop...");
try
{
// Execute the chat loop and stream responses
await foreach (var message in chat.InvokeAsync())
{
Console.WriteLine($"
[{message.AuthorName ?? "System"}]: {message.Content}");
}
Console.WriteLine("
Execution completed successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"
Execution failed: {ex.Message}");
}
}
}
}
Starting multi-agent execution loop... [ValidatorAgent]: I will validate the order ORD-99123. [ValidatorAgent]: VALID: Order is pending and eligible for processing. [ProcessorAgent]: The order ORD-99123 has been successfully validated. I have initiated the processing pipeline. A confirmation email has been queued. [TERMINATE] Execution completed successfully.