use crate::registry::{ToolContext, ToolRegistry};
use anyhow::Result;
use futures::future::join_all;
use serde_json::Value;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::sync::Semaphore;
const BATCH_TOTAL_OUTPUT_LIMIT: usize = 50 * 1024;
pub static FILE_WRITE_LOCKS: std::sync::OnceLock<Arc<Mutex<HashSet<PathBuf>>>> =
std::sync::OnceLock::new();
pub fn init_file_locks() {
FILE_WRITE_LOCKS.get_or_init(|| Arc::new(Mutex::new(HashSet::new())));
}
pub async fn execute_batch(
items: Vec<(String, Value)>, registry: Arc<ToolRegistry>,
ctx_cwd: PathBuf,
ctx_session_id: String,
max_parallel: usize,
) -> Vec<Result<String>> {
let n = items.len();
if n == 0 {
return Vec::new();
}
let per_tool_budget = BATCH_TOTAL_OUTPUT_LIMIT / n;
let sem = Arc::new(Semaphore::new(max_parallel));
let active_file_ops: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
let futures: Vec<_> = items
.into_iter()
.map(|(tool_name, args)| {
let registry = registry.clone();
let sem = sem.clone();
let cwd = ctx_cwd.clone();
let session_id = ctx_session_id.clone();
let active_file_ops = active_file_ops.clone();
async move {
let _permit = sem.acquire().await;
let file_key = args["path"]
.as_str()
.map(PathBuf::from)
.or_else(|| args["file"].as_str().map(PathBuf::from));
if let Some(ref fpath) = file_key {
loop {
{
let ops = active_file_ops.lock().expect("file ops lock poisoned");
if !ops.contains(fpath) {
break;
}
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
active_file_ops
.lock()
.expect("file ops lock poisoned")
.insert(fpath.clone());
}
let tool = match registry.get(&tool_name) {
Some(t) => t.clone(),
None => {
if let Some(ref fpath) = file_key {
active_file_ops
.lock()
.expect("file ops lock poisoned")
.remove(fpath);
}
return Err(anyhow::anyhow!("Tool not found: {tool_name}"));
}
};
let approve_fn: &(dyn Fn(&str) -> bool + Send + Sync) = &|_| true;
let ctx = ToolContext {
cwd: cwd.clone(),
session_id: session_id.clone(),
approve_fn,
yolo: true,
identity: None,
sandbox_enabled: false,
};
let result = tool.execute(args, &ctx).await;
if let Some(ref fpath) = file_key {
active_file_ops
.lock()
.expect("file ops lock poisoned")
.remove(fpath);
}
result.map(|output| {
if output.len() > per_tool_budget {
format!(
"{}...\n[truncated: {}/{} bytes]",
&output[..per_tool_budget],
per_tool_budget,
output.len()
)
} else {
output
}
})
}
})
.collect();
join_all(futures).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::{Tool, ToolRegistry, ToolContext};
use async_trait::async_trait;
struct EchoTool;
#[async_trait]
impl Tool for EchoTool {
fn name(&self) -> &str { "echo" }
fn description(&self) -> &str { "Echo tool" }
fn parameters(&self) -> Value { serde_json::json!({"type": "object"}) }
async fn execute(&self, args: Value, _ctx: &ToolContext<'_>) -> Result<String> {
Ok(args["msg"].as_str().unwrap_or("ok").to_string())
}
}
#[tokio::test]
async fn test_batch_empty() {
let results = execute_batch(vec![], Arc::new(ToolRegistry::new()), "/tmp".into(), "s".into(), 4).await;
assert!(results.is_empty());
}
#[tokio::test]
async fn test_batch_single_tool() {
let mut reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
let items = vec![("echo".to_string(), serde_json::json!({"msg": "hello"}))];
let results = execute_batch(items, Arc::new(reg), "/tmp".into(), "s".into(), 4).await;
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_ref().unwrap(), "hello");
}
#[tokio::test]
async fn test_batch_tool_not_found() {
let items = vec![("nonexistent".to_string(), serde_json::json!({}))];
let results = execute_batch(items, Arc::new(ToolRegistry::new()), "/tmp".into(), "s".into(), 4).await;
assert_eq!(results.len(), 1);
assert!(results[0].is_err());
}
#[tokio::test]
async fn test_batch_parallel_execution() {
let mut reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
let items = vec![
("echo".to_string(), serde_json::json!({"msg": "a"})),
("echo".to_string(), serde_json::json!({"msg": "b"})),
("echo".to_string(), serde_json::json!({"msg": "c"})),
];
let results = execute_batch(items, Arc::new(reg), "/tmp".into(), "s".into(), 2).await;
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn test_init_file_locks() {
init_file_locks();
assert!(FILE_WRITE_LOCKS.get().is_some());
}
}