Skip to main content

lc_agents/
adapter.rs

1// lc-agents/src/adapter.rs
2//! AgentRunnable adapter - bridges AgentExecutor to the Runnable trait.
3//!
4//! This allows agents to participate in LCEL pipelines via `pipe()`.
5
6use async_trait::async_trait;
7use futures_util::Stream;
8use lc_core::runnables::{LcelError, Runnable, RunnableConfig};
9use std::pin::Pin;
10use std::sync::Arc;
11
12use crate::base::AgentExecutor;
13use crate::orchestration::{Orchestrator, RunContext};
14use crate::streaming::AgentStreamEvent;
15
16/// Adapter that wraps an `AgentExecutor` as a `Runnable<String, String>`.
17///
18/// This enables agents to participate in LCEL pipelines:
19///
20/// ```rust,ignore
21/// let agent_runnable = AgentRunnable::new(Arc::new(executor));
22/// let pipeline = prompt.pipe(agent_runnable).pipe(parser);
23/// ```
24pub struct AgentRunnable {
25    executor: Arc<AgentExecutor>,
26}
27
28impl AgentRunnable {
29    /// Create a new adapter wrapping the given executor.
30    pub fn new(executor: Arc<AgentExecutor>) -> Self {
31        Self { executor }
32    }
33}
34
35#[async_trait]
36impl Runnable<String, String> for AgentRunnable {
37    type Error = LcelError;
38
39    async fn invoke(
40        &self,
41        input: String,
42        config: Option<RunnableConfig>,
43    ) -> Result<String, LcelError> {
44        // Merge config callbacks with executor's own callbacks
45        self.executor
46            .invoke_with_config(input, config)
47            .await
48            .map_err(|e| LcelError::Agent(e.to_string()))
49    }
50
51    /// Override stream() to delegate to AgentExecutor::stream(),
52    /// enabling real streaming in LCEL pipelines.
53    async fn stream(
54        &self,
55        input: String,
56        _config: Option<RunnableConfig>,
57    ) -> Result<Pin<Box<dyn Stream<Item = Result<String, LcelError>> + Send>>, LcelError> {
58        use futures_util::StreamExt;
59
60        let event_stream = self.executor.stream(input);
61
62        // Map AgentStreamEvent to String, extracting FinalAnswer content
63        let mapped = event_stream.filter_map(|event_result| async move {
64            match event_result {
65                Ok(event) => match event {
66                    crate::streaming::AgentStreamEvent::FinalAnswer { content } => {
67                        Some(Ok(content))
68                    }
69                    crate::streaming::AgentStreamEvent::Error { message } => {
70                        Some(Err(LcelError::Agent(message)))
71                    }
72                    // Skip other event types (ToolStart, ToolEnd, PipelineStep, etc.)
73                    _ => None,
74                },
75                Err(e) => Some(Err(LcelError::Agent(e.to_string()))),
76            }
77        });
78
79        Ok(Box::pin(mapped))
80    }
81}
82
83/// Adapter that wraps an `AgentExecutor` as `Runnable<String, AgentStreamEvent>`.
84///
85/// Unlike [`AgentRunnable`], `stream()` preserves **all** `AgentStreamEvent`
86/// variants (`Text` / `ToolStart` / `ToolEnd` / `FinalAnswer` / `Error`)
87/// instead of filtering down to `FinalAnswer`. Use this in LCEL pipelines when
88/// you need the fused tool-event + text-token stream (P1-8).
89///
90/// Non-streaming `invoke()` runs the agent and returns the final answer as a
91/// single `AgentStreamEvent::FinalAnswer`.
92pub struct AgentEventRunnable {
93    executor: Arc<AgentExecutor>,
94}
95
96impl AgentEventRunnable {
97    /// Wrap an executor, exposing its full event stream.
98    pub fn new(executor: Arc<AgentExecutor>) -> Self {
99        Self { executor }
100    }
101}
102
103#[async_trait]
104impl Runnable<String, AgentStreamEvent> for AgentEventRunnable {
105    type Error = LcelError;
106
107    async fn invoke(
108        &self,
109        input: String,
110        config: Option<RunnableConfig>,
111    ) -> Result<AgentStreamEvent, LcelError> {
112        let output = self
113            .executor
114            .invoke_with_config(input, config)
115            .await
116            .map_err(|e| LcelError::Agent(e.to_string()))?;
117        Ok(AgentStreamEvent::FinalAnswer { content: output })
118    }
119
120    async fn stream(
121        &self,
122        input: String,
123        _config: Option<RunnableConfig>,
124    ) -> Result<Pin<Box<dyn Stream<Item = Result<AgentStreamEvent, LcelError>> + Send>>, LcelError>
125    {
126        use futures_util::StreamExt;
127
128        let event_stream = self.executor.stream(input);
129        // Preserve every event; only map the error type.
130        let mapped = event_stream
131            .map(|event_result| event_result.map_err(|e| LcelError::Agent(e.to_string())));
132        Ok(Box::pin(mapped))
133    }
134}
135
136/// Adapter that wraps an [`Orchestrator`] (P1-1) as a `Runnable`.
137///
138/// Lets high-level orchestrators (PlanExecute / AdaptiveRAG / CorrectiveRAG /
139/// DeepResearch) participate in LCEL pipelines. `config.metadata["trace_id"]`
140/// flows through to [`RunContext`].
141pub struct OrchestratorRunnable<O: Orchestrator> {
142    orchestrator: O,
143}
144
145impl<O: Orchestrator> OrchestratorRunnable<O> {
146    /// Wrap an orchestrator.
147    pub fn new(orchestrator: O) -> Self {
148        Self { orchestrator }
149    }
150}
151
152#[async_trait]
153impl<O> Runnable<O::Input, O::Output> for OrchestratorRunnable<O>
154where
155    O: Orchestrator,
156    O::Input: Send + Sync + 'static,
157    O::Output: Send + Sync + 'static,
158{
159    type Error = LcelError;
160
161    async fn invoke(
162        &self,
163        input: O::Input,
164        config: Option<RunnableConfig>,
165    ) -> Result<O::Output, LcelError> {
166        let ctx = match &config {
167            Some(cfg) => RunContext::from_config(cfg),
168            None => RunContext::new_random(),
169        };
170        self.orchestrator
171            .run_with_context(input, &ctx)
172            .await
173            .map_err(|e| LcelError::Agent(e.to_string()))
174    }
175}
176
177/// Allow `AgentError` to convert into `LcelError`.
178impl From<crate::base::AgentError> for LcelError {
179    fn from(err: crate::base::AgentError) -> Self {
180        LcelError::Agent(err.to_string())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::streaming::AgentStreamEvent;
188    use std::collections::HashMap;
189
190    #[test]
191    fn agent_error_into_lcel_error() {
192        let agent_err = crate::base::AgentError::MaxIterationsReached;
193        let lcel_err: LcelError = agent_err.into();
194        assert!(matches!(lcel_err, LcelError::Agent(_)));
195        assert!(lcel_err.to_string().contains("Max iterations"));
196    }
197
198    /// Mock agent that always finishes immediately.
199    struct TestFinishAgent;
200
201    #[async_trait]
202    impl crate::BaseAgent for TestFinishAgent {
203        async fn plan(
204            &self,
205            _intermediate_steps: &[crate::types::AgentStep],
206            _inputs: &HashMap<String, String>,
207            _config: Option<&lc_core::runnables::RunnableConfig>,
208        ) -> Result<crate::types::AgentOutput, crate::base::AgentError> {
209            Ok(crate::types::AgentOutput::Finish(
210                crate::types::AgentFinish::new("answer".to_string(), String::new()),
211            ))
212        }
213    }
214
215    /// P1-8: AgentEventRunnable::stream preserves all events (Text + FinalAnswer),
216    /// instead of filter_map'ing to a single string like AgentRunnable.
217    #[tokio::test]
218    async fn agent_event_runnable_preserves_all_events() {
219        use futures_util::StreamExt;
220
221        let executor = Arc::new(crate::base::AgentExecutor::new(
222            Arc::new(TestFinishAgent),
223            vec![],
224        ));
225        let runnable = AgentEventRunnable::new(executor);
226
227        let mut stream = runnable.stream("hi".to_string(), None).await.unwrap();
228        let mut events = Vec::new();
229        while let Some(item) = stream.next().await {
230            events.push(item.unwrap());
231        }
232
233        assert_eq!(events.len(), 2);
234        assert!(matches!(events[0], AgentStreamEvent::Text { .. }));
235        assert!(matches!(events[1], AgentStreamEvent::FinalAnswer { .. }));
236    }
237
238    /// P1-8: non-streaming invoke returns a single FinalAnswer event.
239    #[tokio::test]
240    async fn agent_event_runnable_invoke_returns_final_answer() {
241        let executor = Arc::new(crate::base::AgentExecutor::new(
242            Arc::new(TestFinishAgent),
243            vec![],
244        ));
245        let runnable = AgentEventRunnable::new(executor);
246
247        let event = runnable.invoke("hi".to_string(), None).await.unwrap();
248        match event {
249            AgentStreamEvent::FinalAnswer { content } => assert_eq!(content, "answer"),
250            other => panic!("expected FinalAnswer, got {:?}", other),
251        }
252    }
253}