use async_trait::async_trait;
use mofa_kernel::agent::context::AgentContext;
use mofa_kernel::agent::error::{AgentError, AgentResult};
use mofa_kernel::agent::types::{ChatCompletionRequest, ChatMessage, LLMProvider, ToolDefinition};
use mofa_kernel::agent::{AgentCapabilities, AgentState, MoFAAgent};
use mofa_kernel::agent::{AgentInput, AgentOutput, InputType, OutputType};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::agent::base::BaseAgent;
use crate::agent::context::prompt::PromptContext;
use super::components::tool::SimpleToolRegistry;
use super::{Session, SessionManager};
use mofa_kernel::agent::components::tool::{Tool, ToolInput, ToolRegistry};
#[derive(Clone)]
pub struct AgentExecutorConfig {
pub max_iterations: usize,
pub session_timeout: Option<std::time::Duration>,
pub default_model: Option<String>,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
}
impl Default for AgentExecutorConfig {
fn default() -> Self {
Self {
max_iterations: 10,
session_timeout: None,
default_model: None,
temperature: None,
max_tokens: None,
}
}
}
impl AgentExecutorConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_iterations(mut self, max: usize) -> Self {
self.max_iterations = max;
self
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.default_model = Some(model.into());
self
}
pub fn with_temperature(mut self, temp: f32) -> Self {
self.temperature = Some(temp);
self
}
}
pub struct AgentExecutor {
base: BaseAgent,
llm: Arc<dyn LLMProvider>,
context: Arc<RwLock<PromptContext>>,
tools: Arc<RwLock<SimpleToolRegistry>>,
sessions: Arc<SessionManager>,
config: AgentExecutorConfig,
}
impl AgentExecutor {
pub async fn new(llm: Arc<dyn LLMProvider>, workspace: impl AsRef<Path>) -> AgentResult<Self> {
let workspace = workspace.as_ref();
let context = Arc::new(RwLock::new(PromptContext::new(workspace).await?));
let sessions = Arc::new(SessionManager::with_jsonl(workspace).await?);
let tools = Arc::new(RwLock::new(SimpleToolRegistry::new()));
let base = BaseAgent::new(uuid::Uuid::now_v7().to_string(), "LLMExecutor")
.with_description("LLM-based agent with tool calling")
.with_version("1.0.0")
.with_capabilities(
AgentCapabilities::builder()
.tag("llm")
.tag("tool-calling")
.input_type(InputType::Text)
.output_type(OutputType::Text)
.supports_tools(true)
.build(),
);
Ok(Self {
base,
llm,
context,
tools,
sessions,
config: AgentExecutorConfig::default(),
})
}
pub async fn with_config(
llm: Arc<dyn LLMProvider>,
workspace: impl AsRef<Path>,
config: AgentExecutorConfig,
) -> AgentResult<Self> {
let workspace = workspace.as_ref();
let context = Arc::new(RwLock::new(PromptContext::new(workspace).await?));
let sessions = Arc::new(SessionManager::with_jsonl(workspace).await?);
let tools = Arc::new(RwLock::new(SimpleToolRegistry::new()));
let base = BaseAgent::new(uuid::Uuid::now_v7().to_string(), "LLMExecutor")
.with_description("LLM-based agent with tool calling")
.with_version("1.0.0")
.with_capabilities(
AgentCapabilities::builder()
.tag("llm")
.tag("tool-calling")
.input_type(InputType::Text)
.output_type(OutputType::Text)
.supports_tools(true)
.build(),
);
Ok(Self {
base,
llm,
context,
tools,
sessions,
config,
})
}
pub async fn register_tool(&self, tool: Arc<dyn Tool>) -> AgentResult<()> {
let mut tools = self.tools.write().await;
tools.register(tool)
}
pub async fn process_message(
&mut self,
session_key: &str,
message: &str,
) -> AgentResult<String> {
let session = self.sessions.get_or_create(session_key).await;
let system_prompt = {
let mut ctx = self.context.write().await;
ctx.build_system_prompt().await?
};
let mut messages = self
.build_messages(&session, &system_prompt, message)
.await?;
let response = self.run_agent_loop(&mut messages).await?;
let mut session_updated = session.clone();
session_updated.add_message("user", message);
session_updated.add_message("assistant", &response);
self.sessions.save(&session_updated).await?;
Ok(response)
}
async fn build_messages(
&self,
session: &Session,
system_prompt: &str,
current_message: &str,
) -> AgentResult<Vec<ChatMessage>> {
let mut messages = Vec::new();
messages.push(ChatMessage {
role: "system".to_string(),
content: Some(system_prompt.to_string()),
tool_call_id: None,
tool_calls: None,
});
let history = session.get_history(50); for msg in history {
messages.push(ChatMessage {
role: msg.role,
content: Some(msg.content),
tool_call_id: None,
tool_calls: None,
});
}
messages.push(ChatMessage {
role: "user".to_string(),
content: Some(current_message.to_string()),
tool_call_id: None,
tool_calls: None,
});
Ok(messages)
}
async fn run_agent_loop(&self, messages: &mut Vec<ChatMessage>) -> AgentResult<String> {
for _iteration in 0..self.config.max_iterations {
let tools = {
let tools_guard = self.tools.read().await;
tools_guard.list()
};
let tool_definitions = if tools.is_empty() {
None
} else {
Some(
tools
.iter()
.map(|t| ToolDefinition {
name: t.name.clone(),
description: t.description.clone(),
parameters: t.parameters_schema.clone(),
})
.collect(),
)
};
let request = ChatCompletionRequest {
messages: messages.clone(),
model: self.config.default_model.clone(),
tools: tool_definitions,
temperature: self.config.temperature,
max_tokens: self.config.max_tokens,
};
let response = self.llm.chat(request).await?;
if let Some(tool_calls) = response.tool_calls {
if tool_calls.is_empty() {
return Ok(response.content.unwrap_or_default());
}
messages.push(ChatMessage {
role: "assistant".to_string(),
content: response.content,
tool_call_id: None,
tool_calls: Some(tool_calls.clone()),
});
for tool_call in tool_calls {
let _args_map: HashMap<String, Value> =
if let Value::Object(map) = &tool_call.arguments {
map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
} else {
return Err(AgentError::ExecutionFailed(format!(
"Invalid tool arguments for {}: {:?}",
tool_call.name, tool_call.arguments
)));
};
let result = {
let tools_guard = self.tools.read().await;
if let Some(tool) = tools_guard.get(&tool_call.name) {
let input = ToolInput::from_json(tool_call.arguments.clone());
tool.execute(input, &AgentContext::new("executor")).await
} else {
return Err(AgentError::ExecutionFailed(format!(
"Tool not found: {}",
tool_call.name
)));
}
};
let result_str = if result.success {
result.to_string_output()
} else {
format!(
"Error: {}",
result.error.unwrap_or_else(|| "Unknown error".to_string())
)
};
messages.push(ChatMessage {
role: "tool".to_string(),
content: Some(result_str),
tool_call_id: Some(tool_call.id.clone()),
tool_calls: None,
});
}
} else {
return Ok(response.content.unwrap_or_default());
}
}
Ok("I've completed processing but hit the maximum iteration limit.".to_string())
}
pub fn sessions(&self) -> &Arc<SessionManager> {
&self.sessions
}
pub fn tools(&self) -> &Arc<RwLock<SimpleToolRegistry>> {
&self.tools
}
pub fn context(&self) -> &Arc<RwLock<PromptContext>> {
&self.context
}
pub fn llm(&self) -> &Arc<dyn LLMProvider> {
&self.llm
}
pub fn config(&self) -> &AgentExecutorConfig {
&self.config
}
pub fn base_mut(&mut self) -> &mut BaseAgent {
&mut self.base
}
pub fn base(&self) -> &BaseAgent {
&self.base
}
}
#[async_trait]
impl MoFAAgent for AgentExecutor {
fn id(&self) -> &str {
self.base.id()
}
fn name(&self) -> &str {
self.base.name()
}
fn capabilities(&self) -> &AgentCapabilities {
self.base.capabilities()
}
fn state(&self) -> AgentState {
self.base.state()
}
async fn initialize(&mut self, ctx: &AgentContext) -> AgentResult<()> {
self.base.initialize(ctx).await?;
self.base.transition_to(AgentState::Ready)?;
Ok(())
}
async fn execute(
&mut self,
input: AgentInput,
_ctx: &AgentContext,
) -> AgentResult<AgentOutput> {
let message = input.as_text().unwrap_or("");
let session_key = "default";
let response = self.process_message(session_key, message).await?;
Ok(AgentOutput::text(response))
}
async fn shutdown(&mut self) -> AgentResult<()> {
self.base.shutdown().await
}
}