everruns_core/capabilities/
parallel_tool_calls.rs1use 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 config_schema(&self) -> Option<serde_json::Value> {
116 Some(serde_json::json!({
117 "type": "object",
118 "properties": {
119 "mode": {
120 "type": "string",
121 "title": "Parallel tool calls",
122 "description": "prefer: request parallel tool calls (default); avoid: one tool call per turn, serialized locally; none: provider default.",
123 "enum": ["prefer", "avoid", "none"],
124 "default": "prefer"
125 }
126 }
127 }))
128 }
129
130 fn validate_config(&self, config: &serde_json::Value) -> Result<(), String> {
131 if config.is_null() {
132 return Ok(());
133 }
134 if !config.is_object() {
135 return Err("parallel_tool_calls config must be an object".to_string());
136 }
137 match config.get("mode") {
138 None => Ok(()),
139 Some(serde_json::Value::String(mode))
140 if ParallelToolCallsMode::parse(mode).is_some() =>
141 {
142 Ok(())
143 }
144 Some(value) => Err(format!(
145 "mode must be one of \"prefer\", \"avoid\", \"none\", got {value}"
146 )),
147 }
148 }
149
150 fn localizations(&self) -> Vec<CapabilityLocalization> {
151 vec![
152 CapabilityLocalization {
153 locale: "en",
154 name: None,
155 description: None,
156 config_description: Some(
157 "Chooses whether the agent batches independent tool calls or runs them one at a time.",
158 ),
159 config_overlay: None,
160 },
161 CapabilityLocalization {
162 locale: "uk",
163 name: Some("Паралельні виклики інструментів"),
164 description: Some(
165 "Визначає, чи запитує агент кілька викликів інструментів за хід і чи виконує їх одночасно: prefer (запитувати паралельні), avoid (по одному, послідовно) або none (типова поведінка провайдера).",
166 ),
167 config_description: Some(
168 "Обирає, чи агент об'єднує незалежні виклики інструментів, чи виконує їх по одному.",
169 ),
170 config_overlay: Some(serde_json::json!({
171 "properties": {
172 "mode": {
173 "title": "Паралельні виклики інструментів",
174 "description": "prefer: запитувати паралельні виклики (типово); avoid: один виклик за хід, послідовно; none: типова поведінка провайдера."
175 }
176 }
177 })),
178 },
179 ]
180 }
181
182 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
183 None
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn parse_known_modes() {
193 assert_eq!(
194 ParallelToolCallsMode::parse("prefer"),
195 Some(ParallelToolCallsMode::Prefer)
196 );
197 assert_eq!(
198 ParallelToolCallsMode::parse("avoid"),
199 Some(ParallelToolCallsMode::Avoid)
200 );
201 assert_eq!(
202 ParallelToolCallsMode::parse("none"),
203 Some(ParallelToolCallsMode::None)
204 );
205 assert_eq!(ParallelToolCallsMode::parse("loud"), None);
206 }
207
208 #[test]
209 fn mode_to_preference() {
210 assert_eq!(ParallelToolCallsMode::Prefer.to_preference(), Some(true));
211 assert_eq!(ParallelToolCallsMode::Avoid.to_preference(), Some(false));
212 assert_eq!(ParallelToolCallsMode::None.to_preference(), None);
213 }
214
215 #[test]
216 fn from_config_defaults_to_prefer() {
217 assert_eq!(
219 parallel_tool_calls_from_config(&serde_json::json!({})),
220 Some(true)
221 );
222 assert_eq!(
223 parallel_tool_calls_from_config(&serde_json::Value::Null),
224 Some(true)
225 );
226 }
227
228 #[test]
229 fn from_config_honors_mode() {
230 assert_eq!(
231 parallel_tool_calls_from_config(&serde_json::json!({"mode": "prefer"})),
232 Some(true)
233 );
234 assert_eq!(
235 parallel_tool_calls_from_config(&serde_json::json!({"mode": "avoid"})),
236 Some(false)
237 );
238 assert_eq!(
239 parallel_tool_calls_from_config(&serde_json::json!({"mode": "none"})),
240 None
241 );
242 }
243
244 #[test]
245 fn from_config_malformed_neutralizes() {
246 assert_eq!(
249 parallel_tool_calls_from_config(&serde_json::json!({"mode": "loud"})),
250 None
251 );
252 assert_eq!(
253 parallel_tool_calls_from_config(&serde_json::json!({"mode": 5})),
254 None
255 );
256 assert_eq!(
257 parallel_tool_calls_from_config(&serde_json::json!([])),
258 None
259 );
260 }
261
262 #[test]
263 fn validate_config_accepts_known_modes_only() {
264 let cap = ParallelToolCallsCapability;
265 assert!(cap.validate_config(&serde_json::Value::Null).is_ok());
266 assert!(cap.validate_config(&serde_json::json!({})).is_ok());
267 assert!(
268 cap.validate_config(&serde_json::json!({"mode": "prefer"}))
269 .is_ok()
270 );
271 assert!(
272 cap.validate_config(&serde_json::json!({"mode": "avoid"}))
273 .is_ok()
274 );
275 assert!(
276 cap.validate_config(&serde_json::json!({"mode": "loud"}))
277 .is_err()
278 );
279 assert!(cap.validate_config(&serde_json::json!([])).is_err());
280 }
281
282 #[test]
283 fn localizations_resolve_uk() {
284 let cap = ParallelToolCallsCapability;
285 assert_eq!(
286 cap.localized_name(Some("uk-UA")),
287 "Паралельні виклики інструментів"
288 );
289 }
290}