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 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::*;
#[test]
fn parse_known_modes() {
assert_eq!(
ParallelToolCallsMode::parse("prefer"),
Some(ParallelToolCallsMode::Prefer)
);
assert_eq!(
ParallelToolCallsMode::parse("avoid"),
Some(ParallelToolCallsMode::Avoid)
);
assert_eq!(
ParallelToolCallsMode::parse("none"),
Some(ParallelToolCallsMode::None)
);
assert_eq!(ParallelToolCallsMode::parse("loud"), None);
}
#[test]
fn mode_to_preference() {
assert_eq!(ParallelToolCallsMode::Prefer.to_preference(), Some(true));
assert_eq!(ParallelToolCallsMode::Avoid.to_preference(), Some(false));
assert_eq!(ParallelToolCallsMode::None.to_preference(), None);
}
#[test]
fn from_config_defaults_to_prefer() {
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({})),
Some(true)
);
assert_eq!(
parallel_tool_calls_from_config(&serde_json::Value::Null),
Some(true)
);
}
#[test]
fn from_config_honors_mode() {
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({"mode": "prefer"})),
Some(true)
);
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({"mode": "avoid"})),
Some(false)
);
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({"mode": "none"})),
None
);
}
#[test]
fn from_config_malformed_neutralizes() {
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({"mode": "loud"})),
None
);
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!({"mode": 5})),
None
);
assert_eq!(
parallel_tool_calls_from_config(&serde_json::json!([])),
None
);
}
#[test]
fn validate_config_accepts_known_modes_only() {
let cap = ParallelToolCallsCapability;
assert!(cap.validate_config(&serde_json::Value::Null).is_ok());
assert!(cap.validate_config(&serde_json::json!({})).is_ok());
assert!(
cap.validate_config(&serde_json::json!({"mode": "prefer"}))
.is_ok()
);
assert!(
cap.validate_config(&serde_json::json!({"mode": "avoid"}))
.is_ok()
);
assert!(
cap.validate_config(&serde_json::json!({"mode": "loud"}))
.is_err()
);
assert!(cap.validate_config(&serde_json::json!([])).is_err());
}
#[test]
fn localizations_resolve_uk() {
let cap = ParallelToolCallsCapability;
assert_eq!(
cap.localized_name(Some("uk-UA")),
"Паралельні виклики інструментів"
);
}
}