- Published on
State of Grace
- Authors

- Name
- Benjamin Lee
A demo agent that falls over loses nothing but face. A production one loses a customer. The difference is what happens to its memory when things go wrong.
Watch an agent demo and it looks effortless. The model reasons, calls a tool, reasons again, and arrives at an answer while the audience nods. Then the process crashes at step nine of eleven—a timeout, a dropped connection, a machine rebooted by an overzealous cloud provider—and the whole edifice evaporates. There is nothing to resume, because there was never anything to resume from. The state lived in memory, and memory is the first thing to die.
This is the quiet problem that separates a clever prototype from a system real users can lean on. An agent that cannot survive its own infrastructure is a parlour trick. LangGraph, an open-source framework for building stateful, multi-actor applications with large language models, is built around exactly this anxiety. Its core abstraction is a graph: nodes are functions that perform work, edges define control flow, and a typed state object flows through the entire execution. Crucially, the framework provides durable execution out of the box—agents persist through failures and resume automatically from precisely where they stopped.
That is a meaningful departure from simpler chain-based agents. In a chain, state lives implicitly in the message history, which is to say it lives nowhere anyone can inspect. In LangGraph state is explicit, typed and managed by the framework. That explicitness is dull to describe and indispensable in practice—it is what makes production deployment tractable. As of October 2025, LangGraph Platform, since rechristened LangSmith Deployment, has been used by nearly 400 companies to put agents into production.
Everything flows through the state
Start with the object that ties it all together: a TypedDict you define yourself.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # reducer: appends, doesn't overwrite
steps_taken: int
requires_review: bool
add_messages is a reducer. When two nodes both update messages, the values are merged—appended—rather than one silently clobbering the other. You can define custom reducers for any field that needs merge semantics.
The nodes themselves are plain functions. Each receives the full state and returns a partial update:
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import ToolNode
llm = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(tools)
def call_model(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
return {
"messages": [response],
"steps_taken": state["steps_taken"] + 1,
}
tool_node = ToolNode(tools)
Conditional edges then decide where execution goes next, based on the current state:
from langgraph.graph import StateGraph, END
def route(state: AgentState) -> str:
if state["requires_review"]:
return "human_review"
last = state["messages"][-1]
if getattr(last, "tool_calls", None):
return "tools"
return END
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", tool_node)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", route)
graph.add_edge("tools", "agent")
app = graph.compile()
None of this is glamorous. That is rather the point.
Where the magic actually lives
Checkpointing is what separates a LangGraph agent from a demo agent. Every state transition is persisted to a backend, and three consequences follow. An agent can run for hours or days and survive process restarts. You can inspect the state of any in-flight agent at any moment. And a failed run resumes from the last successful checkpoint, not from scratch—which is where we came in.
LangGraph ships three checkpointer backends, and the choice is mostly a question of ambition.
| Backend | When to use |
|---|---|
MemorySaver | Development and testing only — data disappears on restart |
SqliteSaver | Single-server deployments, local persistence |
PostgresSaver | Distributed systems, horizontal scaling |
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string(
"postgresql://user:pass@host:5432/langgraph"
)
app = graph.compile(checkpointer=checkpointer)
# Each run is keyed by thread_id — same thread resumes the same conversation
config = {"configurable": {"thread_id": "user-session-789"}}
result = app.invoke({"messages": [HumanMessage("Summarize Q3 results")]}, config)
# Resume after failure — picks up from last checkpoint automatically
app.invoke(None, config)
The thread ID is how LangGraph ties checkpoints to a specific conversation or task. Use a stable identifier—user ID, session ID, job ID—according to the use case. Get this wrong and two conversations bleed into one; get it right and an agent can be interrupted, forgotten about for a day and picked up as if nothing had happened.
When the machine asks permission
Sometimes the safest thing an agent can do is stop and ask. LangGraph's interrupt mechanism pauses execution at a defined point and waits for external input before continuing. This is how one builds approval flows, escalation paths and review gates:
from langgraph.types import interrupt
def human_review_node(state: AgentState) -> dict:
# Execution pauses here — the graph is frozen in the checkpointer
decision = interrupt({
"question": "Agent wants to delete production data. Approve?",
"context": state["messages"][-3:],
})
return {"requires_review": False, "approved": decision == "yes"}
The graph resumes when you call app.invoke again with the same thread_id and the human's response. No polling, no timeouts. The state simply waits in the checkpointer until someone picks it back up—patient in a way software rarely is.
Not every job needs the cathedral
Restraint matters. Per the LangChain docs, not every agentic use case warrants LangGraph's full machinery.
| Scenario | Recommendation |
|---|---|
| Single-turn Q&A with tools | create_react_agent from langgraph.prebuilt |
| Multi-step with basic retry | create_react_agent with recursion_limit |
| Long-running with persistence | LangGraph + PostgresSaver |
| Human approval required | LangGraph + interrupt |
| Multiple coordinating agents | LangGraph multi-agent supervisor pattern |
Start with create_react_agent; it covers most agentic tasks with zero boilerplate. Reach for the graph API when you genuinely need explicit branching, persistence or human gates—and not a moment sooner.
Two kinds of memory
An agent that forgets everything between sessions is merely polite, not useful. LangGraph supports two memory scopes. Short-term, or in-thread, memory is the message history within a single thread_id, managed automatically by the checkpointer. Long-term, or cross-thread, memory holds facts that should persist across separate conversations—user preferences, past decisions, learned context—stored externally in a vector store or key-value store and loaded into state at the start of each run.
Most production agents want both. Short-term memory comes free with checkpointing. Long-term memory does not: it requires designing a retrieval step at graph entry, which is the sort of chore that is easy to defer and expensive to skip.
The bottom line
LangGraph's pitch is unfashionably plain. It makes the control flow of an agent as explicit, inspectable and testable as the rest of the application. The state is typed. The transitions are declared. The persistence is built in. None of that will impress a demo audience, because a demo never crashes at step nine. Production always does—and on that day, the boring framework is the one still standing.
Sources: