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};