- Published on
State of the Agent
- Authors

- Name
- Benjamin Lee
The demo always works. It is the ninth step, the one nobody watched, that decides whether you have a product.
Watch an agent solve a task for the first time and it is hard not to be charmed. Hand a large language model a fistful of tools, point it at a problem and let it think out loud: observe, reason, act, repeat. The ReAct loop is clean, legible and faintly miraculous. On a slide it never fails.
Then you point it at something real.
The agent invents a tool that does not exist. It circles the same dead end four times. It sails through step eight and then some downstream service falls over, and there is no way to rewind to step seven and try again. A human ought to sign off on an intermediate result before the thing barrels onward—but nowhere to hang that check. Two agents are meant to cooperate, yet share nothing between them. The charm curdles into a support ticket.
LangGraph, an agent-orchestration framework from the makers of LangChain, is a considered answer to all of this. It models an agent as an explicit graph: nodes are functions, edges are control flow, and state is a typed object that threads through the lot. Cycles, branching, checkpointing, human-in-the-loop—all fall out of that single design choice rather than being bolted on afterwards. It is less glamorous than the demo. That is rather the point.
Why the tidy loop unravels
The basic ReAct loop works fine for toy demos. Give an LLM a list of tools, point it at a task, watch it reason its way to an answer. Clean. Simple.
The trouble is that production is not a demo. It is a long tail of failure modes—hallucinated tool calls, infinite loops, half-finished runs, the awkward moment when a person must intervene—and none of them announce themselves until real users arrive. What a serious system wants is not cleverness but legibility: control flow you can inspect, interrupt and resume. That is the gap LangGraph sets out to fill.
The parts, and how they fit
State
Everything in LangGraph flows through a state object. You define it as a TypedDict:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
tool_calls_made: int
requires_human_review: bool
add_messages is a reducer—it appends new messages rather than replacing the list. You can write your own reducers for any field that needs merge semantics instead of overwrite.
Nodes and edges
Nodes are plain Python functions that take state and return a partial state update:
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import ToolNode
llm = ChatAnthropic(model="claude-sonnet-4-6")
llm_with_tools = llm.bind_tools(tools)
def call_model(state: AgentState) -> AgentState:
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response], "tool_calls_made": state["tool_calls_made"] + 1}
tool_node = ToolNode(tools)
Edges connect the nodes. Conditional edges let the graph branch on the state:
from langgraph.graph import StateGraph, END
def route(state: AgentState) -> str:
last = state["messages"][-1]
if state["requires_human_review"]:
return "human_review"
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return END
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", tool_node)
graph.add_node("human_review", human_review_node)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", route)
graph.add_edge("tools", "agent")
graph.add_edge("human_review", "agent")
app = graph.compile()
Checkpointing
Here is the feature that earns its keep in production. Add a checkpointer and every state transition is persisted:
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-session-42"}}
# Run — can resume from any checkpoint
result = app.invoke({"messages": [HumanMessage("Analyze Q3 revenue data")]}, config)
# Resume after failure or pause
state = app.get_state(config)
app.invoke(None, config) # continues from last checkpoint
In production you swap MemorySaver for SqliteSaver or a Postgres-backed checkpointer. The interface is identical, which is the whole trick: the toy and the real thing look the same to your code.
A pattern worth stealing
Consider a two-phase agent that researches with tools, pauses for a human to approve, then synthesises a final report. It is the shape of a great many useful systems, and it maps cleanly onto the graph:
def should_review(state: AgentState) -> str:
# Require human sign-off after N tool calls or if flagged
if state["tool_calls_made"] >= 5 or state["requires_human_review"]:
return "human_review"
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "synthesize"
graph.add_conditional_edges("agent", should_review)
graph.add_node("synthesize", synthesis_node)
graph.add_edge("synthesize", END)
The human_review node interrupts the graph and surfaces the current state to a UI or a Slack approval flow. Once someone signs off, the graph resumes from that node. The agent waits, patiently, exactly where it was told to.
When not to bother
LangGraph is not the answer to every question, and reaching for it too early is its own kind of over-engineering.
| Scenario | Use |
|---|---|
| Single-turn Q&A with tools | create_react_agent (prebuilt) |
| Multi-step with retry logic | LangGraph basic graph |
| Long-running with checkpoints | LangGraph + persistent checkpointer |
| Human-in-the-loop required | LangGraph + interrupt |
| Multi-agent coordination | LangGraph multi-agent with shared state |
The dull discipline that pays
The biggest shift when moving from simple chains to LangGraph is accepting that state is explicit. You cannot hide it in closure variables or lean on message history alone. Everything the agent needs to know must live in the state object. It feels like overhead at first. It is also what makes the system observable, resumable and testable.
The second lesson: start with create_react_agent from langgraph.prebuilt. It covers 80% of agentic use cases with no boilerplate. Reach for the full graph API only when you need cycles with custom branching, checkpointing or multi-agent coordination.
LangGraph is not magic, and it does not pretend to be. It merely makes the control flow of an agent as explicit and inspectable as the rest of the code—which is the least glamorous thing you can do to a system, and very nearly the only thing that keeps it running.