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