Skip to main content

lc_langgraph/
node.rs

1// crates/lc-langgraph/src/node.rs
2//! Node definition for LangGraph
3//!
4//! Nodes are the execution units in a graph. Each node receives the current
5//! state and returns a state update.
6
7use crate::errors::GraphError;
8use crate::state::{StateSchema, StateUpdate};
9use async_trait::async_trait;
10use std::future::Future;
11use std::marker::PhantomData;
12use std::pin::Pin;
13
14/// Graph Node trait
15///
16/// Nodes are async functions that take a state and return a state update.
17/// They represent the work units in the graph.
18#[async_trait]
19pub trait GraphNode<S: StateSchema>: Send + Sync + 'static {
20    /// Execute the node
21    ///
22    /// # Parameters
23    /// - `state`: Current state of the graph
24    /// - `config`: Optional configuration for this execution
25    ///
26    /// # Returns
27    /// A state update that will be merged into the current state
28    async fn execute(
29        &self,
30        state: &S,
31        config: Option<NodeConfig>,
32    ) -> Result<StateUpdate<S>, GraphError>;
33
34    /// Get node name
35    fn name(&self) -> &str;
36}
37
38/// Metadata key the compiled graph uses to inject the human's decision into a
39/// node that is being resumed after a runtime [`crate::errors::GraphError::InterruptRequest`].
40/// A resume-aware node reads the value under this key from its `NodeConfig.metadata`.
41pub const INTERRUPT_RESUME_KEY: &str = "__lc_interrupt_resume";
42
43/// Node configuration
44#[derive(Debug, Clone, Default)]
45pub struct NodeConfig {
46    /// Maximum recursion depth
47    pub recursion_limit: usize,
48
49    /// Custom metadata
50    pub metadata: std::collections::HashMap<String, serde_json::Value>,
51
52    /// Enable debug tracing
53    pub debug: bool,
54}
55
56/// Node execution result (alias for clarity)
57pub type NodeResult<S> = Result<StateUpdate<S>, GraphError>;
58
59/// Async node function type (boxed future)
60pub type AsyncNodeFn<S> =
61    Box<dyn Fn(&S) -> Pin<Box<dyn Future<Output = NodeResult<S>> + Send>> + Send + Sync>;
62
63/// AsyncFn trait for simpler async node creation
64pub trait AsyncFn<S: StateSchema>: Send + Sync {
65    /// Call the async function with the given state.
66    fn call(&self, state: &S) -> Pin<Box<dyn Future<Output = NodeResult<S>> + Send>>;
67}
68
69impl<S: StateSchema, F, Fut> AsyncFn<S> for F
70where
71    F: Fn(&S) -> Fut + Send + Sync + 'static,
72    Fut: Future<Output = NodeResult<S>> + Send + 'static,
73{
74    fn call(&self, state: &S) -> Pin<Box<dyn Future<Output = NodeResult<S>> + Send>> {
75        Box::pin((self)(state))
76    }
77}
78
79/// AsyncNode - Simple async node wrapper
80pub struct AsyncNode<S: StateSchema, F: AsyncFn<S>> {
81    name: String,
82    func: F,
83    _marker: PhantomData<S>,
84}
85
86impl<S: StateSchema, F: AsyncFn<S>> AsyncNode<S, F> {
87    /// Create a new async node with the given name and function.
88    pub fn new(name: impl Into<String>, func: F) -> Self {
89        Self {
90            name: name.into(),
91            func,
92            _marker: PhantomData,
93        }
94    }
95}
96
97#[async_trait]
98impl<S: StateSchema, F: AsyncFn<S> + 'static> GraphNode<S> for AsyncNode<S, F> {
99    async fn execute(&self, state: &S, _config: Option<NodeConfig>) -> NodeResult<S> {
100        self.func.call(state).await
101    }
102
103    fn name(&self) -> &str {
104        &self.name
105    }
106}
107
108/// Function-based node implementation
109///
110/// Wraps an async function as a GraphNode.
111pub struct FunctionNode<S: StateSchema, F> {
112    name: String,
113    func: F,
114    _marker: PhantomData<S>,
115}
116
117impl<S: StateSchema, F> FunctionNode<S, F>
118where
119    F: Fn(&S) -> Pin<Box<dyn Future<Output = Result<StateUpdate<S>, GraphError>> + Send>>
120        + Send
121        + Sync,
122{
123    /// Create a new function node
124    pub fn new(name: impl Into<String>, func: F) -> Self {
125        Self {
126            name: name.into(),
127            func,
128            _marker: PhantomData,
129        }
130    }
131}
132
133#[async_trait]
134impl<S: StateSchema, F: 'static> GraphNode<S> for FunctionNode<S, F>
135where
136    F: Fn(&S) -> Pin<Box<dyn Future<Output = Result<StateUpdate<S>, GraphError>> + Send>>
137        + Send
138        + Sync,
139{
140    async fn execute(
141        &self,
142        state: &S,
143        _config: Option<NodeConfig>,
144    ) -> Result<StateUpdate<S>, GraphError> {
145        (self.func)(state).await
146    }
147
148    fn name(&self) -> &str {
149        &self.name
150    }
151}
152
153/// Simple sync node wrapper
154///
155/// For nodes that don't need async execution.
156pub struct SyncNode<S: StateSchema, F> {
157    name: String,
158    func: F,
159    _marker: PhantomData<S>,
160}
161
162impl<S: StateSchema, F> SyncNode<S, F>
163where
164    F: Fn(&S) -> Result<StateUpdate<S>, GraphError> + Send + Sync,
165{
166    /// Create a new sync node
167    pub fn new(name: impl Into<String>, func: F) -> Self {
168        Self {
169            name: name.into(),
170            func,
171            _marker: PhantomData,
172        }
173    }
174}
175
176#[async_trait]
177impl<S: StateSchema, F: 'static> GraphNode<S> for SyncNode<S, F>
178where
179    F: Fn(&S) -> Result<StateUpdate<S>, GraphError> + Send + Sync,
180{
181    async fn execute(
182        &self,
183        state: &S,
184        _config: Option<NodeConfig>,
185    ) -> Result<StateUpdate<S>, GraphError> {
186        (self.func)(state)
187    }
188
189    fn name(&self) -> &str {
190        &self.name
191    }
192}
193
194/// Resume-aware node (LangGraph-style interrupt).
195///
196/// The wrapped closure is called twice per runtime interrupt:
197/// - first pass: `resume` is `None` — the node either completes normally or
198///   suspends itself by returning `Err(GraphError::InterruptRequest { payload })`;
199/// - resume: after a human answers, the node is re-entered with `resume` =
200///   `Some(decision)` so the closure can continue past the suspension.
201///
202/// Branch on that second argument instead of re-walking expensive side effects
203/// (an API call, a DB write) that already happened on the first pass — keep any
204/// such work memoized in the state between the two passes.
205pub struct InterruptibleNode<S: StateSchema, F> {
206    name: String,
207    func: F,
208    _marker: PhantomData<S>,
209}
210
211impl<S: StateSchema, F> InterruptibleNode<S, F>
212where
213    F: Fn(&S, Option<&serde_json::Value>) -> Pin<Box<dyn Future<Output = NodeResult<S>> + Send>>
214        + Send
215        + Sync,
216{
217    /// Create a resume-aware node. The closure takes the current state and the
218    /// injected resume decision (`None` on first pass, `Some(decision)` on resume).
219    pub fn new(name: impl Into<String>, func: F) -> Self {
220        Self {
221            name: name.into(),
222            func,
223            _marker: PhantomData,
224        }
225    }
226}
227
228#[async_trait]
229impl<S: StateSchema, F: 'static> GraphNode<S> for InterruptibleNode<S, F>
230where
231    F: Fn(&S, Option<&serde_json::Value>) -> Pin<Box<dyn Future<Output = NodeResult<S>> + Send>>
232        + Send
233        + Sync,
234{
235    async fn execute(
236        &self,
237        state: &S,
238        config: Option<NodeConfig>,
239    ) -> Result<StateUpdate<S>, GraphError> {
240        let resume = config
241            .as_ref()
242            .and_then(|c| c.metadata.get(INTERRUPT_RESUME_KEY))
243            .cloned();
244        (self.func)(state, resume.as_ref()).await
245    }
246
247    fn name(&self) -> &str {
248        &self.name
249    }
250}
251
252/// Placeholder node for entry/exit points
253pub struct SentinelNode {
254    name: String,
255}
256
257impl SentinelNode {
258    /// Create a sentinel node for the `START` marker.
259    pub fn start() -> Self {
260        Self {
261            name: crate::START.to_string(),
262        }
263    }
264
265    /// Create a sentinel node for the `END` marker.
266    pub fn end() -> Self {
267        Self {
268            name: crate::END.to_string(),
269        }
270    }
271
272    /// Create a custom-named sentinel node.
273    pub fn custom(name: impl Into<String>) -> Self {
274        Self { name: name.into() }
275    }
276}
277
278#[async_trait]
279impl<S: StateSchema> GraphNode<S> for SentinelNode {
280    async fn execute(
281        &self,
282        _state: &S,
283        _config: Option<NodeConfig>,
284    ) -> Result<StateUpdate<S>, GraphError> {
285        // Sentinel nodes don't modify state
286        Ok(StateUpdate::unchanged())
287    }
288
289    fn name(&self) -> &str {
290        &self.name
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::state::AgentState;
298
299    #[tokio::test]
300    async fn test_sync_node() {
301        let node = SyncNode::new("test", |state: &AgentState| {
302            Ok(StateUpdate::full(AgentState::new(state.input.clone())))
303        });
304
305        let state = AgentState::new("Hello".to_string());
306        let result = node.execute(&state, None).await;
307        assert!(result.is_ok());
308    }
309
310    #[test]
311    fn test_sentinel_nodes() {
312        let start: SentinelNode = SentinelNode::start();
313        assert_eq!(GraphNode::<AgentState>::name(&start), crate::START);
314
315        let end: SentinelNode = SentinelNode::end();
316        assert_eq!(GraphNode::<AgentState>::name(&end), crate::END);
317    }
318}