Skip to main content

AI Agents in C#: Build Autonomous Agents with Semantic Kernel

Learn how to build AI agents in C# with Semantic Kernel. Step-by-step .NET tutorial with runnable code, plugins, tool calling and best practices. Start building today.

Building AI agents in C# used to mean gluing together HTTP calls, JSON schemas and a lot of hopeful string parsing. That era is over. With Microsoft Semantic Kernel, .NET developers now have a first-class agent framework that handles tool calling, memory, planning and multi-agent collaboration — all with strongly typed C# code, dependency injection and the async patterns you already know. In this tutorial you'll learn how to build autonomous, task-solving AI agents in C# from scratch, understand why each piece exists, and avoid the pitfalls that burn most teams on their first production deployment.

What Is an AI Agent (and How Is It Different From a Chatbot)?

A chatbot answers. An agent acts. The practical difference comes down to one loop:

  • Goal — the agent receives a task, not just a message ("reconcile yesterday's failed payments").
  • Reason — the LLM decides what information or action it needs next.
  • Act — it calls a tool: your C# method, a REST API, a database query.
  • Observe — the tool result is fed back into the conversation.
  • Repeat — until the goal is met or a stop condition fires.

That loop is what makes an agent autonomous. Semantic Kernel implements it for you through automatic function calling, so you write the tools and the framework runs the cycle.

Why Semantic Kernel for .NET Developers?

You could call the OpenAI or Anthropic REST API directly. Most teams start there and regret it around week three. Semantic Kernel gives you:

  • Model portability — swap Azure OpenAI, OpenAI, Ollama or ONNX behind one Kernel abstraction.
  • Automatic tool calling — decorate a C# method, and the SDK generates the JSON schema, handles the model's call request, invokes your method, and returns the result.
  • DI and telemetry — it's built on Microsoft.Extensions.*, so logging, IHttpClientFactory, OpenTelemetry and configuration all work normally.
  • Agent + orchestration primitives — agents, threads, and multi-agent patterns without writing your own state machine.

Installing the Packages

// .NET 8 or later
dotnet new console -n AgentDemo
cd AgentDemo

dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Agents.Core
dotnet add package Microsoft.Extensions.Configuration.UserSecrets

Never hardcode API keys. Use user secrets locally and Key Vault or managed identity in production:

dotnet user-secrets init
dotnet user-secrets set "OpenAI:ApiKey" "sk-..."

Your First AI Agent in C#

Start with the smallest thing that works — a ChatCompletionAgent with a name, instructions and a kernel.

using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;

var config = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .AddEnvironmentVariables()
    .Build();

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "gpt-4o-mini",
    apiKey: config["OpenAI:ApiKey"]!);

Kernel kernel = builder.Build();

ChatCompletionAgent agent = new()
{
    Name = "DevOpsAssistant",
    Instructions =
        """
        You are a senior .NET DevOps engineer.
        Diagnose build and deployment issues precisely.
        If you lack data, call a tool rather than guessing.
        Answer in under 200 words.
        """,
    Kernel = kernel
};

AgentThread thread = new ChatHistoryAgentThread();

await foreach (var response in agent.InvokeAsync(
    "Our nightly build started failing. Where do I begin?", thread))
{
    Console.WriteLine(response.Message.Content);
}

Two concepts matter here. Instructions are the agent's persona and policy — they are sent as the system message on every turn, so keep them short and imperative. AgentThread is the conversation state. Create one per user session; do not share a thread across users, or you will leak one customer's context into another's answer.

Making the Agent Autonomous: Plugins and Tool Calling

An agent without tools is just an expensive text generator. In Semantic Kernel, a plugin is a plain C# class whose methods are marked with [KernelFunction]. The [Description] attributes are not documentation — they are the prompt the model reads to decide when to call your code. Write them as if explaining to a new hire.

using System.ComponentModel;
using Microsoft.SemanticKernel;

public sealed class BuildPlugin(IBuildService builds)
{
    [KernelFunction("get_recent_builds")]
    [Description("Gets the most recent CI builds for a repository, newest first.")]
    public async Task<IReadOnlyList<BuildSummary>> GetRecentBuildsAsync(
        [Description("Repository name, e.g. 'payments-api'")] string repository,
        [Description("How many builds to return, 1-20")] int count = 5,
        CancellationToken ct = default)
    {
        if (count is < 1 or > 20)
            throw new ArgumentOutOfRangeException(nameof(count));

        return await builds.GetRecentAsync(repository, count, ct);
    }

    [KernelFunction("get_build_log")]
    [Description("Returns the last N lines of the log for a specific build id.")]
    public async Task<string> GetBuildLogAsync(
        [Description("The build identifier")] string buildId,
        [Description("Number of trailing log lines, max 300")] int tailLines = 100,
        CancellationToken ct = default)
    {
        var log = await builds.GetLogAsync(buildId, ct);
        return string.Join('\n', log.TakeLast(Math.Min(tailLines, 300)));
    }
}

public record BuildSummary(string Id, string Branch, string Status, DateTimeOffset FinishedAt);

Register the plugin and switch on automatic function calling:

kernel.Plugins.AddFromObject(new BuildPlugin(buildService), "Builds");

ChatCompletionAgent agent = new()
{
    Name = "BuildDoctor",
    Instructions = "Diagnose CI failures. Always inspect real build data before concluding.",
    Kernel = kernel,
    Arguments = new KernelArguments(
        new OpenAIPromptExecutionSettings
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
            Temperature = 0.1,
            MaxTokens = 800
        })
};

AgentThread thread = new ChatHistoryAgentThread();

await foreach (var item in agent.InvokeAsync(
    "Why is payments-api failing on main?", thread))
{
    Console.WriteLine(item.Message.Content);
}

That single line — FunctionChoiceBehavior.Auto() — is what turns a chat model into an autonomous AI agent in C#. Semantic Kernel now runs the reason–act–observe loop: the model requests get_recent_builds, SK invokes your method, appends the JSON result to the thread, and calls the model again. It repeats until the model produces a final answer.

Controlling the Loop

Autonomy without limits is a billing incident. Constrain it explicitly:

var settings = new OpenAIPromptExecutionSettings
{
    // Cap the automatic invocation rounds
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
        options: new FunctionChoiceBehaviorOptions { AllowParallelCalls = true }),
    Temperature = 0.1
};

// Bound the whole operation
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));

await foreach (var item in agent.InvokeAsync(task, thread, cancellationToken: cts.Token))
{
    Console.WriteLine(item.Message.Content);
}

You can also restrict which functions the agent may use on a given turn by passing an explicit function list to FunctionChoiceBehavior.Auto(functions) — useful when a read-only agent must never touch a write tool.

Streaming Responses in ASP.NET Core

Users will not wait 20 seconds staring at a spinner. Stream the output:

app.MapPost("/agent/ask", async (
    AskRequest request,
    ChatCompletionAgent agent,
    IThreadStore threads,
    HttpContext http,
    CancellationToken ct) =>
{
    http.Response.ContentType = "text/event-stream";
    AgentThread thread = await threads.GetOrCreateAsync(request.SessionId, ct);

    await foreach (var chunk in agent.InvokeStreamingAsync(request.Message, thread, cancellationToken: ct))
    {
        if (string.IsNullOrEmpty(chunk.Message.Content)) continue;
        await http.Response.WriteAsync($"data: {chunk.Message.Content}\n\n", ct);
        await http.Response.Body.FlushAsync(ct);
    }
});

Note that ct flows all the way into your plugin methods. If the browser disconnects, the SQL query your tool started gets cancelled too. This is the kind of plumbing you get for free in .NET and would have to build by hand elsewhere.

Multi-Agent Orchestration: Specialists Beat Generalists

One agent with thirty tools performs worse than three agents with ten each. The model's tool-selection accuracy degrades as the schema list grows. Split by responsibility:

ChatCompletionAgent researcher = new()
{
    Name = "Researcher",
    Instructions = "Gather facts using tools. Never propose fixes. Output bullet points only.",
    Kernel = researchKernel
};

ChatCompletionAgent reviewer = new()
{
    Name = "Reviewer",
    Instructions =
        """
        Review the researcher's findings and produce a remediation plan.
        If evidence is missing, reply exactly: NEEDS_MORE_DATA.
        Otherwise end your message with: APPROVED.
        """,
    Kernel = reviewKernel
};

// Simple, explicit orchestration — easy to test and debug
AgentThread shared = new ChatHistoryAgentThread();
string task = "Diagnose the payments-api build failure on main.";

for (int round = 0; round < 3; round++)
{
    await foreach (var r in researcher.InvokeAsync(task, shared)) { }

    string verdict = "";
    await foreach (var r in reviewer.InvokeAsync("Review the findings.", shared))
        verdict = r.Message.Content ?? "";

    if (verdict.Contains("APPROVED", StringComparison.Ordinal))
    {
        Console.WriteLine(verdict);
        break;
    }

    task = "Collect the additional evidence the reviewer requested.";
}

Semantic Kernel also ships higher-level orchestration patterns (sequential, concurrent, handoff and group chat) in Microsoft.SemanticKernel.Agents.Orchestration. Start with the explicit loop above — it is trivially unit-testable — and graduate to the built-in orchestrators when your coordination logic genuinely gets complex.

Best Practices for Production AI Agents in C#

1. Treat tool descriptions as production code

90% of "the agent picked the wrong tool" bugs are description bugs. Be specific about units, formats and boundaries: "Returns balance in cents, not dollars" beats "gets balance".

2. Make every tool idempotent or explicitly guarded

The model can and will retry. A send_invoice tool must accept an idempotency key. For genuinely irreversible actions, require human approval rather than trusting the loop:

kernel.FunctionInvocationFilters.Add(new ApprovalFilter());

public sealed class ApprovalFilter : IFunctionInvocationFilter
{
    private static readonly HashSet<string> Sensitive = ["refund_payment", "delete_resource"];

    public async Task OnFunctionInvocationAsync(
        FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
    {
        if (Sensitive.Contains(context.Function.Name) && !await Approvals.ConfirmAsync(context))
        {
            context.Result = new FunctionResult(context.Function, "Denied by operator.");
            return; // short-circuit: never call next()
        }

        await next(context);
    }
}

3. Set Temperature low for tool-using agents

Creativity helps with prose and hurts with argument selection. Use 0.0–0.2 for agents that call functions.

4. Manage context window growth

Every tool result is appended to the thread. A log-reading agent will blow past the context window in a dozen turns. Truncate tool output at the source (note the tailLines cap above), and use ChatHistorySummarizationReducer to compress older turns.

5. Instrument everything

Enable OpenTelemetry and log every function invocation with arguments, duration and token cost. When an agent misbehaves in production, the trace is the only way to find out why.

6. Test agents like distributed systems

Unit-test plugins as ordinary C# classes — no LLM involved. Then write a small suite of evaluation cases that run against a real model and assert on which tools were called, not on exact wording. LLM output is non-deterministic; tool traces are far more stable.

Common Pitfalls to Avoid

  • Unbounded loops. Always set a cancellation token and a max-iteration guard. A misconfigured agent can burn thousands of tokens in seconds.
  • Leaking secrets into prompts. Tool results become conversation history that goes to the model provider. Redact PII and credentials before returning them.
  • Prompt injection via tool output. If a tool fetches a web page, its content is untrusted input. Never let fetched text be interpreted as instructions — wrap it in clear delimiters and instruct the agent to treat it as data.
  • Sharing one AgentThread globally. Threads are per-conversation state. Register agents as singletons; store threads per session.
  • Blocking on async in tools. .Result inside a [KernelFunction] will deadlock under load, exactly as it does anywhere else in ASP.NET Core.
  • Over-trusting the first model you pick. Benchmark a small model first. Many agent workloads run fine on cheaper models when the tools are well designed.

Conclusion: Key Takeaways

Building AI agents in C# with Semantic Kernel is no longer experimental — it is ordinary .NET engineering with a probabilistic component in the middle. The framework gives you the loop; your job is to give it good tools and firm boundaries.

  • An agent is a goal → reason → act → observe loop; FunctionChoiceBehavior.Auto() is what enables it in Semantic Kernel.
  • Plugins are plain C# classes — [KernelFunction] and precise [Description] attributes do the heavy lifting.
  • AgentThread holds conversation state; scope it per user, never globally.
  • Constrain autonomy with cancellation tokens, low temperature, truncated tool output and invocation filters for sensitive actions.
  • Prefer several focused agents over one agent with a huge tool list — tool-selection accuracy drops as the schema list grows.
  • Test plugins as normal C# code; evaluate agents on tool-call traces rather than exact text.

Start with a single agent and two tools. Get the descriptions right, watch the traces, and add capability only when the current agent is reliably correct. Done that way, AI agents in C# become just another well-behaved service in your .NET solution — testable, observable and safe to ship.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...