use std::collections::HashSet;
fn read_only_tools() -> HashSet<&'static str> {
HashSet::from([
"read_file",
"list_files",
"search",
"find_symbol",
"find_referencing_symbols",
"analyze_image",
"fetch_url",
"web_search",
"capture_web_screenshot",
"capture_screenshot",
"list_sessions",
"get_session_history",
"list_subagents",
"memory_search",
"list_todos",
"search_tools",
"task_complete",
"list_agents",
])
}
fn write_tools() -> HashSet<&'static str> {
HashSet::from([
"write_file",
"edit_file",
"multi_edit",
"run_command",
"insert_before_symbol",
"insert_after_symbol",
"replace_symbol_body",
"rename_symbol",
"notebook_edit",
"apply_patch",
"memory_write",
"write_todos",
"update_todo",
"complete_todo",
"clear_todos",
"send_message",
"schedule",
])
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub name: String,
pub arguments: serde_json::Value,
}
impl ToolCall {
pub fn new(name: impl Into<String>, arguments: serde_json::Value) -> Self {
Self {
name: name.into(),
arguments,
}
}
}
pub struct ParallelPolicy;
impl ParallelPolicy {
pub fn partition(tool_calls: &[ToolCall]) -> Vec<Vec<usize>> {
if tool_calls.len() <= 1 {
return if tool_calls.is_empty() {
vec![]
} else {
vec![vec![0]]
};
}
let ro_tools = read_only_tools();
let w_tools = write_tools();
let mut read_indices: Vec<usize> = Vec::new();
let mut write_indices: Vec<usize> = Vec::new();
let mut other_indices: Vec<usize> = Vec::new();
for (i, tc) in tool_calls.iter().enumerate() {
if Self::is_read_only(tc, &ro_tools) {
read_indices.push(i);
} else if w_tools.contains(tc.name.as_str()) {
write_indices.push(i);
} else {
other_indices.push(i);
}
}
let mut groups: Vec<Vec<usize>> = Vec::new();
if !read_indices.is_empty() {
groups.push(read_indices);
}
if !write_indices.is_empty() {
if Self::can_parallelize_writes(tool_calls, &write_indices) {
groups.push(write_indices);
} else {
for idx in write_indices {
groups.push(vec![idx]);
}
}
}
for idx in other_indices {
groups.push(vec![idx]);
}
groups
}
fn is_read_only(tc: &ToolCall, ro_tools: &HashSet<&str>) -> bool {
ro_tools.contains(tc.name.as_str())
}
fn can_parallelize_writes(tool_calls: &[ToolCall], write_indices: &[usize]) -> bool {
let mut targets: HashSet<String> = HashSet::new();
for &idx in write_indices {
let tc = &tool_calls[idx];
match tc.name.as_str() {
"write_file" | "edit_file" | "notebook_edit" => {
let target = tc
.arguments
.get("file_path")
.or_else(|| tc.arguments.get("notebook_path"))
.and_then(|v| v.as_str())
.unwrap_or("");
if target.is_empty() {
return false;
}
if targets.contains(target) {
return false; }
targets.insert(target.to_string());
}
_ => return false, }
}
targets.len() > 1
}
}
#[cfg(test)]
#[path = "parallel_tests.rs"]
mod tests;