Skip to main content

langchain_rust/agent/
executor.rs

1use std::{collections::HashMap, pin::Pin, sync::Arc};
2
3use async_trait::async_trait;
4use futures::{Stream, StreamExt};
5use serde_json::json;
6use tokio::sync::Mutex;
7use tokio_util::sync::CancellationToken;
8
9use super::{
10    agent::{Agent, PlanEvent},
11    AgentError,
12};
13use crate::observability::traced;
14use crate::schemas::{LogTools, Message};
15use crate::{
16    chain::{chain_trait::Chain, ChainError},
17    language_models::{GenerateResult, TokenUsage},
18    prompt::{human_message_from_inputs, PromptArgs, DEFAULT_IMAGES_KEY},
19    schemas::{
20        agent::{AgentAction, AgentEvent},
21        memory::BaseMemory,
22    },
23    tools::Tool,
24};
25use tracing::Instrument;
26
27/// What an agent run is doing, streamed by [`AgentExecutor::stream_events`].
28#[derive(Debug, Clone, PartialEq)]
29#[non_exhaustive]
30pub enum AgentStreamEvent {
31    /// A chunk of text from the model, as it is generated. With tool-calling agents this
32    /// is the answer itself (or a short preamble before tool calls).
33    Text(String),
34    /// The agent is about to run a tool.
35    ToolStart { tool: String, input: String },
36    /// A tool finished. `output` is what the agent sees; a failed tool shows up here as
37    /// its error message unless the executor uses `with_break_if_error(true)`.
38    ToolEnd { tool: String, output: String },
39    /// Tokens used by one model call of the run, when the LLM reports them. The run's
40    /// total is in [`Chain::call`]'s `GenerateResult::tokens`.
41    Usage(TokenUsage),
42    /// The run is over. Always the last event of a successful run.
43    Finish { output: String },
44}
45
46/// Stream returned by [`AgentExecutor::stream_events`].
47pub type AgentEventStream<'a> =
48    Pin<Box<dyn Stream<Item = Result<AgentStreamEvent, ChainError>> + Send + 'a>>;
49
50/// Output of a run that hit `max_iterations`.
51const MAX_ITERATIONS_OUTPUT: &str = "Max iterations reached";
52/// Error message of a run stopped through `with_cancellation`.
53const CANCELLED: &str = "agent run cancelled";
54
55pub struct AgentExecutor<A>
56where
57    A: Agent,
58{
59    agent: A,
60    max_iterations: Option<i32>,
61    break_if_error: bool,
62    pub memory: Option<Arc<Mutex<dyn BaseMemory>>>,
63    cancellation: Option<CancellationToken>,
64}
65
66impl<A> AgentExecutor<A>
67where
68    A: Agent,
69{
70    pub fn from_agent(agent: A) -> Self {
71        Self {
72            agent,
73            max_iterations: Some(10),
74            break_if_error: false,
75            memory: None,
76            cancellation: None,
77        }
78    }
79
80    pub fn with_max_iterations(mut self, max_iterations: i32) -> Self {
81        self.max_iterations = Some(max_iterations);
82        self
83    }
84
85    pub fn with_memory(mut self, memory: Arc<Mutex<dyn BaseMemory>>) -> Self {
86        self.memory = Some(memory);
87        self
88    }
89
90    pub fn with_break_if_error(mut self, break_if_error: bool) -> Self {
91        self.break_if_error = break_if_error;
92        self
93    }
94
95    /// Stops runs when `token` is cancelled: the in-flight LLM or tool call is dropped and
96    /// the run returns a `ChainError::AgentError` ("agent run cancelled"); check
97    /// `token.is_cancelled()` to tell it apart from other failures. Nothing is written to
98    /// memory for a cancelled run.
99    ///
100    /// Applies to every run of this executor. Dropping the future (or the stream from
101    /// [`stream_events`](Self::stream_events)) also cancels a run.
102    pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
103        self.cancellation = Some(token);
104        self
105    }
106
107    fn get_name_to_tools(&self) -> HashMap<String, Arc<dyn Tool>> {
108        let mut name_to_tool = HashMap::new();
109        for tool in self.agent.get_tools().iter() {
110            tracing::debug!("Loading Tool:{}", tool.name());
111            name_to_tool.insert(tool.name().trim().replace(" ", "_"), tool.clone());
112        }
113        name_to_tool
114    }
115}
116
117impl<A> AgentExecutor<A>
118where
119    A: Agent + Send + Sync,
120{
121    /// Runs the agent and streams what it does: model text as it is generated, each tool
122    /// call and its result, the tokens of each model call (`Usage`, when the LLM reports
123    /// them), and finally the answer.
124    ///
125    /// Memory is updated the same way as [`Chain::call`], once the run finishes. Stop early
126    /// by dropping the stream or with [`with_cancellation`](Self::with_cancellation).
127    ///
128    /// ```rust,ignore
129    /// use futures::StreamExt;
130    /// use langchain_rust::agent::AgentStreamEvent;
131    ///
132    /// let mut events = executor.stream_events(prompt_args! { "input" => "weather in Lima?" });
133    /// while let Some(event) = events.next().await {
134    ///     match event? {
135    ///         AgentStreamEvent::Text(chunk) => print!("{chunk}"),
136    ///         AgentStreamEvent::ToolStart { tool, input } => println!("\n-> {tool}({input})"),
137    ///         AgentStreamEvent::ToolEnd { tool, output } => println!("<- {tool}: {output}"),
138    ///         AgentStreamEvent::Finish { .. } => println!(),
139    ///         _ => {}
140    ///     }
141    /// }
142    /// ```
143    pub fn stream_events(&self, input_variables: PromptArgs) -> AgentEventStream<'_> {
144        let run = async_stream::stream! {
145            // Capacity 1: the run is at most one event ahead of the consumer, so a slow
146            // consumer slows the run down and dropping the stream stops it.
147            let (tx, mut rx) = tokio::sync::mpsc::channel(1);
148            let run = self.run(input_variables, Emitter(Some(tx)));
149            tokio::pin!(run);
150            let result = loop {
151                tokio::select! {
152                    biased;
153                    Some(event) = rx.recv() => yield Ok(event),
154                    result = &mut run => break result,
155                }
156            };
157            while let Ok(event) = rx.try_recv() {
158                yield Ok(event);
159            }
160            match result {
161                Ok(run) => yield Ok(AgentStreamEvent::Finish { output: run.output }),
162                Err(e) => yield Err(e),
163            }
164        };
165        Box::pin(run)
166    }
167
168    /// The agent loop shared by `call` and `stream_events`.
169    async fn run(
170        &self,
171        input_variables: PromptArgs,
172        emitter: Emitter,
173    ) -> Result<RunOutput, ChainError> {
174        let span = tracing::info_span!(
175            "agent.run",
176            max_iterations = self.max_iterations,
177            steps = tracing::field::Empty,
178            total_tokens = tracing::field::Empty,
179            error = tracing::field::Empty,
180        );
181        let run = self.run_inner(input_variables, emitter);
182        let run = async {
183            match &self.cancellation {
184                Some(token) => token
185                    .run_until_cancelled(run)
186                    .await
187                    .unwrap_or_else(|| Err(ChainError::AgentError(CANCELLED.into()))),
188                None => run.await,
189            }
190        };
191        traced(span, run, |span, run: &RunOutput| {
192            if let Some(tokens) = &run.tokens {
193                span.record("total_tokens", tokens.total_tokens);
194            }
195        })
196        .await
197    }
198
199    async fn run_inner(
200        &self,
201        input_variables: PromptArgs,
202        emitter: Emitter,
203    ) -> Result<RunOutput, ChainError> {
204        let mut input_variables = input_variables.clone();
205        let name_to_tools = self.get_name_to_tools();
206        let mut steps: Vec<(AgentAction, String)> = Vec::new();
207        let mut tokens: Option<TokenUsage> = None;
208        if let Some(memory) = &self.memory {
209            let messages = memory.lock().await.messages().await?;
210            input_variables.insert("chat_history".to_string(), json!(messages));
211        } else {
212            input_variables.insert("chat_history".to_string(), json!(Vec::<Message>::new()));
213        }
214
215        loop {
216            let plan_span = tracing::info_span!("agent.plan", step = steps.len());
217            let (agent_event, usage) = async {
218                if emitter.is_streaming() {
219                    self.plan_streaming(&steps, input_variables.clone(), &emitter)
220                        .await
221                } else {
222                    self.agent
223                        .plan_with_usage(&steps, input_variables.clone())
224                        .await
225                }
226            }
227            .instrument(plan_span)
228            .await
229            .map_err(|e| ChainError::AgentError(format!("Error in agent planning: {}", e)))?;
230            if let Some(usage) = usage {
231                tokens.get_or_insert_with(TokenUsage::default).add(&usage);
232            }
233
234            match agent_event {
235                AgentEvent::Action(actions) => {
236                    for action in actions {
237                        tracing::debug!("Action: {:?}", action.tool_input);
238                        let tool = name_to_tools
239                            .get(&action.tool.trim().replace(" ", "_"))
240                            .ok_or_else(|| {
241                                AgentError::ToolError(format!("Tool {} not found", action.tool))
242                            })
243                            .map_err(|e| ChainError::AgentError(e.to_string()))?;
244
245                        emitter
246                            .emit(AgentStreamEvent::ToolStart {
247                                tool: action.tool.clone(),
248                                input: action.tool_input.clone(),
249                            })
250                            .await;
251                        let tool_span = tracing::info_span!(
252                            "tool.call",
253                            tool = %action.tool,
254                            error = tracing::field::Empty,
255                        );
256                        let observation =
257                            match traced(tool_span, tool.call(&action.tool_input), |_, _| {}).await
258                            {
259                                Ok(result) => result,
260                                Err(err) => {
261                                    tracing::info!(
262                                        "The tool return the following error: {}",
263                                        err.to_string()
264                                    );
265                                    if self.break_if_error {
266                                        return Err(ChainError::AgentError(
267                                            AgentError::ToolError(err.to_string()).to_string(),
268                                        ));
269                                    } else {
270                                        format!("The tool return the following error: {}", err)
271                                    }
272                                }
273                            };
274                        emitter
275                            .emit(AgentStreamEvent::ToolEnd {
276                                tool: action.tool.clone(),
277                                output: observation.clone(),
278                            })
279                            .await;
280
281                        steps.push((action, observation));
282                    }
283                }
284                AgentEvent::Finish(finish) => {
285                    tracing::Span::current().record("steps", steps.len());
286                    if let Some(memory) = &self.memory {
287                        let mut memory = memory.lock().await;
288
289                        // Text without extra quotes, plus the images the user sent.
290                        memory
291                            .add_message(human_message_from_inputs(
292                                &input_variables,
293                                "input",
294                                DEFAULT_IMAGES_KEY,
295                            )?)
296                            .await?;
297
298                        let mut tools_ai_message_seen: HashMap<String, ()> = HashMap::default();
299                        for (action, observation) in steps {
300                            // Only OpenAI-tools style agents encode `LogTools` in the log;
301                            // other agents (e.g. ConversationalAgent) store raw LLM text.
302                            let Ok(LogTools { tool_id, tools }) =
303                                serde_json::from_str::<LogTools>(&action.log)
304                            else {
305                                continue;
306                            };
307                            let tools_value: serde_json::Value = serde_json::from_str(&tools)?;
308                            if tools_ai_message_seen.insert(tools, ()).is_none() {
309                                memory
310                                    .add_message(
311                                        Message::new_ai_message("").with_tool_calls(tools_value),
312                                    )
313                                    .await?;
314                            }
315                            memory
316                                .add_message(Message::new_tool_message(observation, tool_id))
317                                .await?;
318                        }
319
320                        memory.add_ai_message(&finish.output).await?;
321                    }
322                    return Ok(RunOutput {
323                        output: finish.output,
324                        tokens,
325                    });
326                }
327            }
328
329            if let Some(max_iterations) = self.max_iterations {
330                if steps.len() >= max_iterations as usize {
331                    tracing::Span::current().record("steps", steps.len());
332                    tracing::warn!(max_iterations, "agent stopped: max iterations reached");
333                    return Ok(RunOutput {
334                        output: MAX_ITERATIONS_OUTPUT.to_string(),
335                        tokens,
336                    });
337                }
338            }
339        }
340    }
341
342    /// Runs `plan_stream`, forwarding its text chunks and usage, and returns the plan.
343    async fn plan_streaming(
344        &self,
345        steps: &[(AgentAction, String)],
346        inputs: PromptArgs,
347        emitter: &Emitter,
348    ) -> Result<(AgentEvent, Option<TokenUsage>), AgentError> {
349        let mut plan = self.agent.plan_stream(steps, inputs);
350        let mut tokens: Option<TokenUsage> = None;
351        while let Some(event) = plan.next().await {
352            match event? {
353                PlanEvent::Text(text) => emitter.emit(AgentStreamEvent::Text(text)).await,
354                PlanEvent::Usage(usage) => {
355                    tokens.get_or_insert_with(TokenUsage::default).add(&usage);
356                    emitter.emit(AgentStreamEvent::Usage(usage)).await;
357                }
358                PlanEvent::Done(event) => return Ok((event, tokens)),
359            }
360        }
361        Err(AgentError::OtherError(
362            "agent plan stream ended without a plan".into(),
363        ))
364    }
365}
366
367/// What a run produced: the answer and, when the LLM reported them, the tokens used by
368/// all its model calls.
369struct RunOutput {
370    output: String,
371    tokens: Option<TokenUsage>,
372}
373
374/// Where the agent loop sends its events: nowhere for `call`, a channel for `stream_events`.
375struct Emitter(Option<tokio::sync::mpsc::Sender<AgentStreamEvent>>);
376
377impl Emitter {
378    fn is_streaming(&self) -> bool {
379        self.0.is_some()
380    }
381
382    async fn emit(&self, event: AgentStreamEvent) {
383        if let Some(tx) = &self.0 {
384            // Fails only when the receiver is gone, i.e. the run is being dropped.
385            let _ = tx.send(event).await;
386        }
387    }
388}
389
390#[async_trait]
391impl<A> Chain for AgentExecutor<A>
392where
393    A: Agent + Send + Sync,
394{
395    /// Runs the agent. `tokens` is the sum over every model call of the run (`None` when
396    /// the LLM doesn't report usage).
397    async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
398        let run = self.run(input_variables, Emitter(None)).await?;
399        Ok(GenerateResult {
400            generation: run.output,
401            tokens: run.tokens,
402            ..Default::default()
403        })
404    }
405
406    async fn invoke(&self, input_variables: PromptArgs) -> Result<String, ChainError> {
407        let result = self.call(input_variables).await?;
408        Ok(result.generation)
409    }
410}