Lab 01 — A LangGraph-Faithful StateGraph Engine From Scratch
Phase 18 · Lab 01 · Phase README · Warmup
The problem
The thing that separates someone who uses LangGraph from someone trusted to own it is
understanding its execution model — and that model is not "a chain of function calls," it's a
Pregel / Bulk-Synchronous-Parallel (BSP) message-passing graph that runs in discrete
super-steps. Build a faithful miniature of StateGraph so the model is muscle memory: channels
with reducers, nodes, normal + conditional edges, compile, and invoke/stream driven by the
super-step loop.
What you build
| Piece | What it does |
|---|---|
last_value / add_messages / add | reducers — how a node's write combines with the channel (overwrite / append / accumulate) |
StateGraph(channels) | the graph: add_node, add_edge, add_conditional_edges, set_entry_point |
compile() → CompiledGraph | validate (entry exists, no dead-ends) and freeze into a runnable |
invoke / stream | the super-step loop: run active nodes → merge writes via reducers → follow edges → repeat, guarded by recursion_limit |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference + a main() that builds a ReAct agent as a StateGraph and shows reducers + the recursion guard |
test_lab.py | 18 tests: reducers, validation, linear/conditional/cyclic execution, streaming, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py
Success criteria
- You can explain the super-step (Pregel/BSP) model: active nodes see the same state, writes merge via reducers, then edges compute the next active set.
-
Your
add_messagesaccumulates andlast_valueoverwrites — and you can say why two writers on one channel need a merge reducer. -
Conditional edges branch via a router; a cycle hits
recursion_limit. -
invokeandstreamagree (invoke is stream consumed to the end). -
All 18 tests pass under both
labandsolution.
How this maps to the real stack
This is the engine underneath langgraph.graph.StateGraph. In real LangGraph:
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages] # your add_messages reducer
g = StateGraph(State)
g.add_node("agent", call_model)
g.add_node("tools", run_tools)
g.add_edge(START, "agent")
g.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END})
g.add_edge("tools", "agent")
app = g.compile() # + checkpointer in Lab 02
Your channels dict is real LangGraph's Annotated TypedDict; your super-step loop is its
Pregel engine; your recursion_limit is the real one (default 25). What you didn't build here —
persistence, streaming token mode, subgraphs — layers on top of exactly this loop (Labs 02 & 03).
Limits. Real LangGraph adds async, true-parallel node execution, richer stream modes, typed state validation, and the LangGraph Platform runtime. The execution model is what you built.
Extensions (your own machine)
- Add fan-out/fan-in: let a node have multiple successor edges (parallel branches) and prove the reducer merges their writes in one super-step.
- Add a
stream_mode="values"(full state per step) alongside the"updates"mode. - Rebuild the same ReAct graph in real LangGraph and diff the behavior against your engine.
Interview / resume signal
"Reimplemented LangGraph's
StateGraphfrom scratch — the Pregel/BSP super-step engine with channel reducers, conditional edges, and a recursion guard — so I can explain and debug its execution model (why reducers exist, how loops/branches work, concurrent writes) rather than just call its API."