use std::sync::Arc;
use behest_core::tool_types::ToolCall;
use crate::Tool;
#[derive(Debug, Clone, Default)]
pub enum ToolExecutionStrategy {
Sequential,
Parallel {
max_concurrency: usize,
},
#[default]
Auto,
}
#[derive(Debug, Clone)]
pub struct ExecutionPlan {
pub groups: Vec<ExecutionGroup>,
}
#[derive(Debug, Clone)]
pub struct ExecutionGroup {
pub parallel: bool,
pub calls: Vec<ToolCall>,
}
impl ExecutionPlan {
#[must_use]
pub fn sequential(calls: Vec<ToolCall>) -> Self {
Self {
groups: vec![ExecutionGroup {
parallel: false,
calls,
}],
}
}
#[must_use]
pub fn parallel(calls: Vec<ToolCall>) -> Self {
Self {
groups: vec![ExecutionGroup {
parallel: true,
calls,
}],
}
}
}
impl ToolExecutionStrategy {
#[must_use]
pub fn plan(&self, calls: &[ToolCall], tools: &[Arc<dyn Tool>]) -> ExecutionPlan {
match self {
Self::Sequential => ExecutionPlan::sequential(calls.to_vec()),
Self::Parallel { .. } => ExecutionPlan::parallel(calls.to_vec()),
Self::Auto => Self::auto_plan(calls, tools),
}
}
fn auto_plan(calls: &[ToolCall], tools: &[Arc<dyn Tool>]) -> ExecutionPlan {
if calls.len() <= 1 {
return ExecutionPlan::sequential(calls.to_vec());
}
let tool_map: std::collections::HashMap<&str, &Arc<dyn Tool>> =
tools.iter().map(|t| (t.name(), t)).collect();
let mut parallel_safe: Vec<ToolCall> = Vec::new();
let mut sequential: Vec<ToolCall> = Vec::new();
let mut pending_approval: Vec<ToolCall> = Vec::new();
for call in calls {
let tool = tool_map.get(call.name.as_str());
let is_read_only = tool.is_some_and(|t| t.is_read_only());
let is_concurrency_safe = tool.is_some_and(|t| t.is_concurrency_safe());
let requires_approval = tool.is_some_and(|t| t.requires_approval());
if requires_approval {
pending_approval.push(call.clone());
} else if is_read_only || is_concurrency_safe {
parallel_safe.push(call.clone());
} else {
sequential.push(call.clone());
}
}
let mut groups: Vec<ExecutionGroup> = Vec::new();
for call in pending_approval {
groups.push(ExecutionGroup {
parallel: false,
calls: vec![call],
});
}
if !parallel_safe.is_empty() {
groups.push(ExecutionGroup {
parallel: true,
calls: parallel_safe,
});
}
for call in sequential {
groups.push(ExecutionGroup {
parallel: false,
calls: vec![call],
});
}
if groups.is_empty() {
ExecutionPlan::sequential(calls.to_vec())
} else {
ExecutionPlan { groups }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FunctionTool;
fn make_ro_tool(name: &str) -> Arc<dyn Tool> {
Arc::new(
FunctionTool::new(name, format!("{name} tool"), serde_json::json!({}), |_| {
Box::pin(async { Ok(crate::ToolOutput::text("ok")) })
})
.read_only(),
)
}
fn make_rw_tool(name: &str) -> Arc<dyn Tool> {
Arc::new(FunctionTool::new(
name,
format!("{name} tool"),
serde_json::json!({}),
|_| Box::pin(async { Ok(crate::ToolOutput::text("ok")) }),
))
}
fn make_call(name: &str) -> ToolCall {
ToolCall::new(format!("call_{name}"), name, serde_json::Value::Null)
}
#[test]
fn auto_parallelizes_ro_tools() {
let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("search"), make_ro_tool("fetch")];
let calls = vec![make_call("search"), make_call("fetch")];
let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 1);
assert!(plan.groups[0].parallel);
assert_eq!(plan.groups[0].calls.len(), 2);
}
#[test]
fn auto_serializes_rw_tools() {
let tools: Vec<Arc<dyn Tool>> = vec![make_rw_tool("create"), make_rw_tool("delete")];
let calls = vec![make_call("create"), make_call("delete")];
let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 2);
assert!(!plan.groups[0].parallel);
assert!(!plan.groups[1].parallel);
}
#[test]
fn auto_mixed_partitions() {
let ro1 = make_ro_tool("search");
let rw1 = make_rw_tool("save");
let ro2 = make_ro_tool("fetch");
let tools: Vec<Arc<dyn Tool>> = vec![ro1, rw1, ro2];
let calls = vec![make_call("search"), make_call("save"), make_call("fetch")];
let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 2);
assert!(plan.groups[0].parallel);
assert_eq!(plan.groups[0].calls.len(), 2);
assert!(!plan.groups[1].parallel);
assert_eq!(plan.groups[1].calls.len(), 1);
}
#[test]
fn sequential_strategy_always_one_group() {
let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a"), make_ro_tool("b")];
let calls = vec![make_call("a"), make_call("b")];
let plan = ToolExecutionStrategy::Sequential.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 1);
assert!(!plan.groups[0].parallel);
assert_eq!(plan.groups[0].calls.len(), 2);
}
#[test]
fn parallel_strategy_always_one_group() {
let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a"), make_ro_tool("b")];
let calls = vec![make_call("a"), make_call("b")];
let plan = ToolExecutionStrategy::Parallel { max_concurrency: 4 }.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 1);
assert!(plan.groups[0].parallel);
}
#[test]
fn single_call_uses_sequential() {
let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a")];
let calls = vec![make_call("a")];
let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
assert_eq!(plan.groups.len(), 1);
assert!(!plan.groups[0].parallel);
}
}