Unit 10.00: Finding the node where state first went wrong
The final output being wrong tells you almost nothing about which node caused it. The state history tells you exactly.
The first wrong value, not the last
A wrong number at the end of a graph has usually been wrong for several steps, with each subsequent node faithfully transforming it. The node to fix is where it first became wrong.
The code runs a graph with a deliberate off-by-one and prints the history.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
total: float
items: list
def load(state: State):
return {"items": [10.0, 20.0, 30.0]}
def sum_items(state: State):
return {"total": sum(state["items"][:-1])} # the bug: drops the last item
def apply_tax(state: State):
return {"total": round(state["total"] * 1.2, 2)}
g = StateGraph(State)
for name, fn in [("load", load), ("sum_items", sum_items), ("apply_tax", apply_tax)]:
g.add_node(name, fn)
g.add_edge(START, "load")
g.add_edge("load", "sum_items")
g.add_edge("sum_items", "apply_tax")
g.add_edge("apply_tax", END)
app = g.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "bug-1"}}
print("final total:", app.invoke({"total": 0.0, "items": []}, config)["total"],
"(expected 72.0)")
print("\nstate after each step, oldest first:")
for snapshot in reversed(list(app.get_state_history(config))):
print(f" next={str(snapshot.next):14} total={snapshot.values.get('total')}")
# Reading forward, `total` is 30.0 when it should be 60.0 -- so the fault is in
# `sum_items`, and `apply_tax` is faithfully multiplying a wrong number.
The final total is 36.0 where 72.0 was expected. Reading the history forwards, total is 30.0 after sum_items when it should be 60.0 - so sum_items is the fault, and apply_tax is correctly multiplying a wrong number by 1.2.
Without the history, apply_tax is the natural suspect because it is the node nearest the wrong output and the one doing the arithmetic that produced it. You could read it carefully several times and find nothing wrong, because there is nothing wrong with it.
The mistake this prevents
The mistake is debugging from the final value backwards through the code rather than through the data. The code paths all look reasonable - they are, individually. What is unreasonable is a specific value at a specific step, and only the recorded state shows you which.
Takeaway
Find the first wrong value in the state history, not the last node before the wrong output. Nodes downstream of a bug behave correctly on bad input, which makes them look guilty.
