Microsoft Agent FrameworkSemantic Kernel.NETC#

Migrating from Semantic Kernel to the Microsoft Agent Framework: A C# Walkthrough

A line-by-line C# migration from Semantic Kernel agents to the Microsoft Agent Framework — namespaces, agent creation, threads, tools, invocation, options and DI, with before/after code for each.

If you have Semantic Kernel agents in production and you have been putting off the move to the Microsoft Agent Framework, this is the article that tells you exactly what changes. Not “why” — we covered the decision in Semantic Kernel vs the Microsoft Agent Framework. This is the mechanical part: the ten places your code has to change, with before-and-after C# for each.

The good news up front: this is not a rewrite. Both frameworks are built on Microsoft.Extensions.AI, so the layer where you configure a model, and the message and content types that flow through your app, carry over unchanged. What changes is the agent layer sitting on top — and it gets consistently smaller.

Before you start: you are not on a deadline

Semantic Kernel is not deprecated. Microsoft has committed to critical bug fixes for the Semantic Kernel agent abstractions for at least a year past the Agent Framework’s 1.0 GA in April 2026. A working production agent is a valid reason to wait.

Migrate when you hit one of these:

  • You want graph-based multi-agent workflows, which Semantic Kernel does not have.
  • You want first-class MCP and A2A support instead of bolting it on.
  • You are about to build a new agent anyway — build that one on the new framework and let the old ones age out.

Do the migration with an eval set in place. Agent behaviour is not something the compiler checks for you, and the whole point of migrating incrementally is being able to prove each step held quality.

1. Namespaces and packages

Semantic Kernel:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;

Agent Framework:

using Microsoft.Extensions.AI;
using Microsoft.Agents.AI;

That second line is the whole story of the migration in miniature. Microsoft.Extensions.AI supplies the shared message and content types — ChatMessage, AIContent, IChatClient. Microsoft.Agents.AI supplies the agent layer. Semantic Kernel’s parallel type hierarchy for the same concepts is gone.

2. Creating an agent

This is where the boilerplate disappears.

Semantic Kernel wanted a Kernel first, then an agent that holds a reference to it:

Kernel kernel = Kernel
    .AddOpenAIChatClient(modelId, apiKey)
    .Build();

ChatCompletionAgent agent = new()
{
    Instructions = "You are a terse support agent.",
    Kernel = kernel
};

Agent Framework builds the agent straight off the chat client:

AIAgent agent = chatClient.AsAIAgent(
    instructions: "You are a terse support agent.");

The Kernel is gone entirely. It was doing two jobs — holding service configuration and holding plugins — and both moved: services live in the IChatClient, tools are passed to the agent.

The same shape works for hosted providers:

AIAgent foundryAgent = aiProjectClient.AsAIAgent(
    model: deploymentName,
    instructions: "You are a terse support agent.");

AIAgent assistantAgent = await assistantClient.CreateAIAgentAsync(
    instructions: "You are a terse support agent.");

And you can hydrate an agent from an existing hosted record rather than recreating it:

ProjectsAgentRecord record = await aiProjectClient.AgentAdministrationClient
    .GetAgentAsync(agentName);
AIAgent foundryAgent = aiProjectClient.AsAIAgent(record);

3. One agent type instead of five

Semantic Kernel gave you a class per service — ChatCompletionAgent, OpenAIAssistantAgent, AzureAIAgent, and so on. Each had slightly different construction and slightly different behaviour, and swapping providers meant swapping types.

The Agent Framework has one: ChatClientAgent, reachable through the AIAgent base type. Any service that ships an IChatClient implementation works through it. In practice you rarely name the concrete type at all — you hold AIAgent and let AsAIAgent() pick.

The migration rule is simple: every agent variable becomes AIAgent. If you have a factory that switches on provider and returns a different Semantic Kernel agent class, it collapses into one method that returns AIAgent.

4. Threads become sessions

Semantic Kernel made the caller know which thread type matched which agent:

AgentThread thread = new OpenAIAssistantAgentThread(this.AssistantClient);
AgentThread thread = new AzureAIAgentThread(this.Client);
AgentThread thread = new OpenAIResponseAgentThread(this.Client);

Agent Framework makes the agent responsible for it:

AgentSession session = await agent.CreateSessionAsync();

One line, and it is correct for whatever provider is underneath. This is worth more than it looks in a multi-tenant service where the provider is a configuration value.

One gap to plan for: AgentSession has no DeleteAsync(). Semantic Kernel’s hosted threads did, but not every provider hosts chat history or supports deleting it, so the Agent Framework does not pretend to offer a universal API. If you were relying on thread deletion — and if you store conversation data for real users, you probably should be, for data-deletion requests — you now track the session yourself and call the provider SDK:

await assistantClient.DeleteThreadAsync(session.ConversationId);

Do not skip this one. Deleting a user’s conversation history on request is usually a compliance obligation, and it quietly stopped being the framework’s job.

5. Tool registration

Semantic Kernel needed four steps to expose one method: attribute the method, wrap it in a plugin, add the plugin to a kernel, hand the kernel to the agent.

KernelFunction function = KernelFunctionFactory.CreateFromMethod(GetWeather);
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("WeatherPlugin", [function]);
kernel.Plugins.Add(plugin);

ChatCompletionAgent agent = new() { Kernel = kernel, /* ... */ };

Agent Framework does it in the agent constructor call:

AIAgent agent = chatClient.AsAIAgent(
    instructions: "You are a terse support agent.",
    tools: [AIFunctionFactory.Create(GetWeather)]);

The [KernelFunction] attribute is also no longer required. A plain method works; a [Description] attribute is optional and only there to improve what the model sees:

// Semantic Kernel — attribute mandatory
public class MenuPlugin
{
    [KernelFunction]
    public static MenuItem[] GetMenu() => /* ... */;
}

// Agent Framework — attribute optional, description recommended
public class MenuTools
{
    [Description("Gets today's menu items with prices.")]
    public static MenuItem[] GetMenu() => /* ... */;
}

Keep the descriptions even though they are optional. They are what the model reads when deciding whether to call your tool, and dropping them is the fastest way to make a migrated agent worse than the one it replaced. If you are choosing between a local tool and a shared one at this point, function calling vs MCP covers the trade-off.

6. Invocation: Invoke becomes Run

This is the change that touches the most call sites, and the one worth reading carefully because the return shape is different.

Semantic Kernel’s non-streaming call was, confusingly, an async stream — you looped even when you wanted one answer:

await foreach (AgentResponseItem<ChatMessageContent> result
    in agent.InvokeAsync(userInput, thread, agentOptions))
{
    Console.WriteLine(result.Message);
}

Agent Framework returns a single object:

AgentResponse response = await agent.RunAsync(userInput, session);
Console.WriteLine(response.Text);

AgentResponse.Text is the final text. AgentResponse.Messages is everything the run produced — tool calls, function results, reasoning updates, the lot. So the await foreach in your old code maps to either .Text (if you only ever used the last message) or iterating .Messages (if you were logging or displaying intermediate steps). Check which one each call site actually did; assuming the first when it was the second is the most common way to lose tool-call telemetry during a migration.

7. Streaming

// Semantic Kernel
await foreach (StreamingChatMessageContent update
    in agent.InvokeStreamingAsync(userInput, thread))
{
    Console.Write(update);
}

// Agent Framework
await foreach (AgentResponseUpdate update
    in agent.RunStreamingAsync(userInput, session))
{
    Console.Write(update);
}

Same shape, richer updates — AgentResponseUpdate carries agent-level information per chunk rather than just text deltas, and it is ToString()-friendly so naive rendering still works. If you are piping this to a browser, our guide to streaming agent responses to a web UI still applies unchanged; only the type name in the loop moves.

8. Options

Semantic Kernel layered provider settings inside kernel arguments inside invoke options:

OpenAIPromptExecutionSettings settings = new() { MaxTokens = 1000 };
AgentInvokeOptions options = new() { KernelArguments = new(settings) };

Agent Framework flattens it:

ChatClientAgentRunOptions options = new(new() { MaxOutputTokens = 1000 });

Note MaxTokens became MaxOutputTokens, matching Microsoft.Extensions.AI’s ChatOptions. And note the caveat: ChatClientAgentRunOptions applies to ChatClientAgent, which is inference-backed. Not every AIAgent implementation accepts inference options — a Copilot Studio or A2A-backed agent does not have a MaxOutputTokens to set.

9. Dependency injection

Semantic Kernel required a Kernel in the container because every agent needed one:

services.AddKernel().AddProvider(/* ... */);

services.AddKeyedSingleton<Microsoft.SemanticKernel.Agents.Agent>(
    TutorName,
    (sp, key) => new ChatCompletionAgent
    {
        Kernel = sp.GetRequiredService<Kernel>(),
    });

Agent Framework registers the agent and nothing else:

services.AddKeyedSingleton<AIAgent>(TutorName, (sp, key) =>
    sp.GetRequiredService<IChatClient>().AsAIAgent(instructions: TutorInstructions));

The base type in your container changes from Agent to AIAgent, and the AddKernel() registration goes away once the last agent stops needing it. Do that removal last — it is the one change that breaks every not-yet-migrated agent at once.

A migration order that works

Ordering matters more than speed here. What I would do:

  1. Upgrade the model configuration first. Get onto Microsoft.Extensions.AI’s IChatClient if you are not already there. Nothing else can move until this does, and it changes no behaviour. See Microsoft.Extensions.AI explained.
  2. Capture a baseline. Run your eval set against the current agents and write down the numbers. Without this, “did the migration break anything?” is unanswerable.
  3. Migrate one leaf agent. Pick the agent with the fewest tools and the least traffic. Convert it end to end — creation, session, tools, invocation. Re-run evals.
  4. Migrate tools by shared library, not by agent. If three agents share a plugin, converting that plugin once and letting all three pick it up beats converting it three times.
  5. Convert the call sites. This is where .Text versus .Messages bites; do it deliberately, not with find-and-replace.
  6. Delete the Kernel registration. Last, once nothing references it.
  7. Re-run evals and compare to the baseline from step 2.

Steps 3 through 7 can happen one agent at a time across several sprints. The framework does not care that half your app is still on Semantic Kernel.

The mapping, in one table

Semantic Kernel Microsoft Agent Framework
Microsoft.SemanticKernel.Agents Microsoft.Agents.AI
Kernel (gone — config on IChatClient)
ChatCompletionAgent, AzureAIAgent, OpenAIAssistantAgent ChatClientAgent, held as AIAgent
new ChatCompletionAgent { Kernel = k } chatClient.AsAIAgent(instructions: …)
new OpenAIAssistantAgentThread(client) await agent.CreateSessionAsync()
AgentThread AgentSession
thread.DeleteAsync() (no API — use the provider SDK with session.ConversationId)
[KernelFunction] + KernelPluginFactory AIFunctionFactory.Create(method) in tools:
agent.InvokeAsync(...)IAsyncEnumerable<…> agent.RunAsync(...)AgentResponse
agent.InvokeStreamingAsync(...) agent.RunStreamingAsync(...)AgentResponseUpdate
OpenAIPromptExecutionSettings.MaxTokens ChatOptions.MaxOutputTokens
services.AddKernel() + keyed Agent keyed AIAgent

Takeaway

The migration is mostly deletion. The Kernel, the plugin wrappers, the provider-specific agent and thread classes, the nested options — all of it collapses into chatClient.AsAIAgent(...), agent.CreateSessionAsync() and agent.RunAsync(...). Two things genuinely need care rather than mechanical translation: the .Text versus .Messages decision at every call site, and replacing hosted-thread deletion with a provider-SDK call if you were relying on it. Everything else you can do a leaf agent at a time, with evals between steps, at whatever pace your roadmap allows.


Have a correction or a topic you want covered? Email mani.bc72@gmail.com.

Frequently asked questions

Is Semantic Kernel deprecated in 2026?

No. Semantic Kernel is not deprecated — the Agent Framework is its next generation, and Microsoft has committed to supporting the Semantic Kernel agent abstractions with critical bug fixes for at least a year after the Agent Framework's 1.0 GA in April 2026. Existing production apps keep working. New agent projects should start on the Agent Framework.

What replaces ChatCompletionAgent in the Microsoft Agent Framework?

ChatClientAgent replaces it, and you almost never construct it directly — you call chatClient.AsAIAgent(instructions: "...") and hold the result as an AIAgent. The same type also replaces OpenAIAssistantAgent and AzureAIAgent, because any service with an IChatClient implementation is now driven through one agent type.

Do I still need a Kernel object?

No. The Kernel was Semantic Kernel's service container and plugin registry. In the Agent Framework, model configuration lives in the IChatClient and tools are passed directly to the agent, so there is no Kernel to build, register or pass around.

Can I migrate incrementally, or is it a rewrite?

Incrementally. Both frameworks sit on Microsoft.Extensions.AI, so your IChatClient setup, message types and content types carry across unchanged. Migrate one agent at a time behind your existing interfaces and keep the rest of the app on Semantic Kernel until you are ready.

What happened to AgentThread?

It became AgentSession, and you no longer construct it yourself. Instead of picking a provider-specific thread type, you call await agent.CreateSessionAsync() and the agent returns the right session for its provider. There is no session-level delete API — if the provider hosts chat history and you need to delete it, use the provider SDK with session.ConversationId.