Skip to main content

lc_langgraph/compiled/
stream.rs

1// crates/lc-langgraph/src/compiled/stream.rs
2//! CompiledGraph stream, find_next_node, find_fan_out_targets,
3//! find_fan_in_target, and execute_parallel_branches methods
4
5use super::graph::CompiledGraph;
6use super::types::{GraphInvocation, ParallelBranch, StreamEvent};
7use crate::edge::GraphEdge;
8use crate::errors::{GraphError, GraphResult};
9use crate::graph::END;
10use crate::node::NodeConfig;
11use crate::state::StateSchema;
12use futures_util::future::join_all;
13use futures_util::Stream;
14use std::collections::HashMap;
15use std::pin::Pin;
16use tokio_stream::wrappers::ReceiverStream;
17
18impl<S: StateSchema + Send + Sync + 'static> CompiledGraph<S> {
19    /// Stream graph execution as a true async stream.
20    ///
21    /// Each `StreamEvent` is emitted as soon as it occurs (node entry,
22    /// node completion, state update), enabling real-time consumption.
23    ///
24    /// This is the preferred streaming API. For the old all-at-once
25    /// behavior, use `stream_collected()`.
26    pub fn stream(
27        &self,
28        input: S,
29    ) -> Pin<Box<dyn Stream<Item = Result<StreamEvent<S>, GraphError>> + Send>> {
30        let (tx, rx) = tokio::sync::mpsc::channel(64);
31
32        let graph = self.clone();
33        tokio::spawn(async move {
34            let mut state = input;
35            let mut current_node = graph.entry_point.clone();
36            let mut recursion_count = 0;
37
38            if tx
39                .send(Ok(StreamEvent::start(state.clone())))
40                .await
41                .is_err()
42            {
43                return;
44            }
45
46            if let Some(ref checkpointer) = graph.checkpointer {
47                match checkpointer.lock().await.save(&state, 0).await {
48                    Ok(_) => {}
49                    Err(e) => {
50                        let _ = tx.send(Err(e)).await;
51                        return;
52                    }
53                }
54            }
55
56            while current_node != END && recursion_count < graph.recursion_limit {
57                if graph.interrupt_before.contains(&current_node) {
58                    let _ = tx
59                        .send(Err(GraphError::ExecutionInterrupted(current_node.clone())))
60                        .await;
61                    return;
62                }
63
64                // 0.22.0 C3 fix: the fan-out source node executes BEFORE
65                // branching (previously the stream loop never checked fan-out,
66                // so `find_next_node` ran only the first branch and silently
67                // dropped the rest). START is virtual — never executed.
68                if current_node != crate::START {
69                    recursion_count += 1;
70
71                    if tx
72                        .send(Ok(StreamEvent::enter_node(
73                            current_node.clone(),
74                            state.clone(),
75                        )))
76                        .await
77                        .is_err()
78                    {
79                        return;
80                    }
81
82                    let node = match graph.get_node(&current_node).await {
83                        Ok(n) => n,
84                        Err(e) => {
85                            let _ = tx.send(Err(e)).await;
86                            return;
87                        }
88                    };
89
90                    let config = NodeConfig {
91                        recursion_limit: graph.recursion_limit,
92                        debug: false,
93                        metadata: HashMap::new(),
94                    };
95
96                    let update = match node.execute(&state, Some(config)).await {
97                        Ok(u) => u,
98                        Err(e) => {
99                            let _ = tx.send(Err(e)).await;
100                            return;
101                        }
102                    };
103
104                    if tx
105                        .send(Ok(StreamEvent::node_complete(
106                            current_node.clone(),
107                            update.clone(),
108                        )))
109                        .await
110                        .is_err()
111                    {
112                        return;
113                    }
114
115                    if let Some(new_state) = update.update {
116                        state = graph.default_reducer.reduce(&state, &new_state);
117                        if tx
118                            .send(Ok(StreamEvent::state_update(state.clone())))
119                            .await
120                            .is_err()
121                        {
122                            return;
123                        }
124                    }
125
126                    if graph.interrupt_after.contains(&current_node) {
127                        if let Some(ref checkpointer) = graph.checkpointer {
128                            let _ = checkpointer
129                                .lock()
130                                .await
131                                .save(&state, recursion_count)
132                                .await;
133                        }
134                        let _ = tx
135                            .send(Err(GraphError::ExecutionInterrupted(format!(
136                                "after_{}",
137                                current_node
138                            ))))
139                            .await;
140                        return;
141                    }
142                }
143
144                // C3: FanOut interception — same semantics as the invoke path:
145                // execute all branches, merge on top of the pre-fan-out state,
146                // then continue at the FanIn target (or END).
147                let fan_out_targets = graph.find_fan_out_targets(&current_node).await;
148                if let Some(targets) = fan_out_targets {
149                    recursion_count += 1;
150                    let branch_results = match graph
151                        .execute_parallel_branches(&targets, &state, recursion_count)
152                        .await
153                    {
154                        Ok(r) => r,
155                        Err(e) => {
156                            let _ = tx.send(Err(e)).await;
157                            return;
158                        }
159                    };
160                    let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
161                    for (name, inv) in branch_results {
162                        parallel_branches.push(ParallelBranch {
163                            name: name.clone(),
164                            final_state: inv.final_state.clone(),
165                            steps: inv.steps.clone(),
166                        });
167                        let _ = tx
168                            .send(Ok(StreamEvent::node_complete(
169                                name.clone(),
170                                crate::state::StateUpdate {
171                                    update: Some(inv.final_state.clone()),
172                                    metadata: HashMap::new(),
173                                },
174                            )))
175                            .await;
176                    }
177                    let base = state.clone();
178                    state = match graph.merge_parallel_states(&parallel_branches, &base) {
179                        Ok(s) => s,
180                        Err(e) => {
181                            let _ = tx.send(Err(e)).await;
182                            return;
183                        }
184                    };
185                    if tx
186                        .send(Ok(StreamEvent::state_update(state.clone())))
187                        .await
188                        .is_err()
189                    {
190                        return;
191                    }
192                    current_node = match graph.find_fan_in_target(&targets).await {
193                        Some(merge_node) => merge_node,
194                        None => END.to_string(),
195                    };
196                    if let Some(ref checkpointer) = graph.checkpointer {
197                        let _ = checkpointer
198                            .lock()
199                            .await
200                            .save(&state, recursion_count)
201                            .await;
202                    }
203                    continue;
204                }
205
206                let next_node = match graph.find_next_node(&current_node, &state).await {
207                    Ok(n) => n,
208                    Err(e) => {
209                        let _ = tx.send(Err(e)).await;
210                        return;
211                    }
212                };
213
214                if let Some(ref checkpointer) = graph.checkpointer {
215                    let _ = checkpointer
216                        .lock()
217                        .await
218                        .save(&state, recursion_count)
219                        .await;
220                }
221
222                current_node = next_node;
223            }
224
225            // Q3: the loop only exits without reaching END when the recursion
226            // limit was hit — report it instead of silently emitting `end`.
227            if current_node != END {
228                let _ = tx
229                    .send(Err(GraphError::RecursionLimitReached(
230                        graph.recursion_limit,
231                    )))
232                    .await;
233                return;
234            }
235
236            let _ = tx.send(Ok(StreamEvent::end(state))).await;
237        });
238
239        Box::pin(ReceiverStream::new(rx))
240    }
241
242    /// Collect all stream events into a Vec (backward-compatible API).
243    ///
244    /// This is the old `stream()` behavior. Prefer `stream()` for
245    /// real-time consumption.
246    pub async fn stream_collected(&self, input: S) -> GraphResult<Vec<StreamEvent<S>>> {
247        let mut events = Vec::new();
248        let mut stream = self.stream(input);
249        use futures_util::StreamExt;
250        while let Some(event) = stream.next().await {
251            events.push(event?);
252        }
253        Ok(events)
254    }
255
256    pub(super) async fn find_next_node(&self, current: &str, state: &S) -> GraphResult<String> {
257        // 1. Check runtime edges — edge is cloned out of the guard before any .await
258        'rt: {
259            let edge = {
260                let re = self.runtime_edges.read().await;
261                match re.iter().find(|e| e.source() == current) {
262                    Some(e) => e.clone(),
263                    None => break 'rt,
264                }
265            }; // re dropped here
266            match edge {
267                GraphEdge::Fixed { target, .. } => return Ok(target),
268                GraphEdge::Conditional {
269                    router_name,
270                    targets,
271                    default_target,
272                    ..
273                } => {
274                    let router = self
275                        .conditional_routers
276                        .get(&router_name)
277                        .cloned()
278                        .or_else(|| {
279                            self.runtime_conditional_routers
280                                .try_read()
281                                .ok()
282                                .and_then(|guard| guard.get(&router_name).cloned())
283                        })
284                        .ok_or_else(|| {
285                            GraphError::ExecutionError(format!(
286                                "Router '{}' not found (runtime)",
287                                router_name
288                            ))
289                        })?;
290                    let route_key = router.route(state).await?;
291                    let target = targets
292                        .get(&route_key)
293                        .or(default_target.as_ref())
294                        .ok_or_else(|| {
295                            GraphError::RoutingError(format!(
296                                "No target for route '{}' (runtime)",
297                                route_key
298                            ))
299                        })?;
300                    return Ok(target.clone());
301                }
302                GraphEdge::FanOut { targets, .. } => {
303                    if targets.is_empty() {
304                        return Err(GraphError::RoutingError(
305                            "FanOut has no targets (runtime)".to_string(),
306                        ));
307                    }
308                    return Ok(targets[0].clone());
309                }
310                GraphEdge::FanIn { .. } => {}
311            }
312        }
313
314        // 2. Check static edges
315        for edge in &self.edges {
316            if edge.source() == current {
317                match edge {
318                    GraphEdge::Fixed { target, .. } => {
319                        return Ok(target.clone());
320                    }
321                    GraphEdge::Conditional {
322                        router_name,
323                        targets,
324                        default_target,
325                        ..
326                    } => {
327                        let router = self
328                            .conditional_routers
329                            .get(router_name)
330                            .cloned()
331                            .or_else(|| {
332                                self.runtime_conditional_routers
333                                    .try_read()
334                                    .ok()
335                                    .and_then(|guard| guard.get(router_name).cloned())
336                            })
337                            .ok_or_else(|| {
338                                GraphError::ExecutionError(format!(
339                                    "Router '{}' not found",
340                                    router_name
341                                ))
342                            })?;
343
344                        let route_key = router.route(state).await?;
345
346                        let target = targets
347                            .get(&route_key)
348                            .or(default_target.as_ref())
349                            .ok_or_else(|| {
350                                GraphError::RoutingError(format!(
351                                    "No target for route '{}'",
352                                    route_key
353                                ))
354                            })?;
355
356                        return Ok(target.clone());
357                    }
358                    GraphEdge::FanOut { targets, .. } => {
359                        if targets.is_empty() {
360                            return Err(GraphError::RoutingError(
361                                "FanOut has no targets".to_string(),
362                            ));
363                        }
364                        return Ok(targets[0].clone());
365                    }
366                    GraphEdge::FanIn { .. } => {
367                        continue;
368                    }
369                }
370            }
371        }
372
373        if current == self.entry_point && self.nodes.len() == 1 {
374            return Ok(END.to_string());
375        }
376
377        // 0.22.0 C3: a FanIn source with no outgoing edge terminates its
378        // branch — the parent fan-out loop merges at the FanIn target.
379        if self.is_fan_in_source(current).await {
380            return Ok(END.to_string());
381        }
382
383        Err(GraphError::RoutingError(format!(
384            "No outgoing edge from node '{}'",
385            current
386        )))
387    }
388
389    /// Whether `node` is a source of any FanIn edge (runtime or static).
390    pub(super) async fn is_fan_in_source(&self, node: &str) -> bool {
391        {
392            let re = self.runtime_edges.read().await;
393            if re.iter().any(|e| {
394                matches!(e, GraphEdge::FanIn { sources, .. } if sources.iter().any(|s| s == node))
395            }) {
396                return true;
397            }
398        }
399        self.edges.iter().any(
400            |e| matches!(e, GraphEdge::FanIn { sources, .. } if sources.iter().any(|s| s == node)),
401        )
402    }
403
404    pub(super) async fn find_fan_out_targets(&self, current: &str) -> Option<Vec<String>> {
405        {
406            let re = self.runtime_edges.read().await;
407            if let Some(GraphEdge::FanOut { targets, .. }) =
408                re.iter().find(|e| e.source() == current)
409            {
410                return Some(targets.clone());
411            }
412        }
413        for edge in &self.edges {
414            if edge.source() == current {
415                if let GraphEdge::FanOut { targets, .. } = edge {
416                    return Some(targets.clone());
417                }
418            }
419        }
420        None
421    }
422
423    pub(super) async fn find_fan_in_target(&self, sources: &[String]) -> Option<String> {
424        {
425            let re = self.runtime_edges.read().await;
426            if let Some(GraphEdge::FanIn {
427                sources: edge_sources,
428                target,
429            }) = re.iter().find(|e| matches!(e, GraphEdge::FanIn { .. }))
430            {
431                if edge_sources.iter().all(|s| sources.contains(s)) {
432                    return Some(target.clone());
433                }
434            }
435        }
436        for edge in &self.edges {
437            if let GraphEdge::FanIn {
438                sources: edge_sources,
439                target,
440            } = edge
441            {
442                if edge_sources.iter().all(|s| sources.contains(s)) {
443                    return Some(target.clone());
444                }
445            }
446        }
447        None
448    }
449
450    pub(super) async fn execute_parallel_branches(
451        &self,
452        targets: &[String],
453        state: &S,
454        recursion_count: usize,
455    ) -> GraphResult<Vec<(String, GraphInvocation<S>)>> {
456        // Q6: branches inherit the main path's recursion count so their depth is
457        // charged against the same shared `recursion_limit` budget. A branch that
458        // would exceed the limit returns `RecursionLimitReached` and aborts the
459        // whole fan-out instead of running unbounded.
460        let futures: Vec<_> = targets
461            .iter()
462            .filter(|t| *t != END)
463            .map(|target| {
464                let target = target.clone();
465                let state_clone = state.clone();
466                async move {
467                    let result = self
468                        .invoke_from_node_with_count(target.clone(), state_clone, recursion_count)
469                        .await;
470                    result.map(|inv| (target, inv))
471                }
472            })
473            .collect();
474
475        let results = join_all(futures).await;
476
477        let mut successful = Vec::new();
478        for result in results {
479            match result {
480                Ok((name, inv)) => successful.push((name, inv)),
481                Err(e) => return Err(e),
482            }
483        }
484
485        Ok(successful)
486    }
487}