LangGraph, OpenAI Agents SDK or Claude Agent SDK: Match the Primitive to the Work
Four sample workloads, voice, durable refunds, code migration and MCP ops, show which agent framework fits when, with notes on lock-in, durability and realtime.
This article is published in English.
Sources: The Best Agent Framework in 2026: LangGraph vs OpenAI Agents SDK vs Claude Agent SDK
Comparisons in 2026 keep returning to three agent frameworks, and each one is built on a different core idea. LangGraph is a graph runtime in which nodes are plain functions and edges decide transitions. The OpenAI Agents SDK revolves around handoffs, where an agent passes work to another through typed sessions, with MCP supported natively. The Claude Agent SDK is the unexpected one: it hands the agent a computer, because Anthropic built it on bash execution, file writes and hooks, convinced that a capable agent needs a genuine shell.
The right pick hinges on something architecture comparisons rarely ask: what kind of work is the agent doing?
The three primitives
LangGraph is aimed at stateful workflows. A state schema is declared, and one typed object moves through the graph from the first node to the last. Because checkpointers save that state at each node, a run can pick up after a crash or after waiting on a human approval with its context intact. The create_agent call that arrived with LangChain 1.0, generally available on October 22, 2025, is only a facade over this runtime, which ends the old LangChain-or-LangGraph debate.
The OpenAI Agents SDK has nine core building blocks, among them Agents, Handoffs, Tools, Guardrails, Sessions and Realtime. Its atomic unit is one agent handing a task to another. Provider independence arrived in 2025 with support for more than 100 models through LiteLLM. MCP is a first-class tool type, and voice ships natively with the gpt-realtime-2 family of models.
The Claude Agent SDK centers on a shell environment. Its toolbox contains Bash, Read, Write, Edit, Glob and Grep, along with lifecycle hooks named PreToolUse, PostToolUse and Stop, and subagents run in separate, isolated context windows. Anthropic changed the name from Claude Code SDK in late 2025 because the harness also powers deep research, video creation and note-taking, not only coding. Only Claude models are allowed, though Bedrock, Vertex and Azure Foundry can host it.
A belief from 2025 to drop
A common assumption from last year says the OpenAI Agents SDK forces OpenAI models. That is about fifteen months out of date. The official repository supports the OpenAI Responses API along with roughly 100 other models through LiteLLM and Any-LLM integrations.
# Install: pip install "openai-agents[litellm]"
# Env: export GEMINI_API_KEY=...
import os
from agents import Agent, Runner, function_tool
from agents.extensions.models.litellm_model import LitellmModel
@function_tool
def current_time_utc() -> str:
"""Return the current UTC time as an ISO-8601 string."""
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat(timespec="seconds")
# OpenAI Agents SDK using Gemini via LiteLLM. No OpenAI key required.
gemini_model = LitellmModel(
model="gemini/gemini-2.5-pro",
api_key=os.environ["GEMINI_API_KEY"],
)
agent = Agent(
name="time-agent",
instructions="Answer time questions using the current_time_utc tool.",
model=gemini_model,
tools=[current_time_utc],
)
result = Runner.run_sync(agent, "What is the current UTC time?")
print(result.final_output)
# -> "The current UTC time is 2026-07-06T14:32:11+00:00."
Creating LitellmModel with a Gemini model name shows the framework routing outside the OpenAI ecosystem, and the @function_tool decorator behaves identically whatever model sits underneath. Non-OpenAI paths are documented as best-effort, with feature parity varying by provider. With strict model limits off the table, the frameworks compete on their architectural primitives.
A test company with four agents
Take a mid-size fintech, call it Meridian, with four agents under construction in separate teams.
- A voice support agent that handles billing questions by phone.
- A multi-agent refund workflow that checks fraud rules before passing the ticket to finance.
- An internal code migration agent that walks a large Java repository, applies syntax transformations and opens pull requests.
- An ops dashboard that queries Sentry, Linear, PagerDuty and Postgres through MCP servers.
Following the four builds shows which framework fits each.
Workload one: voice and realtime streaming
The voice team works to a hard latency budget, under 500 ms for streaming, and user barge-in has to be handled. A speech-to-text step feeding a text model feeding text-to-speech is too slow, since every network hop adds delay.
The OpenAI Agents SDK fits. The gpt-realtime-2 model streams audio tokens natively, and RealtimeAgent wraps that capability. Neither LangGraph nor the Claude Agent SDK offered a native realtime primitive at this level in mid-2026.
# Install: pip install openai-agents
# Env: export OPENAI_API_KEY=...
import asyncio
from agents import function_tool
from agents.realtime import RealtimeAgent, RealtimeRunner
@function_tool
def lookup_billing_balance(account_id: str) -> str:
"""Return the current outstanding balance for an account."""
# In production, this hits the billing service. Here it is a stub.
return "42.17 USD outstanding as of 2026-07-06."
voice_agent = RealtimeAgent(
name="meridian-billing-voice",
instructions=(
"You are Meridian's billing voice assistant. Answer politely, briefly. "
"Confirm the account_id before disclosing any balance."
),
tools=[lookup_billing_balance],
)
async def main():
runner = RealtimeRunner(
starting_agent=voice_agent,
config={"model_settings": {"model_name": "gpt-realtime-2.1"}},
)
# session handles the audio stream and tool calls
session = await runner.run()
async with session:
# Wire the audio input source here via sounddevice or pyaudio
async for event in session:
if event.type == "history_updated":
# The item contains the finalized transcript once the turn ends
print(f"History updated with item: {event.item}")
elif event.type == "error":
print(f"Error: {event.error}")
break
asyncio.run(main())
RealtimeAgent and RealtimeSession look after the incoming and outgoing audio streams as well as the tool-calling loop, and tools declared with @function_tool run in a realtime session just as in a text agent. The framework surfaces the event loop while WebSocket state stays out of sight.
Doing this in LangGraph or the Claude Agent SDK would mean custom WebSocket code, voice activity detection and tricky interrupt semantics, which turns an agent project into a media pipeline project. If company policy requires Claude or Gemini for the voice model, the answer changes. Anthropic added realtime capabilities later, so anyone reading after mid-2026 should check feature parity.
Workload two: durable orchestration with human approval
The refund flow requires a fraud check followed by a finance approval handoff, a wait for human review on refunds above $500, and a final decision written back to Postgres. It must survive server crashes and handle asynchronous human pauses without losing context.
LangGraph fits. Checkpointers combined with the interrupt primitive give exactly the control flow that long-running business processes need. Production teams support the architecture: Uber reports saving roughly 21,000 developer hours using LangGraph for its AutoCover and Validator systems, and LinkedIn published a paper on a hierarchical semantic memory tree for its Hiring Assistant, with its supervisor and subagent design covered separately in LangChain engineering talks.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from langchain_google_genai import ChatGoogleGenerativeAI
# LangGraph is provider-agnostic. Here it uses Gemini.
llm = ChatGoogleGenerativeAI(model="gemini-2.5-pro", temperature=0)
class RefundState(TypedDict):
order_id: str
amount_usd: float
customer_reason: str
fraud_risk: Literal["low", "medium", "high"] | None
finance_decision: Literal["approved", "denied"] | None
human_review_needed: bool
def fraud_check(state: RefundState) -> RefundState:
"""Run the LLM-backed fraud check against the customer's stated reason."""
prompt = (
f"Assess fraud risk for refund of ${state['amount_usd']:.2f}. "
f"Customer reason: {state['customer_reason']!r}. "
"Respond with one word: low, medium, or high."
)
verdict = llm.invoke(prompt).content.strip().lower()
if verdict not in {"low", "medium", "high"}:
verdict = "high" # fail-closed on ambiguous LLM output
return {**state, "fraud_risk": verdict}
def finance_approval(state: RefundState) -> RefundState:
"""Above $500 or medium risk, pause for a human. Otherwise auto-approve."""
needs_human = state["amount_usd"] > 500 or state["fraud_risk"] in {"medium", "high"}
if needs_human:
# Pause the graph. On resume, interrupt returns the human's decision.
human_decision = interrupt({
"order_id": state["order_id"],
"amount_usd": state["amount_usd"],
"fraud_risk": state["fraud_risk"],
"prompt": "Approve (yes/no)?",
})
return {**state, "human_review_needed": True, "finance_decision": human_decision}
return {**state, "human_review_needed": False, "finance_decision": "approved"}
# Build the graph
graph = StateGraph(RefundState)
graph.add_node("fraud_check", fraud_check)
graph.add_node("finance_approval", finance_approval)
graph.add_edge(START, "fraud_check")
graph.add_conditional_edges(
"fraud_check",
lambda s: "finance_approval" if s["fraud_risk"] != "high" else END,
)
graph.add_edge("finance_approval", END)
# Checkpointer. For production, swap MemorySaver for PostgresSaver.
compiled = graph.compile(checkpointer=MemorySaver())
# Run it. Interrupt fires on the $850 refund and the graph pauses.
config = {"configurable": {"thread_id": "order-4291"}}
result = compiled.invoke(
{
"order_id": "4291",
"amount_usd": 850.00,
"customer_reason": "arrived damaged, no photo",
"fraud_risk": None,
"finance_decision": None,
"human_review_needed": False,
},
config=config,
)
# Later, a human reviewer says yes. Resume with Command.
final = compiled.invoke(Command(resume="approved"), config=config)
The pause-and-resume pattern shows durable execution in code. Calling interrupt() inside the finance approval node writes the current state to the checkpointer, and the system may shut down at that point. Hours later, a Command with resume set to approved continues from the paused spot under the same thread ID, without re-running the fraud check.
A durability caveat applies. The open-source runtime saves state in MemorySaver, which lives in process memory. Real crash recovery under load needs LangGraph Platform backed by Postgres, or a custom PostgresSaver.
The OpenAI Agents SDK provides Sessions for conversation state and Sandbox Agents for long-horizon work, but nothing equal to node-level checkpointing across an arbitrary graph. The Claude Agent SDK keeps JSONL session state locally, or uses Managed Agents for hosted durability at the cost of Claude-only models. For now LangGraph owns durable orchestration.
Workload three: file-and-shell work
The migration agent from the developer experience team must traverse a 400,000-line Java repository, apply syntax changes, run unit tests and open pull requests. That means reading and editing files, running shell commands and reacting to compiler errors. Bash and Edit are nearly the whole workload.
The Claude Agent SDK maps onto this directly, since Anthropic's thesis is to give agents a computer. It has native tools for Bash, Read, Write, Edit, Glob and Grep, and its hooks act as a control surface. A PreToolUse hook can enforce guardrails against destructive commands, and a PostToolUse hook can run formatters or linters. Subagents split the transformation work across isolated contexts, so a long compiler error log handed to a subagent does not pollute the main agent's window.
# Install: pip install claude-agent-sdk
# Env: export ANTHROPIC_API_KEY=...
import anyio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
AgentDefinition,
HookMatcher,
)
# PreToolUse hook: block Bash calls that look like rm -rf
async def block_dangerous_bash(input_data, tool_use_id, context):
if input_data.get("tool_name") == "Bash":
cmd = input_data.get("tool_input", {}).get("command", "")
if "rm -rf" in cmd or "rm -rf" in cmd:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf blocked by policy",
}
}
return {}
# Subagent: runs in isolated context to lint one file
lint_agent = AgentDefinition(
description="Run linters on a single file and return a concise report.",
prompt=(
"You are the lint subagent. Given a file path, run the project's linter "
"on it and return a one-paragraph summary of failures. Do not fix anything."
),
tools=["Bash", "Read"], # Note: tools is deprecated in favor of skills in recent SDKs
)
options = ClaudeAgentOptions(
system_prompt=(
"You are Meridian's code migration agent. Walk the target directory, "
"apply the migration, run tests, and open a PR. Prefer small commits."
),
allowed_tools=["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
hooks={"PreToolUse": [HookMatcher(hooks=[block_dangerous_bash])]},
agents={"lint": lint_agent},
# resume="mig-run-2026-07-06-01", # uncomment to resume a prior session
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query(
"Migrate services/payments/ from Java 17 to Java 21. "
"For every file you touch, delegate to the `lint` subagent afterward. "
"Do NOT commit or open PRs yet. Stop after changes are on disk."
)
async for message in client.receive_response():
print(message)
anyio.run(main)
The allowed_tools list sets the agent's environment. Hooks give a guardrail surface that sits inside the agent loop and not around it: a PreToolUse hook runs before a tool does, and a deny decision stops the call immediately. The lint subagent carries its own tools and context window. Recent SDK releases deprecate the tools field on AgentDefinition in favor of skills, though it keeps working. The commented resume line illustrates restoring session state from JSONL files on local disk.
In LangGraph these shell primitives would have to be written as custom Python functions. The OpenAI SDK's Sandbox Agents run in containers, which is similar in architecture, but the primitive set is not named or exposed in the same file-centric way. The catch is model lock-in. A team with a firm rule against lock-in at the model layer cannot use the Claude Agent SDK, and falls back to LangGraph while rebuilding the bash and edit primitives itself.
Workload four: MCP-heavy orchestration
The platform team's dashboard answers a single question typed by an engineer. Behind it the agent pulls errors from Sentry, tickets from Linear, incidents from PagerDuty and user records from Postgres. Nearly all its effort goes to coordinating third-party MCP servers, and very little to composing the answer.
Here the OpenAI Agents SDK and the Claude Agent SDK come out level. In both, MCP is a primary building block and servers behave like local functions. LangGraph can use MCP as well, but in mid-2026 doing so takes more boilerplate, and teams commonly wrap servers into tools themselves. The tiebreaker between the two leaders is a secondary constraint.
- Choose the OpenAI Agents SDK for first-party tracing, strict guardrails and handoffs to specialist sub-agents, or when routing simple synthesis tasks to cheaper providers through LiteLLM is attractive.
- Choose the Claude Agent SDK for the hook lifecycle, strict subagent isolation and Claude's tool-use reliability.
Custom API wrappers for common SaaS tools eat engineering time. MCP standardizes context and tool discovery, so the framework only has to stay out of the way, and both SDKs support the pattern natively.
Summary by workload
- Realtime voice with barge-in: OpenAI Agents SDK.
- Durable multi-agent flows with human approval: LangGraph.
- File, shell and code-migration work: Claude Agent SDK.
- Heavy MCP orchestration: OpenAI Agents SDK or Claude Agent SDK.
A note on CrewAI
Many readers expect CrewAI in a comparison like this one. On checking, the evidence for its current version, Flows adoption and enterprise case studies was not as solid as for the other three, so it was left out instead of padding the piece with thinner sourcing.
The real choice
Four agents ended up on three frameworks: the voice and platform agents on the OpenAI SDK, the refund flow on LangGraph and the migration agent on the Claude Agent SDK. Arguing about which framework wins is the wrong frame. The job is to align a framework's central primitive with the shape of the work.
The next round will probably reopen the argument over durability as LangGraph Platform competes with Anthropic's Managed Agents. LangGraph may gain first-class realtime primitives, or Anthropic may open its SDK to other model providers. Until then the primitives dictate the architecture.