1use super::{Capability, CapabilityLocalization, SystemPromptContext};
26use async_trait::async_trait;
27
28pub const PARALLEL_TOOL_CALLS_CAPABILITY_ID: &str = "parallel_tool_calls";
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ParallelToolCallsMode {
34 Prefer,
36 Avoid,
38 None,
40}
41
42impl ParallelToolCallsMode {
43 pub fn parse(value: &str) -> Option<Self> {
45 match value {
46 "prefer" => Some(Self::Prefer),
47 "avoid" => Some(Self::Avoid),
48 "none" => Some(Self::None),
49 _ => None,
50 }
51 }
52
53 pub fn to_preference(self) -> Option<bool> {
58 match self {
59 Self::Prefer => Some(true),
60 Self::Avoid => Some(false),
61 Self::None => None,
62 }
63 }
64}
65
66pub fn parallel_tool_calls_from_config(config: &serde_json::Value) -> Option<bool> {
76 if config.is_null() {
77 return ParallelToolCallsMode::Prefer.to_preference();
78 }
79 let object = config.as_object()?;
80 match object.get("mode") {
81 None => ParallelToolCallsMode::Prefer.to_preference(),
82 Some(serde_json::Value::String(mode)) => {
83 ParallelToolCallsMode::parse(mode).and_then(ParallelToolCallsMode::to_preference)
84 }
85 Some(_) => None,
86 }
87}
88
89pub struct ParallelToolCallsCapability;
94
95#[async_trait]
96impl Capability for ParallelToolCallsCapability {
97 fn id(&self) -> &str {
98 PARALLEL_TOOL_CALLS_CAPABILITY_ID
99 }
100
101 fn name(&self) -> &str {
102 "Parallel Tool Calls"
103 }
104
105 fn description(&self) -> &str {
106 "Controls whether the agent requests multiple tool calls per turn and \
107 runs them concurrently: prefer (request parallel), avoid (one at a \
108 time, serialized), or none (provider default)."
109 }
110
111 fn category(&self) -> Option<&str> {
112 Some("Optimization")
113 }
114
115 fn parallel_tool_calls_preference(&self, config: &serde_json::Value) -> Option<bool> {
116 parallel_tool_calls_from_config(config)
117 }
118
119 fn config_schema(&self) -> Option<serde_json::Value> {
120 Some(serde_json::json!({
121 "type": "object",
122 "properties": {
123 "mode": {
124 "type": "string",
125 "title": "Parallel tool calls",
126 "description": "prefer: request parallel tool calls (default); avoid: one tool call per turn, serialized locally; none: provider default.",
127 "enum": ["prefer", "avoid", "none"],
128 "default": "prefer"
129 }
130 }
131 }))
132 }
133
134 fn validate_config(&self, config: &serde_json::Value) -> Result<(), String> {
135 if config.is_null() {
136 return Ok(());
137 }
138 if !config.is_object() {
139 return Err("parallel_tool_calls config must be an object".to_string());
140 }
141 match config.get("mode") {
142 None => Ok(()),
143 Some(serde_json::Value::String(mode))
144 if ParallelToolCallsMode::parse(mode).is_some() =>
145 {
146 Ok(())
147 }
148 Some(value) => Err(format!(
149 "mode must be one of \"prefer\", \"avoid\", \"none\", got {value}"
150 )),
151 }
152 }
153
154 fn localizations(&self) -> Vec<CapabilityLocalization> {
155 vec![
156 CapabilityLocalization {
157 locale: "en",
158 name: None,
159 description: None,
160 config_description: Some(
161 "Chooses whether the agent batches independent tool calls or runs them one at a time.",
162 ),
163 config_overlay: None,
164 },
165 CapabilityLocalization {
166 locale: "uk",
167 name: Some("Паралельні виклики інструментів"),
168 description: Some(
169 "Визначає, чи запитує агент кілька викликів інструментів за хід і чи виконує їх одночасно: prefer (запитувати паралельні), avoid (по одному, послідовно) або none (типова поведінка провайдера).",
170 ),
171 config_description: Some(
172 "Обирає, чи агент об'єднує незалежні виклики інструментів, чи виконує їх по одному.",
173 ),
174 config_overlay: Some(serde_json::json!({
175 "properties": {
176 "mode": {
177 "title": "Паралельні виклики інструментів",
178 "description": "prefer: запитувати паралельні виклики (типово); avoid: один виклик за хід, послідовно; none: типова поведінка провайдера."
179 }
180 }
181 })),
182 },
183 ]
184 }
185
186 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
187 None
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use serde_json::json;
195
196 #[test]
197 fn configuration_validation_and_runtime_preference_agree() {
198 let cap = ParallelToolCallsCapability;
199 for (config, valid, preference) in [
200 (json!(null), true, Some(true)),
201 (json!({}), true, Some(true)),
202 (json!({"mode":"prefer"}), true, Some(true)),
203 (json!({"mode":"avoid"}), true, Some(false)),
204 (json!({"mode":"none"}), true, None),
205 (json!({"mode":"loud"}), false, None),
206 (json!({"mode":"PREFER"}), false, None),
207 (json!({"mode":""}), false, None),
208 (json!({"mode":null}), false, None),
209 (json!({"mode":true}), false, None),
210 (json!({"mode":5}), false, None),
211 (json!({"mode":{}}), false, None),
212 (json!({"mode":[]}), false, None),
213 (json!([]), false, None),
214 (json!(true), false, None),
215 (json!("prefer"), false, None),
216 ] {
217 assert_eq!(
218 cap.validate_config(&config).is_ok(),
219 valid,
220 "config={config}"
221 );
222 assert_eq!(
223 cap.parallel_tool_calls_preference(&config),
224 preference,
225 "config={config}"
226 );
227 }
228 }
229 #[test]
230 fn localizations_resolve_uk() {
231 let cap = ParallelToolCallsCapability;
232 assert_eq!(
233 cap.localized_name(Some("uk-UA")),
234 "Паралельні виклики інструментів"
235 );
236 }
237}