Semantic Kernel to Microsoft Agent Framework: a real migration

For seven months the honest advice was "don't." Semantic Kernel went into maintenance mode on the first of October 2025 alongside AutoGen, Microsoft Agent Framework arrived in public preview the same day, and rewriting a working production system onto a preview SDK is trading a known risk for an unknown one on somebody else's schedule. That was the call I made at the time, in the framework comparison, and I'd make it again.

The calculus changed in April, when MAF hit 1.0 with stable APIs and a long-term support commitment. Maintenance mode is not an outage — a Semantic Kernel system keeps running and keeps getting security fixes — but "the platform this is built on will never gain another feature" is a slow-acting problem, and now there is a stable destination. So we migrated one. This is what it actually cost, component by component, and the two decisions that kept it to a sprint.

What you are actually migrating

Semantic Kernel is not one abstraction, and the migration effort is wildly uneven across its pieces. Sorting them first is the difference between an estimate and a guess.

Semantic Kernel pieceMAF equivalentEffort
Native functions (KernelFunction over your own methods)Tools — plain functions with descriptionsTrivial. Mostly attribute and signature changes
Prompt functions (templated prompts as functions)Prompts + tools, or a workflow stepLow. The template survives; the wrapper changes
Plugins (grouped functions)Tool collections, or an MCP serverLow — and the fork in the road worth taking (see below)
Chat completion + connectorsAzureOpenAIChatClient and friendsLow. Client construction, retry and options mapping
Filters (function/prompt/auto-invoke interception)MiddlewareModerate. Same idea, different hook points and ordering
Agents & group chatAgents + orchestration / workflowsModerate. MAF inherited AutoGen's patterns, which is not SK's model
Memory / vector connectorsYour store, behind your own portDo not port it. Rewrite it as yours.
PlannersNo like-for-like — function calling or an explicit workflowHigh if you leaned on them; most teams had already moved off
TelemetryOpenTelemetry, GenAI semantic conventionsLow, and an upgrade — this gets better, not just different

The two rows that decide your timeline are memory and planners. Everything else is mechanical work whose size you can estimate by counting functions.

Decision one: extract the domain before you touch the framework

The single highest-leverage thing we did, and it happened in the first two days: pull every native function's body into a plain module with no framework imports, and leave a thin SK wrapper calling it. Ship that. The system is still on Semantic Kernel, still in production, still passing tests — and now the part with your business logic in it belongs to you.

Two properties make this worth doing before anything else. It's independently valuable — a reviewable, testable domain layer is an improvement even if you never migrate. And it converts the migration from "rewrite the agent" into "write a new adapter," because after this step the MAF version is a file that registers the same functions a different way.

// Step 1 — the domain layer. No Microsoft.SemanticKernel, no Microsoft.Agents.
// Testable with xUnit, callable from a console app, wrappable by anything.
public sealed class ClaimsService(IClaimsRepository repo)
{
    public async Task<ClaimStatus> GetStatusAsync(string claimId, CancellationToken ct)
        => await repo.FindStatusAsync(claimId, ct)
           ?? throw new ClaimNotFoundException(claimId);   // actionable, not "400"
}

// Step 2 — the SK adapter that ships TODAY, unchanged behaviour.
public sealed class ClaimsPlugin(ClaimsService svc)
{
    [KernelFunction, Description("Get the current status of a claim by its id.")]
    public Task<ClaimStatus> GetClaimStatus(string claimId) => svc.GetStatusAsync(claimId, default);
}

// Step 3 — the MAF adapter, written later, next to the SK one. Both compile,
// both are tested, and the cutover is a configuration flag rather than a branch.
public static class ClaimsTools
{
    public static AIFunction GetClaimStatus(ClaimsService svc) =>
        AIFunctionFactory.Create(
            (string claimId) => svc.GetStatusAsync(claimId, default),
            name: "get_claim_status",
            description: "Get the current status of a claim by its id.");
}

The C# is illustrative rather than copy-paste — the exact factory helpers moved during preview and you should check the current API — but the shape is the point. Two adapters, one domain, no behaviour change on either side.

Decision two: do not port the memory layer

Semantic Kernel's memory and vector-store connectors were convenient, and porting them to MAF's equivalents would have been the obvious move. We didn't, and it's the decision I'd defend hardest.

Conversation history and durable facts are product data with a business owner, not framework artifacts — the argument I made at length in agent state you own. Migrating them from one framework's abstraction into another framework's abstraction means paying a conversion cost to end up in the same position: your most valuable state living inside a component you don't control, waiting for the next migration. We spent three days moving conversations and learned facts into our own tables behind two small ports, and the MAF cutover then had nothing to say about them.

The concrete payoff arrived immediately. Because history lived in our tables, we could run both agents against the same conversations during the cutover, compare outputs on real traffic, and roll back by flipping a flag — none of which is possible when each framework owns its own copy of the truth.

Filters to middleware, and the ordering surprise

Semantic Kernel's filters and MAF's middleware are the same concept — intercept around model and function invocation, for logging, PII redaction, authorization, caching, retries. The port is mechanical. The part that cost us an afternoon of confusion was ordering and short-circuiting semantics: which hook sees the arguments before validation, which sees the result after a retry, and what happens when one refuses to proceed.

Write the tests before the migration, not after. For each filter, a test that asserts the observable behaviour — "a request with an unauthorized tenant never reaches the tool," "a redacted field never appears in the trace," "a refused call returns this shape to the model" — and then make the middleware pass them. Those tests are framework-agnostic, which means they're the specification, and they caught two behaviour changes we would otherwise have shipped.

graph LR
  D1["Day 1-2
extract domain layer
SK wrappers stay"] --> D2["Day 3-5
conversations + facts
into your own tables"] D2 --> D3["Week 2
MAF adapters side by side
filters → middleware + tests"] D3 --> D4["Week 2
OTel traces + eval suite
on BOTH agents"] D4 --> SHADOW{"Shadow run
same conversations, both agents"} SHADOW -->|"outputs and evals agree"| CUT["Flag flip per tenant
rollback = flip back"] SHADOW -->|"they don't"| FIX["Fix, re-run
the eval suite is the arbiter"] FIX --> SHADOW CUT --> DONE["Retire SK adapters
once no tenant is on them"]

The order that made this a sprint. Note that the two steps which sound like migration work — the MAF adapters and the middleware — come third, after the two that are valuable regardless. Note also that the cutover is per tenant behind a flag, with the eval suite as the arbiter of whether the new agent behaves like the old one; "it looked fine when I tried it" is not a migration criterion.

The eval suite is what makes a migration provable

You cannot diff an agent. Two implementations will phrase things differently, choose tools in a different order, and both be correct — which means the question "did the migration change behaviour?" has no answer from inspection. The only thing that answered it for us was running the same evaluation suite against both agents on the same dataset and comparing scores, plus tool-call accuracy per case.

If you don't already have that harness, build it before the migration rather than during — the method is in evaluating LLM and agent systems. A migration without evals is a rewrite you're hoping about, and the failures are the quiet kind: the new agent is slightly worse at one intent nobody tested, and you find out from a customer six weeks later. Two cases in our suite caught real regressions — one middleware ordering change and one tool description that MAF's function factory had subtly reworded, changing when the model reached for it.

What genuinely has no equivalent, and what to do about it. Planners. If your system leans on SK's planners to decompose goals, there is no like-for-like landing spot; you replace them with plain function calling (usually better) or an explicit workflow graph (usually clearer). Budget real design time, not porting time. Group-chat behaviour. MAF inherited AutoGen's multi-agent patterns rather than SK's, so a conversation-driven multi-agent setup will not reproduce exactly — port it last and test it hardest, because it's the piece the two frameworks model most differently. Bespoke connectors. A custom memory or vector connector you wrote against SK's interfaces has no home; this is the argument for owning that layer rather than porting it. And a scheduling note that matters more than any of them: don't migrate and upgrade your model in the same change. We did that once by accident on a smaller system, spent a day arguing about whether MAF had made the agent worse, and discovered the model version had moved underneath us. One variable at a time.

Was it worth it?

For this system, yes, and for three reasons that had nothing to do with MAF being new. Observability improved — OpenTelemetry with GenAI semantic conventions out of the box, which meant our traces went into the same backend as everything else and our evaluation harness could read them without an adapter. The workflow model gave us checkpointing we had previously hand-rolled badly, which turned an approval step from a scheduled-job kludge into a paused run. And the .NET and Python story converged, which for a shop with .NET services and a Python data platform removed an entire second architecture.

The costs were real too. Preview-era documentation lagged the samples in places even after 1.0, our multi-agent conversation logic needed genuine redesign rather than porting, and we spent a week on the state layer that we'd have spent eventually anyway. Call it three weeks of engineering for a system of moderate size, most of which produced artifacts — a domain layer, our own state tables, an eval suite, framework-agnostic filter tests — that outlive both frameworks.

If you're on Semantic Kernel today and asking whether to move: not urgently, and not blindly. Do the two decoupling steps now, because they're valuable independent of any migration and they shrink the eventual one to an adapter change. Then migrate when you have a reason — a feature you need, a model or hosting capability that only lands on the new path, or simply a quarter with room in it. The one thing I would not do is start a new agent on Semantic Kernel in 2026. That's a migration you're scheduling for later at full price.

What to carry away

Sort the pieces before estimating: native functions, prompt functions, connectors and telemetry are mechanical; filters and multi-agent behaviour are moderate; planners and bespoke memory connectors are where the timeline goes. Then do the two things that pay off regardless of whether you migrate — extract the domain layer out of framework wrappers, and move conversation history and durable facts into tables you own. After those, the migration is adapters plus middleware, written side by side with the originals.

Cut over per tenant behind a flag, with an evaluation suite as the arbiter rather than a subjective read, because you cannot diff two agents by looking at them. Write your filter behaviour as framework-agnostic tests first so they act as the specification. And change one variable at a time — a framework migration and a model upgrade in the same release will cost you a day of arguing about which one broke it.