State Machines & Graph Agents — The Agentic Control Loop
State Machines & Graph Agents
Modeling Agents as Graphs
The most reliable production agents are not free-form loops - they are state machines modeled as directed graphs. This is the core insight behind LangGraph and similar frameworks.
Why Graphs Beat While-Loops
| Property | While-Loop Agent | Graph-Based Agent |
|---|---|---|
| Debuggability | Hard - opaque loop iterations | Easy - visualize exact path through nodes |
| Persistence | Lost on crash | State can be saved/resumed at any node |
| Determinism | Low - LLM decides everything | High - transitions can be deterministic |
| Human-in-Loop | Awkward to implement | Natural - pause at any node, wait for approval |
| Testing | Difficult - full runs required | Easy - test individual nodes in isolation |
Anatomy of a Graph Agent
// LangGraph v1.x Structure (Annotation.Root + START/END + interrupt):
const AgentState = Annotation.Root({
messages: Annotation<string[]>(), plan: Annotation<string>(), quality: Annotation<number>()
});
const graph = new StateGraph(AgentState);
graph.addNode("planner", plannerAgent); // Creates a plan
graph.addNode("executor", executorAgent); // Executes plan steps
graph.addNode("reviewer", reviewerAgent); // Reviews output quality
graph.addNode("human_gate", () => ({ approved: interrupt("Approve plan?") })); // Dynamic interrupt()
// Edges define the flow:
graph.addEdge(START, "planner");
graph.addEdge("planner", "executor");
graph.addEdge("executor", "reviewer");
graph.addConditionalEdges("reviewer", (state) => {
if (state.quality >= 0.8) return "human_gate";
return "executor"; // Loop back for another attempt
});
graph.addEdge("human_gate", END);
const app = graph.compile({ checkpointer: new InMemorySaver() }); // PostgresSaver in production
LangGraph v1.x & LangChain v1 Standards: For standard tool-calling loops without custom topologies, import create_agent from langchain.agents (which replaces the deprecated langgraph.prebuilt.create_react_agent). For custom state machines requiring Human-in-the-Loop pauses, call interrupt(payload) dynamically inside any node (replacing legacy NodeInterrupt and static compiler breakpoints) and resume execution via Command(resume=value) backed by InMemorySaver (local dev) or PostgresSaver (production).
Key Concepts
- Nodes: Individual processing units - can be LLM calls, tool executions, or pure functions.
- Edges: Connections between nodes. Can be unconditional (always follow) or conditional (branch based on state).
- State: A shared data structure (often a TypedDict or Pydantic model) that flows through the graph.
- Checkpoints: Snapshots of state at each node - enables time-travel debugging, persistence, and resumption.
Common Graph Patterns
| Pattern | Structure | Use Case |
|---|---|---|
| Linear Pipeline | A → B → C → END | Sequential processing (research → write → edit) |
| Fan-Out/Fan-In | A → [B1, B2, B3] → C | Parallel execution (search 3 sources, then merge) |
| Retry Loop | A → B → (fail? → A) | Self-correcting code generation |
| Router | A → {B1 | B2 | B3} | Intent classification → specialized handler |
| Human-in-Loop | A → PAUSE → B | Approval gate before irreversible action |
Define a cyclical agent state machine using LangGraph v1.x with typed state schemas, conditional edges, and InMemorySaver checkpointing.