use super::types::{AgentAction, AgentFinish, AgentOutput, AgentStep};
use super::streaming::state::AgentStreamEvent;
use super::hooks::{AgentHook, ToolCallAction, ToolCallContext, ToolResultContext};
use async_trait::async_trait;
use lc_callbacks::{CallbackManager, RunTree, RunType};
use lc_core::runnables::RunnableConfig;
use lc_core::tools::BaseTool;
use lc_memory::BaseMemory;
use serde_json::json;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
use futures_util::Stream;
#[derive(Debug, thiserror::Error)]
pub enum AgentError {
#[error("Output parsing error: {0}")]
OutputParsingError(String),
#[error("Tool not found: {0}")]
ToolNotFound(String),
#[error("Tool execution error: {0}")]
ToolExecutionError(String),
#[error("Max iterations reached")]
MaxIterationsReached,
#[error("Agent error: {0}")]
Other(String),
}
#[async_trait]
pub trait BaseAgent: Send + Sync {
async fn plan(
&self,
intermediate_steps: &[AgentStep],
inputs: &HashMap<String, String>,
) -> Result<AgentOutput, AgentError>;
fn input_keys(&self) -> Vec<&str> {
vec!["input"]
}
fn get_allowed_tools(&self) -> Option<Vec<&str>> {
None
}
fn return_stopped_response(&self, _intermediate_steps: &[AgentStep]) -> AgentFinish {
AgentFinish::new(
"Agent stopped due to iteration limit or time limit.".to_string(),
String::new(),
)
}
}
pub struct AgentExecutor {
agent: Arc<dyn BaseAgent>,
tools: Vec<Arc<dyn BaseTool>>,
max_iterations: usize,
verbose: bool,
memory: Option<Arc<tokio::sync::Mutex<dyn BaseMemory>>>,
callbacks: Option<Arc<CallbackManager>>,
hooks: Vec<Arc<dyn AgentHook>>,
}
impl AgentExecutor {
pub fn new(agent: Arc<dyn BaseAgent>, tools: Vec<Arc<dyn BaseTool>>) -> Self {
Self {
agent,
tools,
max_iterations: 10,
verbose: false,
memory: None,
callbacks: None,
hooks: Vec::new(),
}
}
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn with_memory(mut self, memory: Arc<tokio::sync::Mutex<dyn BaseMemory>>) -> Self {
self.memory = Some(memory);
self
}
pub fn with_callbacks(mut self, callbacks: Arc<CallbackManager>) -> Self {
self.callbacks = Some(callbacks);
self
}
pub fn hook(mut self, hook: impl AgentHook + 'static) -> Self {
self.hooks.push(Arc::new(hook));
self
}
pub async fn invoke(&self, input: String) -> Result<String, AgentError> {
let mut root_run = RunTree::new(
"AgentExecutor",
RunType::Chain,
json!({"input": input.clone()}),
);
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_chain_start(&root_run, &root_run.inputs).await;
}
}
let mut inputs = HashMap::new();
inputs.insert("input".to_string(), input.clone());
if let Some(memory) = &self.memory {
let memory_vars = memory
.lock()
.await
.load_memory_variables(&inputs)
.await
.map_err(|e| AgentError::Other(format!("Failed to load memory: {}", e)))?;
if let Some(history) = memory_vars.get("history") {
if let Some(history_str) = history.as_str() {
inputs.insert("history".to_string(), history_str.to_string());
}
}
}
let intermediate_steps: Vec<AgentStep> = Vec::new();
let result = self
.run_agent_loop(inputs.clone(), intermediate_steps, &mut root_run)
.await;
if let Some(memory) = &self.memory {
if let Ok(ref output) = result {
let mut outputs = HashMap::new();
outputs.insert("output".to_string(), output.clone());
memory
.lock()
.await
.save_context(&inputs, &outputs)
.await
.map_err(|e| AgentError::Other(format!("Failed to save memory: {}", e)))?;
}
}
match &result {
Ok(output) => {
root_run.end(json!({"output": output}));
if let Some(ref callbacks) = self.callbacks {
if let Some(ref outputs) = root_run.outputs {
for handler in callbacks.handlers() {
handler.on_chain_end(&root_run, outputs).await;
}
}
}
}
Err(e) => {
root_run.end_with_error(e.to_string());
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_chain_error(&root_run, &e.to_string()).await;
}
}
}
}
result
}
pub async fn invoke_with_config(
&self,
input: String,
config: Option<RunnableConfig>,
) -> Result<String, AgentError> {
let effective_callbacks = config
.as_ref()
.and_then(|c| c.callbacks.clone())
.or_else(|| self.callbacks.clone());
let merged_executor = AgentExecutor {
agent: self.agent.clone(),
tools: self.tools.clone(),
max_iterations: self.max_iterations,
verbose: self.verbose,
memory: self.memory.clone(),
callbacks: effective_callbacks,
hooks: self.hooks.clone(),
};
merged_executor.invoke(input).await
}
pub fn stream(&self, input: String) -> Pin<Box<dyn Stream<Item = Result<AgentStreamEvent, AgentError>> + Send>> {
let (tx, rx) = tokio::sync::mpsc::channel(32);
let agent = self.agent.clone();
let tools = self.tools.clone();
let max_iterations = self.max_iterations;
let verbose = self.verbose;
tokio::spawn(async move {
let mut intermediate_steps: Vec<AgentStep> = Vec::new();
let mut inputs = HashMap::new();
inputs.insert("input".to_string(), input);
for iteration in 0..max_iterations {
if verbose {
log::info!("=== Stream Iteration {} ===", iteration + 1);
}
let output = match agent.plan(&intermediate_steps, &inputs).await {
Ok(o) => o,
Err(e) => {
let _ = tx.send(Ok(AgentStreamEvent::Error { message: e.to_string() })).await;
return;
}
};
match output {
AgentOutput::Finish(finish) => {
let content = finish.output().unwrap_or("").to_string();
let _ = tx.send(Ok(AgentStreamEvent::FinalAnswer { content })).await;
return;
}
AgentOutput::Action(action) => {
let tool_name = action.tool.clone();
let tool_input_str = match &action.tool_input {
super::types::ToolInput::String { value: s } => s.clone(),
super::types::ToolInput::Object { value: v } => {
serde_json::to_string(v).unwrap_or_default()
}
};
let _ = tx.send(Ok(AgentStreamEvent::ToolStart {
name: tool_name.clone(),
input: tool_input_str.clone(),
})).await;
let observation = match execute_tool_for_stream(&tools, &action).await {
Ok(obs) => obs,
Err(e) => {
let _ = tx.send(Ok(AgentStreamEvent::Error { message: e.to_string() })).await;
return;
}
};
let _ = tx.send(Ok(AgentStreamEvent::ToolEnd {
name: tool_name,
output: observation.clone(),
})).await;
intermediate_steps.push(AgentStep::new(action, observation));
}
AgentOutput::Actions(actions) => {
for action in &actions {
let tool_name = action.tool.clone();
let tool_input_str = match &action.tool_input {
super::types::ToolInput::String { value: s } => s.clone(),
super::types::ToolInput::Object { value: v } => {
serde_json::to_string(v).unwrap_or_default()
}
};
let _ = tx.send(Ok(AgentStreamEvent::ToolStart {
name: tool_name.clone(),
input: tool_input_str,
})).await;
}
let observations = execute_tools_parallel_for_stream(&tools, &actions).await;
for (action, observation) in actions.into_iter().zip(observations.into_iter()) {
let _ = tx.send(Ok(AgentStreamEvent::ToolEnd {
name: action.tool.clone(),
output: observation.clone(),
})).await;
intermediate_steps.push(AgentStep::new(action, observation));
}
}
}
}
let finish = agent.return_stopped_response(&intermediate_steps);
let content = finish.output().unwrap_or("").to_string();
let _ = tx.send(Ok(AgentStreamEvent::FinalAnswer { content })).await;
});
Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))
}
async fn run_agent_loop(
&self,
inputs: HashMap<String, String>,
mut intermediate_steps: Vec<AgentStep>,
root_run: &mut RunTree,
) -> Result<String, AgentError> {
for iteration in 0..self.max_iterations {
if self.verbose {
log::info!("=== Iteration {} ===", iteration + 1);
}
let output = self.agent.plan(&intermediate_steps, &inputs).await?;
match output {
AgentOutput::Finish(finish) => {
if self.verbose {
log::info!("Final answer: {:?}", finish.return_values);
}
return Ok(finish.output().unwrap_or("").to_string());
}
AgentOutput::Action(action) => {
if self.verbose {
log::info!("Action: {}({})", action.tool, action.tool_input);
}
let observation = self.execute_tool(&action, root_run).await?;
if self.verbose {
log::info!("Observation: {}", observation);
}
intermediate_steps.push(AgentStep::new(action, observation));
}
AgentOutput::Actions(actions) => {
if self.verbose {
log::info!("Parallel actions: {} count", actions.len());
for action in &actions {
log::info!(" - {}({})", action.tool, action.tool_input);
}
}
let observations = self.execute_tools_parallel(&actions, root_run).await?;
if self.verbose {
for (i, obs) in observations.iter().enumerate() {
log::info!("Observation {}: {}", i + 1, obs);
}
}
for (action, observation) in actions.into_iter().zip(observations.into_iter()) {
intermediate_steps.push(AgentStep::new(action, observation));
}
}
}
}
if self.verbose {
log::info!("Max iterations reached: {}", self.max_iterations);
}
let finish = self.agent.return_stopped_response(&intermediate_steps);
Ok(finish.output().unwrap_or("").to_string())
}
async fn execute_tools_parallel(
&self,
actions: &[super::types::AgentAction],
root_run: &RunTree,
) -> Result<Vec<String>, AgentError> {
use futures_util::future::join_all;
let futures: Vec<_> = actions
.iter()
.map(|action| self.execute_tool(action, root_run))
.collect();
let results = join_all(futures).await;
let mut observations = Vec::with_capacity(results.len());
for result in results {
match result {
Ok(output) => observations.push(output),
Err(e) => observations.push(format!("[Tool execution error: {}]", e)),
}
}
Ok(observations)
}
async fn execute_tool(
&self,
action: &AgentAction,
root_run: &RunTree,
) -> Result<String, AgentError> {
let tool = self
.tools
.iter()
.find(|t| t.name() == action.tool)
.ok_or_else(|| AgentError::ToolNotFound(action.tool.clone()))?;
let _input_str = match &action.tool_input {
super::types::ToolInput::String { value: s } => s.clone(),
super::types::ToolInput::Object { value: v } => serde_json::to_string(v)
.map_err(|e| AgentError::Other(format!("Failed to serialize tool input: {}", e)))?,
};
let mut tool_ctx = ToolCallContext {
name: action.tool.clone(),
arguments: match &action.tool_input {
super::types::ToolInput::String { value: s } => {
serde_json::from_str::<serde_json::Value>(s)
.unwrap_or(serde_json::Value::String(s.clone()))
}
super::types::ToolInput::Object { value: v } => v.clone(),
},
tool_id: String::new(),
};
for hook in &self.hooks {
match hook.on_before_tool_call(&mut tool_ctx) {
ToolCallAction::Continue => {}
ToolCallAction::Modify { name, arguments } => {
tool_ctx.name = name;
tool_ctx.arguments = arguments;
}
ToolCallAction::Reject { reason } => {
return Err(AgentError::Other(format!("Tool call rejected by hook: {}", reason)));
}
ToolCallAction::Skip => {
return Ok("[Skipped by hook]".to_string());
}
}
}
let tool_name = tool_ctx.name.clone();
let input_for_tool = serde_json::to_string(&tool_ctx.arguments)
.unwrap_or_else(|_| tool_ctx.arguments.to_string());
let mut tool_run = root_run.create_child(
&tool_name,
RunType::Tool,
json!({"input": input_for_tool.clone()}),
);
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler
.on_tool_start(&tool_run, &tool_name, &input_for_tool)
.await;
}
}
let result = tool.run(input_for_tool.clone()).await;
match result {
Ok(output) => {
tool_run.end(json!({"output": output.clone()}));
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_tool_end(&tool_run, &output).await;
}
}
let mut result_ctx = ToolResultContext {
name: tool_name,
result: output.clone(),
tool_id: String::new(),
};
for hook in &self.hooks {
if let Err(e) = hook.on_after_tool_call(&mut result_ctx) {
log::warn!("Hook on_after_tool_call error: {}", e);
}
}
Ok(result_ctx.result)
}
Err(e) => {
tool_run.end_with_error(e.to_string());
if let Some(ref callbacks) = self.callbacks {
for handler in callbacks.handlers() {
handler.on_tool_error(&tool_run, &e.to_string()).await;
}
}
Err(AgentError::ToolExecutionError(e.to_string()))
}
}
}
}
impl std::fmt::Debug for AgentExecutor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentExecutor")
.field("max_iterations", &self.max_iterations)
.field("verbose", &self.verbose)
.field("tools_count", &self.tools.len())
.field("has_memory", &self.memory.is_some())
.finish()
}
}
async fn execute_tool_for_stream(
tools: &[Arc<dyn BaseTool>],
action: &AgentAction,
) -> Result<String, AgentError> {
let tool = tools
.iter()
.find(|t| t.name() == action.tool)
.ok_or_else(|| AgentError::ToolNotFound(action.tool.clone()))?;
let input_str = match &action.tool_input {
super::types::ToolInput::String { value: s } => s.clone(),
super::types::ToolInput::Object { value: v } => serde_json::to_string(v)
.map_err(|e| AgentError::Other(format!("Failed to serialize tool input: {}", e)))?,
};
tool.run(input_str)
.await
.map_err(|e| AgentError::ToolExecutionError(e.to_string()))
}
async fn execute_tools_parallel_for_stream(
tools: &[Arc<dyn BaseTool>],
actions: &[AgentAction],
) -> Vec<String> {
use futures_util::future::join_all;
let futures: Vec<_> = actions
.iter()
.map(|action| execute_tool_for_stream(tools, action))
.collect();
let results = join_all(futures).await;
results
.into_iter()
.map(|result| result.unwrap_or_else(|e| format!("[Tool execution error: {}]", e)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use lc_memory::ConversationBufferMemory;
#[tokio::test]
async fn test_agent_executor_with_memory() {
struct TestAgent;
#[async_trait]
impl BaseAgent for TestAgent {
async fn plan(
&self,
_intermediate_steps: &[AgentStep],
inputs: &HashMap<String, String>,
) -> Result<AgentOutput, AgentError> {
if let Some(history) = inputs.get("history") {
if history.contains("Zhang San") {
return Ok(AgentOutput::Finish(AgentFinish::new(
"Your name is Zhang San".to_string(),
String::new(),
)));
}
}
let input = inputs.get("input").unwrap();
Ok(AgentOutput::Finish(AgentFinish::new(
format!("Received: {}", input),
String::new(),
)))
}
}
let memory = Arc::new(tokio::sync::Mutex::new(ConversationBufferMemory::new()));
let executor = AgentExecutor::new(Arc::new(TestAgent), vec![]).with_memory(memory);
let result1 = executor
.invoke("My name is Zhang San".to_string())
.await
.unwrap();
println!("Round 1: {}", result1);
let result2 = executor
.invoke("What is my name?".to_string())
.await
.unwrap();
println!("Round 2: {}", result2);
assert!(result2.contains("Zhang San"));
}
}