Skip to content

State as a Typed Contract, and the Cost of Getting It Wrong

The same exercise, rebuilt twice, and what changed between them

This course's own source material has an odd feature worth naming before anything else: the first "hello, how are you, goodbye" exercise exists as two separate files, built on two different component scaffolds, ten days apart. Both files define a LangGraph agent's State: the shared object every step of the agent passes forward, reads from, and writes back to. One file, dated January 9th, defines state as messages: list with no type parameter at all. The other, dated January 29th, uses the fuller Annotated[list[BaseMessage], add_messages] form. Nobody flags the earlier version as wrong or the later one as a fix. They just both exist, teaching the same exercise, quietly disagreeing about what a properly typed message list looks like.

That drift is itself the point of this lesson. If two versions of a five-minute "hello world" exercise can't agree on how to type one field, the problem isn't carelessness. It's that State looks like a data bag you fill in as you go, and nothing about that shape stops you from being sloppy about it.

Before you start

Prerequisite: Lesson 01 (the current agent-construction API). This lesson goes deeper into the State/TypedDict pattern lesson 01 used without fully explaining. After this lesson, you can: design a State type where every field has a clear owner and, where it needs one, a reducer, and explain why extending state casually is where most agent bugs actually come from.

What State actually is

A LangGraph State is a TypedDict every node in your graph reads from and writes back to. That's the whole mechanism. But "every node reads and writes it" is exactly why sloppiness compounds: a field one node assumes is always present, that another node forgot to initialize, produces a KeyError three nodes downstream of where the actual mistake was made. The contract has to hold everywhere, or it doesn't hold anywhere.

python
from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

Two parts do real work here, and it's worth separating them. list[BaseMessage] is the type. add_messages is a reducer: a function LangGraph calls to merge a node's return value into the existing state, rather than blindly overwriting it. Without add_messages, returning {"messages": [new_message]} from a node would replace the entire history with a single message. With it, LangGraph appends instead.

The extension the source material never explains

This course's own exercise 1.2 ("Message Memory") extends the base State like this:

python
class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    summary: str
    window_size: int

Two new fields, zero discussion of what happens to them on each node call. summary has no reducer, so every node that touches it overwrites it completely; if two nodes in one turn both try to update the summary, only the last write survives, silently. window_size never changes after initialization in the exercise's own code, which raises the question of why it's in State at all rather than being a plain function argument or a config value. Neither is a bug in the strict sense; the exercise is a scaffold, not shipped code. But it's the exact pattern that becomes a real bug once someone copies this shape into a project with more than one node touching summary.

Step 1: Ask what needs a reducer

Any field more than one node might update in the same turn needs a reducer, or you get last-write-wins with no warning. messages almost always needs one (add_messages). A scalar like window_size, set once and read many times, usually doesn't.

Step 2: Ask what belongs in State at all

If a value never changes after the graph starts, it's configuration, not state. Passing it through State works, but it invites every node to treat it as mutable when it isn't. A RunnableConfig or a closure is often the more honest home for a true constant.

Step 3: Type it precisely, not just present

messages: list and messages: Annotated[list[BaseMessage], add_messages] both run. Only the second tells you, and every future reader, what kind of object lives in that list and how updates to it are supposed to behave. The gap between those two versions is exactly the gap this course's own two competing "hello langgraph" files quietly demonstrate.

With a reducer versus without one, same two nodes, same turn

Why this is worth taking seriously before it costs you something

tools-memory-and-multi-agent-systems lesson 18 makes the stakes of state design concrete with a real comparison, worth citing here even though it's a different kind of memory than LangGraph's in-graph state: the same Edinburgh venue-booking task, run three times. With nothing retained between runs, the agent re-searches all twelve candidate venues every time: £0.42 and 14 minutes. With state correctly persisted and reused, it skips the nine already-ruled-out venues and only re-checks the one fact that might have gone stale: £0.04 and 90 seconds. That's a cross-session memory system, not a LangGraph State object, and the two aren't the same mechanism. But the underlying argument transfers directly: state you can trust turns repeat work into a one-time cost, and state you can't trust (untyped, unreduced, silently overwritten) is what makes an agent redo work it already did, inside a single run just as much as across sessions.

Quick check — You add a `retry_count: int` field to your State, incremented by a node each time a tool call fails. Two parallel branches of your graph can both increment it in the same turn. What's missing?

What this buys you before lesson 03

You can now look at any State definition and ask the two questions that matter: does this field need a reducer, and does it belong in state at all. Lesson 03 builds directly on this, going deeper into multi-field state management across a longer pipeline than the two-node toy graphs this lesson worked with.

Continue to Lesson 03

State management at depth: sliding windows, summarization, and multi-node pipelines. The full version of what this lesson's source material only sketched.

Have a question about this lesson?

Reply here and it goes straight to Rod. Same as replying to one of his emails.