259 lines
6.3 KiB
Markdown
259 lines
6.3 KiB
Markdown
# LangGraph State Management
|
|
|
|
**Version:** 1.0.0
|
|
**Last Updated:** 2026-02-23
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document details how LangGraph manages state, based on direct source code analysis.
|
|
|
|
---
|
|
|
|
## State Definition
|
|
|
|
### TypedDict Schema
|
|
|
|
From `types.py`:
|
|
|
|
```python
|
|
from typing import TypedDict
|
|
|
|
class AgentState(TypedDict):
|
|
messages: list
|
|
next_action: str
|
|
```
|
|
|
|
LangGraph validates state schema at compile time.
|
|
|
|
---
|
|
|
|
## State Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ STATE FLOW IN LANGGRAPH │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
Input (dict)
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ Superstep N │
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Node A │────▶│ Channel │ │
|
|
│ │ (reads) │◀────│ (update) │ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
│ │ │ │
|
|
│ └──────────────────────┘ │
|
|
│ ▼ │
|
|
│ [Checkpoint if enabled] │
|
|
└──────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ Superstep N+1 (or return final state) │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Reducers
|
|
|
|
### How Reducers Work
|
|
|
|
Reducers define how multiple updates are merged:
|
|
|
|
```python
|
|
# From graph/state.py
|
|
def add_messages(left: list, right: list) -> list:
|
|
return left + right
|
|
```
|
|
|
|
### Built-in Reducers
|
|
|
|
| Reducer | Function | Behavior |
|
|
|---------|----------|----------|
|
|
| `add_messages` | `list + list` | Append |
|
|
| `last` | `(a, b) => b` | Last wins |
|
|
| `max` | `max(a, b)` | Maximum |
|
|
| `min` | `min(a, b)` | Minimum |
|
|
|
|
### Custom Reducers
|
|
|
|
```python
|
|
from typing import Annotated
|
|
|
|
def merge_contexts(a: dict, b: dict) -> dict:
|
|
return {**a, **b}
|
|
|
|
class AgentState(TypedDict):
|
|
context: Annotated[dict, merge_contexts]
|
|
```
|
|
|
|
---
|
|
|
|
## Checkpointing
|
|
|
|
### Durability Modes
|
|
|
|
From `types.py`:
|
|
|
|
```python
|
|
Durability = Literal["sync", "async", "exit"]
|
|
```
|
|
|
|
| Mode | Behavior |
|
|
|------|----------|
|
|
| `sync` | Persist before next superstep |
|
|
| `async` | Persist while next superstep runs |
|
|
| `exit` | Persist only when graph exits |
|
|
|
|
### Checkpoint Metadata
|
|
|
|
```python
|
|
config = {
|
|
"configurable": {
|
|
"thread_id": "user-123",
|
|
"checkpoint_id": "1ef-abc123"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Checkpoint Backends
|
|
|
|
| Backend | Module | Use Case |
|
|
|---------|--------|----------|
|
|
| `InMemorySaver` | `langgraph.checkpoint.memory` | Testing |
|
|
| `SqliteSaver` | `langgraph.checkpoint.sqlite` | Local dev |
|
|
| `PostgresSaver` | `langgraph.checkpoint.postgres` | Production |
|
|
|
|
---
|
|
|
|
## Thread Model
|
|
|
|
### What is a Thread?
|
|
|
|
A thread (`thread_id`) isolates state:
|
|
|
|
```
|
|
Thread "user-123":
|
|
├── checkpoint-001 (step 0)
|
|
├── checkpoint-002 (step 1)
|
|
├── checkpoint-003 (step 2)
|
|
└── [current state]
|
|
```
|
|
|
|
### Thread Isolation
|
|
|
|
- Independent checkpoints per thread
|
|
- Parallel threads via multiple `thread_id` values
|
|
- Resume from any checkpoint in a thread
|
|
|
|
---
|
|
|
|
## Interrupts (Human-in-the-Loop)
|
|
|
|
### Interrupt Mechanism
|
|
|
|
From `types.py`:
|
|
|
|
```python
|
|
class Interrupt:
|
|
value: Any
|
|
when: Literal["during", "after"]
|
|
```
|
|
|
|
### Using Interrupts
|
|
|
|
```python
|
|
from langgraph.types import interrupt
|
|
|
|
def human_review(state):
|
|
# Pause for human input
|
|
feedback = interrupt({"task": "review", "data": state})
|
|
return {"feedback": feedback}
|
|
```
|
|
|
|
### Command (Modify State)
|
|
|
|
```python
|
|
from langgraph.types import Command
|
|
|
|
def process_with_override(state):
|
|
return Command(
|
|
update={"status": "processed"},
|
|
resume={"feedback": "approved"}
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## State Updates
|
|
|
|
### Node Returns Partial State
|
|
|
|
```python
|
|
def node_a(state):
|
|
# Return only what this node updates
|
|
return {"messages": [AIMessage("hello")]}
|
|
```
|
|
|
|
### Merge Process
|
|
|
|
```
|
|
Node A returns: {"messages": [msg1], "counter": 1}
|
|
Node B returns: {"messages": [msg2], "counter": 2}
|
|
|
|
After reducer (add_messages for messages, last for counter):
|
|
{"messages": [msg1, msg2], "counter": 2}
|
|
```
|
|
|
|
---
|
|
|
|
## Checkpoint Implementation
|
|
|
|
### From Source (`pregel/_checkpoint.py`)
|
|
|
|
```python
|
|
def create_checkpoint(
|
|
channels: dict[str, BaseChannel],
|
|
versions: dict[str, int],
|
|
metadata: CheckpointMetadata
|
|
) -> Checkpoint:
|
|
"""Create a checkpoint from current channel values."""
|
|
return {
|
|
"channel_values": {k: v.checkpoint() for k, v in channels.items()},
|
|
"channel_versions": versions,
|
|
"metadata": metadata,
|
|
}
|
|
```
|
|
|
|
### Resuming from Checkpoint
|
|
|
|
```python
|
|
# Load channels from checkpoint
|
|
def channels_from_checkpoint(checkpoint: Checkpoint) -> dict:
|
|
return {
|
|
k: BaseChannel.from_checkpoint(v)
|
|
for k, v in checkpoint["channel_values"].items()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Key Differences from OpenClaw
|
|
|
|
| Aspect | LangGraph | OpenClaw |
|
|
|--------|-----------|----------|
|
|
| **State Storage** | Channels in memory | Multi-layer memory |
|
|
| **Persistence** | Checkpoints | Session-memory hook |
|
|
| **Isolation** | thread_id | Session key |
|
|
| **Resumption** | checkpoint_id | Session restore |
|
|
| **Updates** | Reducers | Direct merge |
|
|
|
|
---
|
|
|
|
*Generated from source code analysis*
|