Skip to main content

lc_langgraph/compiled/
parallel.rs

1// crates/lc-langgraph/src/compiled/parallel.rs
2//! CompiledGraph invoke_parallel, invoke_dynamic, and merge_parallel_states methods
3
4use super::graph::CompiledGraph;
5use super::types::{
6    DynamicInjection, DynamicPlanner, ExecutionStep, GraphInvocation, ParallelBranch,
7    ParallelInvocation,
8};
9use crate::errors::{GraphError, GraphResult};
10use crate::node::NodeConfig;
11use crate::state::StateSchema;
12use crate::END;
13use std::collections::HashMap;
14
15impl<S: StateSchema> CompiledGraph<S> {
16    /// Run the graph while capturing parallel fan-out branches into a [`ParallelInvocation`].
17    pub async fn invoke_parallel(&self, input: S) -> GraphResult<ParallelInvocation<S>> {
18        let mut state = input;
19        let mut current_node = self.entry_point.clone();
20        let mut steps: Vec<ExecutionStep> = Vec::new();
21        let mut recursion_count = 0;
22        let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
23
24        if let Some(ref checkpointer) = self.checkpointer {
25            let checkpoint_id = checkpointer.lock().await.save(&state, 0).await?;
26            steps.push(ExecutionStep::checkpoint(
27                checkpoint_id,
28                current_node.clone(),
29            ));
30        }
31
32        loop {
33            if current_node == END {
34                break;
35            }
36
37            // Q3: check the limit before executing a step (same guard as `invoke`).
38            if recursion_count >= self.recursion_limit {
39                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
40            }
41
42            if self.interrupt_before.contains(&current_node) {
43                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
44            }
45
46            recursion_count += 1;
47
48            let fan_out_targets = self.find_fan_out_targets(&current_node).await;
49
50            if let Some(targets) = fan_out_targets {
51                let branch_results = self
52                    .execute_parallel_branches(&targets, &state, recursion_count)
53                    .await?;
54
55                for (name, inv) in branch_results {
56                    parallel_branches.push(ParallelBranch {
57                        name: name.clone(),
58                        final_state: inv.final_state.clone(),
59                        steps: inv.steps.clone(),
60                    });
61                    steps.push(ExecutionStep::ParallelNode {
62                        branch: name,
63                        metadata: HashMap::new(),
64                    });
65                }
66
67                let merge_target = self.find_fan_in_target(&targets).await;
68                if let Some(merge_node) = merge_target {
69                    state = self.merge_parallel_states(&parallel_branches)?;
70                    current_node = merge_node;
71                } else {
72                    // H6: same as the main path in invoke.rs — all branch results are merged
73                    // through the reducer, rather than keeping only `parallel_branches.last()`
74                    // and dropping the rest.
75                    state = self.merge_parallel_states(&parallel_branches)?;
76                    current_node = END.to_string();
77                }
78            } else {
79                let node = self.get_node(&current_node).await?;
80
81                let config = NodeConfig {
82                    recursion_limit: self.recursion_limit,
83                    debug: false,
84                    metadata: HashMap::new(),
85                };
86
87                let update = node.execute(&state, Some(config)).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                current_node = self.find_next_node(&current_node, &state).await?;
106            }
107        }
108
109        Ok(ParallelInvocation {
110            final_state: state,
111            steps,
112            recursion_count,
113            parallel_branches,
114        })
115    }
116
117    /// Execute the graph with dynamic task injection support.
118    ///
119    /// After each node completes, checks the task_inbox for newly submitted tasks.
120    /// If tasks are found, uses the provided `planner` to convert them into new
121    /// nodes/edges and injects them into the runtime registries before continuing.
122    pub async fn invoke_dynamic(
123        &self,
124        input: S,
125        planner: &dyn DynamicPlanner<S>,
126    ) -> GraphResult<GraphInvocation<S>> {
127        let mut state = input;
128        let mut current_node = self.entry_point.clone();
129        let mut steps: Vec<ExecutionStep> = Vec::new();
130        let mut recursion_count = 0;
131
132        if let Some(ref checkpointer) = self.checkpointer {
133            let checkpoint_id = checkpointer.lock().await.save(&state, 0).await?;
134            steps.push(ExecutionStep::checkpoint(
135                checkpoint_id,
136                current_node.clone(),
137            ));
138        }
139
140        loop {
141            if current_node == END {
142                break;
143            }
144
145            // Q3: check the limit before executing a step (same guard as `invoke`).
146            if recursion_count >= self.recursion_limit {
147                return Err(GraphError::RecursionLimitReached(self.recursion_limit));
148            }
149
150            // Check for pending dynamically-submitted tasks
151            let pending = {
152                let mut inbox = self.task_inbox.lock().await;
153                inbox.drain(..).collect::<Vec<_>>()
154            };
155            if !pending.is_empty() {
156                let injection: DynamicInjection<S> = planner
157                    .plan(&pending, &state)
158                    .await
159                    .map_err(|e| GraphError::RuntimeError(e.to_string()))?;
160
161                {
162                    let mut rn = self.runtime_nodes.write().await;
163                    for (name, node) in injection.nodes {
164                        rn.insert(name, node);
165                    }
166                }
167                {
168                    let mut re = self.runtime_edges.write().await;
169                    re.extend(injection.edges);
170                }
171            }
172
173            if 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            let config = NodeConfig {
181                recursion_limit: self.recursion_limit,
182                debug: false,
183                metadata: HashMap::new(),
184            };
185            let update = node.execute(&state, Some(config)).await?;
186
187            if let Some(new_state) = update.update {
188                state = self.default_reducer.reduce(&state, &new_state);
189            }
190            steps.push(ExecutionStep::node(
191                current_node.clone(),
192                update.metadata.clone(),
193            ));
194
195            if self.interrupt_after.contains(&current_node) {
196                return Err(GraphError::ExecutionInterrupted(format!(
197                    "after_{}",
198                    current_node
199                )));
200            }
201
202            let next_node = self.find_next_node(&current_node, &state).await?;
203
204            if let Some(ref checkpointer) = self.checkpointer {
205                let checkpoint_id = checkpointer
206                    .lock()
207                    .await
208                    .save(&state, recursion_count)
209                    .await?;
210                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
211            }
212
213            current_node = next_node;
214        }
215
216        Ok(GraphInvocation {
217            final_state: state,
218            steps,
219            recursion_count,
220        })
221    }
222
223    pub(super) fn merge_parallel_states(&self, branches: &[ParallelBranch<S>]) -> GraphResult<S> {
224        if branches.is_empty() {
225            return Err(GraphError::StateError(
226                "Cannot merge states: no parallel branches completed".to_string(),
227            ));
228        }
229
230        let mut merged = branches[0].final_state.clone();
231        for branch in branches.iter().skip(1) {
232            merged = self.default_reducer.reduce(&merged, &branch.final_state);
233        }
234        Ok(merged)
235    }
236}