Back to Writing Microsoft Agent Framework 1.0 for .NET: The Agentic Runtime .NET Developers Have Been Waiting For

Microsoft Agent Framework 1.0 for .NET: The Agentic Runtime .NET Developers Have Been Waiting For

I'll be honest: watching Python developers casually spin up multi-agent systems while we .NET folks were wrestling with the AutoGen-vs-Semantic-Kernel decision has been… humbling. AutoGen gave us the multi-agent patterns but felt research-grade. Semantic Kernel gave us enterprise rigor but its agent story was bolted on. Neither was the right answer, and choosing between them felt like picking between a race car with no brakes and a tank with no engine.

That changed in the spring of 2026. Microsoft Agent Framework 1.0 hit General Availability — and with it, the .NET ecosystem finally has what Python shops have had for a while: a unified, opinionated, production-ready runtime for building agentic AI. C# is my home platform. I have been waiting for this.

This article is for all three people in the room when these decisions get made: the product owner asking "what does this buy us?", the architect asking "how does this fit into what we already have?", and the developer asking "show me the code." I built a real example app — a personal expense-tracking AI agent — and I'll walk through the key patterns along the way.

Contents


The Problem: AutoGen vs Semantic Kernel Was a False Dilemma

Before diving into what's new, it's worth understanding what made the old situation untenable.

AutoGen came out of Microsoft Research's AI Frontiers Lab. It was brilliant for multi-agent orchestration — you could spin up a team of specialized LLM personas, have them collaborate on tasks, implement dynamic group chats, and build emergent, event-driven workflows. The problem? It was research-grade. State management was in-memory, error handling was fragile, and production observability was an afterthought.

Semantic Kernel took the opposite approach. It was engineered for the enterprise: type safety, Dependency Injection, prompt templating, vector database connectors, and robust application lifecycle management. The [KernelFunction] attribute pattern meant your existing C# business logic plugged right in. But its multi-agent story felt stitched together — ChatCompletionAgent, OpenAIAssistantAgent, the monolithic Kernel service locator... it was a lot of ceremony for not enough payoff.

The result: teams building real production AI in .NET were stuck either hacking enterprise features onto AutoGen or bolting agentic behavior onto Semantic Kernel. Neither approach scaled well.

Microsoft Agent Framework 1.0 is the strategic consolidation of both lineages. It inherits Semantic Kernel's DI patterns, middleware pipeline, and type safety — and AutoGen's multi-agent orchestration, graph-based workflows, and MCP-first tool integration. One framework, one NuGet ecosystem, one set of abstractions.


For Product Owners: What This Actually Unlocks

If you're a product owner, technology director, or someone who signs off on platform decisions, here's what matters.

You Can Now Automate the Processes That Resisted Automation

Traditional automation works on rigid, deterministic inputs. Agentic AI works on open-ended, contextual ones. The Microsoft Agent Framework makes it practical to automate multi-step business processes that have historically required human judgment at every decision point: complex customer onboarding sequences, financial transaction workflows with conditional approval paths, supply chain triage, and multi-domain research synthesis.

The key is the framework's stateful multi-agent workflow model. You define the business rules and control flow deterministically (in code or YAML). The AI handles the cognitive tasks — interpreting unstructured input, selecting the right tool, generating natural language output — within the boundaries you set. Neither humans nor LLMs are bottlenecks for the tasks that belong to the other.

Governance Isn't Bolted On

The McKinsey 2025 Global AI Trust Survey identified the lack of governance and visibility as the primary barrier to enterprise AI adoption. This framework treats compliance as a first-class concern:

  • Prompt shields with spotlighting to block prompt injection attacks
  • Native PII detection before data leaves the corporate network boundary
  • Task adherence monitoring to keep agents within their operational parameters
  • Agent 365 Observability integration — your existing Microsoft Defender and Microsoft Purview investments extend to cover agent activity

This isn't marketing. The implementation is at the infrastructure level, not a UI checkbox. For organisations subject to data residency requirements, the framework supports bring-your-own Azure Storage, so conversation state never touches Microsoft-managed multi-tenant infrastructure unless you choose it.

The ROI Numbers Are Real

Microsoft's documented enterprise deployments give concrete benchmarks:

Value Driver Mechanism Reported Impact
Vendor consolidation Single SDK replacing fragmented AI tooling Up to $12M in savings
Process automation Stateful multi-agent workflows 80% reduction in incident response effort; 30% faster remediation
Operational productivity Automated triage and identity risk management Up to 47% productivity gain; 90% fewer manual reset tickets

Commerzbank piloted the framework for avatar-driven customer support and noted that it "simplifies complex coding requirements, natively supports MCP for extensive agentic solutions, and reduces continuous IT operations workload while ensuring more natural and compliant customer interactions."


For Architects: A Principled Separation of Concerns

The most important architectural decision in the Microsoft Agent Framework isn't a NuGet package choice. It's a conceptual one: the framework enforces a strict separation between Agents (probabilistic reasoning) and Workflows (deterministic execution policy).

Agents vs Workflows — Why This Matters

In legacy bespoke AI implementations, developers frequently forced a single agent to manage both cognitive reasoning and procedural execution control. The agent was expected to decide what to do and enforce when to stop, maintain state across turns, handle error recovery, and orchestrate sub-tasks. The LLM ended up writing brittle if/else trees in English inside its system prompt.

The framework makes this separation explicit and enforced:

Agents are long-lived, stateful runtime components responsible purely for reasoning. They interpret unstructured inputs, autonomously decide which registered tools to call, maintain conversational context, and generate responses. What they do next is determined by the model — dynamic, contextual, probabilistic.

Workflows are directed graphs that define the execution policy: which agents run in which order, how data flows between them, what the termination conditions are, and where human-in-the-loop gates sit. Workflow edges are deterministic, type-safe, and governed by conventional C# logic.

This separation gives you something genuinely valuable: AI that handles cognitive tasks within a bounded, observable, auditable execution structure.

Deployment Models: Client-Side vs Server-Managed

The framework supports two deployment architectures. Choosing the right one early saves significant refactoring later.

Model Produced Type Description Best For
Client-Side (Responses Agent) ChatClientAgent Agent managed at runtime via AIProjectClient. No persistent server-side resource. Maximum control over orchestration, state persistence, and lifecycle. Wraps any IChatClient — local Ollama, Azure OpenAI, OpenAI direct.
Server-Managed (Foundry Agent) FoundryAgent Agent definitions created, versioned, and managed through Azure AI Foundry portal or APIs. Strict versioned definitions, production SLAs, Microsoft-managed security. Offloads thread and tool management from the client.

The Client-Side model uses IChatClient from Microsoft.Extensions.AI, which means the application is agnostic to the inference provider. You can swap from Azure OpenAI to a local model with a one-line change. This is the model I used for the example below.

Context Management at Scale

Multi-agent systems have an exponential context problem. As agents collaborate, sharing intermediate outputs and tool results, token limits are hit quickly — degrading response quality and driving up inference costs.

The framework addresses this architecturally rather than by hoping the model is frugal. Architects can inject custom ChatHistoryProvider implementations backed by Azure Cosmos DB, Redis, Mem0, Neo4j, or any custom store. You control what context each downstream agent actually needs: full transcript, compacted summary, or selective pruning. The framework's AgentSession object externalises all state, so agents can pause for async human approval, survive process restarts, and resume without losing context.

Interoperability Protocols (MCP, A2A, AG-UI)

Three protocol integrations define the framework's extensibility surface:

MCP (Model Context Protocol) is the first-class tool integration protocol. Instead of writing custom API wrappers, you connect to MCP servers — GitHub, Cosmos DB, file systems, SaaS platforms — and the framework handles authentication, network handshakes, and tool-to-AIFunction conversion automatically.

A2A (Agent-to-Agent) enables cross-platform agent discovery and communication. A .NET agent hosted via A2A can collaborate with agents built in Python, Java, or any other framework. The Microsoft.Agents.AI.Hosting.A2A.AspNetCore package maps both HTTP+JSON (with SSE streaming) and JSON-RPC 2.0 bindings:

app.MapA2AHttpJson("legal-compliance-agent", "/a2a/legal/http");
app.MapA2AJsonRpc("legal-compliance-agent",  "/a2a/legal/rpc");

AG-UI bridges backend agent logic to frontend web interfaces without custom WebSocket implementations. It standardises streaming of text chunks, tool execution statuses, and UI state updates via Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.


For Developers: Building a Real Agent

I learn best by building something real, so I sat down one evening with a blank dotnet new webapi and a question: how long does it take to go from zero to a working AI agent using this framework? The answer turned out to be "shorter than you'd expect, and more interesting than you'd think" — because along the way I stumbled into a pattern I hadn't planned for at all.

The app I built is a personal expense tracker with two modes. Mode one is the expected thing: an AI assistant that manages expenses in SQLite and can analyse your monthly spending against budget targets. Mode two surprised me: an LLM running on the server, orchestrating tools that execute inside the user's browser — GPS coordinates, screen information, native confirmation dialogs. I didn't set out to build that second mode. I built it because the framework's architecture made it naturally expressible once I understood how the tool pipeline worked.

The full source is at mafzaal/MicrosoftAgentsDemo. Let me walk you through the journey.

Starting Point: Four NuGet Packages

The framework is modular and the package set for this project is small:

<PackageReference Include="Microsoft.Agents.AI.Foundry"          Version="1.3.0" />
<PackageReference Include="Microsoft.Agents.AI.Hosting.OpenAI"   Version="1.3.0-alpha.260423.1" />
<PackageReference Include="Azure.AI.OpenAI"                       Version="2.1.0" />
<PackageReference Include="Azure.Identity"                        Version="1.21.0" />

The Foundry package is the core: agent abstractions, session management, AgentSkillsProvider, and the AddAIAgent() host extension. The Hosting.OpenAI package adds OpenAI-compatible REST endpoints — useful if you want any OpenAI-SDK client to talk directly to your .NET agent without writing a custom client.

Wiring the Chat Client

The first thing I had to figure out was how to plug Azure OpenAI into the framework's middleware pipeline. The design follows the same builder pattern you already know from ASP.NET Core:

// Program.cs — authentication: API key when present, DefaultAzureCredential otherwise
AzureOpenAIClient openAIClient = apiKey is not null
    ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey))
    : new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());

IChatClient chatClient = openAIClient
    .GetChatClient(deploymentName)
    .AsIChatClient()
    .AsBuilder()
    .UseFunctionInvocation()        // auto-executes server-side tool calls
    .UseAIContextProviders(skillsProvider)  // injects skills on demand
    .Build();

builder.Services.AddSingleton<IChatClient>(chatClient);

The DefaultAzureCredential fallback means you can run locally with az login and deploy to Azure with Managed Identity — no code changes. This is the Microsoft.Extensions.AI.IChatClient abstraction, which means you could swap AzureOpenAIClient for a local Ollama client with a one-line change.

Agent Registration via Dependency Injection

This is where the .NET-native design really shows. The framework extends IHostApplicationBuilder with AddAIAgent(), so agent configuration follows the exact same pattern as any other ASP.NET Core service:

var agentBuilder = builder.AddAIAgent(
    "expense-assistant",
    instructions: """
        You are a helpful personal finance assistant with access to an expense tracker.
        When the user asks to add, view, update, or delete expenses, use the appropriate tool.
        Always confirm actions and present amounts in a friendly, readable format.
        For summaries, present the data in a clear, organised way.
        """);

foreach (var tool in tools)
    agentBuilder.WithAITool(tool);

agentBuilder.WithInMemorySessionStore();

The agent inherits DI-registered logging, caching, and configuration automatically. No separate Kernel object, no service locator, no global state. It is just a service.

Tool Registration: Your Existing Code, Unchanged

The first thing I tested was whether my existing SQLite data layer needed rewriting. It doesn't. Any C# method becomes an agent-callable tool via AIFunctionFactory.Create(). The framework reads [Description] attributes from method and parameter signatures to generate the JSON schema the LLM uses for function calling:

// ExpenseTools.cs
[Description("Add a new expense. Returns confirmation with the assigned ID.")]
public string AddExpense(
    [Description("Amount spent (positive number, e.g. 12.50)")] decimal amount,
    [Description("Category: Food, Transport, Housing, Entertainment, Health, Utilities, Shopping, or Other")] string category,
    [Description("Short description of what was purchased or paid")] string description,
    [Description("Date in YYYY-MM-DD format. Omit to default to today.")] string? date = null)
{
    // standard SQLite INSERT, returns "Added expense #7: "Coffee" — $4.50 [Food] on 2026-05-05"
}

Registration in Program.cs is one line per method:

AITool[] tools =
[
    getCurrentDateTime,   // inline lambda: () => DateTimeOffset.Now.ToString("f")
    AIFunctionFactory.Create(typeof(ExpenseTools).GetMethod(nameof(ExpenseTools.AddExpense))!,        expenseTools),
    AIFunctionFactory.Create(typeof(ExpenseTools).GetMethod(nameof(ExpenseTools.GetExpenses))!,       expenseTools),
    AIFunctionFactory.Create(typeof(ExpenseTools).GetMethod(nameof(ExpenseTools.UpdateExpense))!,     expenseTools),
    AIFunctionFactory.Create(typeof(ExpenseTools).GetMethod(nameof(ExpenseTools.DeleteExpense))!,     expenseTools),
    AIFunctionFactory.Create(typeof(ExpenseTools).GetMethod(nameof(ExpenseTools.GetExpenseSummary))!, expenseTools),
];

The moment this was wired up, I typed "What did I spend on food last month?" into the browser and watched the agent invoke GetExpenses with category = "Food" and a date range it derived from context. No routing code. No intent classifier. The LLM decided which tool to call and with which arguments — I just described the tools clearly.

AgentClassSkill: Progressive Context Disclosure

Here's where I started enjoying the framework. I wanted the agent to be able to analyse monthly spending against budget targets — but that capability involves a table of category limits, a SQL query, step-by-step instructions. Loading all of that into every request would be wasteful and noisy.

AgentClassSkill solves this elegantly. You bundle the instructions, static resources, and callable scripts into a class, and the AgentSkillsProvider injects the whole bundle into the conversation only when the user's message matches the skill's description:

internal sealed class ExpenseBudgetSkill : AgentClassSkill<ExpenseBudgetSkill>
{
    public override AgentSkillFrontmatter Frontmatter { get; } = new(
        "expense-budget",
        "Analyse monthly spending against budget targets, flag overspend, and suggest savings. " +
        "Use when the user asks about budgets, spending limits, remaining allowance, or financial health.");

    protected override string Instructions => """
        1. Read the `budget-targets` resource for the recommended monthly spending limits per category.
        2. Run the `check-budget` script with the month in YYYY-MM format.
        3. For each category: show amount spent, budget limit, percentage used, and whether it is within budget.
        4. For over-budget categories, suggest one actionable tip to reduce spending.
        """;

    [AgentSkillResource("budget-targets")]
    [Description("Recommended monthly spending limits per expense category.")]
    public string BudgetTargets => """
        | Category      | Monthly Limit |
        |---------------|--------------|
        | Food          | $400          |
        | Transport     | $150          |
        | Housing       | $1,500        |
        | Entertainment | $100          |
        | Health        | $200          |
        | Utilities     | $200          |
        | Shopping      | $300          |
        | Other         | $100          |
        | **Total**     | **$2,950**    |
        """;

    [AgentSkillScript("check-budget")]
    [Description("Query actual spending for a given month (YYYY-MM) and compare each category against budget.")]
    private string CheckBudget(string month)
    {
        // SQLite query → per-category breakdown + totals in JSON
    }
}

The [AgentSkillResource] attribute marks read-only data the model can reference. The [AgentSkillScript] attribute marks callable functions scoped to this skill. When I ask "How's my budget looking this month?", the skill activates, the instructions load, the budget table appears in context, the check-budget script runs against SQLite, and the agent responds with a category-by-category breakdown. When I ask "Add a coffee for $4.50", none of that overhead is present.

The registration is one line:

var skillsProvider = new AgentSkillsProvider(new ExpenseBudgetSkill(expenseDb));

That's token economy done architecturally.

The Middleware Pipeline

The middleware pipeline follows the same Use() builder pattern as ASP.NET Core middleware. You can intercept requests before LLM inference (pre-processing: data masking, prompt enrichment, logging) and responses after (post-processing: schema validation, content filtering, cost attribution):

var agent = chatClient.CreateAIAgent(
    name: "EnterpriseComplianceAgent",
    instructions: "You are a secure, highly restricted data assistant.")
    .AsBuilder()
    .Use(async (messages, options, next, cancellationToken) =>
    {
        logger.LogInformation("Pre-inference: {Count} messages", messages.Count());

        var result = await next(messages, options, cancellationToken);

        logger.LogInformation("Post-inference: applying content safety filters");
        return result;
    })
    .UseOpenTelemetry(configure: c => c.EnableSensitiveData = false)
    .Build();

For production, add PII masking before the next() call. Add JSON schema validation after. The same interception model you've been using in ASP.NET Core middleware for years — just applied to the LLM request/response cycle.

Session Management and Streaming

Once the agent is running, exposing it over SSE is straightforward. Sessions are keyed by a threadId the client persists across turns — the server maintains the full conversation history inside AgentSession:

app.MapPost("/api/chat/stream", async (
    HttpContext context,
    ChatRequest request,
    [FromKeyedServices("expense-assistant")] AIAgent agentSvc) =>
{
    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";

    var (session, threadId) = await GetOrCreateSession(request.ThreadId, agentSvc);

    // Tell the client its thread ID so it can continue the conversation later
    await SseWriteAsync(context, new { type = "thread", threadId }, ct);

    // Stream the agent's response token-by-token
    await foreach (var update in agentSvc.RunStreamingAsync(request.Message, session))
    {
        if (!string.IsNullOrEmpty(update.Text))
            await SseWriteAsync(context, new { type = "chunk", text = update.Text }, ct);
    }

    await context.Response.WriteAsync("data: [DONE]\n\n");
});

The browser accumulates chunk events into a string and renders it as Markdown. No WebSocket, no custom streaming protocol. For production, swap WithInMemorySessionStore() for a ChatHistoryProvider backed by Cosmos DB or Redis — the endpoint code is unchanged.

The Pattern I Didn't Plan: Client-Side Tool Execution

Here's where the story gets interesting.

About halfway through the project, I started thinking about a common limitation in AI assistants: the server has no access to the user's environment. It can't ask "where are you right now?" and get a GPS coordinate. It can't read the user's screen resolution or confirm an action with a native browser dialog. Traditionally you'd solve this by having the client call an API, gather the data, and include it in the next message — a clunky round-trip that breaks conversational flow.

The Microsoft Agent Framework suggests a cleaner model. What if the LLM on the server could declare tools that the client will execute? The server says "I need your location" and the browser — not the server — runs the Geolocation API, then sends the result back. The LLM never needs to know the tools ran in a different process.

The key insight is that UseFunctionInvocation() auto-executes ALL tools on the server. For client tools, I need the opposite: let the model decide to call a tool, but instead of the server executing it, pause the loop and emit a tool_call SSE event for the browser. This requires bypassing the middleware and running the agentic loop manually:

// Extract the underlying chat client from the middleware chain.
// This reuses its auth config while skipping UseFunctionInvocation().
var underlyingOpenAIClient = chatClient.GetService<global::OpenAI.Chat.ChatClient>();
IChatClient rawChatClient = underlyingOpenAIClient is not null
    ? underlyingOpenAIClient.AsIChatClient()
    : openAIClient.GetChatClient(deploymentName).AsIChatClient();

Client tools are declared on the server — the LLM sees their schema and can call them like any other tool — but marked as browser-only:

var clientToolNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
    { "get_user_location", "get_screen_info", "confirm_with_user" };

AIFunction ctGetLocation = AIFunctionFactory.Create(
    () => "executed by client",
    name: "get_user_location",
    description: "Retrieves the user's current geographic location via the browser Geolocation API.");

The manual agentic loop then partitions tool calls by location:

// Inside POST /api/clienttools/stream — manual loop, capped at 10 iterations
for (var iter = 0; iter < 10; iter++)
{
    var response = await rawChatClient.GetResponseAsync(msgs, ctChatOptions, ct);
    history.AddMessages(response);

    var toolCalls = response.Messages.Last().Contents
        .OfType<FunctionCallContent>().ToList();

    if (toolCalls.Count == 0)
    {
        // Model produced final text — emit it and exit the loop
        var text = string.Concat(...);
        await SseAsync(new { type = "chunk", text });
        break;
    }

    var serverCalls = toolCalls.Where(tc => ctServerFuncs.ContainsKey(tc.Name ?? "")).ToList();
    var clientCalls = toolCalls.Where(tc => clientToolNames.Contains(tc.Name ?? "")).ToList();

    // Server tools: run immediately and continue the loop
    foreach (var tc in serverCalls)
    {
        var result = await ctServerFuncs[tc.Name!].InvokeAsync(...);
        history.Add(new ChatMessage(ChatRole.Tool,
            [new FunctionResultContent(tc.CallId!, result?.ToString() ?? "null")]));
    }

    // Client tools: emit as SSE events and pause — browser will resume the loop
    if (clientCalls.Count > 0)
    {
        foreach (var tc in clientCalls)
            await SseAsync(new { type = "tool_call", callId = tc.CallId, name = tc.Name, arguments = tc.Arguments });
        break;  // wait for browser POST with toolResults
    }
}

In the browser, app.js implements each client tool natively:

const clientToolHandlers = {
	get_user_location: () =>
		new Promise((resolve) => {
			navigator.geolocation.getCurrentPosition(
				(p) =>
					resolve(
						JSON.stringify({
							latitude: parseFloat(p.coords.latitude.toFixed(6)),
							longitude: parseFloat(p.coords.longitude.toFixed(6)),
							accuracy_metres: Math.round(p.coords.accuracy)
						})
					),
				(e) => resolve(JSON.stringify({ error: e.message }))
			);
		}),

	get_screen_info: () =>
		Promise.resolve(
			JSON.stringify({
				screen: { width: screen.width, height: screen.height },
				window: { width: innerWidth, height: innerHeight, devicePixelRatio },
				browser: { userAgent: navigator.userAgent, language: navigator.language },
				timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
			})
		),

	confirm_with_user: (args) => {
		const confirmed = confirm(args?.question ?? 'Do you confirm?');
		return Promise.resolve(JSON.stringify({ confirmed }));
	}
};

When the browser receives a tool_call event, it executes the handler and POSTs the results back to /api/clienttools/stream with no message — just { threadId, toolResults: [{callId, result}] }. The server resumes the loop from where it paused.

The SSE protocol for this endpoint:

Event type Direction Payload
thread server→browser threadId
status server→browser text ("Running GetExpenses…")
chunk server→browser text (markdown token to append)
tool_call server→browser callId, name, arguments
error server→browser text
[DONE] server→browser (literal string)

The result: the user types "Where am I?" and the LLM on the server decides to call get_user_location. The browser's Geolocation API fires, GPS coordinates come back, and the agent responds with a human-readable location — all within the same conversational turn, completely transparently. The model never knew the tool ran in a different process. The user never saw a broken flow.

I found this pattern genuinely exciting. It opens up a class of agentic applications that aren't possible when the server is the only execution environment: expense deletion with a native confirm dialog before anything is changed, location-aware queries, accessibility-aware responses based on the user's actual screen configuration. The architecture handles it cleanly because the framework's tool abstraction doesn't assume where execution happens — it just assumes results come back.

OpenAI-Compatible Endpoints (Optional)

One genuinely useful addition: the Microsoft.Agents.AI.Hosting.OpenAI package exposes your agent behind OpenAI-compatible protocol endpoints. Any client built against the OpenAI SDK — Python, TypeScript, curl — can talk to your .NET agent without modification:

builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
builder.AddOpenAIConversations();

// ...

app.MapOpenAIChatCompletions(agentBuilder);  // POST /expense-assistant/v1/chat/completions
app.MapOpenAIResponses(agentBuilder);        // POST /expense-assistant/v1/responses
app.MapOpenAIConversations();                // POST /v1/conversations

This is excellent for gradual migration. Existing tooling keeps working while you build out the agent capabilities.


Migrating from Semantic Kernel or AutoGen

From Semantic Kernel

The migration is primarily a namespace and abstraction consolidation. Key changes:

  1. Namespace update: Microsoft.SemanticKernel.AgentsMicrosoft.Agents.AI and Microsoft.Agents.AI.Foundry
  2. Agent type consolidation: ChatCompletionAgent, OpenAIAssistantAgentChatClientAgent or FoundryAgent. The IChatClient abstraction from Microsoft.Extensions.AI replaces model-specific bindings.
  3. Kernel decoupling: The monolithic Kernel service locator is gone. Agents get their dependencies through standard ASP.NET Core DI and modular context providers.

Critically, your existing [KernelFunction] plugins don't need to be rewritten. From Semantic Kernel 1.38+, the .as_agent_framework_tool() compatibility layer converts legacy SK functions into tools consumable by the new framework. Existing VectorStore connectors (Azure AI Search, Qdrant, Pinecone) remain fully compatible — your RAG infrastructure survives the migration intact.

From AutoGen

AutoGen's conversational patterns — group chats, round-robin task delegation, hierarchical agent assignment — map directly to the framework's graph-based Workflow engine. The key difference: what were previously stochastic, in-memory agent conversations become formally governed, type-safe graph executions with explicit termination conditions and deterministic routing.

This is a net improvement. Multi-agent interactions that were previously prone to infinite loops and hallucination cascades are now bounded by a workflow graph that you control.


Programmatic vs Declarative Workflows

I haven't used the Workflow engine in the expense app yet (single-agent use case), but it's worth understanding for anything more complex.

Programmatic Workflows use the strongly typed WorkflowBuilder fluent API. Maximum flexibility, full access to .NET backend services, bespoke routing logic. Best for core backend services where workflow logic is tightly coupled to domain logic.

Declarative Workflows define the graph as YAML. The LLM context, routing conditions, and agent instructions can all be updated without recompiling the application. Domain experts and product managers can modify workflow behaviour by updating configuration files — a real advantage for rapidly evolving business processes.

The declarative action vocabulary covers variable management (SetVariable, ParseValue), control flow (If, Foreach, ConditionGroup), agent invocation (InvokeAzureAgent, InvokeMcpTool), and human-in-the-loop gates (Question, RequestExternalInput). Enough to model most enterprise workflows without writing custom code.


Observability: Built for Production, Not Just Development

The framework emits OpenTelemetry traces, logs, and metrics following GenAI Semantic Conventions. Add .UseOpenTelemetry() to the agent builder and every reasoning step, tool invocation, and MCP call becomes an OTel span with model latency, token consumption, and tool execution timing captured automatically.

Sensitive data — prompts, model responses, function call arguments — is excluded from telemetry by default. You must explicitly opt in for local development. This is the right default for any organisation operating under GDPR, HIPAA, or similar constraints.

The Agent 365 Observability SDK (Microsoft.Agents.A365.Observability.Hosting) integrates this telemetry with the Microsoft 365 admin center and feeds into Microsoft Defender and Microsoft Purview for compliance monitoring and threat detection across all deployed custom agents.


My Take: Worth the Wait

I went into this project expecting to validate a framework checklist. I came out having built something I'm genuinely proud of — an expense assistant with real CRUD capabilities, intelligent budget analysis that doesn't waste tokens when you don't need it, and a client-tools pattern I'll be using in production.

The .NET ecosystem now has a framework that:

  • Treats agents as first-class DI-managed services, not bolted-on experiments
  • Separates cognitive reasoning from execution policy at the architecture level, not as a suggestion
  • Provides a real middleware pipeline for cross-cutting concerns
  • Brings MCP, A2A, and AG-UI as first-class protocols rather than third-party add-ons
  • Makes IChatClient the portable inference abstraction — swap Azure OpenAI for local Ollama with one line
  • Enables tool execution in the client's environment, not just on the server

Is it perfect? No. The alpha packages (1.3.0-alpha.*) mean some APIs will still shift. The MAAI001 warning suppression in the .csproj is a reminder that not every surface is stable yet. Documentation is catching up to the implementation.

But the foundation is right. The architectural decisions — separating agents from workflows, IChatClient as the portable inference abstraction, AgentClassSkill for progressive context disclosure, the middleware pipeline model, the clean split between server and client tool execution — these are principled, not accidental. They reflect years of learning from both AutoGen and Semantic Kernel teams.

For .NET developers who've been watching Python agentic frameworks with envy, this is the moment to start building. The platform has arrived — and it turns out there were ideas worth waiting for.


Get in Touch

Building AI agents in .NET — whether it's an expense assistant, an enterprise workflow engine, or something with tools that run in the browser? I'd love to hear what you're working on.

Connect with me:

Whether you're exploring the Microsoft Agent Framework for the first time, migrating from Semantic Kernel, or designing production-grade multi-agent workflows, I'd love to hear from you!


Resources

Share this article