Revised from direct source code analysis

This commit is contained in:
Solaria Lumis Havens
2026-02-23 13:11:51 -06:00
parent 6046a9a609
commit 8e37dedbd9
6 changed files with 1134 additions and 239 deletions
+158 -56
View File
@@ -7,85 +7,108 @@
## Overview
This document details how LangGraph manages state throughout the graph execution lifecycle.
This document details how LangGraph manages state, based on direct source code analysis.
---
## State Schema
## State Definition
### Typed State
### TypedDict Schema
LangGraph uses Python's `TypedDict` for type-safe state:
From `types.py`:
```python
from typing import TypedDict
class AgentState(TypedDict):
messages: list
context: dict
checkpoint_id: str | None
next_action: str
```
### State Flow
LangGraph validates state schema at compile time.
---
## State Flow
```
┌─────────────────────────────────────────────────────────────┐
│ STATE FLOW IN LANGGRAPH │
│ STATE FLOW IN LANGGRAPH
└─────────────────────────────────────────────────────────────┘
Input State
Input (dict)
┌──────────────┐
Node A │ ──▶ State Update (via reducer)
(transform)
└──────────────┘
▼ (messages sent)
┌──────────────┐
Node B │ ──▶ State Update
(transform)
└──────────────┘
┌──────────────────────────────────────────────────────────
Superstep N │
┌──────────────┐ ┌──────────────┐
│ │ Node A │────▶│ Channel │ │
│ │ (reads) │◀────│ (update) │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
└──────────────────────┘ │
│ [Checkpoint if enabled] │
└──────────────────────────────────────────────────────────┘
Output State
┌──────────────────────────────────────────────────────────┐
│ Superstep N+1 (or return final state) │
└──────────────────────────────────────────────────────────┘
```
---
## Reducers
### What Are Reducers?
### How Reducers Work
Reducers define how state updates are merged when multiple nodes produce updates.
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 | Behavior |
|---------|----------|
| `add_messages` | Append to list |
| `operator.or` | Union of sets |
| `last` | Last value wins |
| 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
def merge_dicts(left: dict, right: dict) -> dict:
"""Merge two dictionaries, with right taking precedence."""
result = left.copy()
result.update(right)
return result
from typing import Annotated
def merge_contexts(a: dict, b: dict) -> dict:
return {**a, **b}
class AgentState(TypedDict):
context: Annotated[dict, merge_contexts]
```
---
## Checkpointing
### How Checkpointing Works
### Durability Modes
1. **Snapshot:** At each checkpoint, serialize full state
2. **Store:** Save to backend (SQLite, Postgres, etc.)
3. **Resume:** On failure, load from last checkpoint
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
@@ -93,18 +116,18 @@ def merge_dicts(left: dict, right: dict) -> dict:
config = {
"configurable": {
"thread_id": "user-123",
"checkpoint_id": "checkpoint-abc123"
"checkpoint_id": "1ef-abc123"
}
}
```
### Checkpoint Backends
| Backend | Use Case |
|---------|----------|
| **Memory** | Testing, short-lived |
| **SQLite** | Single machine, local |
| **Postgres** | Production, distributed |
| Backend | Module | Use Case |
|---------|--------|----------|
| `InMemorySaver` | `langgraph.checkpoint.memory` | Testing |
| `SqliteSaver` | `langgraph.checkpoint.sqlite` | Local dev |
| `PostgresSaver` | `langgraph.checkpoint.postgres` | Production |
---
@@ -112,32 +135,68 @@ config = {
### What is a Thread?
A thread (`thread_id`) represents an isolated conversation or task:
A thread (`thread_id`) isolates state:
```
Thread ID: "user-123"
├── Checkpoint 1 (checkpoint-001)
├── Checkpoint 2 (checkpoint-002)
├── Checkpoint 3 (checkpoint-003) ← Current
└── State (current)
Thread "user-123":
├── checkpoint-001 (step 0)
├── checkpoint-002 (step 1)
├── checkpoint-003 (step 2)
└── [current state]
```
### Thread Isolation
- Each `thread_id` has independent state
- Multiple threads can run in parallel
- Human-in-the-loop works per-thread
- 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
Nodes return partial state updates:
### Node Returns Partial State
```python
def node_a(state):
# Return only what this node updates
return {"messages": [AIMessage("hello")]}
```
@@ -147,10 +206,53 @@ def node_a(state):
Node A returns: {"messages": [msg1], "counter": 1}
Node B returns: {"messages": [msg2], "counter": 2}
After reducer:
After reducer (add_messages for messages, last for counter):
{"messages": [msg1, msg2], "counter": 2}
```
---
*Generated for the WE*
## 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*