← AI Agents Frameworks & Ecosystem
🤖 AI Agents

Semantic Kernel: .NET Multi-Agent Systems in C#

Source: mortalapps.com
TL;DR
  • 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 KernelAgent designed 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 AgentGroupChat should 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:

  1. Selection: The SelectionStrategy evaluates the current ChatHistory. It selects the appropriate agent based on predefined rules (e.g., a round-robin sequence or an LLM-based router).
  2. Execution: The selected agent receives the chat history. If the agent requires external data, it invokes the registered KernelPlugin functions using automatic function calling.
  3. Response Generation: The agent appends its final response to the shared ChatHistory.
  4. Termination Evaluation: The TerminationStrategy inspects 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

A complete, production-ready C# console application demonstrating a multi-agent system with native plugins, Entra ID authentication, and custom selection/termination strategies.
Csharp
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}");
            }
        }
    }
}
Expected Output
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.

Key Takeaways

Semantic Kernel provides a first-class, type-safe multi-agent framework natively in .NET, eliminating the need for Python bridges.
Using native C# plugins with dependency injection allows agents to safely access enterprise databases and APIs.
Passwordless authentication via Entra ID and DefaultAzureCredential is the standard for securing agentic systems in Azure.
Custom Selection and Termination strategies are essential to maintain deterministic control and prevent infinite agent loops.
Native OpenTelemetry integration enables enterprise-grade tracing, cost attribution, and performance monitoring.
Stateless API design allows agent conversation history to be persisted in external databases, enabling horizontal scaling.

Frequently Asked Questions

What is the difference between Semantic Kernel and LangChain/LangGraph? +
Semantic Kernel is built natively for .NET and C#, integrating directly with Microsoft's dependency injection and Azure SDKs. LangChain and LangGraph are primarily Python-centric, requiring complex inter-process communication or microservices to integrate into a .NET enterprise stack.
Can I use Semantic Kernel with non-Azure OpenAI models? +
Yes. Semantic Kernel supports standard OpenAI, Hugging Face, and local models (via Ollama or llama.cpp) by implementing the standard `IChatCompletionService` interface.
How do I prevent multi-agent systems from looping infinitely? +
You must implement a custom `TerminationStrategy` with a strict `MaxIterations` limit and define clear termination tokens (e.g., '[COMPLETE]') that agents output when their task is finished.
How does Semantic Kernel handle dependency injection for plugins? +
You can register your native plugin classes in the standard .NET `IServiceCollection`. When building the `Kernel`, you resolve these plugins from the `IServiceProvider`, allowing them to use constructor injection for their own dependencies.
Is the Semantic Kernel Agent Framework production-ready? +
Yes. The `Microsoft.SemanticKernel.Agents` package is designed for enterprise production use, providing stable abstractions for stateful agents and multi-agent group chats.
How do I persist agent conversation state across web requests? +
You serialize the `ChatMessageContent` collection from the `AgentGroupChat` and store it in a database like Cosmos DB or Redis. On subsequent requests, you retrieve the history and reload it into a new chat instance.
Can I run Semantic Kernel agents locally without Azure? +
Yes. You can run agents locally by configuring Semantic Kernel to use local models via Ollama and authenticating local development tools using your personal Azure CLI credentials.
What happens if a native C# plugin throws an exception? +
The exception is caught by the Semantic Kernel execution pipeline. You can configure the agent to receive the error message as tool output, allowing it to attempt self-correction, or let it propagate up to your application's global exception handler.
How do I monitor the token usage of individual agents in a group chat? +
You can intercept token usage metrics using OpenTelemetry. Semantic Kernel publishes detailed activities that contain token counts for prompt and completion phases, allowing you to attribute costs to specific agents.
Should I use an LLM or C# code to select the next agent in a chat? +
For enterprise workflows, a deterministic C# `SelectionStrategy` is preferred because it reduces latency, eliminates routing errors, and avoids the token costs associated with using an LLM router.