Skip to main content

lc_core/runnables/
sequence.rs

1// lc-core/src/runnables/sequence.rs
2//! RunnableSequence - the core LCEL pipeline type.
3//!
4//! A `RunnableSequence<I, O>` chains multiple `Runnable` steps together,
5//! where the output of each step feeds into the input of the next.
6//! Internally, steps are stored as `Box<dyn RunnableAny>` (type-erased),
7//! but the `I` and `O` type parameters preserve the pipeline's
8//! input and output types at compile time.
9
10use super::any::{into_runnable_any, RunnableAny};
11use super::config::RunnableConfig;
12use super::error::LcelError;
13use super::runnable_trait::Runnable;
14use async_trait::async_trait;
15use futures_util::{Stream, StreamExt};
16use std::any::Any;
17use std::marker::PhantomData;
18use std::pin::Pin;
19
20/// A sequence of `Runnable` steps composed into a pipeline.
21///
22/// Created via the `pipe()` method on `RunnableExt`, or directly
23/// with `from_single` / `from_pair`.
24///
25/// # Type Safety
26///
27/// The `I` and `O` type parameters represent the pipeline's overall
28/// input and output types. Intermediate types are erased at runtime
29/// via `RunnableAny`, but the compiler guarantees type compatibility
30/// at each `pipe()` call site.
31///
32/// # Flattening
33///
34/// When two `RunnableSequence` values are piped together, their
35/// internal steps are merged (flattened) rather than nested,
36/// avoiding unnecessary indirection.
37pub struct RunnableSequence<I: Send + Sync + 'static, O: Send + Sync + 'static> {
38    steps: Vec<Box<dyn RunnableAny>>,
39    _marker: PhantomData<(I, O)>,
40}
41
42impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug
43    for RunnableSequence<I, O>
44{
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("RunnableSequence")
47            .field("steps", &self.steps.len())
48            .field("input", &std::any::type_name::<I>())
49            .field("output", &std::any::type_name::<O>())
50            .finish()
51    }
52}
53
54impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableSequence<I, O> {
55    /// Create a sequence from a single runnable step.
56    pub fn from_single<R>(runnable: R) -> Self
57    where
58        R: Runnable<I, O> + 'static,
59        R::Error: Into<LcelError>,
60    {
61        Self {
62            steps: vec![into_runnable_any(runnable)],
63            _marker: PhantomData,
64        }
65    }
66
67    /// Create a sequence from two runnable steps.
68    ///
69    /// The output type of the first must match the input type of the second.
70    pub fn from_pair<R1, R2, M>(first: R1, second: R2) -> RunnableSequence<I, O>
71    where
72        M: Send + Sync + 'static,
73        R1: Runnable<I, M> + 'static,
74        R1::Error: Into<LcelError>,
75        R2: Runnable<M, O> + 'static,
76        R2::Error: Into<LcelError>,
77    {
78        Self {
79            steps: vec![into_runnable_any(first), into_runnable_any(second)],
80            _marker: PhantomData,
81        }
82    }
83
84    /// Append a step to this sequence, returning a new sequence
85    /// with the updated output type.
86    pub fn pipe<O2, R>(self, other: R) -> RunnableSequence<I, O2>
87    where
88        O2: Send + Sync + 'static,
89        R: Runnable<O, O2> + Send + Sync + 'static,
90        R::Error: Into<LcelError>,
91    {
92        let mut steps = self.steps;
93        steps.push(into_runnable_any(other));
94        RunnableSequence {
95            steps,
96            _marker: PhantomData,
97        }
98    }
99
100    /// Number of steps in this sequence.
101    pub fn len(&self) -> usize {
102        self.steps.len()
103    }
104
105    /// Whether this sequence has no steps.
106    pub fn is_empty(&self) -> bool {
107        self.steps.is_empty()
108    }
109
110    /// Access the steps as a slice.
111    pub fn steps(&self) -> &[Box<dyn RunnableAny>] {
112        &self.steps
113    }
114}
115
116#[async_trait]
117impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableSequence<I, O> {
118    type Error = LcelError;
119
120    /// Execute the pipeline: feed input through each step sequentially.
121    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
122        let mut current: Box<dyn Any + Send> = Box::new(input);
123        for step in &self.steps {
124            current = step.invoke_any(current, config.clone()).await?;
125        }
126        current.downcast::<O>().map(|b| *b).map_err(|_| {
127            LcelError::TypeMismatch(format!(
128                "final downcast failed: expected {}",
129                std::any::type_name::<O>()
130            ))
131        })
132    }
133
134    /// Batch processing: each step processes all inputs before
135    /// passing to the next step. This allows LLM providers to
136    /// optimize batch requests.
137    async fn batch(
138        &self,
139        inputs: Vec<I>,
140        config: Option<RunnableConfig>,
141    ) -> Result<Vec<O>, LcelError> {
142        let mut current: Vec<Box<dyn Any + Send>> = inputs
143            .into_iter()
144            .map(|i| Box::new(i) as Box<dyn Any + Send>)
145            .collect();
146
147        for step in &self.steps {
148            current = step.batch_any(current, config.clone()).await?;
149        }
150
151        current
152            .into_iter()
153            .map(|boxed| {
154                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
155                    LcelError::TypeMismatch(format!(
156                        "batch final downcast: expected {}",
157                        std::any::type_name::<O>()
158                    ))
159                })
160            })
161            .collect()
162    }
163
164    /// Streaming: the first step runs through `stream_any` (single input →
165    /// item stream) and every following step runs through `transform_any`.
166    /// Steps that override `stream` (e.g. LLMs) therefore emit a real token
167    /// stream instead of being collapsed to a single `invoke`.
168    async fn stream(
169        &self,
170        input: I,
171        config: Option<RunnableConfig>,
172    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
173        if self.steps.is_empty() {
174            return Ok(Box::pin(futures_util::stream::empty()));
175        }
176
177        let mut steps = self.steps.iter();
178        let Some(first) = steps.next() else {
179            return Ok(Box::pin(futures_util::stream::empty()));
180        };
181
182        let input_boxed: Box<dyn Any + Send> = Box::new(input);
183        let mut current_stream = first.stream_any(input_boxed, config.clone()).await?;
184
185        // Chain each subsequent step's transform
186        for step in steps {
187            current_stream = step.transform_any(current_stream, config.clone()).await?;
188        }
189
190        // Downcast the final stream from Any to O
191        let output_stream = current_stream.map(|result| {
192            result.and_then(|boxed| {
193                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
194                    LcelError::TypeMismatch(format!(
195                        "stream final downcast: expected {}",
196                        std::any::type_name::<O>()
197                    ))
198                })
199            })
200        });
201
202        Ok(Box::pin(output_stream))
203    }
204
205    /// Transform: chain each step's transform to enable
206    /// stream-to-stream pipeline processing.
207    async fn transform(
208        &self,
209        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
210        config: Option<RunnableConfig>,
211    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send + '_>>, LcelError> {
212        // Upcast input stream from I to Any
213        let mut current_stream: Pin<
214            Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>,
215        > = Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
216
217        // Chain each step's transform
218        for step in &self.steps {
219            current_stream = step.transform_any(current_stream, config.clone()).await?;
220        }
221
222        // Downcast the final stream from Any to O
223        let output_stream = current_stream.map(|result| {
224            result.and_then(|boxed| {
225                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
226                    LcelError::TypeMismatch(format!(
227                        "transform final downcast: expected {}",
228                        std::any::type_name::<O>()
229                    ))
230                })
231            })
232        });
233
234        Ok(Box::pin(output_stream))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use futures_util::StreamExt;
242
243    struct Double;
244
245    #[async_trait]
246    impl Runnable<i32, i32> for Double {
247        type Error = std::convert::Infallible;
248
249        async fn invoke(
250            &self,
251            input: i32,
252            _config: Option<RunnableConfig>,
253        ) -> Result<i32, Self::Error> {
254            Ok(input * 2)
255        }
256    }
257
258    struct AddOne;
259
260    #[async_trait]
261    impl Runnable<i32, i32> for AddOne {
262        type Error = std::convert::Infallible;
263
264        async fn invoke(
265            &self,
266            input: i32,
267            _config: Option<RunnableConfig>,
268        ) -> Result<i32, Self::Error> {
269            Ok(input + 1)
270        }
271    }
272
273    struct I32ToString;
274
275    #[async_trait]
276    impl Runnable<i32, String> for I32ToString {
277        type Error = std::convert::Infallible;
278
279        async fn invoke(
280            &self,
281            input: i32,
282            _config: Option<RunnableConfig>,
283        ) -> Result<String, Self::Error> {
284            Ok(format!("value={}", input))
285        }
286    }
287
288    #[tokio::test]
289    async fn invoke_two_steps() {
290        // Double → AddOne: 5 * 2 + 1 = 11
291        let seq = RunnableSequence::from_pair(Double, AddOne);
292        let result = seq.invoke(5, None).await.unwrap();
293        assert_eq!(result, 11);
294    }
295
296    #[tokio::test]
297    async fn invoke_three_steps() {
298        // Double → AddOne → I32ToString: 3 * 2 + 1 = "value=7"
299        let seq = RunnableSequence::from_pair(Double, AddOne).pipe(I32ToString);
300        let result = seq.invoke(3, None).await.unwrap();
301        assert_eq!(result, "value=7");
302    }
303
304    #[tokio::test]
305    async fn batch_works() {
306        let seq = RunnableSequence::from_pair(Double, AddOne);
307        let results = seq.batch(vec![1, 2, 3], None).await.unwrap();
308        assert_eq!(results, vec![3, 5, 7]);
309    }
310
311    #[tokio::test]
312    async fn stream_works() {
313        let seq = RunnableSequence::from_pair(Double, AddOne);
314        let mut stream = seq.stream(10, None).await.unwrap();
315        let result = stream.next().await.unwrap().unwrap();
316        assert_eq!(result, 21);
317    }
318
319    #[tokio::test]
320    async fn transform_works_elementwise() {
321        let seq = RunnableSequence::from_pair(Double, AddOne);
322        let input = Box::pin(futures_util::stream::iter(vec![
323            Ok(1i32),
324            Ok(2i32),
325            Ok(3i32),
326        ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
327
328        // Default transform maps each item through the chain elementwise
329        // (LangChain default semantics): 1→3, 2→5, 3→7.
330        let mut output = seq.transform(input, None).await.unwrap();
331        let mut results = Vec::new();
332        while let Some(item) = output.next().await {
333            results.push(item.unwrap());
334        }
335        assert_eq!(results, vec![3, 5, 7]);
336    }
337
338    // A runnable that overrides `stream` to emit several items, mimicking an
339    // LLM token stream. `invoke` returns a distinguishable value so tests can
340    // prove the streaming path was actually taken.
341    struct StreamingTokenLLM;
342
343    #[async_trait]
344    impl Runnable<i32, i32> for StreamingTokenLLM {
345        type Error = std::convert::Infallible;
346
347        async fn invoke(
348            &self,
349            input: i32,
350            _config: Option<RunnableConfig>,
351        ) -> Result<i32, Self::Error> {
352            Ok(input * 1000)
353        }
354
355        async fn stream(
356            &self,
357            input: i32,
358            _config: Option<RunnableConfig>,
359        ) -> Result<Pin<Box<dyn Stream<Item = Result<i32, Self::Error>> + Send>>, Self::Error>
360        {
361            let stream =
362                futures_util::stream::iter(vec![Ok(input), Ok(input + 100), Ok(input + 200)]);
363            Ok(Box::pin(stream))
364        }
365    }
366
367    #[tokio::test]
368    async fn stream_uses_real_streaming_for_first_step() {
369        // Single step with a real `stream` override: `sequence.stream` must
370        // emit every streamed item (proving it goes through `stream_any`, not
371        // a single `invoke`).
372        let seq: RunnableSequence<i32, i32> = RunnableSequence::from_single(StreamingTokenLLM);
373        let mut stream = seq.stream(5, None).await.unwrap();
374        let mut results = Vec::new();
375        while let Some(item) = stream.next().await {
376            results.push(item.unwrap());
377        }
378        assert_eq!(results, vec![5, 105, 205]);
379    }
380
381    #[tokio::test]
382    async fn stream_chains_subsequent_steps_elementwise() {
383        // StreamingTokenLLM → AddOne: the token stream [5, 105, 205] is
384        // transformed elementwise by AddOne → [6, 106, 206].
385        let seq = RunnableSequence::from_pair(StreamingTokenLLM, AddOne);
386        let mut stream = seq.stream(5, None).await.unwrap();
387        let mut results = Vec::new();
388        while let Some(item) = stream.next().await {
389            results.push(item.unwrap());
390        }
391        assert_eq!(results, vec![6, 106, 206]);
392    }
393
394    #[tokio::test]
395    async fn from_single_works() {
396        let seq: RunnableSequence<i32, i32> = RunnableSequence::from_single(Double);
397        let result = seq.invoke(4, None).await.unwrap();
398        assert_eq!(result, 8);
399    }
400
401    #[tokio::test]
402    async fn pipe_on_sequence_works() {
403        let seq = RunnableSequence::from_single(Double)
404            .pipe(AddOne)
405            .pipe(I32ToString);
406        let result = seq.invoke(5, None).await.unwrap();
407        assert_eq!(result, "value=11"); // 5*2+1=11
408    }
409
410    #[tokio::test]
411    async fn len_and_empty() {
412        let seq = RunnableSequence::from_pair(Double, AddOne);
413        assert_eq!(seq.len(), 2);
414        assert!(!seq.is_empty());
415    }
416}