use super::{Capability, CapabilityLocalization, SystemPromptContext};
use async_trait::async_trait;
pub const PARALLEL_TOOL_CALLS_CAPABILITY_ID: &str = "parallel_tool_calls";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParallelToolCallsMode {
Prefer,
Avoid,
None,
}
impl ParallelToolCallsMode {
pub fn parse(value: &str) -> Option<Self> {
match value {
"prefer" => Some(Self::Prefer),
"avoid" => Some(Self::Avoid),
"none" => Some(Self::None),
_ => None,
}
}
pub fn to_preference(self) -> Option<bool> {
match self {
Self::Prefer => Some(true),
Self::Avoid => Some(false),
Self::None => None,
}
}
}
pub fn parallel_tool_calls_from_config(config: &serde_json::Value) -> Option<bool> {
if config.is_null() {
return ParallelToolCallsMode::Prefer.to_preference();
}
let object = config.as_object()?;
match object.get("mode") {
None => ParallelToolCallsMode::Prefer.to_preference(),
Some(serde_json::Value::String(mode)) => {
ParallelToolCallsMode::parse(mode).and_then(ParallelToolCallsMode::to_preference)
}
Some(_) => None,
}
}
pub struct ParallelToolCallsCapability;
#[async_trait]
impl Capability for ParallelToolCallsCapability {
fn id(&self) -> &str {
PARALLEL_TOOL_CALLS_CAPABILITY_ID
}
fn name(&self) -> &str {
"Parallel Tool Calls"
}
fn description(&self) -> &str {
"Controls whether the agent requests multiple tool calls per turn and \
runs them concurrently: prefer (request parallel), avoid (one at a \
time, serialized), or none (provider default)."
}
fn category(&self) -> Option<&str> {
Some("Optimization")
}
fn parallel_tool_calls_preference(&self, config: &serde_json::Value) -> Option<bool> {
parallel_tool_calls_from_config(config)
}
fn config_schema(&self) -> Option<serde_json::Value> {
Some(serde_json::json!({
"type": "object",
"properties": {
"mode": {
"type": "string",
"title": "Parallel tool calls",
"description": "prefer: request parallel tool calls (default); avoid: one tool call per turn, serialized locally; none: provider default.",
"enum": ["prefer", "avoid", "none"],
"default": "prefer"
}
}
}))
}
fn validate_config(&self, config: &serde_json::Value) -> Result<(), String> {
if config.is_null() {
return Ok(());
}
if !config.is_object() {
return Err("parallel_tool_calls config must be an object".to_string());
}
match config.get("mode") {
None => Ok(()),
Some(serde_json::Value::String(mode))
if ParallelToolCallsMode::parse(mode).is_some() =>
{
Ok(())
}
Some(value) => Err(format!(
"mode must be one of \"prefer\", \"avoid\", \"none\", got {value}"
)),
}
}
fn localizations(&self) -> Vec<CapabilityLocalization> {
vec![
CapabilityLocalization {
locale: "en",
name: None,
description: None,
config_description: Some(
"Chooses whether the agent batches independent tool calls or runs them one at a time.",
),
config_overlay: None,
},
CapabilityLocalization {
locale: "uk",
name: Some("Паралельні виклики інструментів"),
description: Some(
"Визначає, чи запитує агент кілька викликів інструментів за хід і чи виконує їх одночасно: prefer (запитувати паралельні), avoid (по одному, послідовно) або none (типова поведінка провайдера).",
),
config_description: Some(
"Обирає, чи агент об'єднує незалежні виклики інструментів, чи виконує їх по одному.",
),
config_overlay: Some(serde_json::json!({
"properties": {
"mode": {
"title": "Паралельні виклики інструментів",
"description": "prefer: запитувати паралельні виклики (типово); avoid: один виклик за хід, послідовно; none: типова поведінка провайдера."
}
}
})),
},
]
}
async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn configuration_validation_and_runtime_preference_agree() {
let cap = ParallelToolCallsCapability;
for (config, valid, preference) in [
(json!(null), true, Some(true)),
(json!({}), true, Some(true)),
(json!({"mode":"prefer"}), true, Some(true)),
(json!({"mode":"avoid"}), true, Some(false)),
(json!({"mode":"none"}), true, None),
(json!({"mode":"loud"}), false, None),
(json!({"mode":"PREFER"}), false, None),
(json!({"mode":""}), false, None),
(json!({"mode":null}), false, None),
(json!({"mode":true}), false, None),
(json!({"mode":5}), false, None),
(json!({"mode":{}}), false, None),
(json!({"mode":[]}), false, None),
(json!([]), false, None),
(json!(true), false, None),
(json!("prefer"), false, None),
] {
assert_eq!(
cap.validate_config(&config).is_ok(),
valid,
"config={config}"
);
assert_eq!(
cap.parallel_tool_calls_preference(&config),
preference,
"config={config}"
);
}
}
#[test]
fn localizations_resolve_uk() {
let cap = ParallelToolCallsCapability;
assert_eq!(
cap.localized_name(Some("uk-UA")),
"Паралельні виклики інструментів"
);
}
}