use super::AgentError;
use crate::types::{AgentAction, ToolInput};
use lc_core::tools::{BaseTool, ToolError};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
pub(crate) fn index_tools(tools: &[Arc<dyn BaseTool>]) -> HashMap<String, Arc<dyn BaseTool>> {
let mut map: HashMap<String, Arc<dyn BaseTool>> = HashMap::with_capacity(tools.len());
for tool in tools {
map.entry(tool.name().to_string())
.or_insert_with(|| tool.clone());
}
map
}
pub(crate) const UNTRUSTED_OPEN: &str = "<untrusted_data>";
pub(crate) const UNTRUSTED_CLOSE: &str = "</untrusted_data>";
pub(crate) fn wrap_tool_output(output: &str) -> String {
let escaped = output.replace(UNTRUSTED_CLOSE, "\\</untrusted_data>");
format!("{UNTRUSTED_OPEN}{escaped}{UNTRUSTED_CLOSE}")
}
pub(crate) fn tool_error_observation(err: &AgentError) -> String {
format!("[Tool execution error: {err}]")
}
pub(crate) async fn run_tool_with_timeout(
tool: &Arc<dyn BaseTool>,
input: String,
timeout: Option<Duration>,
) -> Result<String, ToolError> {
let fut = tool.run(input);
match timeout {
Some(d) => match tokio::time::timeout(d, fut).await {
Ok(result) => result,
Err(_) => Err(ToolError::Timeout(d.as_secs())),
},
None => fut.await,
}
}
pub(crate) async fn execute_tool_for_stream(
tools: &HashMap<String, Arc<dyn BaseTool>>,
action: &AgentAction,
timeout: Option<Duration>,
spotlight: bool,
rule_of_two: bool,
) -> Result<String, AgentError> {
let tool = tools
.get(&action.tool)
.ok_or_else(|| AgentError::ToolNotFound(action.tool.clone()))?;
if rule_of_two && tool.risk().count_armed() >= 3 {
return Ok(
"[BLOCKED by Rule of Two: tool declares untrusted-input + sensitive-access + state-changing]"
.to_string(),
);
}
let input_str = match &action.tool_input {
ToolInput::String { value: s } => s.clone(),
ToolInput::Object { value: v } => serde_json::to_string(v)
.map_err(|e| AgentError::Other(format!("Failed to serialize tool input: {}", e)))?,
};
let output = run_tool_with_timeout(tool, input_str, timeout)
.await
.map_err(|e| match e {
ToolError::ControlAbort(msg) => AgentError::Other(format!("Tool call aborted: {msg}")),
other => AgentError::ToolExecutionError(other.to_string()),
})?;
Ok(if spotlight {
wrap_tool_output(&output)
} else {
output
})
}
pub(crate) async fn execute_tools_parallel_for_stream(
tools: &HashMap<String, Arc<dyn BaseTool>>,
actions: &[AgentAction],
timeout: Option<Duration>,
max_concurrency: usize,
spotlight: bool,
rule_of_two: bool,
) -> Result<Vec<String>, AgentError> {
use futures_util::future::join_all;
let sem = Arc::new(Semaphore::new(max_concurrency));
let futures = actions.iter().map(|action| {
let sem = sem.clone();
async move {
let _permit = sem
.acquire_owned()
.await
.map_err(|e| AgentError::Other(format!("concurrency semaphore closed: {e}")))?;
execute_tool_for_stream(tools, action, timeout, spotlight, rule_of_two).await
}
});
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 @ AgentError::ToolExecutionError(_)) => {
observations.push(tool_error_observation(&e))
}
Err(e) => return Err(e),
}
}
Ok(observations)
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
struct NamedTool(&'static str);
#[async_trait]
impl BaseTool for NamedTool {
fn name(&self) -> &str {
self.0
}
fn description(&self) -> &str {
"A11 index test tool"
}
async fn run(&self, input: String) -> Result<String, ToolError> {
Ok(input)
}
}
fn named(name: &'static str) -> Arc<dyn BaseTool> {
Arc::new(NamedTool(name))
}
#[test]
fn index_resolves_each_tool_by_name() {
let tools: Vec<Arc<dyn BaseTool>> = vec![named("alpha"), named("beta"), named("gamma")];
let idx = index_tools(&tools);
assert!(idx.contains_key("alpha"));
assert!(idx.contains_key("beta"));
assert!(idx.contains_key("gamma"));
assert!(!idx.contains_key("delta"));
assert_eq!(idx.len(), 3);
}
#[test]
fn index_preserves_first_match_on_name_collision() {
let first = named("dup");
let second = named("dup");
let tools: Vec<Arc<dyn BaseTool>> = vec![first.clone(), second.clone()];
let idx = index_tools(&tools);
let resolved = idx.get("dup").unwrap();
assert!(
Arc::ptr_eq(resolved, &first),
"first-inserted tool must win on name collision"
);
}
}