Three weeks ago a client signed off on a multi-agent design built on AutoGen's group-chat abstractions. Two working prototypes, a reference architecture I'd written, a delivery plan. On October 1st Microsoft shipped the Microsoft Agent Framework in public preview and put both AutoGen and Semantic Kernel into maintenance mode โ bug fixes and security patches, no new features. Three weeks after that, LangChain and LangGraph both hit 1.0. In a single month the two default answers to "which agent framework" either got replaced by their own vendor or stabilized under a no-breaking-changes promise.
The client's question was the obvious one: do we rewrite? And answering it properly forced me to be precise about something I'd been sloppy on, which is where framework lock-in actually comes from. An agent framework is three separable things: an orchestration model, a runtime and hosting story, and a pile of integrations. The integrations are commodity, the runtime is replaceable, and the orchestration model is what you're actually marrying. Rewriting because an SDK changed is cheap. Rewriting because you encoded your control flow in someone's DSL is not.
So this is a comparison of Microsoft Agent Framework, AutoGen, Semantic Kernel, LangChain/LangGraph, and Azure Prompt Flow โ what each one actually is, which orchestration shape each imposes, what the migration really costs, and how to structure code so the next month like this one costs you a week instead of a quarter. Everything here is as of late October 2025, and given how the last month went, treat every version claim as perishable.
What just happened, in dates
- October 1, 2025 โ Microsoft Agent Framework enters public preview. It's an open-source SDK for Python and .NET that merges AutoGen's agent abstractions and multi-agent orchestration with Semantic Kernel's plugin model, enterprise connectors, and telemetry. Simultaneously, AutoGen and Semantic Kernel move to maintenance mode: they keep working, they stop growing.
- October 22, 2025 โ LangChain 1.0 and LangGraph 1.0 go generally available. LangChain 1.0 centres on a new
create_agentabstraction (supersedingcreate_react_agent) with a middleware model; LangGraph 1.0 is the durable runtime underneath it, and both come with a commitment to no breaking changes until 2.0. - Azure Prompt Flow โ no announcement, which is its own kind of signal. It's supported, it's in Azure AI Foundry's hub-based projects and Azure ML, and it is plainly not where Microsoft's agent investment is going.
If you want the honest one-line version of my advice before the detail: build new .NET or Azure-committed work on Microsoft Agent Framework and accept preview churn; build new Python work that needs durability on LangGraph 1.0; don't start anything new on Prompt Flow; and don't rewrite a working AutoGen system this quarter. The rest of this article is why.
The four orchestration shapes
Every one of these frameworks makes you express control flow in one of four shapes, and the shape is the decision. Pick the shape your problem actually has, because forcing a problem into the wrong shape is where agent projects go to die.
graph LR
subgraph DAG["1 ยท Static DAG โ you decide every step"]
D1["node: retrieve"] --> D2["node: prompt"] --> D3["node: parse"]
end
subgraph LOOP["2 ยท Single-agent tool loop โ model decides"]
L1["model"] --> L2{"needs a tool?"}
L2 -->|"yes"| L3["call tool"] --> L1
L2 -->|"no"| L4["answer"]
end
subgraph CONV["3 ยท Multi-agent conversation โ agents decide"]
C1["planner"] <--> C2["coder"]
C2 <--> C3["critic"]
C1 <--> C3
end
subgraph SM["4 ยท Graph state machine โ you decide the map,
the model decides the route"]
S1["triage"] --> S2{"route"}
S2 -->|"refund"| S3["refund node"]
S2 -->|"technical"| S4["diagnose node"]
S3 --> S5["human approval"]
S4 --> S5
S5 --> S6["resolve"]
end
The four shapes, in increasing order of how much autonomy you hand over โ except for shape 4, which is the interesting one because it isn't on that line at all. A graph state machine gives the model freedom to choose the path while you keep the map, which is why it's the shape that survives contact with compliance reviews. Prompt Flow does shape 1. Semantic Kernel and create_agent do shape 2. AutoGen does shape 3. LangGraph and MAF's workflows do shape 4.
What each framework actually is
Azure Prompt Flow โ a DAG authoring tool, not an agent framework
Prompt Flow is a visual and YAML-based tool for authoring directed-acyclic-graph LLM pipelines, with batch runs and built-in evaluation flows, deployable to a managed online endpoint. Nodes are LLM calls, prompt templates, or Python functions; you wire them together and you get a repeatable flow plus a decent evaluation story โ that evaluation story was genuinely ahead of its time and is still the best reason anyone has for using it.
Its limits are structural, not cosmetic. A DAG can't loop, so an agent that decides to call a tool again is outside the paradigm. The graph lives in YAML that a visual designer owns, which means your control flow diffs badly, tests awkwardly, and can't be refactored by ordinary means. And it's tied to the hub-based project shape in Azure AI Foundry rather than the newer project model. My call: if you have a working prompt flow doing non-agentic RAG, leave it alone. For new work, write the pipeline in plain Python and keep the one idea worth stealing โ the evaluation flow โ as a separate harness.
# flow.dag.yaml โ the Prompt Flow shape. Readable, and that's the problem:
# your control flow is configuration, so a loop or a conditional retry
# means leaving the paradigm rather than editing a function.
inputs:
question:
type: string
outputs:
answer:
type: string
reference: ${generate.output}
nodes:
- name: retrieve
type: python
source: {type: code, path: retrieve.py}
inputs:
question: ${inputs.question}
top_k: 8
- name: generate
type: llm
source: {type: code, path: answer.jinja2}
connection: aoai_connection
api: chat
inputs:
deployment_name: gpt-4o
temperature: 0
context: ${retrieve.output}
question: ${inputs.question}
AutoGen โ the multi-agent conversation lab
AutoGen is a multi-agent framework where work happens through conversation between specialized agents โ a planner talks to a coder, a critic reviews, a group-chat manager decides who speaks next. The 0.4 rewrite earlier in 2025 turned it into an async, event-driven actor architecture with a layered design (a low-level core, the AgentChat API on top, and extensions), which was a real engineering improvement over the 0.2 line.
What it's genuinely great at is exploring multi-agent patterns fast. `SelectorGroupChat` and friends let you test whether a planner-critic split beats a single agent in an afternoon. What it's not great at is being the thing you operate: conversation-driven control flow is hard to bound, hard to make deterministic, and hard to explain to a risk committee. And as of three weeks ago it's in maintenance mode, so its ceiling is now fixed.
Semantic Kernel โ the enterprise SDK
Semantic Kernel is a model-orchestration SDK built around a kernel, plugins (native functions and prompt functions), filters, and connectors, with a .NET-first heritage that shows in the good ways: dependency injection, structured telemetry, and an API that a C# enterprise team recognizes immediately. Its agent abstractions arrived later and always felt like an addition rather than the core idea.
It's also in maintenance mode now. If you have a production Semantic Kernel system, it keeps working and keeps getting security fixes; the plugin model you built is also the thing that ports most cleanly to MAF, since MAF inherited it deliberately.
Microsoft Agent Framework โ the convergence
Microsoft Agent Framework (MAF) is Microsoft's unified open-source agent SDK for Python and .NET, combining AutoGen's agent and multi-agent orchestration abstractions with Semantic Kernel's plugin model, connectors, and enterprise observability โ plus a typed, checkpointable workflow model for graph-shaped orchestration. Public preview as of October 1st.
The pieces that matter to me as an architect, in order:
- Workflows โ the graph shape (shape 4 above), with typed edges and checkpointing, which is what makes long-running and resumable agent processes expressible without hand-rolling a state machine.
- OpenTelemetry-based observability out of the box, following the emerging GenAI semantic conventions. This is the difference between an agent you can debug in production and one you can only re-run and hope.
- Two languages, one model. For enterprises where the data platform is Python and the line-of-business services are .NET, this genuinely reduces the number of architectures you maintain.
- MCP and agent-to-agent interop as first-class concerns rather than adapters โ worth reading alongside what MCP actually standardizes, because MCP is what makes tools portable across every framework on this list.
- A hosting path into Azure AI Foundry's Agent Service when you want identity, thread persistence, and traces managed rather than built.
The catch is the obvious one: it's preview. APIs will move, the docs are ahead of the samples in places, and the migration guides from AutoGen and Semantic Kernel are the thinnest part of the package right now. I'd build on it today for work shipping in a quarter or two, with the shielding pattern below. I wouldn't sign a fixed-price contract that depends on its API surface staying still.
# Microsoft Agent Framework, preview as of October 2025 โ expect API churn.
# The shape to notice: a chat client, an agent with instructions and tools,
# and plain Python functions as tools. That last part is the portable bit.
import asyncio
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
def get_claim_status(claim_id: str) -> str:
"""Look up the current status of an insurance claim by its id."""
return claims_api.status(claim_id) # your own code, no framework in it
async def main() -> None:
client = AzureOpenAIChatClient(
credential=AzureCliCredential(), # managed identity in production
model_id="gpt-4o",
endpoint="https://my-aoai.openai.azure.com/",
)
agent = ChatAgent(
chat_client=client,
name="ClaimsAssistant",
instructions=(
"Answer questions about claim status. Use get_claim_status for any "
"claim-specific fact. Never estimate a payout amount."
),
tools=[get_claim_status],
)
result = await agent.run("What's the status of claim AC-8814?")
print(result.text)
asyncio.run(main())
LangChain 1.0 and LangGraph 1.0 โ durability as the product
LangGraph is a durable runtime for stateful agent graphs: nodes and edges you define, a persisted state object, checkpointers that let a run survive a process restart, and interrupts that pause execution for human input and resume it later. LangChain 1.0 sits on top of it as the model, tool, and message abstraction layer, with create_agent as the one-liner entry point and a middleware model for hooking the lifecycle around model and tool calls.
Durable execution is the differentiator, and it's the feature that most teams underestimate until the first time an agent process dies four minutes into a nine-minute task. Checkpointing plus interrupts is also the cleanest way I know to implement real human approval inside an agent โ the run stops, a human decides, the run continues from exactly where it was, without you inventing a job queue and a resume protocol. If your agent touches money, health data, or anything irreversible, that capability isn't a nice-to-have.
The trade-off is that LangGraph's state schema and graph definition are your application's structure. That's the lock-in that matters, and it's a fair trade when you're using what it gives you. It's a bad trade if you're using LangGraph to run a single tool-calling agent that a fifty-line loop would handle โ a mistake I've made and paid for in debugging someone else's abstractions.
# LangChain 1.0 / LangGraph 1.0: create_agent plus a checkpointer.
# The checkpointer is the whole reason to be here โ it makes the run resumable
# and gives you a real human-in-the-loop pause instead of a fake one.
from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver
def issue_refund(claim_id: str, amount_cents: int) -> str:
"""Issue a refund. Irreversible โ requires human approval upstream."""
return payments.refund(claim_id, amount_cents)
with PostgresSaver.from_conn_string(PG_CONN) as checkpointer:
checkpointer.setup()
agent = create_agent(
model="azure_openai:gpt-4o",
tools=[get_claim_status, issue_refund],
system_prompt="Resolve claim questions. Refunds need approval.",
checkpointer=checkpointer, # durable: survives a restart
)
# thread_id is the durability key: same id resumes the same conversation
cfg = {"configurable": {"thread_id": "claim-AC-8814"}}
for chunk in agent.stream({"messages": [("user", "Refund claim AC-8814")]}, cfg):
print(chunk)
Side by side
| Framework | Shape | Languages | Durability | Status, Oct 2025 | Reach for it when |
|---|---|---|---|---|---|
| Azure Prompt Flow | Static DAG (YAML + designer) | Python | None โ stateless flow runs | Supported, no investment signal | You already have one doing non-agentic RAG |
| AutoGen | Multi-agent conversation | Python (.NET partial) | Limited; you persist state yourself | Maintenance mode | Exploring multi-agent patterns fast |
| Semantic Kernel | Single-agent tool loop + plugins | .NET, Python, Java | You build it | Maintenance mode | You already run it in production |
| Microsoft Agent Framework | Tool loop + multi-agent + typed workflows | Python, .NET | Workflow checkpointing | Public preview (Oct 1) | New Azure/.NET work; you need both languages |
| LangGraph 1.0 | Graph state machine | Python, JS/TS | First-class โ checkpointers, interrupts, resume | GA (Oct 22), no breaking changes to 2.0 | Long-running, resumable, human-approval flows |
| LangChain 1.0 | Tool loop via create_agent + middleware | Python, JS/TS | Inherited from LangGraph | GA (Oct 22) | Fast start, model portability, big integration set |
What actually locks you in
Here's the audit I did on the client's AutoGen prototype to answer "do we rewrite," and I'd run the same one on any agent codebase. Sort every line of code into four buckets by how expensive it is to move:
| What | Portability | Why |
|---|---|---|
| Tool implementations (the function that calls your API) | Free | Plain functions. Every framework wraps them; none owns them โ if you wrote them that way. |
| Prompts and instructions | Nearly free | Strings and templates. Moving them is copy-paste plus retesting. |
| Model and provider config | Cheap | Every framework has a client abstraction; you rewrite the initialization. |
| Observability wiring | Medium | Cheap if it's OpenTelemetry, expensive if it's a framework-proprietary callback system. |
| Orchestration graph + state schema | Expensive | This is the application. Different frameworks disagree about what state is. |
| Persisted memory / checkpoint format | Expensive, and worse: it has data | You can rewrite code. You can't easily re-shape a million rows of in-flight agent state. |
The conclusion writes itself: keep the top four buckets fat and the bottom two thin. Concretely, the pattern I now insist on โ plain domain functions with docstrings, zero framework imports, and a thin adapter layer per framework that registers them. It looks like over-engineering for exactly as long as it takes for a vendor to reorganize their SDKs.
# tools/claims.py โ the domain layer. No framework imports. Ever.
# Testable with pytest, callable from a script, wrappable by anything.
from dataclasses import dataclass
@dataclass(frozen=True)
class ClaimStatus:
claim_id: str
state: str
updated: str
def get_claim_status(claim_id: str) -> ClaimStatus:
"""Look up the current status of an insurance claim by its id."""
row = claims_db.fetch_one(claim_id)
return ClaimStatus(claim_id, row["state"], row["updated_at"].isoformat())
# adapters/maf.py โ framework at the edge, ~10 lines
from agent_framework import ChatAgent
from tools.claims import get_claim_status
def build_agent(chat_client) -> ChatAgent:
return ChatAgent(chat_client=chat_client, name="ClaimsAssistant",
instructions=INSTRUCTIONS, tools=[get_claim_status])
# adapters/langgraph.py โ the same domain function, different host
from langchain.agents import create_agent
from tools.claims import get_claim_status
def build_agent(checkpointer):
return create_agent(model="azure_openai:gpt-4o", tools=[get_claim_status],
system_prompt=INSTRUCTIONS, checkpointer=checkpointer)
The same argument applies to the transport: expose your tools over MCP and any framework can consume them, including the one that replaces the one you picked. That's the most durable hedge available right now, and it costs almost nothing to adopt.
So which one do I pick?
graph TD
START{"Does the work need to survive
a restart or pause for a human?"} -->|"yes"| DUR["LangGraph 1.0
checkpointers + interrupts, GA today"]
START -->|"no"| STACK{"What does your team ship in?"}
STACK -->|".NET, or .NET + Python"| MAF["Microsoft Agent Framework
accept preview churn"]
STACK -->|"Python, Azure-committed"| MAF2{"Shipping this quarter?"}
MAF2 -->|"yes, with an SLA"| SK["Semantic Kernel or plain SDK calls
then migrate to MAF"]
MAF2 -->|"no, next quarter+"| MAF
STACK -->|"Python, cloud-agnostic"| LC["LangChain 1.0 create_agent
LangGraph when it gets stateful"]
SHAPE{"Is it really an agent,
or a pipeline?"} -.->|"pipeline: no loop, no tool choice"| PLAIN["Plain Python + an eval harness.
Not Prompt Flow. Not a framework."]
START -.-> SHAPE
The dotted branch is the one I use most. A large share of "agent" projects are pipelines with a language model in them โ no loop, no tool selection, no autonomy โ and they are cheaper, faster, and more reliable as three hundred lines of Python with a test suite. Reach for an agent framework when the model genuinely needs to choose what happens next.
And the migration question specifically, since that's what started this: a working AutoGen system in maintenance mode is not an emergency. Maintenance mode means security fixes continue; it doesn't mean the code stops running. Rewriting a working system onto a preview framework trades known risk for unknown risk, on someone else's schedule. What I told the client: keep the prototype, extract the tools into framework-free modules this sprint (a week of work, useful regardless), build the next agent on MAF, and revisit porting the first one when MAF hits GA and its migration guides are more than a page. If the multi-agent conversation pattern turns out to be load-bearing, that's the piece to port last and test hardest, because it's the piece the two frameworks model most differently.
Two traps, one of which just cost me real time. First: the preview trap. Public preview means no SLA, API churn, and a support story that doesn't exist yet. I've had a preview SDK's breaking change eat two days in a delivery week โ that's survivable on internal work and unacceptable on a fixed-price milestone. Decide which one you're on before you pick, and if it's the latter, ship on something GA and migrate later. Second, and bigger: don't let the framework own your state. Conversation history, agent memory, and in-flight workflow position are your application's data. When they live only inside a framework's checkpoint format, "we'll switch frameworks" becomes a data-migration project rather than a refactor, and the estimate stops being a week. Persist the state you'd need to reconstruct a session in a schema you control, even when the framework also persists its own.
What to carry away
The orchestration model is the commitment; everything else is plumbing. Four shapes exist โ static DAG, single-agent tool loop, multi-agent conversation, and graph state machine โ and the last one is where serious production work has been landing, because it lets the model choose the route while you keep the map. Prompt Flow is shape one and I wouldn't start there. AutoGen is shape three and is now capped. Semantic Kernel is capped too, but its plugin model is the part that survives, inside MAF.
Microsoft Agent Framework is the right bet for new Azure and .NET work if you can absorb preview churn โ the convergence is genuine, the workflow model and OpenTelemetry story are the substantive parts, and Microsoft has now committed its agent future to it. LangGraph 1.0 is the right bet when durability is the requirement: checkpointing and interrupts are the cleanest human-approval mechanism available, and it went GA with a stability promise this month. If your "agent" is actually a pipeline, write the pipeline.
Whatever you choose, structure the code so that next month's announcement is an adapter change: domain tools as plain functions with no framework imports, tools exposed over MCP where you can, state persisted in a schema you own, and telemetry through OpenTelemetry rather than a proprietary callback. October made the argument for me โ two of the four frameworks in this article changed status in three weeks. The teams that shrugged were the ones whose agents were mostly their own code.