Microsoft Agent Framework: The Successor to Semantic Kernel and AutoGen

By · · AI Engineering

Microsoft Agent Framework: a unified SDK with multi-agent collaboration, extensible architecture, and open-source cross-platform support

I've written about both Semantic Kernel and AutoGen on this blog before, and the split was always a bit awkward. Semantic Kernel was the .NET-first, enterprise-leaning option with kernels, plugins, filters and telemetry. AutoGen was the Python-friendly playground for multi-agent conversations. Two teams at Microsoft, two SDKs, two mental models for what was basically the same job.

That split is done. The two teams have shipped one framework, the Microsoft Agent Framework (MAF), and they're calling it the direct successor to both. From the overview docs:

Agent Framework combines AutoGen's simple agent abstractions with Semantic Kernel's enterprise features (session-based state management, type safety, middleware, telemetry) and adds graph-based workflows for explicit multi-agent orchestration.

So: .NET and Python with the same API shape, a hosting story tied to Microsoft Foundry, model clients for most of the names you'd expect (Azure OpenAI, OpenAI, Anthropic, Ollama, GitHub Copilot, Google Gemini, ONNX, a few more), and a graph-based workflow engine that neither parent framework really had.

What follows is a tour of what MAF feels like once you start typing. Agents and workflows, the building blocks underneath them, the orchestration patterns, and the hosting story. Code in both Python and C#, because the framework treats them as peers and so should the post.

The mental model

Everything in MAF is either an agent or a workflow.

Primitive What it is
Agent A single LLM-driven loop that can call tools and MCP servers. Use it when the task is open-ended or conversational.
Workflow A graph of executors (agents, functions, or custom nodes) connected by edges, with type-safe routing, checkpointing, and human-in-the-loop support. Use it when the process has well-defined steps.

The docs are also honest about when you should use neither:

If you can write a function to handle the task, do that instead of using an AI agent.

Which I appreciate. Half the agent posts I read in 2025 were people building agents for jobs a case statement could have done.

Underneath both pillars, the same five names keep coming up: ChatClient (the model abstraction), AgentSession (state), ContextProvider (memory and dynamic instructions), middleware (interception), and MCP clients (tool integration). Learn those and most of the framework stops being surprising.

Installing

Python:

pip install agent-framework

.NET:

dotnet add package Microsoft.Agents.AI
dotnet add package Microsoft.Agents.AI.Foundry
dotnet add package Azure.AI.Projects
dotnet add package Azure.Identity

The Python install is one meta-package that pulls in subpackages for each provider. On .NET you pick the integration packages you need.

Hello agent

Smallest possible agent in C#, against Azure OpenAI:

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;

var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName)
    .AsAIAgent(
        instructions: "You are good at telling jokes.",
        name: "Joker");

Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));

await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
{
    Console.WriteLine(update);
}

Pick a client, call .AsAIAgent(...), get an AIAgent. Run it or stream it.

The Python version of the same shape, this time against a Foundry project:

import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential


async def main() -> None:
    client = FoundryChatClient(
        project_endpoint="https://your-project.services.ai.azure.com",
        model="gpt-4o",
        credential=AzureCliCredential(),
    )

    agent = Agent(
        client=client,
        name="HelloAgent",
        instructions="You are a friendly assistant. Keep your answers brief.",
    )

    result = await agent.run("What is the capital of France?")
    print(f"Agent: {result}")

    print("Agent (streaming): ", end="", flush=True)
    async for chunk in agent.run("Tell me a one-sentence fun fact.", stream=True):
        if chunk.text:
            print(chunk.text, end="", flush=True)
    print()


if __name__ == "__main__":
    asyncio.run(main())

A couple of things you might miss on first read. stream=True is a flag on the same run call, not a separate API. And auth goes through azure-identity, so there's no API key sitting in the sample. Small choices, but the kind of small choice that tells you whether the people who built it have ever had to clean up after a hardcoded key getting committed.

Tools

Tools are how the agent does anything beyond producing tokens. In Python you decorate a function with @tool. In .NET you wrap a method with AIFunctionFactory.Create.

Python:

from random import randint
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from pydantic import Field


# NOTE: approval_mode="never_require" is fine for samples.
# Use "always_require" in production for human-in-the-loop confirmation.
@tool(approval_mode="never_require")
def get_weather(
    location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
    """Get the weather for a given location."""
    conditions = ["sunny", "cloudy", "rainy", "stormy"]
    return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."


agent = Agent(
    client=FoundryChatClient(credential=AzureCliCredential()),
    name="WeatherAgent",
    instructions="You are a helpful weather agent. Use the get_weather tool to answer questions.",
    tools=[get_weather],
)

.NET:

using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
    => 
quot;The weather in {location} is cloudy with a high of 15°C."; AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) .GetChatClient(deploymentName) .AsAIAgent( instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]); Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));

The approval_mode argument is one I wish more frameworks shipped with. It lets the framework pause a run, surface the tool call to a human, and resume once someone clicks approve. Not a bolt-on, just a flag.

Tools can also come from MCP servers. MAF ships an MCP client, so you can pull in remote tool catalogues without wrapping anything yourself. That matters more in 2026 than it would have a year ago, because the useful tool surfaces (databases, internal APIs, SaaS connectors) are showing up as MCP servers now instead of per-framework adapters.

Sessions: state that carries across turns

A single run call is stateless. To keep conversation history around, create an AgentSession:

AgentSession session = await agent.CreateSessionAsync();

Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
Console.WriteLine(await agent.RunAsync(
    "Now add some emojis and tell it in the voice of a pirate's parrot.",
    session));

Python is the same shape:

session = agent.create_session()

result = await agent.run("My name is Alice and I love hiking.", session=session)
print(f"Agent: {result}\n")

result = await agent.run("What do you remember about me?", session=session)
print(f"Agent: {result}")

The session holds messages, tool call history, and arbitrary state that providers can read and write. This is the piece SK had and AutoGen mostly punted on, and it's what makes MAF feel like an SDK you'd ship rather than a demo you'd show.

Context providers: memory you can actually reason about

If you've built agents the hard way, you know what "memory" usually means: injecting user preferences, retrieved knowledge, or dynamic instructions into every call without losing your mind. MAF formalises this with context providers. They're objects with before_run and after_run hooks that can read and mutate the prompt and the session state.

from agent_framework import Agent, AgentSession, ContextProvider, SessionContext


class UserMemoryProvider(ContextProvider):
    """A context provider that remembers user info in session state."""

    DEFAULT_SOURCE_ID = "user_memory"

    def __init__(self):
        super().__init__(self.DEFAULT_SOURCE_ID)

    async def before_run(self, *, agent, session, context: SessionContext, state):
        user_name = state.get("user_name")
        if user_name:
            context.extend_instructions(
                self.source_id,
                f"The user's name is {user_name}. Always address them by name.",
            )
        else:
            context.extend_instructions(
                self.source_id,
                "You don't know the user's name yet. Ask for it politely.",
            )

    async def after_run(self, *, agent, session, context: SessionContext, state):
        for msg in context.input_messages:
            text = msg.text if hasattr(msg, "text") else ""
            if isinstance(text, str) and "my name is" in text.lower():
                state["user_name"] = text.lower().split("my name is")[-1].strip().split()[0].capitalize()

Attach it to the agent and it runs on every call:

agent = Agent(
    client=client,
    name="MemoryAgent",
    instructions="You are a friendly assistant.",
    context_providers=[UserMemoryProvider()],
)

I like this design. Anything you'd otherwise stitch in by hand (RAG retrieval, a user-preferences cache, a safety filter) is a context provider. They compose cleanly because each one only writes to its own source_id slice of state, so two providers can't quietly stomp on each other's keys.

Workflows: graphs of executors

This is the part of MAF I was most curious about. AutoGen had conversations between agents. SK had plan-and-execute. Neither could really say "run this graph deterministically, with checkpoints, with a human approval node sitting on this edge." Workflows are the answer to that gap.

A workflow is built from executors (anything that processes an input and produces an output) connected by edges. Simplest possible workflow in C#:

using Microsoft.Agents.AI.Workflows;

Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");

var reverse = new ReverseTextExecutor();

WorkflowBuilder builder = new(uppercase);
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
var workflow = builder.Build();

await using Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
foreach (WorkflowEvent evt in run.NewEvents)
{
    if (evt is ExecutorCompletedEvent executorComplete)
    {
        Console.WriteLine(
quot;{executorComplete.ExecutorId}: {executorComplete.Data}"); } } internal sealed class ReverseTextExecutor() : Executor<string, string>("ReverseTextExecutor") { public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) => ValueTask.FromResult(string.Concat(message.Reverse())); }

In Python it's Executor classes or @executor-decorated functions, wired with a WorkflowBuilder:

from agent_framework import Executor, WorkflowBuilder, WorkflowContext, executor, handler
from typing_extensions import Never


class UpperCase(Executor):
    def __init__(self, id: str):
        super().__init__(id=id)

    @handler
    async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
        await ctx.send_message(text.upper())


@executor(id="reverse_text")
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
    await ctx.yield_output(text[::-1])


upper = UpperCase(id="upper_case")
workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, reverse_text).build()

events = await workflow.run("hello world")
print(events.get_outputs())  # ['DLROW OLLEH']

The graph API gives you fan-out, fan-in, switch/case, loops, and superstep-based checkpointing. The framework can persist workflow state at each superstep boundary, so a crash mid-workflow doesn't lose anything: restart the process and it picks up where it stopped. That's the durability story SK never quite finished.

For lower-ceremony cases there's a functional workflow API where the whole thing is one async function and the framework infers the graph:

from agent_framework import Agent, workflow

writer = Agent(name="WriterAgent", instructions="Write a short poem (4 lines max) about the given topic.", client=client)
reviewer = Agent(name="ReviewerAgent", instructions="Review the given poem in one sentence. Is it good?", client=client)


@workflow
async def poem_workflow(topic: str) -> str:
    poem = (await writer.run(f"Write a poem about: {topic}")).text
    review = (await reviewer.run(f"Review this poem: {poem}")).text
    return f"Poem:\n{poem}\n\nReview: {review}"


result = await poem_workflow.run("a cat learning to code")

Agents inside workflows are just function calls. No special wrappers, no message bus to learn. The workflow engine still sees the structure because each await boundary becomes an executor.

Orchestration patterns

On top of the workflow graph, MAF ships pre-built orchestration builders for the patterns you actually want.

Builder Topology Use when
SequentialBuilder A then B then C A pipeline of agents that each see the conversation so far.
ConcurrentBuilder Fan-out, fan-in Several agents work on the same input in parallel; an aggregator combines the results.
HandoffBuilder Mesh One agent decides which other agent should take over. The framework auto-registers handoff tools, so the LLM literally calls transfer_to_refund_agent(...).
GroupChatBuilder Manager plus participants Round-table conversation with a selector that picks who speaks next.
MagenticBuilder Manager-led plan and execute The Magentic pattern from the AutoGen research: a planning manager that drives a team of specialist agents to solve a harder task.

Sequential workflow in five lines:

from agent_framework.orchestrations import SequentialBuilder

workflow = SequentialBuilder(participants=[writer, reviewer], output_from="all").build()
result = await workflow.run("Write a tagline for a budget-friendly eBike.")

A handoff workflow looks like a triage agent with three specialists wired into a mesh. The triage agent doesn't call the specialists directly. It calls a handoff tool that the framework auto-generated, and the workflow engine routes the conversation to the chosen specialist while preserving the history:

from agent_framework.orchestrations import HandoffBuilder

triage, refund, order, returns = create_agents(client)

workflow = HandoffBuilder(
    coordinator=triage,
    participants=[refund, order, returns],
).build()

Magentic is the one I keep coming back to. You hand it a couple of specialist agents (a researcher and a coder in the sample) and a question like "estimate the energy efficiency of these ML models". A manager LLM plans the work, decides who runs each step, and reconciles the outputs. It's the AutoGen Magentic pattern, but now there's checkpointing and an event stream around it, so you can actually see what happened when a run goes sideways at 2am.

Providers

MAF doesn't pick a model vendor for you. The current set, straight from the samples directory:

Switching providers is a constructor change. The AIAgent / Agent surface above doesn't move. That's the part I've been waiting on since SK and AutoGen split the world in two.

Middleware

Cross-cutting concerns (logging, retry, redaction, guardrails) are middleware. The pattern is the next delegate you'd expect from ASP.NET or Express. The samples include an auto_retry example that wraps an agent in exponential backoff without the agent code knowing anything about it. Middleware composes, and it works around a single agent or around a whole workflow node.

Hosting

A few of the hosting targets that ship out of the box:

Observability

OpenTelemetry is built in. Spans for agent runs, tool calls, workflow executors, and model client calls all land in whatever tracing pipeline you already have. I've spent more hours than I want to admit retrofitting traces into SK and AutoGen, so this one matters more to me than it probably should.

Declarative agents and skills

Two smaller pieces worth a mention.

Declarative agents let you define an agent in YAML instead of code. Instructions, model, tools, context providers, all in a versionable file. Useful when product or compliance owns the prompt and devs own the runtime.

Agent skills let you build domain-specific knowledge bases (from files, inline code, or class libraries) that agents can discover and use at runtime. It's a structured replacement for the "stuff everything into the system prompt" pattern: only the relevant skill gets pulled in for a given query.

There's also a DevUI for poking at agent state and workflows during development, and an AF Labs subdirectory for experimental work like agent benchmarking and RL.

Where this leaves Semantic Kernel and AutoGen

Both repos are still up. The migration guides on Microsoft Learn are detailed enough that I think the team genuinely wants people to move, not just gesture at it. Kernels become chat clients, plugins become tools, AutoGen team chats become orchestration builders. None of that translation is particularly hard.

What I actually care about, more than the consolidation, is what it lets you do once everything lives in one place. The same agent code goes from agent.RunAsync("hello") on a laptop to a Magentic team running for hours inside a Durable workflow on Foundry, with OpenTelemetry traces and an MCP tool catalogue feeding everyone. SK couldn't really do the multi-agent half. AutoGen couldn't really do the production half. MAF is the first time I've looked at Microsoft's agent story and not had to mentally split it into two.

I'm going to build something real on it over the next month and write that up separately. There'll be sharp edges (there always are), but the shape of the thing looks right.


Links