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, 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                recursion_count += 1;
65
66                if tx
67                    .send(Ok(StreamEvent::enter_node(
68                        current_node.clone(),
69                        state.clone(),
70                    )))
71                    .await
72                    .is_err()
73                {
74                    return;
75                }
76
77                let node = match graph.get_node(&current_node).await {
78                    Ok(n) => n,
79                    Err(e) => {
80                        let _ = tx.send(Err(e)).await;
81                        return;
82                    }
83                };
84
85                let config = NodeConfig {
86                    recursion_limit: graph.recursion_limit,
87                    debug: false,
88                    metadata: HashMap::new(),
89                };
90
91                let update = match node.execute(&state, Some(config)).await {
92                    Ok(u) => u,
93                    Err(e) => {
94                        let _ = tx.send(Err(e)).await;
95                        return;
96                    }
97                };
98
99                if tx
100                    .send(Ok(StreamEvent::node_complete(
101                        current_node.clone(),
102                        update.clone(),
103                    )))
104                    .await
105                    .is_err()
106                {
107                    return;
108                }
109
110                if let Some(new_state) = update.update {
111                    state = graph.default_reducer.reduce(&state, &new_state);
112                    if tx
113                        .send(Ok(StreamEvent::state_update(state.clone())))
114                        .await
115                        .is_err()
116                    {
117                        return;
118                    }
119                }
120
121                if graph.interrupt_after.contains(&current_node) {
122                    if let Some(ref checkpointer) = graph.checkpointer {
123                        let _ = checkpointer
124                            .lock()
125                            .await
126                            .save(&state, recursion_count)
127                            .await;
128                    }
129                    let _ = tx
130                        .send(Err(GraphError::ExecutionInterrupted(format!(
131                            "after_{}",
132                            current_node
133                        ))))
134                        .await;
135                    return;
136                }
137
138                let next_node = match graph.find_next_node(&current_node, &state).await {
139                    Ok(n) => n,
140                    Err(e) => {
141                        let _ = tx.send(Err(e)).await;
142                        return;
143                    }
144                };
145
146                if let Some(ref checkpointer) = graph.checkpointer {
147                    let _ = checkpointer
148                        .lock()
149                        .await
150                        .save(&state, recursion_count)
151                        .await;
152                }
153
154                current_node = next_node;
155            }
156
157            // Q3: the loop only exits without reaching END when the recursion
158            // limit was hit — report it instead of silently emitting `end`.
159            if current_node != END {
160                let _ = tx
161                    .send(Err(GraphError::RecursionLimitReached(
162                        graph.recursion_limit,
163                    )))
164                    .await;
165                return;
166            }
167
168            let _ = tx.send(Ok(StreamEvent::end(state))).await;
169        });
170
171        Box::pin(ReceiverStream::new(rx))
172    }
173
174    /// Collect all stream events into a Vec (backward-compatible API).
175    ///
176    /// This is the old `stream()` behavior. Prefer `stream()` for
177    /// real-time consumption.
178    pub async fn stream_collected(&self, input: S) -> GraphResult<Vec<StreamEvent<S>>> {
179        let mut events = Vec::new();
180        let mut stream = self.stream(input);
181        use futures_util::StreamExt;
182        while let Some(event) = stream.next().await {
183            events.push(event?);
184        }
185        Ok(events)
186    }
187
188    pub(super) async fn find_next_node(&self, current: &str, state: &S) -> GraphResult<String> {
189        // 1. Check runtime edges — edge is cloned out of the guard before any .await
190        'rt: {
191            let edge = {
192                let re = self.runtime_edges.read().await;
193                match re.iter().find(|e| e.source() == current) {
194                    Some(e) => e.clone(),
195                    None => break 'rt,
196                }
197            }; // re dropped here
198            match edge {
199                GraphEdge::Fixed { target, .. } => return Ok(target),
200                GraphEdge::Conditional {
201                    router_name,
202                    targets,
203                    default_target,
204                    ..
205                } => {
206                    let router = self
207                        .conditional_routers
208                        .get(&router_name)
209                        .cloned()
210                        .or_else(|| {
211                            self.runtime_conditional_routers
212                                .try_read()
213                                .ok()
214                                .and_then(|guard| guard.get(&router_name).cloned())
215                        })
216                        .ok_or_else(|| {
217                            GraphError::ExecutionError(format!(
218                                "Router '{}' not found (runtime)",
219                                router_name
220                            ))
221                        })?;
222                    let route_key = router.route(state).await?;
223                    let target = targets
224                        .get(&route_key)
225                        .or(default_target.as_ref())
226                        .ok_or_else(|| {
227                            GraphError::RoutingError(format!(
228                                "No target for route '{}' (runtime)",
229                                route_key
230                            ))
231                        })?;
232                    return Ok(target.clone());
233                }
234                GraphEdge::FanOut { targets, .. } => {
235                    if targets.is_empty() {
236                        return Err(GraphError::RoutingError(
237                            "FanOut has no targets (runtime)".to_string(),
238                        ));
239                    }
240                    return Ok(targets[0].clone());
241                }
242                GraphEdge::FanIn { .. } => {}
243            }
244        }
245
246        // 2. Check static edges
247        for edge in &self.edges {
248            if edge.source() == current {
249                match edge {
250                    GraphEdge::Fixed { target, .. } => {
251                        return Ok(target.clone());
252                    }
253                    GraphEdge::Conditional {
254                        router_name,
255                        targets,
256                        default_target,
257                        ..
258                    } => {
259                        let router = self
260                            .conditional_routers
261                            .get(router_name)
262                            .cloned()
263                            .or_else(|| {
264                                self.runtime_conditional_routers
265                                    .try_read()
266                                    .ok()
267                                    .and_then(|guard| guard.get(router_name).cloned())
268                            })
269                            .ok_or_else(|| {
270                                GraphError::ExecutionError(format!(
271                                    "Router '{}' not found",
272                                    router_name
273                                ))
274                            })?;
275
276                        let route_key = router.route(state).await?;
277
278                        let target = targets
279                            .get(&route_key)
280                            .or(default_target.as_ref())
281                            .ok_or_else(|| {
282                                GraphError::RoutingError(format!(
283                                    "No target for route '{}'",
284                                    route_key
285                                ))
286                            })?;
287
288                        return Ok(target.clone());
289                    }
290                    GraphEdge::FanOut { targets, .. } => {
291                        if targets.is_empty() {
292                            return Err(GraphError::RoutingError(
293                                "FanOut has no targets".to_string(),
294                            ));
295                        }
296                        return Ok(targets[0].clone());
297                    }
298                    GraphEdge::FanIn { .. } => {
299                        continue;
300                    }
301                }
302            }
303        }
304
305        if current == self.entry_point && self.nodes.len() == 1 {
306            return Ok(END.to_string());
307        }
308
309        Err(GraphError::RoutingError(format!(
310            "No outgoing edge from node '{}'",
311            current
312        )))
313    }
314
315    pub(super) async fn find_fan_out_targets(&self, current: &str) -> Option<Vec<String>> {
316        {
317            let re = self.runtime_edges.read().await;
318            if let Some(GraphEdge::FanOut { targets, .. }) =
319                re.iter().find(|e| e.source() == current)
320            {
321                return Some(targets.clone());
322            }
323        }
324        for edge in &self.edges {
325            if edge.source() == current {
326                if let GraphEdge::FanOut { targets, .. } = edge {
327                    return Some(targets.clone());
328                }
329            }
330        }
331        None
332    }
333
334    pub(super) async fn find_fan_in_target(&self, sources: &[String]) -> Option<String> {
335        {
336            let re = self.runtime_edges.read().await;
337            if let Some(GraphEdge::FanIn {
338                sources: edge_sources,
339                target,
340            }) = re.iter().find(|e| matches!(e, GraphEdge::FanIn { .. }))
341            {
342                if edge_sources.iter().all(|s| sources.contains(s)) {
343                    return Some(target.clone());
344                }
345            }
346        }
347        for edge in &self.edges {
348            if let GraphEdge::FanIn {
349                sources: edge_sources,
350                target,
351            } = edge
352            {
353                if edge_sources.iter().all(|s| sources.contains(s)) {
354                    return Some(target.clone());
355                }
356            }
357        }
358        None
359    }
360
361    pub(super) async fn execute_parallel_branches(
362        &self,
363        targets: &[String],
364        state: &S,
365        recursion_count: usize,
366    ) -> GraphResult<Vec<(String, GraphInvocation<S>)>> {
367        // Q6: branches inherit the main path's recursion count so their depth is
368        // charged against the same shared `recursion_limit` budget. A branch that
369        // would exceed the limit returns `RecursionLimitReached` and aborts the
370        // whole fan-out instead of running unbounded.
371        let futures: Vec<_> = targets
372            .iter()
373            .filter(|t| *t != END)
374            .map(|target| {
375                let target = target.clone();
376                let state_clone = state.clone();
377                async move {
378                    let result = self
379                        .invoke_from_node_with_count(target.clone(), state_clone, recursion_count)
380                        .await;
381                    result.map(|inv| (target, inv))
382                }
383            })
384            .collect();
385
386        let results = join_all(futures).await;
387
388        let mut successful = Vec::new();
389        for result in results {
390            match result {
391                Ok((name, inv)) => successful.push((name, inv)),
392                Err(e) => return Err(e),
393            }
394        }
395
396        Ok(successful)
397    }
398}