State machines for agents
Scenario
Two parallel agent nodes run at the same time. They both return a dictionary with the key `messages`.
Does one node's output accidentally overwrite the other's, causing a race condition?
Mental model
Think of agent state as a shared clipboard that gets passed from worker to worker.
In a state machine graph, each node (agent or tool) takes the current state, does its work, and returns an update. Instead of overwriting the whole clipboard, rules called 'reducers' decide how to safely merge the new notes with the old notes.
Deep dive
Production agents no longer use basic chains; they use **directed graphs (DAGs)**. In frameworks like LangGraph, the AI and tools are 'nodes'. The shared memory passed between them is the 'state'.
Graphs pass a shared state object between execution nodes.
When a node finishes, it doesn't just overwrite the state variable. If it returns a list of messages, the framework needs to know: do I replace the old messages, or add to them? We solve this with **reducers** (like `add`), which safely concatenate outputs—even from parallel nodes.
Reducers merge updates instead of overwriting the previous state.
Python's `TypedDict` is often used for state, but it has a fatal flaw: it doesn't check data at runtime. A rogue tool might inject junk data into the state, polluting it. To stop this, production systems use strict data validators like **Pydantic** to reject bad data immediately.
TypedDict allows silent state pollution; Pydantic enforces strict boundaries.
Prediction: If a developer uses a basic `TypedDict` and a tool accidentally returns a nested dictionary instead of a string, the agent might run fine for a minute, then crash completely three steps later when another node tries to read it.
Always use runtime validation for agent state to catch errors early.
Code examples
A strict, reducer-enabled state schema
from pydantic import BaseModel, ConfigDict
from typing import Annotated
import operator
class AgentState(BaseModel):
# Forbid extra random keys to prevent state pollution
model_config = ConfigDict(extra='forbid')
# The 'add' reducer safely combines lists
messages: Annotated[list, operator.add]
current_task: str
This Pydantic model guarantees that rogue tools cannot add illegal keys (thanks to `extra='forbid'`). The `operator.add` reducer ensures that parallel nodes returning messages will concatenate their lists rather than overwriting each other.
Common mistakes
- Assuming state is completely overwritten: In standard Python, `state = new_state` overwrites all prior data. In LangGraph, state is incrementally updated. Reducers instruct the graph engine to merge updates (e.g., appending a new message) rather than replacing the state. This is why parallel nodes do not cause race conditions.
- Trusting TypedDict to prevent state pollution: TypedDict implies strict type safety, but it does not perform runtime validation. Rogue data can pollute the state at runtime and crash downstream nodes. Production graphs use Pydantic BaseModels with `extra='forbid'` to intercept illegal state mutations instantly.
Glossary
- state machine
- A system that moves between different states (like nodes in a graph) while passing shared data along.
- reducer
- A function that tells the framework how to combine new data with old data (e.g., adding to a list instead of overwriting it).
- pollution
- When rogue data accidentally gets written into the agent's state, causing unexpected crashes downstream.
Recall questions
- What is the purpose of a 'reducer' in a state machine graph?
- Two parallel agent nodes return dictionary updates for the same key. Why doesn't this cause a race condition in LangGraph?
- An agent crashes downstream because a tool injected an unexpected nested dictionary into a TypedDict state object. How do you fix this?
Questions & answers
A developer complains that their LangGraph state is losing the early conversation history every time the agent loops. What is the most likely architectural flaw?
The developer likely forgot to apply an `operator.add` reducer to the messages field in their state schema, causing the framework to overwrite the list entirely on each node execution instead of appending to it.
Approach: Identify the role of reducers in graph-based state management.
How do you prevent a rogue tool from injecting arbitrary nested dictionaries into your agent's state?
By defining the state schema using Pydantic BaseModels with `extra='forbid'` rather than Python's basic TypedDict. This enforces runtime validation and instantly rejects illegal mutations.
Approach: Focus on runtime validation boundaries versus static type hinting.
Continue learning
Previous: Agent autonomy spectrum
Next: Multi-agent orchestration patterns