1use std::sync::Arc;
9
10use behest_core::tool_types::ToolCall;
11
12use crate::Tool;
13
14#[derive(Debug, Clone, Default)]
16pub enum ToolExecutionStrategy {
17 Sequential,
19 Parallel {
21 max_concurrency: usize,
23 },
24 #[default]
29 Auto,
30}
31
32#[derive(Debug, Clone)]
34pub struct ExecutionPlan {
35 pub groups: Vec<ExecutionGroup>,
38}
39
40#[derive(Debug, Clone)]
42pub struct ExecutionGroup {
43 pub parallel: bool,
45 pub calls: Vec<ToolCall>,
47}
48
49impl ExecutionPlan {
50 #[must_use]
52 pub fn sequential(calls: Vec<ToolCall>) -> Self {
53 Self {
54 groups: vec![ExecutionGroup {
55 parallel: false,
56 calls,
57 }],
58 }
59 }
60
61 #[must_use]
63 pub fn parallel(calls: Vec<ToolCall>) -> Self {
64 Self {
65 groups: vec![ExecutionGroup {
66 parallel: true,
67 calls,
68 }],
69 }
70 }
71}
72
73impl ToolExecutionStrategy {
74 #[must_use]
76 pub fn plan(&self, calls: &[ToolCall], tools: &[Arc<dyn Tool>]) -> ExecutionPlan {
77 match self {
78 Self::Sequential => ExecutionPlan::sequential(calls.to_vec()),
79 Self::Parallel { .. } => ExecutionPlan::parallel(calls.to_vec()),
80 Self::Auto => Self::auto_plan(calls, tools),
81 }
82 }
83
84 fn auto_plan(calls: &[ToolCall], tools: &[Arc<dyn Tool>]) -> ExecutionPlan {
85 if calls.len() <= 1 {
86 return ExecutionPlan::sequential(calls.to_vec());
87 }
88
89 let tool_map: std::collections::HashMap<&str, &Arc<dyn Tool>> =
91 tools.iter().map(|t| (t.name(), t)).collect();
92
93 let mut parallel_safe: Vec<ToolCall> = Vec::new();
95 let mut sequential: Vec<ToolCall> = Vec::new();
96 let mut pending_approval: Vec<ToolCall> = Vec::new();
97
98 for call in calls {
99 let tool = tool_map.get(call.name.as_str());
100 let is_read_only = tool.is_some_and(|t| t.is_read_only());
101 let is_concurrency_safe = tool.is_some_and(|t| t.is_concurrency_safe());
102 let requires_approval = tool.is_some_and(|t| t.requires_approval());
103
104 if requires_approval {
105 pending_approval.push(call.clone());
106 } else if is_read_only || is_concurrency_safe {
107 parallel_safe.push(call.clone());
108 } else {
109 sequential.push(call.clone());
110 }
111 }
112
113 let mut groups: Vec<ExecutionGroup> = Vec::new();
114
115 for call in pending_approval {
117 groups.push(ExecutionGroup {
118 parallel: false,
119 calls: vec![call],
120 });
121 }
122
123 if !parallel_safe.is_empty() {
125 groups.push(ExecutionGroup {
126 parallel: true,
127 calls: parallel_safe,
128 });
129 }
130
131 for call in sequential {
133 groups.push(ExecutionGroup {
134 parallel: false,
135 calls: vec![call],
136 });
137 }
138
139 if groups.is_empty() {
140 ExecutionPlan::sequential(calls.to_vec())
141 } else {
142 ExecutionPlan { groups }
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::FunctionTool;
151
152 fn make_ro_tool(name: &str) -> Arc<dyn Tool> {
153 Arc::new(
154 FunctionTool::new(name, format!("{name} tool"), serde_json::json!({}), |_| {
155 Box::pin(async { Ok(serde_json::Value::String("ok".to_string())) })
156 })
157 .read_only(),
158 )
159 }
160
161 fn make_rw_tool(name: &str) -> Arc<dyn Tool> {
162 Arc::new(FunctionTool::new(
163 name,
164 format!("{name} tool"),
165 serde_json::json!({}),
166 |_| Box::pin(async { Ok(serde_json::Value::String("ok".to_string())) }),
167 ))
168 }
169
170 fn make_call(name: &str) -> ToolCall {
171 ToolCall::new(format!("call_{name}"), name, serde_json::Value::Null)
172 }
173
174 #[test]
175 fn auto_parallelizes_ro_tools() {
176 let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("search"), make_ro_tool("fetch")];
177 let calls = vec![make_call("search"), make_call("fetch")];
178
179 let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
180 assert_eq!(plan.groups.len(), 1);
181 assert!(plan.groups[0].parallel);
182 assert_eq!(plan.groups[0].calls.len(), 2);
183 }
184
185 #[test]
186 fn auto_serializes_rw_tools() {
187 let tools: Vec<Arc<dyn Tool>> = vec![make_rw_tool("create"), make_rw_tool("delete")];
188 let calls = vec![make_call("create"), make_call("delete")];
189
190 let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
191 assert_eq!(plan.groups.len(), 2);
192 assert!(!plan.groups[0].parallel);
193 assert!(!plan.groups[1].parallel);
194 }
195
196 #[test]
197 fn auto_mixed_partitions() {
198 let ro1 = make_ro_tool("search");
199 let rw1 = make_rw_tool("save");
200 let ro2 = make_ro_tool("fetch");
201 let tools: Vec<Arc<dyn Tool>> = vec![ro1, rw1, ro2];
202 let calls = vec![make_call("search"), make_call("save"), make_call("fetch")];
203
204 let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
205 assert_eq!(plan.groups.len(), 2);
207 assert!(plan.groups[0].parallel);
208 assert_eq!(plan.groups[0].calls.len(), 2);
209 assert!(!plan.groups[1].parallel);
210 assert_eq!(plan.groups[1].calls.len(), 1);
211 }
212
213 #[test]
214 fn sequential_strategy_always_one_group() {
215 let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a"), make_ro_tool("b")];
216 let calls = vec![make_call("a"), make_call("b")];
217
218 let plan = ToolExecutionStrategy::Sequential.plan(&calls, &tools);
219 assert_eq!(plan.groups.len(), 1);
220 assert!(!plan.groups[0].parallel);
221 assert_eq!(plan.groups[0].calls.len(), 2);
222 }
223
224 #[test]
225 fn parallel_strategy_always_one_group() {
226 let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a"), make_ro_tool("b")];
227 let calls = vec![make_call("a"), make_call("b")];
228
229 let plan = ToolExecutionStrategy::Parallel { max_concurrency: 4 }.plan(&calls, &tools);
230 assert_eq!(plan.groups.len(), 1);
231 assert!(plan.groups[0].parallel);
232 }
233
234 #[test]
235 fn single_call_uses_sequential() {
236 let tools: Vec<Arc<dyn Tool>> = vec![make_ro_tool("a")];
237 let calls = vec![make_call("a")];
238
239 let plan = ToolExecutionStrategy::Auto.plan(&calls, &tools);
240 assert_eq!(plan.groups.len(), 1);
241 assert!(!plan.groups[0].parallel);
242 }
243}