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, INTERRUPT_RESUME_KEY};
8use crate::state::{StateSchema, StateUpdate};
9use crate::END;
10use std::collections::HashMap;
11
12impl<S: StateSchema> CompiledGraph<S> {
13    /// Run a single node, translating a runtime [`GraphError::InterruptRequest`]
14    /// into a producer-facing [`GraphError::DynamicInterrupt`]. Before surfacing
15    /// the interrupt the current state is persisted (when a checkpointer is
16    /// attached) so a crash between interrupt and resume cannot lose the run.
17    async fn run_node(
18        &self,
19        state: &S,
20        current_node: &str,
21        recursion_count: usize,
22        resume: Option<&serde_json::Value>,
23    ) -> GraphResult<StateUpdate<S>> {
24        let node = self.get_node(current_node).await?;
25        let mut metadata = HashMap::new();
26        if let Some(value) = resume {
27            metadata.insert(INTERRUPT_RESUME_KEY.to_string(), value.clone());
28        }
29        let config = NodeConfig {
30            recursion_limit: self.recursion_limit,
31            debug: false,
32            metadata,
33        };
34        match node.execute(state, Some(config)).await {
35            Err(GraphError::InterruptRequest { ref payload }) => {
36                if let Some(ref cp) = self.checkpointer {
37                    cp.lock().await.save(state, recursion_count).await?;
38                }
39                Err(GraphError::DynamicInterrupt {
40                    node: current_node.to_string(),
41                    payload: payload.clone(),
42                })
43            }
44            other => other,
45        }
46    }
47    /// Run the graph from its entry point with the given input state.
48    pub async fn invoke(&self, input: S) -> GraphResult<GraphInvocation<S>> {
49        let mut state = input;
50        let mut current_node = self.entry_point.clone();
51        let mut steps: Vec<ExecutionStep> = Vec::new();
52        let mut recursion_count = 0;
53
54        if let Some(ref checkpointer) = self.checkpointer {
55            let checkpoint_id = checkpointer.lock().await.save(&state, 0).await?;
56            steps.push(ExecutionStep::checkpoint(
57                checkpoint_id,
58                current_node.clone(),
59            ));
60        }
61
62        loop {
63            if current_node == END {
64                break;
65            }
66
67            // Q3: check the limit BEFORE executing a step. A `loop` with this
68            // guard means a graph that legitimately uses exactly `limit` steps
69            // and then reaches END is NOT misreported as exceeding the limit
70            // (the old `count >= limit` post-check fired at `count == limit`).
71            if recursion_count >= self.recursion_limit {
72                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
73            }
74
75            if self.interrupt_before.contains(&current_node) {
76                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
77            }
78
79            // 0.22.0 C3 fix: the fan-out source node executes BEFORE branching
80            // (previously the fan-out check ran first and the source node was
81            // silently skipped). START is a virtual entry — never executed.
82            if current_node != crate::START {
83                recursion_count += 1;
84
85                let update = self
86                    .run_node(&state, &current_node, recursion_count, None)
87                    .await?;
88
89                if let Some(new_state) = update.update {
90                    state = self.default_reducer.reduce(&state, &new_state);
91                }
92
93                steps.push(ExecutionStep::node(
94                    current_node.clone(),
95                    update.metadata.clone(),
96                ));
97
98                if self.interrupt_after.contains(&current_node) {
99                    return Err(GraphError::ExecutionInterrupted(format!(
100                        "after_{}",
101                        current_node
102                    )));
103                }
104            }
105
106            // FanOut: after the source node has run, execute all branches in
107            // parallel and merge (on top of the pre-fan-out state).
108            let fan_out_targets = self.find_fan_out_targets(&current_node).await;
109            if let Some(targets) = fan_out_targets {
110                recursion_count += 1;
111                let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
112
113                // Q6: branches share the main-path recursion budget so the limit
114                // cannot be bypassed by fanning out into deep sub-executions.
115                let branch_results = self
116                    .execute_parallel_branches(&targets, &state, recursion_count)
117                    .await?;
118                for (name, inv) in branch_results {
119                    parallel_branches.push(ParallelBranch {
120                        name: name.clone(),
121                        final_state: inv.final_state.clone(),
122                        steps: inv.steps.clone(),
123                    });
124                    steps.push(ExecutionStep::ParallelNode {
125                        branch: name,
126                        metadata: HashMap::new(),
127                    });
128                }
129
130                let merge_target = self.find_fan_in_target(&targets).await;
131                // C3: fold on top of the pre-fan-out main-path state.
132                let base = state.clone();
133                if let Some(merge_node) = merge_target {
134                    state = self.merge_parallel_states(&parallel_branches, &base)?;
135                    current_node = merge_node;
136                } else {
137                    state = self.merge_parallel_states(&parallel_branches, &base)?;
138                    current_node = END.to_string();
139                }
140
141                if let Some(ref checkpointer) = self.checkpointer {
142                    let checkpoint_id = checkpointer
143                        .lock()
144                        .await
145                        .save(&state, recursion_count)
146                        .await?;
147                    steps.push(ExecutionStep::checkpoint(
148                        checkpoint_id,
149                        current_node.clone(),
150                    ));
151                }
152                continue;
153            }
154
155            let next_node = self.find_next_node(&current_node, &state).await?;
156
157            if let Some(ref checkpointer) = self.checkpointer {
158                let checkpoint_id = checkpointer
159                    .lock()
160                    .await
161                    .save(&state, recursion_count)
162                    .await?;
163                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
164            }
165
166            current_node = next_node;
167        }
168
169        Ok(GraphInvocation {
170            final_state: state,
171            steps,
172            recursion_count,
173        })
174    }
175
176    /// Continue execution from a saved [`GraphExecution`] (e.g. after an interrupt).
177    pub async fn invoke_with_execution(
178        &self,
179        execution: GraphExecution<S>,
180    ) -> GraphResult<GraphInvocation<S>> {
181        let mut state = execution.state;
182        // A node-requested (runtime) interrupt always re-enters the interrupted
183        // node with the human's decision. A compile-time `after_` interrupt skips
184        // the node (it already ran) and continues at its successor.
185        let rerun_interrupted = execution.pending_interrupt.is_some()
186            || !execution.interrupted_at.starts_with("after_");
187        let mut current_node = if rerun_interrupted {
188            execution.current_node
189        } else {
190            self.find_next_node(&execution.current_node, &state).await?
191        };
192        let mut steps = execution.steps;
193        let mut recursion_count = execution.recursion_count;
194        let mut resume_value = execution.pending_interrupt.map(|p| p.value);
195        let first_node = current_node.clone();
196
197        loop {
198            if current_node == END {
199                break;
200            }
201
202            // Q3: check the limit before executing a step (same guard as `invoke`).
203            if recursion_count >= self.recursion_limit {
204                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
205            }
206
207            if current_node != first_node && self.interrupt_before.contains(&current_node) {
208                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
209            }
210
211            recursion_count += 1;
212
213            // Only the resume target node receives the injected decision.
214            let inject = resume_value.take();
215            let update = self
216                .run_node(&state, &current_node, recursion_count, inject.as_ref())
217                .await?;
218
219            if let Some(new_state) = update.update {
220                state = self.default_reducer.reduce(&state, &new_state);
221            }
222
223            steps.push(ExecutionStep::node(
224                current_node.clone(),
225                update.metadata.clone(),
226            ));
227
228            if self.interrupt_after.contains(&current_node) {
229                return Err(GraphError::ExecutionInterrupted(format!(
230                    "after_{}",
231                    current_node
232                )));
233            }
234
235            let next_node = self.find_next_node(&current_node, &state).await?;
236
237            if let Some(ref checkpointer) = self.checkpointer {
238                let checkpoint_id = checkpointer
239                    .lock()
240                    .await
241                    .save(&state, recursion_count)
242                    .await?;
243                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
244            }
245
246            current_node = next_node;
247        }
248
249        Ok(GraphInvocation {
250            final_state: state,
251            steps,
252            recursion_count,
253        })
254    }
255
256    /// Resume execution from the given execution context.
257    pub async fn resume(&self, execution: GraphExecution<S>) -> GraphResult<GraphInvocation<S>> {
258        self.invoke_with_execution(execution).await
259    }
260
261    /// Run the graph starting from the given node with the given input state.
262    pub async fn invoke_from_node(
263        &self,
264        start_node: String,
265        input: S,
266    ) -> GraphResult<GraphInvocation<S>> {
267        self.invoke_from_node_with_count(start_node, input, 0).await
268    }
269
270    /// Like [`invoke_from_node`](Self::invoke_from_node), but continues from an
271    /// existing recursion count.
272    ///
273    /// Q3/Q6: this is the single enforcement point for the recursion limit on
274    /// the "start from an arbitrary node" path. Parallel FanOut branches call
275    /// this with the main path's current count so their depth stays visible to
276    /// the shared `recursion_limit` budget instead of restarting from zero.
277    pub(super) async fn invoke_from_node_with_count(
278        &self,
279        start_node: String,
280        input: S,
281        mut recursion_count: usize,
282    ) -> GraphResult<GraphInvocation<S>> {
283        let mut state = input;
284        let mut current_node = start_node;
285        let mut steps: Vec<ExecutionStep> = Vec::new();
286
287        loop {
288            if current_node == END {
289                break;
290            }
291
292            if recursion_count >= self.recursion_limit {
293                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
294            }
295
296            if self.interrupt_before.contains(&current_node) {
297                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
298            }
299
300            recursion_count += 1;
301
302            let update = self
303                .run_node(&state, &current_node, recursion_count, None)
304                .await?;
305
306            if let Some(new_state) = update.update {
307                state = self.default_reducer.reduce(&state, &new_state);
308            }
309
310            steps.push(ExecutionStep::node(
311                current_node.clone(),
312                update.metadata.clone(),
313            ));
314
315            if self.interrupt_after.contains(&current_node) {
316                return Err(GraphError::ExecutionInterrupted(format!(
317                    "after_{}",
318                    current_node
319                )));
320            }
321
322            current_node = self.find_next_node(&current_node, &state).await?;
323        }
324
325        Ok(GraphInvocation {
326            final_state: state,
327            steps,
328            recursion_count,
329        })
330    }
331}