use alloc::{boxed::Box, string::String, vec::Vec};
use core::future;
use serde::{Deserialize, Serialize};
use super::ToolRegistry;
use crate::{
rust_tool::BoxToolFuture,
types::{ToolContext, ToolError, ToolOutput},
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum BatchExecutionMode {
#[default]
Parallel,
ParallelAllowDestructive,
SequentialStopOnError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllowDestructiveBatch(pub bool);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BatchToolCall {
#[serde(default)]
pub id: String,
pub name: String,
#[serde(default)]
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BatchToolCallStr<'a> {
pub id: &'a str,
pub name: &'a str,
pub arguments_json: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchToolCallOutcome {
pub id: String,
pub name: String,
pub result: Result<ToolOutput, ToolError>,
}
impl ToolRegistry {
pub async fn dispatch_batch(
&self,
calls: &[BatchToolCall],
mode: BatchExecutionMode,
ctx: &ToolContext,
) -> Vec<BatchToolCallOutcome> {
match mode {
BatchExecutionMode::Parallel | BatchExecutionMode::ParallelAllowDestructive => {
let allow_destructive =
matches!(mode, BatchExecutionMode::ParallelAllowDestructive)
|| ctx
.get_ext::<AllowDestructiveBatch>()
.is_some_and(|ext| ext.0);
let futures: Vec<BoxToolFuture<'_>> = calls
.iter()
.map(|call| -> BoxToolFuture<'_> {
if !allow_destructive && self.is_destructive(&call.name) {
tracing::warn!(
tool = %call.name,
"Destructive tool rejected in parallel batch execution without explicit confirmation"
);
let fut: BoxToolFuture<'_> = Box::pin(future::ready(Err(
ToolError::destructive_in_parallel_batch(&call.name),
)));
return fut;
}
match self.dispatch_boxed(&call.name, call.arguments.clone(), ctx) {
Ok(fut) => fut,
Err(err) => {
let fut: BoxToolFuture<'_> = Box::pin(future::ready(Err(err)));
fut
}
}
})
.collect();
let results = futures_util::future::join_all(futures).await;
calls
.iter()
.zip(results)
.map(|(call, result)| BatchToolCallOutcome {
id: call.id.clone(),
name: call.name.clone(),
result,
})
.collect()
}
BatchExecutionMode::SequentialStopOnError => {
let mut outcomes = Vec::with_capacity(calls.len());
for call in calls {
let result = self.dispatch(&call.name, call.arguments.clone(), ctx).await;
let should_stop = match &result {
Ok(_) => false,
Err(err) => {
tracing::debug!(
error = %err,
tool = %call.name,
"Sequential batch execution stopped due to error"
);
true
}
};
outcomes.push(BatchToolCallOutcome {
id: call.id.clone(),
name: call.name.clone(),
result,
});
if should_stop {
break;
}
}
outcomes
}
}
}
pub async fn dispatch_batch_str(
&self,
calls: &[BatchToolCallStr<'_>],
mode: BatchExecutionMode,
ctx: &ToolContext,
) -> Vec<BatchToolCallOutcome> {
match mode {
BatchExecutionMode::Parallel | BatchExecutionMode::ParallelAllowDestructive => {
let allow_destructive =
matches!(mode, BatchExecutionMode::ParallelAllowDestructive)
|| ctx
.get_ext::<AllowDestructiveBatch>()
.is_some_and(|ext| ext.0);
let futures: Vec<BoxToolFuture<'_>> = calls
.iter()
.map(|call| -> BoxToolFuture<'_> {
if !allow_destructive && self.is_destructive(call.name) {
tracing::warn!(
tool = %call.name,
"Destructive tool rejected in parallel batch execution without explicit confirmation"
);
let fut: BoxToolFuture<'_> = Box::pin(future::ready(Err(
ToolError::destructive_in_parallel_batch(call.name),
)));
return fut;
}
match self.dispatch_boxed_str(call.name, call.arguments_json, ctx) {
Ok(fut) => fut,
Err(err) => Box::pin(future::ready(Err(err))),
}
})
.collect();
let results = futures_util::future::join_all(futures).await;
calls
.iter()
.zip(results)
.map(|(call, result)| BatchToolCallOutcome {
id: String::from(call.id),
name: String::from(call.name),
result,
})
.collect()
}
BatchExecutionMode::SequentialStopOnError => {
let mut outcomes = Vec::with_capacity(calls.len());
for call in calls {
let result = self.dispatch_str(call.name, call.arguments_json, ctx).await;
let should_stop = match &result {
Ok(_) => false,
Err(err) => {
tracing::debug!(
error = %err,
tool = %call.name,
"Sequential batch execution stopped due to error"
);
true
}
};
outcomes.push(BatchToolCallOutcome {
id: String::from(call.id),
name: String::from(call.name),
result,
});
if should_stop {
break;
}
}
outcomes
}
}
}
}