Skip to main content

lc_agents/executor/
mod.rs

1// lc-agents/src/executor/mod.rs
2//! Agent base traits and executor implementation.
3
4use crate::types::{AgentFinish, AgentOutput, AgentStep};
5use async_trait::async_trait;
6use lc_core::language_models::TokenUsage;
7use lc_core::runnables::RunnableConfig;
8use std::collections::HashMap;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::atomic::AtomicUsize;
12
13/// Cache-namespace counter for each `AgentExecutor` instance.
14///
15/// P2-1: the same `(inputs, intermediate_steps)` may be planned into different actions
16/// by different Agents across executors, so a shared cache would cross-contaminate
17/// results. Giving each instance a unique namespace isolates cache keys by construction,
18/// without hurting deterministic hits across multiple `invoke`s on the same instance.
19static CACHE_NS: AtomicUsize = AtomicUsize::new(0);
20
21/// Agent error types.
22#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum AgentError {
25    /// Output parsing error.
26    #[error("Output parsing error: {0}")]
27    OutputParsingError(String),
28
29    /// Tool not found.
30    #[error("Tool not found: {0}")]
31    ToolNotFound(String),
32
33    /// Tool execution error.
34    #[error("Tool execution error: {0}")]
35    ToolExecutionError(String),
36
37    /// Max iterations reached.
38    #[error("Max iterations reached")]
39    MaxIterationsReached,
40
41    /// Budget exceeded (approval/budget gate §4.2). Callers catch it to distinguish a
42    /// "budget stop" from "the model did not converge".
43    #[error("Budget exceeded: {0:?}")]
44    BudgetExceeded(BudgetExceeded),
45
46    /// Cross-process resume (§4.2): checkpoint read / write / restore failed.
47    #[error("Resume error: {0}")]
48    Resume(String),
49
50    /// Other error.
51    #[error("Agent error: {0}")]
52    Other(String),
53}
54
55/// Base Agent trait.
56///
57/// Defines the core interface for agents. Agent is responsible for planning,
58/// not execution. Execution is handled by AgentExecutor.
59#[async_trait]
60pub trait BaseAgent: Send + Sync {
61    /// Plans the next action.
62    ///
63    /// # Arguments
64    /// * `intermediate_steps` - History of executed steps.
65    /// * `inputs` - User input.
66    /// * `config` - Per-run config for this planning round. The executor
67    ///   supplies a config carrying its callback manager plus trace-linkage
68    ///   metadata, so the LLM provider fires `on_llm_*` for every planning
69    ///   round and the LLM run joins the agent chain's trace tree; `None`
70    ///   means "no observers configured" and providers dispatch nothing
71    ///   (the pre-0.22.4 behavior). Implementations forward it verbatim to
72    ///   `chat` / `stream_chat` and never inspect or strip it.
73    ///
74    /// # Returns
75    /// * `AgentOutput::Action` - Action to execute.
76    /// * `AgentOutput::Finish` - Final answer.
77    async fn plan(
78        &self,
79        intermediate_steps: &[AgentStep],
80        inputs: &HashMap<String, String>,
81        config: Option<&RunnableConfig>,
82    ) -> Result<AgentOutput, AgentError>;
83
84    /// Plans the next action, streaming any model text through `on_token` as it
85    /// is produced.
86    ///
87    /// # Arguments
88    /// * `intermediate_steps` - History of executed steps.
89    /// * `inputs` - User input.
90    /// * `on_token` - Called with each chunk of model text as it becomes
91    ///   available, taking **ownership** of the chunk (so the returned future
92    ///   never borrows the token and stays `'static`). Streaming-capable agents
93    ///   (e.g. ReAct, function-calling) emit free text per token; steps that
94    ///   produce no free text (e.g. a function-calling step invoking a tool)
95    ///   emit nothing. Non-streaming agents deliver the whole answer in one
96    ///   call.
97    ///
98    /// # Returns
99    /// * `AgentOutput::Action` - Action to execute.
100    /// * `AgentOutput::Finish` - Final answer.
101    ///
102    /// The default implementation delegates to [`BaseAgent::plan`] and forwards
103    /// the whole final-answer text as a single chunk — behaviorally identical
104    /// to calling `plan()` directly. Streaming-capable agents (e.g. ReAct)
105    /// override this to emit per-token chunks from the model's streaming chat
106    /// API; callers must still call [`BaseAgent::plan`] for the non-streaming
107    /// path (e.g. `invoke`) so that path is unaffected.
108    async fn plan_stream(
109        &self,
110        intermediate_steps: &[AgentStep],
111        inputs: &HashMap<String, String>,
112        on_token: &mut (dyn FnMut(String) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send),
113        config: Option<&RunnableConfig>,
114    ) -> Result<AgentOutput, AgentError> {
115        let output = self.plan(intermediate_steps, inputs, config).await?;
116        if let AgentOutput::Finish(finish) = &output {
117            on_token(finish.output().unwrap_or("").to_string()).await;
118        }
119        Ok(output)
120    }
121
122    /// Returns input keys.
123    fn input_keys(&self) -> Vec<&str> {
124        vec!["input"]
125    }
126
127    /// Returns allowed tools list.
128    fn get_allowed_tools(&self) -> Option<Vec<&str>> {
129        None
130    }
131
132    /// Returns stopped response when max iterations reached.
133    fn return_stopped_response(&self, _intermediate_steps: &[AgentStep]) -> AgentFinish {
134        AgentFinish::new(
135            "Agent stopped due to iteration limit or time limit.".to_string(),
136            String::new(),
137        )
138    }
139
140    /// Returns the token usage from the most recent `plan()` call, if available.
141    ///
142    /// Agents that make LLM calls inside `plan()` may override this to report
143    /// cost metrics to `AgentExecutor` (P1-5). Defaults to `None`.
144    fn last_token_usage(&self) -> Option<TokenUsage> {
145        None
146    }
147}
148
149/// Minimum allowed `max_iterations`.
150const MIN_MAX_ITERATIONS: usize = 1;
151
152/// Upper bound for `max_iterations` — guards against runaway loops.
153const MAX_MAX_ITERATIONS: usize = 100;
154
155/// Default number of tools executed concurrently.
156const DEFAULT_MAX_CONCURRENCY: usize = 8;
157
158mod agent_loop;
159mod budget;
160mod compaction;
161mod engine;
162mod file_memory_tool;
163mod hooks;
164mod semantic_memory;
165#[cfg(test)]
166mod tests;
167mod tools;
168
169pub use budget::{BudgetConfig, BudgetExceeded};
170pub use compaction::{
171    estimate_step_tokens, CompactionConfig, CompactionStrategy, CompactionTrigger,
172};
173pub use engine::AgentExecutor;
174pub use file_memory_tool::{mount, FileMemoryTool};