Skip to main content

lc_langgraph/compiled/
invoke.rs

1// crates/lc-langgraph/src/compiled/invoke.rs
2//! CompiledGraph invoke, invoke_with_execution, resume, and invoke_from_node methods
3
4use super::graph::CompiledGraph;
5use super::types::{ExecutionStep, GraphExecution, GraphInvocation, ParallelBranch};
6use crate::errors::{GraphError, GraphResult};
7use crate::node::NodeConfig;
8use crate::state::StateSchema;
9use crate::END;
10use std::collections::HashMap;
11
12impl<S: StateSchema> CompiledGraph<S> {
13    /// Run the graph from its entry point with the given input state.
14    pub async fn invoke(&self, input: S) -> GraphResult<GraphInvocation<S>> {
15        let mut state = input;
16        let mut current_node = self.entry_point.clone();
17        let mut steps: Vec<ExecutionStep> = Vec::new();
18        let mut recursion_count = 0;
19
20        if let Some(ref checkpointer) = self.checkpointer {
21            let checkpoint_id = checkpointer.lock().await.save(&state, 0).await?;
22            steps.push(ExecutionStep::checkpoint(
23                checkpoint_id,
24                current_node.clone(),
25            ));
26        }
27
28        loop {
29            if current_node == END {
30                break;
31            }
32
33            // Q3: check the limit BEFORE executing a step. A `loop` with this
34            // guard means a graph that legitimately uses exactly `limit` steps
35            // and then reaches END is NOT misreported as exceeding the limit
36            // (the old `count >= limit` post-check fired at `count == limit`).
37            if recursion_count >= self.recursion_limit {
38                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
39            }
40
41            if self.interrupt_before.contains(&current_node) {
42                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
43            }
44
45            // Check for FanOut edge — execute all branches in parallel and merge
46            let fan_out_targets = self.find_fan_out_targets(&current_node).await;
47            if let Some(targets) = fan_out_targets {
48                recursion_count += 1;
49                let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
50
51                // Q6: branches share the main-path recursion budget so the limit
52                // cannot be bypassed by fanning out into deep sub-executions.
53                let branch_results = self
54                    .execute_parallel_branches(&targets, &state, recursion_count)
55                    .await?;
56                for (name, inv) in branch_results {
57                    parallel_branches.push(ParallelBranch {
58                        name: name.clone(),
59                        final_state: inv.final_state.clone(),
60                        steps: inv.steps.clone(),
61                    });
62                    steps.push(ExecutionStep::ParallelNode {
63                        branch: name,
64                        metadata: HashMap::new(),
65                    });
66                }
67
68                let merge_target = self.find_fan_in_target(&targets).await;
69                if let Some(merge_node) = merge_target {
70                    state = self.merge_parallel_states(&parallel_branches)?;
71                    current_node = merge_node;
72                } else {
73                    state = self.merge_parallel_states(&parallel_branches)?;
74                    current_node = END.to_string();
75                }
76
77                if let Some(ref checkpointer) = self.checkpointer {
78                    let checkpoint_id = checkpointer
79                        .lock()
80                        .await
81                        .save(&state, recursion_count)
82                        .await?;
83                    steps.push(ExecutionStep::checkpoint(
84                        checkpoint_id,
85                        current_node.clone(),
86                    ));
87                }
88                continue;
89            }
90
91            recursion_count += 1;
92
93            let node = self.get_node(&current_node).await?;
94
95            let config = NodeConfig {
96                recursion_limit: self.recursion_limit,
97                debug: false,
98                metadata: HashMap::new(),
99            };
100
101            let update = node.execute(&state, Some(config)).await?;
102
103            if let Some(new_state) = update.update {
104                state = self.default_reducer.reduce(&state, &new_state);
105            }
106
107            steps.push(ExecutionStep::node(
108                current_node.clone(),
109                update.metadata.clone(),
110            ));
111
112            if self.interrupt_after.contains(&current_node) {
113                return Err(GraphError::ExecutionInterrupted(format!(
114                    "after_{}",
115                    current_node
116                )));
117            }
118
119            let next_node = self.find_next_node(&current_node, &state).await?;
120
121            if let Some(ref checkpointer) = self.checkpointer {
122                let checkpoint_id = checkpointer
123                    .lock()
124                    .await
125                    .save(&state, recursion_count)
126                    .await?;
127                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
128            }
129
130            current_node = next_node;
131        }
132
133        Ok(GraphInvocation {
134            final_state: state,
135            steps,
136            recursion_count,
137        })
138    }
139
140    /// Continue execution from a saved [`GraphExecution`] (e.g. after an interrupt).
141    pub async fn invoke_with_execution(
142        &self,
143        execution: GraphExecution<S>,
144    ) -> GraphResult<GraphInvocation<S>> {
145        let mut state = execution.state;
146        let mut current_node = if execution.interrupted_at.starts_with("after_") {
147            self.find_next_node(&execution.current_node, &state).await?
148        } else {
149            execution.current_node
150        };
151        let mut steps = execution.steps;
152        let mut recursion_count = execution.recursion_count;
153        let first_node = current_node.clone();
154
155        loop {
156            if current_node == END {
157                break;
158            }
159
160            // Q3: check the limit before executing a step (same guard as `invoke`).
161            if recursion_count >= self.recursion_limit {
162                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
163            }
164
165            if current_node != first_node && self.interrupt_before.contains(&current_node) {
166                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
167            }
168
169            recursion_count += 1;
170
171            let node = self.get_node(&current_node).await?;
172
173            let config = NodeConfig {
174                recursion_limit: self.recursion_limit,
175                debug: false,
176                metadata: HashMap::new(),
177            };
178
179            let update = node.execute(&state, Some(config)).await?;
180
181            if let Some(new_state) = update.update {
182                state = self.default_reducer.reduce(&state, &new_state);
183            }
184
185            steps.push(ExecutionStep::node(
186                current_node.clone(),
187                update.metadata.clone(),
188            ));
189
190            if self.interrupt_after.contains(&current_node) {
191                return Err(GraphError::ExecutionInterrupted(format!(
192                    "after_{}",
193                    current_node
194                )));
195            }
196
197            let next_node = self.find_next_node(&current_node, &state).await?;
198
199            if let Some(ref checkpointer) = self.checkpointer {
200                let checkpoint_id = checkpointer
201                    .lock()
202                    .await
203                    .save(&state, recursion_count)
204                    .await?;
205                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
206            }
207
208            current_node = next_node;
209        }
210
211        Ok(GraphInvocation {
212            final_state: state,
213            steps,
214            recursion_count,
215        })
216    }
217
218    /// Resume execution from the given execution context.
219    pub async fn resume(&self, execution: GraphExecution<S>) -> GraphResult<GraphInvocation<S>> {
220        self.invoke_with_execution(execution).await
221    }
222
223    /// Run the graph starting from the given node with the given input state.
224    pub async fn invoke_from_node(
225        &self,
226        start_node: String,
227        input: S,
228    ) -> GraphResult<GraphInvocation<S>> {
229        self.invoke_from_node_with_count(start_node, input, 0).await
230    }
231
232    /// Like [`invoke_from_node`](Self::invoke_from_node), but continues from an
233    /// existing recursion count.
234    ///
235    /// Q3/Q6: this is the single enforcement point for the recursion limit on
236    /// the "start from an arbitrary node" path. Parallel FanOut branches call
237    /// this with the main path's current count so their depth stays visible to
238    /// the shared `recursion_limit` budget instead of restarting from zero.
239    pub(super) async fn invoke_from_node_with_count(
240        &self,
241        start_node: String,
242        input: S,
243        mut recursion_count: usize,
244    ) -> GraphResult<GraphInvocation<S>> {
245        let mut state = input;
246        let mut current_node = start_node;
247        let mut steps: Vec<ExecutionStep> = Vec::new();
248
249        loop {
250            if current_node == END {
251                break;
252            }
253
254            if recursion_count >= self.recursion_limit {
255                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
256            }
257
258            if self.interrupt_before.contains(&current_node) {
259                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
260            }
261
262            recursion_count += 1;
263
264            let node = self.get_node(&current_node).await?;
265
266            let config = NodeConfig {
267                recursion_limit: self.recursion_limit,
268                debug: false,
269                metadata: HashMap::new(),
270            };
271
272            let update = node.execute(&state, Some(config)).await?;
273
274            if let Some(new_state) = update.update {
275                state = self.default_reducer.reduce(&state, &new_state);
276            }
277
278            steps.push(ExecutionStep::node(
279                current_node.clone(),
280                update.metadata.clone(),
281            ));
282
283            if self.interrupt_after.contains(&current_node) {
284                return Err(GraphError::ExecutionInterrupted(format!(
285                    "after_{}",
286                    current_node
287                )));
288            }
289
290            current_node = self.find_next_node(&current_node, &state).await?;
291        }
292
293        Ok(GraphInvocation {
294            final_state: state,
295            steps,
296            recursion_count,
297        })
298    }
299}