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:
- Upgrade the model configuration first. Get onto
Microsoft.Extensions.AI’sIChatClientif you are not already there. Nothing else can move until this does, and it changes no behaviour. See Microsoft.Extensions.AI explained. - 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.
- 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.
- 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.
- Convert the call sites. This is where
.Textversus.Messagesbites; do it deliberately, not with find-and-replace. - Delete the
Kernelregistration. Last, once nothing references it. - 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.