1use serde_json::Value;
8
9#[derive(Debug, Clone)]
11pub struct FallbackStep {
12 pub tool_name: String,
13 pub args: Value,
14}
15
16#[derive(Debug, Clone)]
18pub struct FallbackRecommendation {
19 pub tool_name: String,
20 pub args: Value,
21 pub chain: Vec<FallbackStep>,
24}
25
26impl FallbackRecommendation {
27 pub fn is_valid(&self) -> bool {
30 !self.tool_name.trim().is_empty()
31 }
32
33 pub fn step_count(&self) -> usize {
35 1 + self.chain.len()
36 }
37
38 pub fn steps(&self) -> impl Iterator<Item = FallbackStep> {
40 std::iter::once(FallbackStep {
41 tool_name: self.tool_name.clone(),
42 args: self.args.clone(),
43 })
44 .chain(self.chain.iter().cloned())
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct PreparedToolCall {
51 pub canonical_name: String,
52 pub readonly_classification: bool,
53 pub parallel_safe_after_preflight: bool,
54 pub effective_args: Value,
55 pub fallback_recommendation: Option<FallbackRecommendation>,
56 pub already_preflighted: bool,
57}
58
59impl PreparedToolCall {
60 pub fn new(
61 canonical_name: String,
62 readonly_classification: bool,
63 parallel_safe_after_preflight: bool,
64 effective_args: Value,
65 ) -> Self {
66 Self {
67 canonical_name,
68 readonly_classification,
69 parallel_safe_after_preflight,
70 effective_args,
71 fallback_recommendation: None,
72 already_preflighted: true,
73 }
74 }
75
76 pub fn with_fallback_recommendation(mut self, fallback_recommendation: Option<FallbackRecommendation>) -> Self {
77 self.fallback_recommendation = fallback_recommendation;
78 self
79 }
80
81 pub fn can_parallelize(&self) -> bool {
82 self.readonly_classification && self.parallel_safe_after_preflight
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PreparedToolBatchKind {
89 Sequential,
90 ParallelReadonly,
91}
92
93#[derive(Debug, Clone)]
95pub struct PreparedToolBatch {
96 pub kind: PreparedToolBatchKind,
97 pub calls: Vec<PreparedToolCall>,
98}
99
100impl PreparedToolBatch {
101 pub fn plan_layout(
102 parallelizable: impl IntoIterator<Item = bool>,
103 allow_parallel: bool,
104 ) -> Vec<(PreparedToolBatchKind, usize)> {
105 Self::plan_layout_with_limit(parallelizable, allow_parallel, 0)
106 }
107
108 pub fn plan_layout_with_limit(
111 parallelizable: impl IntoIterator<Item = bool>,
112 allow_parallel: bool,
113 max_parallel: usize,
114 ) -> Vec<(PreparedToolBatchKind, usize)> {
115 let mut layout = Vec::new();
116 let mut parallel_batch_len = 0usize;
117
118 for can_parallelize in parallelizable {
119 if allow_parallel && can_parallelize {
120 parallel_batch_len += 1;
121 continue;
122 }
123
124 if parallel_batch_len > 0 {
125 push_parallel_batch_layout(&mut layout, &mut parallel_batch_len, max_parallel);
126 }
127 layout.push((PreparedToolBatchKind::Sequential, 1));
128 }
129
130 push_parallel_batch_layout(&mut layout, &mut parallel_batch_len, max_parallel);
131
132 layout
133 }
134
135 #[deprecated(note = "tool names do not affect scheduling; use plan_layout")]
141 pub fn plan_layout_with_names<'a>(
142 calls: impl IntoIterator<Item = (bool, &'a str)>,
143 allow_parallel: bool,
144 ) -> Vec<(PreparedToolBatchKind, usize)> {
145 Self::plan_layout(calls.into_iter().map(|(can_parallelize, _)| can_parallelize), allow_parallel)
146 }
147
148 pub fn plan(calls: impl IntoIterator<Item = PreparedToolCall>, allow_parallel: bool) -> Vec<Self> {
149 Self::plan_with_limit(calls, allow_parallel, 0)
150 }
151
152 pub fn plan_with_limit(
153 calls: impl IntoIterator<Item = PreparedToolCall>,
154 allow_parallel: bool,
155 max_parallel: usize,
156 ) -> Vec<Self> {
157 let calls: Vec<_> = calls.into_iter().collect();
158 let layout = Self::plan_layout_with_limit(
159 calls.iter().map(PreparedToolCall::can_parallelize),
160 allow_parallel,
161 max_parallel,
162 );
163 let mut calls = calls.into_iter();
164
165 layout
166 .into_iter()
167 .map(|(kind, len)| Self { kind, calls: calls.by_ref().take(len).collect() })
168 .collect()
169 }
170}
171
172fn push_parallel_batch_layout(
173 layout: &mut Vec<(PreparedToolBatchKind, usize)>,
174 parallel_batch_len: &mut usize,
175 max_parallel: usize,
176) {
177 let mut remaining = *parallel_batch_len;
178 let effective_limit = if max_parallel == 0 { usize::MAX } else { max_parallel };
179
180 while remaining > 0 {
181 let batch_len = remaining.min(effective_limit);
182 match batch_len {
183 1 => layout.push((PreparedToolBatchKind::Sequential, 1)),
184 len => layout.push((PreparedToolBatchKind::ParallelReadonly, len)),
185 }
186 remaining -= batch_len;
187 }
188
189 *parallel_batch_len = 0;
190}
191
192pub fn is_parallel_safe_tool_batch(calls: &[PreparedToolCall]) -> bool {
194 calls.iter().all(PreparedToolCall::can_parallelize)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{PreparedToolBatch, PreparedToolBatchKind};
200
201 #[test]
202 fn bounded_layout_splits_parallel_groups_without_reordering() {
203 let layout = PreparedToolBatch::plan_layout_with_limit([true, true, true, false, true], true, 2);
204
205 assert_eq!(
206 layout,
207 vec![
208 (PreparedToolBatchKind::ParallelReadonly, 2),
209 (PreparedToolBatchKind::Sequential, 1),
210 (PreparedToolBatchKind::Sequential, 1),
211 (PreparedToolBatchKind::Sequential, 1),
212 ]
213 );
214 }
215
216 #[test]
217 fn single_parallel_call_is_serialized_when_limit_is_one() {
218 let layout = PreparedToolBatch::plan_layout_with_limit([true], true, 1);
219
220 assert_eq!(layout, vec![(PreparedToolBatchKind::Sequential, 1)]);
221 }
222}