Home / Articles / Practical notes: The Best Agent Framework in 2026: LangGraph vs OpenAI Agents

This article is published in English.

Practical notes: The Best Agent Framework in 2026: LangGraph vs OpenAI Agents

Operable walkthrough of Practical notes: The Best Agent Framework in 2026: LangGraph vs OpenAI Agents: contracts, checks, and drop-in code slots for teams shipping this pattern.

2343 words

This walkthrough rebuilds the path from raw materials to a working system for: The Best Agent Framework in 2026: LangGraph vs OpenAI Agents SDK vs Claude Agent SDK. The focus is operable steps, explicit checks, and code that you can drop into a repo without guessing intent. For Overview, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

The Three Primitives

When working through The Three Primitives, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

A 2025 Belief to Drop

When working through A 2025 Belief to Drop, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

# 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."

Introducing Meridian

When working through Introducing Meridian, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through Introducing Meridian, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Workload 1: Voice and Realtime Streaming

Workload 1: Voice and Realtime Streaming works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

# 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())

Workload 2: Durable Multi-Agent Orchestration with HITL

Workload 2: Durable Multi-Agent Orchestration with HITL works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

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)

Workload 3: Coding-Adjacent and File-and-Shell Centric

Workload 3: Coding-Adjacent and File-and-Shell Centric works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. Workload 3: Coding-Adjacent and File-and-Shell Centric works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

# 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)

Workload 4: MCP-Heavy Tool Orchestration

For Workload 4: MCP-Heavy Tool Orchestration, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

The Decision Matrix

For The Decision Matrix, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

On CrewAI

For On CrewAI, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For On CrewAI, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

The Real Choice

When working through The Real Choice, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

Operational checklist

Operational checklist works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope.

Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

Add a smoke test that exercises the critical path in CI with fixtures, not live paid APIs, whenever budgets allow.

Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation. Prefer boring reliability over clever one-off demos.

Batch note for 2c64e0b378d9: keep provider keys out of the repo, set a per-session token ceiling, and store transcripts next to the eval fixtures so later model swaps stay comparable.

hardening note 0 works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline.

Hardening detail 0/821: measure wall time, error class, and token spend for this note, then decide whether to keep the change based on a fixed question set rather than anecdote.

For hardening note 1, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Hardening detail 1/821: measure wall time, error class, and token spend for this note, then decide whether to keep the change based on a fixed question set rather than anecdote.