Choosing an agent framework, or writing the loop yourself

Pull-quote: “Microsoft’s own agent documentation says it plainly: if you can write a function to handle the task, do that instead of using an AI agent.”
The agent loop is about twenty lines of Python, and every agent framework on the market contains a version of it. That fact changes the question. You are not choosing between writing an agent and buying one. You are choosing whether to buy durable execution, persistence, multi-agent orchestration, and a tracing integration, and paying for them in control and upgrade cost.
This post is for the engineer asked to pick a framework before the team knows what it is building. It gives you the loop so you can see the size of the thing being abstracted, a verified account of what each current option provides according to its own documentation, an honest list of what you hand over, and a decision procedure that frequently ends with “write the loop”.
By the end you will be able to run a working agent loop, name the capability each major framework adds on top of it, predict the debugging and upgrade cost of adopting one, and defend either choice to a team that has already picked a side.
What you will be able to do
- Write a working agent loop with tool dispatch in under thirty lines, and know how to tell it is running correctly.
- State what LangGraph, LangChain, LlamaIndex, CrewAI, Microsoft Agent Framework, Pydantic AI, and the OpenAI Agents SDK each add.
- Name the four things you give up in control, and the debugging symptom each one produces.
- Estimate upgrade cost using the published migration history of the framework you are considering.
- Run a five-question procedure that returns a defensible answer rather than a preference.
How much code is the agent loop, really?
The loop is short: call the model with the conversation and the tool schemas, append the reply, execute any tool calls it emitted, append the results, repeat until the model replies without a tool call. Here it is against an OpenAI-compatible chat completions endpoint, with nothing removed.
import json
def run(client, model, messages, tools, handlers, max_steps=12):
for _ in range(max_steps):
reply = client.chat.completions.create(
model=model, messages=messages, tools=tools, tool_choice="auto"
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
return reply.content
for call in reply.tool_calls:
try:
args = json.loads(call.function.arguments)
result = handlers[call.function.name](**args)
except Exception as exc:
result = {"error": f"{type(exc).__name__}: {exc}"}
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result)[:4000],
})
raise RuntimeError(f"no final answer after {max_steps} steps")
You know it works when a question requiring the tool produces exactly two model calls: the first returns a tool call and no content, the second returns content and no tool calls. If you only ever see one call, your tool schemas are not reaching the model, which is usually a serialisation problem rather than a prompting one.
Three details there are the ones people get wrong. Tool errors are returned to the model as content rather than raised, because a model that sees the error message can often correct its own arguments. Tool results are truncated, because one unbounded result can consume the entire context window. The step limit raises rather than returning quietly, because a loop that exits at its cap and returns whatever it has is how you ship a wrong answer with no signal.

What does each framework actually give you?
Each one bundles the same loop with a different centre of gravity: orchestration, retrieval, role-based collaboration, enterprise integration, or type safety. The table states what each provides according to its own current documentation.
| Option | What it adds on top of the loop | What you hand over |
|---|---|---|
| LangGraph | Durable execution, persistence, streaming, human-in-the-loop interrupts, mixed deterministic and model-driven steps in one graph | Control flow expressed as a graph, plus a state and checkpoint model |
| LangChain | A configurable harness through create_agent, extended by middleware for context, planning, and delegation |
Prompt and message assembly, tool binding, provider abstraction |
| LlamaIndex | Prebuilt agents including FunctionAgent and AgentWorkflow, on top of a retrieval and indexing stack |
Query engines and index abstractions as your data path |
| CrewAI | Role-based agent teams called Crews, inside event-driven stateful workflows called Flows | Agent identity as roles and goals, and the delegation policy between them |
| Microsoft Agent Framework | Agents, an opinionated harness, and graph-based workflows with typed routing, checkpointing, and human-in-the-loop | The Microsoft agent and workflow object model across .NET, Python, and Go |
| Pydantic AI | The loop plus typed validated outputs, dependency injection, durable execution, and tool approval gates | Agent structure expressed through types and composable capabilities |
| OpenAI Agents SDK | A small primitive set: agents, handoffs, guardrails, sessions, and built-in tracing | Turn management, tool execution, and session state |
LangGraph is the lowest-level of the group and says so: its documentation calls it a low-level orchestration framework and runtime for long-running stateful agents, and notes it can be used without LangChain at all (LangGraph overview). Its distinguishing capability is mixing deterministic hand-coded steps with model-driven steps inside one graph, which fits when part of your process must be auditable and part must be flexible. LangChain is the framework layer above it: an agent is created with create_agent and configured through middleware, with prebuilt middleware for summarisation, memory, filesystem access, planning, and subagent delegation (LangChain agents documentation).
LlamaIndex comes at agents from retrieval. Its documentation describes an agent as software given a task and a set of tools, where a tool can be an arbitrary function or a full query engine, and offers FunctionAgent for tool calling and AgentWorkflow for managing multiple agents, alongside ReActAgent and CodeActAgent (LlamaIndex building an agent). Choose it when the agent’s main job is answering over your corpus.
CrewAI separates the two concerns explicitly. Flows are structured event-driven workflows carrying state and control flow; Crews are teams of role-playing agents that collaborate on a delegated task. Its own production guidance is to start with a Flow and call a Crew only for steps that need autonomy (CrewAI introduction), advice worth taking literally, because it concedes that the autonomous-collaboration part is the risky part.
Microsoft Agent Framework is the successor to both AutoGen and Semantic Kernel, built by the same teams, combining AutoGen’s agent abstractions with Semantic Kernel’s session state, type safety, middleware, and telemetry, and adding graph workflows with typed routing and checkpointing (Microsoft Agent Framework overview). Note the same page’s guidance: if you can write a function to handle the task, write the function.
Pydantic AI ships the loop with type safety as the organising idea: typed outputs validated by Pydantic with automatic retry when validation fails, dependency injection for passing connections into tools, durable execution across restarts, and human-in-the-loop tool approval (Pydantic AI documentation). OpenAI Agents SDK keeps the primitive count deliberately small and draws the line for you, recommending the Responses API directly when you want to own the loop, tool dispatch, and state handling (OpenAI Agents SDK).

What do you actually give up in control?
You give up ownership of the message list, the retry semantics, the error surface, and the debugging path. Each produces a specific and recognisable symptom.
The message list. Most frameworks assemble the array sent to the model: system prompt, history, tool schemas, summarised context, injected memories. When quality drops, the first question is what the model actually received, and the answer now lives inside someone else’s assembly code. Log the outbound payload verbatim on every call from day one, before you need it.
Retry semantics. A framework decides what happens on a rate limit, a truncated response, a schema validation failure, or a tool exception. The defaults are usually reasonable and almost never match your cost model. The symptom is a bill that does not reconcile with your request count, because a validation failure quietly triggered three more generations.
The error surface. A stack trace ten frames deep inside an orchestration library tells you where the framework broke, not where your logic did. Time to diagnosis roughly doubles for anyone who has not read the framework’s source, which in practice means everyone except the person who chose it.
The upgrade path. Once your control flow is expressed in a framework’s object model, you inherit its release cadence and its API churn.
What does the upgrade cost look like over two years?
Look at each framework’s own migration history, because it is the most honest available evidence and it is published. Microsoft’s documentation states that Agent Framework is the direct successor to both Semantic Kernel and AutoGen, and links migration guides from each, so teams who standardised on either in good faith now have a migration in the backlog. The OpenAI Agents SDK describes itself as a production-ready upgrade of Swarm, its previous experiment. LangChain’s current agent entry point is create_agent configured by middleware, which is not the interface the same library presented two years earlier.
None of that is a criticism. The field is moving quickly and the frameworks are improving. It does mean adoption is a recurring commitment rather than a one-time decision, and the recurring cost is proportional to how much of your control flow lives inside their abstractions. Three practices keep it bounded.
- Pin exact versions and upgrade deliberately. Treat a framework major version like a database major version: scheduled, tested against your evaluation set, reversible.
- Keep domain logic outside the framework’s types. Tools should be plain functions your own code can call directly, wrapped at the boundary. If your business rules only run inside an agent, you cannot migrate and you cannot unit test.
- Own your traces. Emit OpenTelemetry spans from your own wrapper as well as using the framework’s tracing, so the observability you rely on during an incident does not disappear with the dependency.
When is a plain loop the correct answer?
When you are running one agent, in one process, with a bounded tool set, and no requirement to pause for hours and resume. Under those conditions a framework adds indirection and no capability, and two vendors say as much in their own documentation.
The threshold is four specific requirements. Durability: must a run survive a process restart. Pausing: must a run wait for a human decision that might arrive tomorrow. Concurrency between agents: do several agents need shared state rather than sequential calls. Fan-out orchestration: are there branching, joining, and retry-per-branch semantics. If none are true, the loop above plus your own code covers it.
Be honest about what you build regardless. A plain loop is not free, it is just yours.
- A per-tool timeout, so one slow call cannot hold the loop open indefinitely.
- Retry with backoff on rate limits and transient server errors, with a cap and a dead letter path.
- A context budget and a truncation policy, applied before the call rather than after a failure.
- Structured tracing of every request and response, with secrets redacted.
- A hard cap on tool result size, and a step limit that raises rather than returning silently.
- An evaluation harness, because you cannot change a prompt safely without one.
That is roughly a week of engineering, written once. Adopting a framework does not remove the list either, it changes which parts are configured rather than coded. The comparison is a week of your code against a permanent dependency, judged on whether you need durability, pausing, concurrency, or fan-out.

How do you decide?
Answer five questions in writing, in this order, and let the answers pick for you.
- Does a run need to survive a restart or a deployment? If yes, you need durable execution and persistence, which is LangGraph, Microsoft Agent Framework, or Pydantic AI territory. If no, the whole feature class is irrelevant to you.
- Does a run pause for a human and resume later? If yes, you need first-class interrupts and checkpointing rather than a queue you built yourself.
- Is the agent’s core job answering over your own corpus? If yes, start from a retrieval stack such as LlamaIndex rather than bolting retrieval onto an orchestration library.
- Do multiple agents need to coordinate, or is it one agent with several tools? Most teams that say multi-agent mean the latter. Read what actually makes something an agent before answering.
- What is your organisation’s existing platform commitment? A team already standardised on Azure and .NET gets real value from Microsoft Agent Framework that a Python-only team does not.
If questions one, two, and four are all no, write the loop. That is the common case, and it is not a compromise.
Where this goes wrong
A framework chosen before the requirements. The symptom is a graph with three nodes and one edge, or a Crew of four agents where one would do. The cause is picking the tool during the enthusiasm phase. The fix is to build the plain loop first as a spike, discover which capability requirements you actually hit, and adopt against evidence.
Multi-agent used where multi-tool was meant. The symptom is agents passing summaries to each other, losing detail at every hop, and a token bill several times the estimate. The cause is treating role-play as an architecture. The fix is one agent with well-designed tools, following tool design for agents, splitting only when context isolation is the real requirement.
Business logic living inside framework types. The symptom is that you cannot unit test a rule without instantiating an agent. The cause is writing domain logic in tool bodies rather than calling into your own modules. The fix is a thin adapter layer: tools that validate arguments and call plain functions.
No verbatim log of the outbound payload. The symptom is an argument about whether the model received the updated system prompt, settled by nobody. The cause is trusting framework tracing to show the final assembled request. The fix is to log the payload yourself at the transport boundary.
Upgrading on the framework’s schedule instead of yours. The symptom is an unrelated feature branch blocked by a breaking change in a transitive dependency. The cause is loose version pinning. The fix is exact pins, a scheduled upgrade window, and an evaluation set that runs before the merge.
Common questions
Is LangChain the same thing as LangGraph?
No. LangGraph is a low-level orchestration runtime for long-running stateful agents, providing durable execution, persistence, streaming, and human-in-the-loop, and its documentation notes it can be used without LangChain. LangChain is the framework layer above it, supplying model and tool integrations plus a configurable harness through create_agent. You can use either alone or both together.
Do I need a framework to use MCP or to call tools?
No. Tool calling is a model API feature: you send tool schemas with the request and the model returns structured calls for your code to execute. MCP is a separate open protocol for connecting to external tool servers, with client libraries that exist independently of any agent framework. Frameworks make both more convenient; neither requires one.
What is the actual difference between a workflow and an agent?
A workflow has its control flow decided in advance by you; an agent decides its next action at runtime. Microsoft’s guidance is a good rule: use an agent when the task is open-ended and needs autonomous tool use, and a workflow when the process has well-defined steps and you need explicit control over execution order. Most production systems are a workflow with one or two agentic steps inside it.
How many tools is too many for a plain loop?
The constraint is the model’s selection accuracy, not the loop. Beyond roughly a dozen tools, selection errors and context cost both rise, and the answer is grouping or routing rather than a framework. A framework does not improve tool selection; it gives you somewhere structured to put the routing logic you would have written anyway.
If I start with a plain loop, how painful is it to adopt a framework later?
Not very, if you kept tools as plain functions and prompts as data. Migration then means re-registering the same functions and moving your control flow, which is bounded. It becomes painful when domain logic was written inside tool bodies and prompts were assembled inline, because then the migration is a rewrite of code that has nothing to do with agents.
Next steps
Spend one day building the loop above with two real tools from your own system, and record which of the four capability requirements you hit. That day produces a better framework decision than a week of comparison reading, because the requirements arrive as facts rather than predictions.
Then read agent harness architecture for the components that sit around any loop regardless of who wrote it, and the agent failure taxonomy for the failure modes a framework does not prevent. If the vocabulary in the comparison table is doing more work than it should, the vocabulary of modern AI systems defines each term precisely.
Zorost runs both patterns in production, plain loops where the work is bounded and orchestration runtimes where a run has to survive a restart, and we publish what we test in the open in AI Fieldwork.
