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                        // Surface a node-requested interrupt as the caller-facing
99                        // error (matching the `invoke` path) instead of leaking the
100                        // internal control-flow signal.
101                        Err(crate::errors::GraphError::InterruptRequest { ref payload }) => {
102                            let _ = tx
103                                .send(Err(crate::errors::GraphError::DynamicInterrupt {
104                                    node: current_node.clone(),
105                                    payload: payload.clone(),
106                                }))
107                                .await;
108                            return;
109                        }
110                        Err(e) => {
111                            let _ = tx.send(Err(e)).await;
112                            return;
113                        }
114                    };
115
116                    if tx
117                        .send(Ok(StreamEvent::node_complete(
118                            current_node.clone(),
119                            update.clone(),
120                        )))
121                        .await
122                        .is_err()
123                    {
124                        return;
125                    }
126
127                    if let Some(new_state) = update.update {
128                        state = graph.default_reducer.reduce(&state, &new_state);
129                        if tx
130                            .send(Ok(StreamEvent::state_update(state.clone())))
131                            .await
132                            .is_err()
133                        {
134                            return;
135                        }
136                    }
137
138                    if graph.interrupt_after.contains(&current_node) {
139                        if let Some(ref checkpointer) = graph.checkpointer {
140                            let _ = checkpointer
141                                .lock()
142                                .await
143                                .save(&state, recursion_count)
144                                .await;
145                        }
146                        let _ = tx
147                            .send(Err(GraphError::ExecutionInterrupted(format!(
148                                "after_{}",
149                                current_node
150                            ))))
151                            .await;
152                        return;
153                    }
154                }
155
156                // C3: FanOut interception — same semantics as the invoke path:
157                // execute all branches, merge on top of the pre-fan-out state,
158                // then continue at the FanIn target (or END).
159                let fan_out_targets = graph.find_fan_out_targets(&current_node).await;
160                if let Some(targets) = fan_out_targets {
161                    recursion_count += 1;
162                    let branch_results = match graph
163                        .execute_parallel_branches(&targets, &state, recursion_count)
164                        .await
165                    {
166                        Ok(r) => r,
167                        Err(e) => {
168                            let _ = tx.send(Err(e)).await;
169                            return;
170                        }
171                    };
172                    let mut parallel_branches: Vec<ParallelBranch<S>> = Vec::new();
173                    for (name, inv) in branch_results {
174                        parallel_branches.push(ParallelBranch {
175                            name: name.clone(),
176                            final_state: inv.final_state.clone(),
177                            steps: inv.steps.clone(),
178                        });
179                        let _ = tx
180                            .send(Ok(StreamEvent::node_complete(
181                                name.clone(),
182                                crate::state::StateUpdate {
183                                    update: Some(inv.final_state.clone()),
184                                    metadata: HashMap::new(),
185                                },
186                            )))
187                            .await;
188                    }
189                    let base = state.clone();
190                    state = match graph.merge_parallel_states(&parallel_branches, &base) {
191                        Ok(s) => s,
192                        Err(e) => {
193                            let _ = tx.send(Err(e)).await;
194                            return;
195                        }
196                    };
197                    if tx
198                        .send(Ok(StreamEvent::state_update(state.clone())))
199                        .await
200                        .is_err()
201                    {
202                        return;
203                    }
204                    current_node = match graph.find_fan_in_target(&targets).await {
205                        Some(merge_node) => merge_node,
206                        None => END.to_string(),
207                    };
208                    if let Some(ref checkpointer) = graph.checkpointer {
209                        let _ = checkpointer
210                            .lock()
211                            .await
212                            .save(&state, recursion_count)
213                            .await;
214                    }
215                    continue;
216                }
217
218                let next_node = match graph.find_next_node(&current_node, &state).await {
219                    Ok(n) => n,
220                    Err(e) => {
221                        let _ = tx.send(Err(e)).await;
222                        return;
223                    }
224                };
225
226                if let Some(ref checkpointer) = graph.checkpointer {
227                    let _ = checkpointer
228                        .lock()
229                        .await
230                        .save(&state, recursion_count)
231                        .await;
232                }
233
234                current_node = next_node;
235            }
236
237            // Q3: the loop only exits without reaching END when the recursion
238            // limit was hit — report it instead of silently emitting `end`.
239            if current_node != END {
240                let _ = tx
241                    .send(Err(GraphError::RecursionLimitReached(
242                        graph.recursion_limit,
243                    )))
244                    .await;
245                return;
246            }
247
248            let _ = tx.send(Ok(StreamEvent::end(state))).await;
249        });
250
251        Box::pin(ReceiverStream::new(rx))
252    }
253
254    /// Collect all stream events into a Vec (backward-compatible API).
255    ///
256    /// This is the old `stream()` behavior. Prefer `stream()` for
257    /// real-time consumption.
258    pub async fn stream_collected(&self, input: S) -> GraphResult<Vec<StreamEvent<S>>> {
259        let mut events = Vec::new();
260        let mut stream = self.stream(input);
261        use futures_util::StreamExt;
262        while let Some(event) = stream.next().await {
263            events.push(event?);
264        }
265        Ok(events)
266    }
267
268    pub(super) async fn find_next_node(&self, current: &str, state: &S) -> GraphResult<String> {
269        // 1. Check runtime edges — edge is cloned out of the guard before any .await
270        'rt: {
271            let edge = {
272                let re = self.runtime_edges.read().await;
273                match re.iter().find(|e| e.source() == current) {
274                    Some(e) => e.clone(),
275                    None => break 'rt,
276                }
277            }; // re dropped here
278            match edge {
279                GraphEdge::Fixed { target, .. } => return Ok(target),
280                GraphEdge::Conditional {
281                    router_name,
282                    targets,
283                    default_target,
284                    ..
285                } => {
286                    let router = self
287                        .conditional_routers
288                        .get(&router_name)
289                        .cloned()
290                        .or_else(|| {
291                            self.runtime_conditional_routers
292                                .try_read()
293                                .ok()
294                                .and_then(|guard| guard.get(&router_name).cloned())
295                        })
296                        .ok_or_else(|| {
297                            GraphError::ExecutionError(format!(
298                                "Router '{}' not found (runtime)",
299                                router_name
300                            ))
301                        })?;
302                    let route_key = router.route(state).await?;
303                    let target = targets
304                        .get(&route_key)
305                        .or(default_target.as_ref())
306                        .ok_or_else(|| {
307                            GraphError::RoutingError(format!(
308                                "No target for route '{}' (runtime)",
309                                route_key
310                            ))
311                        })?;
312                    return Ok(target.clone());
313                }
314                GraphEdge::FanOut { targets, .. } => {
315                    if targets.is_empty() {
316                        return Err(GraphError::RoutingError(
317                            "FanOut has no targets (runtime)".to_string(),
318                        ));
319                    }
320                    return Ok(targets[0].clone());
321                }
322                GraphEdge::FanIn { .. } => {}
323            }
324        }
325
326        // 2. Check static edges
327        for edge in &self.edges {
328            if edge.source() == current {
329                match edge {
330                    GraphEdge::Fixed { target, .. } => {
331                        return Ok(target.clone());
332                    }
333                    GraphEdge::Conditional {
334                        router_name,
335                        targets,
336                        default_target,
337                        ..
338                    } => {
339                        let router = self
340                            .conditional_routers
341                            .get(router_name)
342                            .cloned()
343                            .or_else(|| {
344                                self.runtime_conditional_routers
345                                    .try_read()
346                                    .ok()
347                                    .and_then(|guard| guard.get(router_name).cloned())
348                            })
349                            .ok_or_else(|| {
350                                GraphError::ExecutionError(format!(
351                                    "Router '{}' not found",
352                                    router_name
353                                ))
354                            })?;
355
356                        let route_key = router.route(state).await?;
357
358                        let target = targets
359                            .get(&route_key)
360                            .or(default_target.as_ref())
361                            .ok_or_else(|| {
362                                GraphError::RoutingError(format!(
363                                    "No target for route '{}'",
364                                    route_key
365                                ))
366                            })?;
367
368                        return Ok(target.clone());
369                    }
370                    GraphEdge::FanOut { targets, .. } => {
371                        if targets.is_empty() {
372                            return Err(GraphError::RoutingError(
373                                "FanOut has no targets".to_string(),
374                            ));
375                        }
376                        return Ok(targets[0].clone());
377                    }
378                    GraphEdge::FanIn { .. } => {
379                        continue;
380                    }
381                }
382            }
383        }
384
385        if current == self.entry_point && self.nodes.len() == 1 {
386            return Ok(END.to_string());
387        }
388
389        // 0.22.0 C3: a FanIn source with no outgoing edge terminates its
390        // branch — the parent fan-out loop merges at the FanIn target.
391        if self.is_fan_in_source(current).await {
392            return Ok(END.to_string());
393        }
394
395        Err(GraphError::RoutingError(format!(
396            "No outgoing edge from node '{}'",
397            current
398        )))
399    }
400
401    /// Whether `node` is a source of any FanIn edge (runtime or static).
402    pub(super) async fn is_fan_in_source(&self, node: &str) -> bool {
403        {
404            let re = self.runtime_edges.read().await;
405            if re.iter().any(|e| {
406                matches!(e, GraphEdge::FanIn { sources, .. } if sources.iter().any(|s| s == node))
407            }) {
408                return true;
409            }
410        }
411        self.edges.iter().any(
412            |e| matches!(e, GraphEdge::FanIn { sources, .. } if sources.iter().any(|s| s == node)),
413        )
414    }
415
416    pub(super) async fn find_fan_out_targets(&self, current: &str) -> Option<Vec<String>> {
417        {
418            let re = self.runtime_edges.read().await;
419            if let Some(GraphEdge::FanOut { targets, .. }) =
420                re.iter().find(|e| e.source() == current)
421            {
422                return Some(targets.clone());
423            }
424        }
425        for edge in &self.edges {
426            if edge.source() == current {
427                if let GraphEdge::FanOut { targets, .. } = edge {
428                    return Some(targets.clone());
429                }
430            }
431        }
432        None
433    }
434
435    pub(super) async fn find_fan_in_target(&self, sources: &[String]) -> Option<String> {
436        {
437            let re = self.runtime_edges.read().await;
438            if let Some(GraphEdge::FanIn {
439                sources: edge_sources,
440                target,
441            }) = re.iter().find(|e| matches!(e, GraphEdge::FanIn { .. }))
442            {
443                if edge_sources.iter().all(|s| sources.contains(s)) {
444                    return Some(target.clone());
445                }
446            }
447        }
448        for edge in &self.edges {
449            if let GraphEdge::FanIn {
450                sources: edge_sources,
451                target,
452            } = edge
453            {
454                if edge_sources.iter().all(|s| sources.contains(s)) {
455                    return Some(target.clone());
456                }
457            }
458        }
459        None
460    }
461
462    pub(super) async fn execute_parallel_branches(
463        &self,
464        targets: &[String],
465        state: &S,
466        recursion_count: usize,
467    ) -> GraphResult<Vec<(String, GraphInvocation<S>)>> {
468        // Q6: branches inherit the main path's recursion count so their depth is
469        // charged against the same shared `recursion_limit` budget. A branch that
470        // would exceed the limit returns `RecursionLimitReached` and aborts the
471        // whole fan-out instead of running unbounded.
472        let futures: Vec<_> = targets
473            .iter()
474            .filter(|t| *t != END)
475            .map(|target| {
476                let target = target.clone();
477                let state_clone = state.clone();
478                async move {
479                    let result = self
480                        .invoke_from_node_with_count(target.clone(), state_clone, recursion_count)
481                        .await;
482                    result.map(|inv| (target, inv))
483                }
484            })
485            .collect();
486
487        let results = join_all(futures).await;
488
489        let mut successful = Vec::new();
490        for result in results {
491            match result {
492                Ok((name, inv)) => successful.push((name, inv)),
493                Err(e) => return Err(e),
494            }
495        }
496
497        Ok(successful)
498    }
499}