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    pub async fn invoke_parallel(&self, input: S) -> GraphResult<ParallelInvocation<S>> {
17        let mut state = input;
18        let mut current_node = self.entry_point.clone();
19        let mut steps: Vec<ExecutionStep> = Vec::new();
20        let mut recursion_count = 0;
21        let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
22
23        if let Some(ref checkpointer) = self.checkpointer {
24            let checkpoint_id = checkpointer.lock().await.save(&state).await?;
25            steps.push(ExecutionStep::checkpoint(
26                checkpoint_id,
27                current_node.clone(),
28            ));
29        }
30
31        while current_node != END && recursion_count < self.recursion_limit {
32            if self.interrupt_before.contains(&current_node) {
33                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
34            }
35
36            recursion_count += 1;
37
38            let fan_out_targets = self.find_fan_out_targets(&current_node).await;
39
40            if let Some(targets) = fan_out_targets {
41                let branch_results = self.execute_parallel_branches(&targets, &state).await?;
42
43                for (name, inv) in branch_results {
44                    parallel_branches.push(ParallelBranch {
45                        name: name.clone(),
46                        final_state: inv.final_state.clone(),
47                        steps: inv.steps.clone(),
48                    });
49                    steps.push(ExecutionStep::ParallelNode {
50                        branch: name,
51                        metadata: HashMap::new(),
52                    });
53                }
54
55                let merge_target = self.find_fan_in_target(&targets).await;
56                if let Some(merge_node) = merge_target {
57                    state = self.merge_parallel_states(&parallel_branches)?;
58                    current_node = merge_node;
59                } else {
60                    state = parallel_branches
61                        .last()
62                        .map(|b| b.final_state.clone())
63                        .unwrap_or(state);
64                    current_node = END.to_string();
65                }
66            } else {
67                let node = self.get_node(&current_node).await?;
68
69                let config = NodeConfig {
70                    recursion_limit: self.recursion_limit,
71                    debug: false,
72                    metadata: HashMap::new(),
73                };
74
75                let update = node.execute(&state, Some(config)).await?;
76
77                if let Some(new_state) = update.update {
78                    state = self.default_reducer.reduce(&state, &new_state);
79                }
80
81                steps.push(ExecutionStep::node(
82                    current_node.clone(),
83                    update.metadata.clone(),
84                ));
85
86                if self.interrupt_after.contains(&current_node) {
87                    return Err(GraphError::ExecutionInterrupted(format!(
88                        "after_{}",
89                        current_node
90                    )));
91                }
92
93                current_node = self.find_next_node(&current_node, &state).await?;
94            }
95        }
96
97        if recursion_count >= self.recursion_limit {
98            return Err(GraphError::RecursionLimitReached(self.recursion_limit));
99        }
100
101        Ok(ParallelInvocation {
102            final_state: state,
103            steps,
104            recursion_count,
105            parallel_branches,
106        })
107    }
108
109    /// Execute the graph with dynamic task injection support.
110    ///
111    /// After each node completes, checks the task_inbox for newly submitted tasks.
112    /// If tasks are found, uses the provided `planner` to convert them into new
113    /// nodes/edges and injects them into the runtime registries before continuing.
114    pub async fn invoke_dynamic(
115        &self,
116        input: S,
117        planner: &dyn DynamicPlanner<S>,
118    ) -> GraphResult<GraphInvocation<S>> {
119        let mut state = input;
120        let mut current_node = self.entry_point.clone();
121        let mut steps: Vec<ExecutionStep> = Vec::new();
122        let mut recursion_count = 0;
123
124        if let Some(ref checkpointer) = self.checkpointer {
125            let checkpoint_id = checkpointer.lock().await.save(&state).await?;
126            steps.push(ExecutionStep::checkpoint(
127                checkpoint_id,
128                current_node.clone(),
129            ));
130        }
131
132        while current_node != END && recursion_count < self.recursion_limit {
133            // Check for pending dynamically-submitted tasks
134            let pending = {
135                let mut inbox = self.task_inbox.lock().await;
136                inbox.drain(..).collect::<Vec<_>>()
137            };
138            if !pending.is_empty() {
139                let injection: DynamicInjection<S> = planner
140                    .plan(&pending, &state)
141                    .await
142                    .map_err(|e| GraphError::RuntimeError(e.to_string()))?;
143
144                {
145                    let mut rn = self.runtime_nodes.write().await;
146                    for (name, node) in injection.nodes {
147                        rn.insert(name, node);
148                    }
149                }
150                {
151                    let mut re = self.runtime_edges.write().await;
152                    re.extend(injection.edges);
153                }
154            }
155
156            if self.interrupt_before.contains(&current_node) {
157                return Err(GraphError::ExecutionInterrupted(current_node.clone()));
158            }
159
160            recursion_count += 1;
161
162            let node = self.get_node(&current_node).await?;
163            let config = NodeConfig {
164                recursion_limit: self.recursion_limit,
165                debug: false,
166                metadata: HashMap::new(),
167            };
168            let update = node.execute(&state, Some(config)).await?;
169
170            if let Some(new_state) = update.update {
171                state = self.default_reducer.reduce(&state, &new_state);
172            }
173            steps.push(ExecutionStep::node(
174                current_node.clone(),
175                update.metadata.clone(),
176            ));
177
178            if self.interrupt_after.contains(&current_node) {
179                return Err(GraphError::ExecutionInterrupted(format!(
180                    "after_{}",
181                    current_node
182                )));
183            }
184
185            let next_node = self.find_next_node(&current_node, &state).await?;
186
187            if let Some(ref checkpointer) = self.checkpointer {
188                let checkpoint_id = checkpointer.lock().await.save(&state).await?;
189                steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
190            }
191
192            current_node = next_node;
193        }
194
195        if recursion_count >= self.recursion_limit {
196            return Err(GraphError::RecursionLimitReached(self.recursion_limit));
197        }
198
199        Ok(GraphInvocation {
200            final_state: state,
201            steps,
202            recursion_count,
203        })
204    }
205
206    pub(super) fn merge_parallel_states(&self, branches: &[ParallelBranch<S>]) -> GraphResult<S> {
207        if branches.is_empty() {
208            return Err(GraphError::StateError(
209                "Cannot merge states: no parallel branches completed".to_string(),
210            ));
211        }
212
213        let mut merged = branches[0].final_state.clone();
214        for branch in branches.iter().skip(1) {
215            merged = self.default_reducer.reduce(&merged, &branch.final_state);
216        }
217        Ok(merged)
218    }
219}