Building AI Agents with .NET 9 and Semantic Kernel Agents

By · · AI Engineering

AI agent tooling has matured, and .NET developers now have first-class support for building agentic applications through Semantic Kernel's Agents framework. With .NET 9 (released November 2024) bringing performance improvements and new language features, the combination is a solid platform for building AI agents entirely in C#.

We will go through Semantic Kernel's agent abstractions, build individual agents with tools, and orchestrate multi-agent conversations using AgentGroupChat.

The Semantic Kernel agents framework

Semantic Kernel provides two primary agent types:

Both share the same base abstractions, so you can mix them in multi-agent scenarios.

Setting up a .NET 9 project

dotnet new console -n AgentDemo -f net9.0
cd AgentDemo
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Agents.Core
dotnet add package Microsoft.SemanticKernel.Agents.OpenAI
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI

Building a ChatCompletionAgent instance

Let us start with a simple agent that can answer questions and use tools:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "gpt-4o",
    apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!
);
builder.Plugins.AddFromType<TimePlugin>();
builder.Plugins.AddFromType<WeatherPlugin>();

var kernel = builder.Build();

var agent = new ChatCompletionAgent
{
    Name = "Assistant",
    Instructions = """
        You are a helpful assistant that can answer questions,
        check the current time, and look up weather information.
        Always be concise and accurate. When using tools, explain
        what information you found.
        """,
    Kernel = kernel,
    Arguments = new KernelArguments(
        new OpenAIPromptExecutionSettings
        {
            ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
            Temperature = 0.7,
        }
    ),
};

The agent is configured with plugins that expose functions the LLM can call. Let us define those plugins:

public class TimePlugin
{
    [KernelFunction("get_current_time")]
    [Description("Gets the current date and time in the specified timezone")]
    public string GetCurrentTime(
        [Description("The timezone, e.g., 'UTC', 'US/Eastern'")] string timezone = "UTC")
    {
        try
        {
            var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
            var time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
            return 
quot;Current time in {timezone}: {time:yyyy-MM-dd HH:mm:ss}"; } catch { return
quot;Current UTC time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"; } } } public class WeatherPlugin { private readonly HttpClient _httpClient = new(); [KernelFunction("get_weather")] [Description("Gets the current weather for a specified city")] public async Task<string> GetWeatherAsync( [Description("The city name")] string city) { // Using a free weather API for demonstration try { var response = await _httpClient.GetStringAsync(
quot;https://wttr.in/{city}?format=%C+%t+%h"); return
quot;Weather in {city}: {response}"; } catch { return
quot;Unable to fetch weather for {city}."; } } }

Invoking the agent

Agents operate on a chat history, processing messages and generating responses:

var history = new ChatHistory();

// Single turn
history.AddUserMessage("What's the weather like in London?");

await foreach (var message in agent.InvokeAsync(history))
{
    Console.WriteLine(
quot;[{message.AuthorName}]: {message.Content}"); history.Add(message); } // Continue the conversation history.AddUserMessage("What time is it there?"); await foreach (var message in agent.InvokeAsync(history)) { Console.WriteLine(
quot;[{message.AuthorName}]: {message.Content}"); history.Add(message); }

The InvokeAsync method returns an IAsyncEnumerable, which means you get streaming responses. The agent automatically handles tool calls, executing the appropriate plugin functions and feeding results back to the LLM.

OpenAIAssistantAgent

For scenarios requiring persistent threads, file handling, or code execution, the OpenAI Assistants API provides managed infrastructure:

using Microsoft.SemanticKernel.Agents.OpenAI;
using OpenAI.Files;

var openAIClient = OpenAIAssistantAgent.CreateOpenAIClient(
    apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!
);

var assistantAgent = await OpenAIAssistantAgent.CreateAsync(
    clientProvider: OpenAIClientProvider.FromClient(openAIClient),
    definition: new OpenAIAssistantDefinition("gpt-4o")
    {
        Name = "DataAnalyst",
        Instructions = """
            You are a data analyst. When given data, analyze it
            thoroughly and provide insights. Use the code interpreter
            to create visualizations when appropriate.
            """,
        EnableCodeInterpreter = true,
    },
    kernel: new Kernel(),
);

The EnableCodeInterpreter flag gives the agent the ability to write and execute Python code on OpenAI's infrastructure. This is powerful for data analysis tasks where the agent can generate charts, run statistical tests, and process uploaded files.

Multi-agent collaboration with AgentGroupChat

Where Semantic Kernel Agents gets interesting is orchestrating multiple agents in a conversation. AgentGroupChat manages turn-taking and termination:

using Microsoft.SemanticKernel.Agents.Chat;

// Define specialized agents
var researcher = new ChatCompletionAgent
{
    Name = "Researcher",
    Instructions = """
        You are a research specialist. Investigate the given topic
        and provide detailed, factual findings. Focus on technical
        accuracy and cite specific details. When your research is
        complete, summarize your key findings clearly.
        """,
    Kernel = kernel,
    Arguments = new KernelArguments(
        new OpenAIPromptExecutionSettings
        {
            ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
        }
    ),
};

var writer = new ChatCompletionAgent
{
    Name = "Writer",
    Instructions = """
        You are a technical writer. Take research findings and
        transform them into clear, engaging content. Structure
        your writing with headings and include practical examples.
        """,
    Kernel = kernel,
};

var reviewer = new ChatCompletionAgent
{
    Name = "Reviewer",
    Instructions = """
        You are a content reviewer. Evaluate the writer's output for
        accuracy, clarity, and completeness. Provide specific feedback.
        When the content is ready for publication, say APPROVED.
        """,
    Kernel = kernel,
};

Now set up the group chat with termination and selection strategies:

var terminationStrategy = new ApprovalTerminationStrategy
{
    Agents = [reviewer],
    MaximumIterations = 10,
};

var selectionStrategy = new SequentialSelectionStrategy();

var groupChat = new AgentGroupChat(researcher, writer, reviewer)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = terminationStrategy,
        SelectionStrategy = selectionStrategy,
    },
};

groupChat.AddChatMessage(new ChatMessageContent(
    AuthorRole.User,
    "Create a technical overview of WebAssembly's role in server-side applications."
));

await foreach (var message in groupChat.InvokeAsync())
{
    Console.WriteLine(
quot;\n[{message.AuthorName}]:"); Console.WriteLine(message.Content); }

Custom termination strategies

The built-in termination strategies are useful, but you often need custom logic:

public class ApprovalTerminationStrategy : TerminationStrategy
{
    protected override Task<bool> ShouldAgentTerminateAsync(
        Agent agent,
        IReadOnlyList<ChatMessageContent> history,
        CancellationToken cancellationToken = default)
    {
        var lastMessage = history.LastOrDefault()?.Content ?? "";
        var isApproved = lastMessage.Contains(
            "APPROVED", StringComparison.OrdinalIgnoreCase);

        return Task.FromResult(isApproved);
    }
}

public class QualityGateTerminationStrategy : TerminationStrategy
{
    public int MinWordCount { get; set; } = 500;
    public required string[] RequiredSections { get; set; }

    protected override Task<bool> ShouldAgentTerminateAsync(
        Agent agent,
        IReadOnlyList<ChatMessageContent> history,
        CancellationToken cancellationToken = default)
    {
        var lastMessage = history.LastOrDefault()?.Content ?? "";

        var hasMinWords = lastMessage.Split(' ').Length >= MinWordCount;
        var hasAllSections = RequiredSections.All(section =>
            lastMessage.Contains(section, StringComparison.OrdinalIgnoreCase));

        return Task.FromResult(hasMinWords && hasAllSections);
    }
}

These strategies give you fine-grained control over when the multi-agent conversation should stop. You can gate on content quality, specific keywords, or any custom logic.

Building a practical agent service

Let us put it all together into an ASP.NET Core minimal API:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<Kernel>(sp =>
{
    var kernelBuilder = Kernel.CreateBuilder();
    kernelBuilder.AddOpenAIChatCompletion(
        modelId: "gpt-4o",
        apiKey: builder.Configuration["OpenAI:ApiKey"]!
    );
    kernelBuilder.Plugins.AddFromType<TimePlugin>();
    kernelBuilder.Plugins.AddFromType<WeatherPlugin>();
    return kernelBuilder.Build();
});

var app = builder.Build();

app.MapPost("/api/agent/chat", async (
    ChatRequest request,
    Kernel kernel) =>
{
    var agent = new ChatCompletionAgent
    {
        Name = "Assistant",
        Instructions = "You are a helpful AI assistant with tool access.",
        Kernel = kernel,
        Arguments = new KernelArguments(
            new OpenAIPromptExecutionSettings
            {
                ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
            }
        ),
    };

    var history = new ChatHistory();
    history.AddUserMessage(request.Message);

    var responses = new List<string>();
    await foreach (var message in agent.InvokeAsync(history))
    {
        if (!string.IsNullOrEmpty(message.Content))
        {
            responses.Add(message.Content);
        }
    }

    return Results.Ok(new { response = string.Join("\n", responses) });
});

app.MapPost("/api/agent/research", async (
    ResearchRequest request,
    Kernel kernel) =>
{
    var researcher = new ChatCompletionAgent
    {
        Name = "Researcher",
        Instructions = "Research the topic thoroughly and provide findings.",
        Kernel = kernel,
    };

    var writer = new ChatCompletionAgent
    {
        Name = "Writer",
        Instructions = "Write a clear article based on research findings.",
        Kernel = kernel,
    };

    var groupChat = new AgentGroupChat(researcher, writer)
    {
        ExecutionSettings = new AgentGroupChatSettings
        {
            TerminationStrategy = new ApprovalTerminationStrategy
            {
                MaximumIterations = 6,
            },
            SelectionStrategy = new SequentialSelectionStrategy(),
        },
    };

    groupChat.AddChatMessage(new ChatMessageContent(
        AuthorRole.User,
        
quot;Research and write about: {request.Topic}" )); var messages = new List<object>(); await foreach (var message in groupChat.InvokeAsync()) { messages.Add(new { agent = message.AuthorName, content = message.Content }); } return Results.Ok(new { messages }); }); app.Run(); record ChatRequest(string Message); record ResearchRequest(string Topic);

Key takeaways

Semantic Kernel's Agents framework brings structured, type-safe agent development to .NET. The ChatCompletionAgent gives you full local control, while OpenAIAssistantAgent offloads state management and code execution to OpenAI's infrastructure. AgentGroupChat provides flexible multi-agent orchestration with customizable termination and selection strategies.

With .NET 9's performance improvements and C#'s strong type system, you get compile-time safety, excellent IDE support, and production-ready patterns for building AI agents. If your organization runs on .NET, there is no need to reach for Python to build agentic applications. The Semantic Kernel ecosystem has you covered.