use echo_core::agent::CancellationToken;
use echo_core::error::{AgentError, ReactError, Result};
use echo_core::guard::GuardManager;
use echo_core::llm::types::{Message, MessageContent, Role};
use echo_core::llm::{ChatRequest, LlmClient, ToolDefinition};
use echo_core::tools::{ToolParameters, ToolResult};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use crate::tools::ToolManager;
#[derive(Debug, Clone)]
pub struct LightweightConfig {
pub name: String,
pub description: String,
pub model_override: Option<String>,
pub system_prompt_override: Option<String>,
pub tool_filter: Option<Vec<String>>,
pub max_iterations: Option<usize>,
pub timeout_secs: u64,
}
impl LightweightConfig {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
model_override: None,
system_prompt_override: None,
tool_filter: None,
max_iterations: None,
timeout_secs: 0,
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model_override = Some(model.into());
self
}
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt_override = Some(prompt.into());
self
}
pub fn with_tool_filter(mut self, tools: Vec<String>) -> Self {
self.tool_filter = Some(tools);
self
}
pub fn with_max_iterations(mut self, max: usize) -> Self {
self.max_iterations = Some(max);
self
}
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
}
pub struct LightweightSubagent {
pub config: LightweightConfig,
llm_client: Arc<dyn LlmClient>,
tool_manager: Arc<ToolManager>,
guard_manager: Option<Arc<GuardManager>>,
parent_model: String,
parent_system_prompt: String,
parent_max_iterations: usize,
messages: RwLock<Vec<Message>>,
}
impl LightweightSubagent {
pub fn new(
config: LightweightConfig,
llm_client: Arc<dyn LlmClient>,
tool_manager: Arc<ToolManager>,
guard_manager: Option<Arc<GuardManager>>,
parent_model: String,
parent_system_prompt: String,
parent_max_iterations: usize,
) -> Self {
let mut sub = Self {
config,
llm_client,
tool_manager,
guard_manager,
parent_model,
parent_system_prompt,
parent_max_iterations,
messages: RwLock::new(Vec::new()),
};
sub.init_messages();
sub
}
fn init_messages(&mut self) {
}
pub fn model_name(&self) -> &str {
self.config
.model_override
.as_deref()
.unwrap_or(&self.parent_model)
}
pub fn system_prompt(&self) -> &str {
self.config
.system_prompt_override
.as_deref()
.unwrap_or(&self.parent_system_prompt)
}
pub fn max_iterations(&self) -> usize {
self.config
.max_iterations
.unwrap_or(self.parent_max_iterations)
}
pub async fn execute(&self, task: &str) -> Result<String> {
self.execute_with_cancel(task, CancellationToken::new())
.await
}
pub async fn execute_with_cancel(
&self,
task: &str,
cancel: CancellationToken,
) -> Result<String> {
let system_msg = Message {
role: Role::System,
content: MessageContent::Text(self.system_prompt().to_string()),
tool_calls: None,
name: None,
tool_call_id: None,
reasoning_content: None,
};
let user_msg = Message {
role: Role::User,
content: MessageContent::Text(task.to_string()),
tool_calls: None,
name: None,
tool_call_id: None,
reasoning_content: None,
};
{
let mut msgs = self.messages.write().await;
msgs.clear();
msgs.push(system_msg);
msgs.push(user_msg);
}
let max_iter = self.max_iterations();
let tool_defs = self.get_tool_definitions();
for _iteration in 0..max_iter {
if cancel.is_cancelled() {
return Err(ReactError::Agent(Box::new(
AgentError::InitializationFailed("Sub-agent cancelled".to_string()),
)));
}
let messages = self.messages.read().await.clone();
let has_tools = !tool_defs.is_empty();
let request = ChatRequest {
messages,
tools: if has_tools {
Some(tool_defs.clone())
} else {
None
},
..Default::default()
};
let response = self.llm_client.chat(request).await.map_err(|e| {
ReactError::Other(format!("Sub-agent '{}' LLM error: {}", self.config.name, e))
})?;
let assistant_msg = response.message.clone();
if let Some(ref guard_mgr) = self.guard_manager {
let text = assistant_msg.text_content().unwrap_or_default();
let guard_result = guard_mgr
.check_all(&text, echo_core::guard::GuardDirection::Output)
.await?;
if guard_result.is_blocked() {
return Err(ReactError::Other(format!(
"Sub-agent '{}' output blocked by guard",
self.config.name
)));
}
}
let has_tool_calls = assistant_msg
.tool_calls
.as_ref()
.map(|tcs| !tcs.is_empty())
.unwrap_or(false);
if has_tool_calls {
let tool_calls = assistant_msg.tool_calls.clone().unwrap();
self.messages.write().await.push(assistant_msg);
for tc in &tool_calls {
if cancel.is_cancelled() {
return Err(ReactError::Agent(Box::new(
AgentError::InitializationFailed("Sub-agent cancelled".to_string()),
)));
}
let params: ToolParameters =
serde_json::from_str(&tc.function.arguments).unwrap_or_default();
let tool_result = self
.tool_manager
.execute_tool(&tc.function.name, params)
.await
.unwrap_or_else(|e| ToolResult::error(format!("Tool error: {}", e)));
let tool_msg = Message {
role: Role::Tool,
content: MessageContent::Text(tool_result.output),
tool_call_id: Some(tc.id.clone()),
tool_calls: None,
name: None,
reasoning_content: None,
};
self.messages.write().await.push(tool_msg);
}
continue; }
return Ok(assistant_msg.text_content().unwrap_or_default().to_string());
}
Err(ReactError::Agent(Box::new(
AgentError::MaxIterationsExceeded(max_iter),
)))
}
fn get_tool_definitions(&self) -> Vec<ToolDefinition> {
let tool_names = self.tool_manager.list_tools();
let names_iter: Box<dyn Iterator<Item = String>> =
if let Some(ref filter) = self.config.tool_filter {
Box::new(
tool_names
.into_iter()
.filter(move |name| filter.contains(name)),
)
} else {
Box::new(tool_names.into_iter())
};
names_iter
.filter_map(|name| {
self.tool_manager
.get_tool(&name)
.map(|t| ToolDefinition::from_tool(&**t))
})
.collect()
}
pub async fn reset(&self) {
self.messages.write().await.clear();
}
pub async fn message_count(&self) -> usize {
self.messages.read().await.len()
}
pub fn timeout(&self) -> Option<Duration> {
if self.config.timeout_secs > 0 {
Some(Duration::from_secs(self.config.timeout_secs))
} else {
None
}
}
}