Skip to main content

lc_langgraph/
state.rs

1// crates/lc-langgraph/src/state.rs
2//! State management for LangGraph
3//!
4//! This module provides the state abstraction for graph execution.
5//! States are data structures that flow through nodes in the graph.
6
7use crate::errors::{GraphError, GraphResult};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt::Debug;
11use std::sync::Arc;
12
13/// State Schema trait
14pub trait StateSchema:
15    Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Debug
16{
17    /// Create initial state from input
18    fn from_input(input: Self) -> Self {
19        input
20    }
21
22    /// Get state as JSON for debugging/checkpointing.
23    ///
24    /// Returns a `Result` instead of silently producing `Value::Null` (Q2):
25    /// a serialization failure is data corruption in the state machine and
26    /// must surface at the call site, not be swallowed.
27    fn to_json(&self) -> GraphResult<serde_json::Value> {
28        serde_json::to_value(self).map_err(|e| {
29            GraphError::StateError(format!("Failed to serialize state to JSON: {}", e))
30        })
31    }
32}
33
34/// State update representation
35///
36/// Nodes return StateUpdate which contains partial updates to the state.
37/// The reducer pattern determines how updates are merged into the full state.
38#[derive(Debug, Clone, Serialize)]
39pub struct StateUpdate<S: StateSchema> {
40    /// Full or partial state update
41    pub update: Option<S>,
42
43    /// Additional metadata (for debugging/tracing)
44    pub metadata: HashMap<String, serde_json::Value>,
45}
46
47impl<S: StateSchema> StateUpdate<S> {
48    /// Create a full state update
49    pub fn full(state: S) -> Self {
50        Self {
51            update: Some(state),
52            metadata: HashMap::new(),
53        }
54    }
55
56    /// Create update with metadata
57    pub fn with_metadata(state: S, metadata: HashMap<String, serde_json::Value>) -> Self {
58        Self {
59            update: Some(state),
60            metadata,
61        }
62    }
63
64    /// Create a no-change update (for nodes that don't modify state)
65    pub fn unchanged() -> Self {
66        Self {
67            update: None,
68            metadata: HashMap::new(),
69        }
70    }
71
72    /// Add metadata entry
73    pub fn add_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
74        self.metadata.insert(key.into(), value);
75    }
76}
77
78/// Reducer trait for merging state updates
79///
80/// Reducers define how state updates are merged into the current state.
81/// This enables patterns like `add_messages` which appends rather than replaces.
82pub trait Reducer<S: StateSchema>: Send + Sync {
83    /// Reduce current state with an update
84    fn reduce(&self, current: &S, update: &S) -> S;
85}
86
87/// Default reducer that replaces state entirely
88pub struct ReplaceReducer;
89
90impl<S: StateSchema> Reducer<S> for ReplaceReducer {
91    fn reduce(&self, _current: &S, update: &S) -> S {
92        update.clone()
93    }
94}
95
96/// 0.22.0 C3 fix: composed merge reducer — applies every per-field reducer
97/// registered via [`StateGraph::set_reducer`](crate::StateGraph::set_reducer)
98/// on top of the default (replace) merge.
99///
100/// Each field reducer (e.g. [`AppendMessagesReducer`]) merges **its own
101/// field** into the running result; fields without a registered reducer take
102/// the update's values. Previously the registered reducers were dropped at
103/// `compile()` and every merge point used plain replace, so accumulating
104/// fields (messages / steps) were silently overwritten after each node.
105pub struct MergeReducer<S: StateSchema> {
106    fallback: Arc<dyn Reducer<S>>,
107    fields: Vec<Arc<dyn Reducer<S>>>,
108}
109
110impl<S: StateSchema> MergeReducer<S> {
111    /// Composes `fields` (in registration order) over `fallback`.
112    pub fn new(fallback: Arc<dyn Reducer<S>>, fields: Vec<Arc<dyn Reducer<S>>>) -> Self {
113        Self { fallback, fields }
114    }
115}
116
117impl<S: StateSchema> Reducer<S> for MergeReducer<S> {
118    fn reduce(&self, current: &S, update: &S) -> S {
119        let mut merged = self.fallback.reduce(current, update);
120        for reducer in &self.fields {
121            let next = reducer.reduce(current, &merged);
122            merged = next;
123        }
124        merged
125    }
126}
127
128/// Append reducer for vector fields (like add_messages pattern)
129///
130/// This reducer appends new items to vector fields in the state.
131/// Useful for message history, steps history, etc.
132pub struct AppendReducer<S: StateSchema, T: Clone + Send + Sync> {
133    /// Function that reads the vector field from a state.
134    pub field_accessor: fn(&S) -> &[T],
135    /// Function that writes the merged vector back into a state.
136    pub field_mutator: fn(&mut S, Vec<T>),
137}
138
139impl<S: StateSchema, T: Clone + Send + Sync> Reducer<S> for AppendReducer<S, T> {
140    fn reduce(&self, current: &S, update: &S) -> S {
141        let current_items = (self.field_accessor)(current);
142        let update_items = (self.field_accessor)(update);
143
144        let mut merged: Vec<T> = current_items.to_vec();
145        merged.extend(update_items.iter().cloned());
146
147        let mut result = current.clone();
148        (self.field_mutator)(&mut result, merged);
149        result
150    }
151}
152
153/// AgentState Messages Reducer - Appends messages instead of replacing
154pub struct AppendMessagesReducer;
155
156impl Reducer<AgentState> for AppendMessagesReducer {
157    fn reduce(&self, current: &AgentState, update: &AgentState) -> AgentState {
158        let mut result = update.clone();
159        result.messages = current.messages.clone();
160        result.messages.extend(update.messages.iter().cloned());
161        result
162    }
163}
164
165/// AgentState Steps Reducer - Appends steps instead of replacing
166pub struct AppendStepsReducer;
167
168impl Reducer<AgentState> for AppendStepsReducer {
169    fn reduce(&self, current: &AgentState, update: &AgentState) -> AgentState {
170        let mut result = update.clone();
171        result.steps = current.steps.clone();
172        result.steps.extend(update.steps.iter().cloned());
173        result
174    }
175}
176
177/// Common state with messages (agent-style)
178///
179/// This provides a pre-built state schema for agent-style graphs
180/// that track messages through the execution.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct AgentState {
183    /// Input query
184    pub input: String,
185
186    /// Chat messages history
187    pub messages: Vec<MessageEntry>,
188
189    /// Intermediate steps
190    pub steps: Vec<StepEntry>,
191
192    /// Output result
193    pub output: Option<String>,
194}
195
196impl StateSchema for AgentState {}
197
198impl AgentState {
199    /// Create new agent state with input
200    pub fn new(input: impl Into<String>) -> Self {
201        let input = input.into();
202        let msg = MessageEntry::human(input.clone());
203        Self {
204            input,
205            messages: vec![msg],
206            steps: vec![],
207            output: None,
208        }
209    }
210
211    /// Add a message to history
212    pub fn add_message(&mut self, message: MessageEntry) {
213        self.messages.push(message);
214    }
215
216    /// Add a step to history
217    pub fn add_step(&mut self, step: StepEntry) {
218        self.steps.push(step);
219    }
220
221    /// Set output
222    pub fn set_output(&mut self, output: impl Into<String>) {
223        self.output = Some(output.into());
224    }
225}
226
227/// Message entry for agent state
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct MessageEntry {
230    /// Role of the message sender.
231    pub role: MessageRole,
232    /// Text content of the message.
233    pub content: String,
234}
235
236impl MessageEntry {
237    /// Create a human message entry.
238    pub fn human(content: impl Into<String>) -> Self {
239        Self {
240            role: MessageRole::Human,
241            content: content.into(),
242        }
243    }
244
245    /// Create an AI message entry.
246    pub fn ai(content: impl Into<String>) -> Self {
247        Self {
248            role: MessageRole::AI,
249            content: content.into(),
250        }
251    }
252
253    /// Create a system message entry.
254    pub fn system(content: impl Into<String>) -> Self {
255        Self {
256            role: MessageRole::System,
257            content: content.into(),
258        }
259    }
260
261    /// Create a tool message entry.
262    pub fn tool(content: impl Into<String>) -> Self {
263        Self {
264            role: MessageRole::Tool,
265            content: content.into(),
266        }
267    }
268}
269
270/// Message role types
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
272pub enum MessageRole {
273    /// System message.
274    System,
275    /// Human message.
276    Human,
277    /// AI assistant message.
278    AI,
279    /// Tool result message.
280    Tool,
281}
282
283/// Step entry for intermediate execution steps
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct StepEntry {
286    /// Action that was taken.
287    pub action: String,
288    /// Observation or result of the action.
289    pub observation: String,
290}
291
292impl StepEntry {
293    /// Create a new step entry.
294    pub fn new(action: impl Into<String>, observation: impl Into<String>) -> Self {
295        Self {
296            action: action.into(),
297            observation: observation.into(),
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_append_messages_reducer() {
308        let mut current = AgentState::new("Hello".to_string());
309        current.add_message(MessageEntry::ai("Response 1".to_string()));
310
311        let mut update = AgentState::new("Hello".to_string());
312        update.add_message(MessageEntry::ai("Response 2".to_string()));
313        update.set_output("Done".to_string());
314
315        let reducer = AppendMessagesReducer;
316        let result = reducer.reduce(&current, &update);
317
318        assert_eq!(result.messages.len(), 4);
319        assert_eq!(result.output, Some("Done".to_string()));
320    }
321
322    #[test]
323    fn test_append_steps_reducer() {
324        let mut current = AgentState::new("Test".to_string());
325        current.add_step(StepEntry::new(
326            "Action 1".to_string(),
327            "Result 1".to_string(),
328        ));
329
330        let mut update = AgentState::new("Test".to_string());
331        update.add_step(StepEntry::new(
332            "Action 2".to_string(),
333            "Result 2".to_string(),
334        ));
335
336        let reducer = AppendStepsReducer;
337        let result = reducer.reduce(&current, &update);
338
339        assert_eq!(result.steps.len(), 2);
340    }
341}