1use super::traits::{Tool, ToolResult};
2use crate::config::{DelegateAgentConfig, SwarmConfig, SwarmStrategy};
3use crate::providers::{self, Provider};
4use crate::security::SecurityPolicy;
5use crate::security::policy::ToolOperation;
6use async_trait::async_trait;
7use serde_json::json;
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12const SWARM_AGENT_TIMEOUT_SECS: u64 = 120;
14
15pub struct SwarmTool {
18 swarms: Arc<HashMap<String, SwarmConfig>>,
19 agents: Arc<HashMap<String, DelegateAgentConfig>>,
20 security: Arc<SecurityPolicy>,
21 fallback_credential: Option<String>,
22 provider_runtime_options: providers::ProviderRuntimeOptions,
23}
24
25impl SwarmTool {
26 pub fn new(
27 swarms: HashMap<String, SwarmConfig>,
28 agents: HashMap<String, DelegateAgentConfig>,
29 fallback_credential: Option<String>,
30 security: Arc<SecurityPolicy>,
31 provider_runtime_options: providers::ProviderRuntimeOptions,
32 ) -> Self {
33 Self {
34 swarms: Arc::new(swarms),
35 agents: Arc::new(agents),
36 security,
37 fallback_credential,
38 provider_runtime_options,
39 }
40 }
41
42 fn create_provider_for_agent(
43 &self,
44 agent_config: &DelegateAgentConfig,
45 agent_name: &str,
46 ) -> Result<Box<dyn Provider>, ToolResult> {
47 let credential = agent_config
48 .api_key
49 .clone()
50 .or_else(|| self.fallback_credential.clone());
51
52 providers::create_provider_with_options(
53 &agent_config.provider,
54 credential.as_deref(),
55 &self.provider_runtime_options,
56 )
57 .map_err(|e| ToolResult {
58 success: false,
59 output: String::new(),
60 error: Some(format!(
61 "Failed to create provider '{}' for agent '{agent_name}': {e}",
62 agent_config.provider
63 )),
64 })
65 }
66
67 async fn call_agent(
68 &self,
69 agent_name: &str,
70 agent_config: &DelegateAgentConfig,
71 prompt: &str,
72 timeout_secs: u64,
73 ) -> Result<String, String> {
74 let provider = self
75 .create_provider_for_agent(agent_config, agent_name)
76 .map_err(|r| r.error.unwrap_or_default())?;
77
78 let temperature = agent_config.temperature.unwrap_or(0.7);
79
80 let result = tokio::time::timeout(
81 Duration::from_secs(timeout_secs),
82 provider.chat_with_system(
83 agent_config.system_prompt.as_deref(),
84 prompt,
85 &agent_config.model,
86 temperature,
87 ),
88 )
89 .await;
90
91 match result {
92 Ok(Ok(response)) => {
93 if response.trim().is_empty() {
94 Ok("[Empty response]".to_string())
95 } else {
96 Ok(response)
97 }
98 }
99 Ok(Err(e)) => Err(format!("Agent '{agent_name}' failed: {e}")),
100 Err(_) => Err(format!(
101 "Agent '{agent_name}' timed out after {timeout_secs}s"
102 )),
103 }
104 }
105
106 async fn execute_sequential(
107 &self,
108 swarm_config: &SwarmConfig,
109 prompt: &str,
110 context: &str,
111 ) -> anyhow::Result<ToolResult> {
112 let mut current_input = if context.is_empty() {
113 prompt.to_string()
114 } else {
115 format!("[Context]\n{context}\n\n[Task]\n{prompt}")
116 };
117
118 let per_agent_timeout = swarm_config.timeout_secs / swarm_config.agents.len().max(1) as u64;
119 let mut results = Vec::new();
120
121 for (i, agent_name) in swarm_config.agents.iter().enumerate() {
122 let agent_config = match self.agents.get(agent_name) {
123 Some(cfg) => cfg,
124 None => {
125 return Ok(ToolResult {
126 success: false,
127 output: String::new(),
128 error: Some(format!("Swarm references unknown agent '{agent_name}'")),
129 });
130 }
131 };
132
133 let agent_prompt = if i == 0 {
134 current_input.clone()
135 } else {
136 format!("[Previous agent output]\n{current_input}\n\n[Original task]\n{prompt}")
137 };
138
139 match self
140 .call_agent(agent_name, agent_config, &agent_prompt, per_agent_timeout)
141 .await
142 {
143 Ok(output) => {
144 results.push(format!(
145 "[{agent_name} ({}/{})] {output}",
146 agent_config.provider, agent_config.model
147 ));
148 current_input = output;
149 }
150 Err(e) => {
151 return Ok(ToolResult {
152 success: false,
153 output: results.join("\n\n"),
154 error: Some(e),
155 });
156 }
157 }
158 }
159
160 Ok(ToolResult {
161 success: true,
162 output: format!(
163 "[Swarm sequential — {} agents]\n\n{}",
164 swarm_config.agents.len(),
165 results.join("\n\n")
166 ),
167 error: None,
168 })
169 }
170
171 async fn execute_parallel(
172 &self,
173 swarm_config: &SwarmConfig,
174 prompt: &str,
175 context: &str,
176 ) -> anyhow::Result<ToolResult> {
177 let full_prompt = if context.is_empty() {
178 prompt.to_string()
179 } else {
180 format!("[Context]\n{context}\n\n[Task]\n{prompt}")
181 };
182
183 let mut join_set = tokio::task::JoinSet::new();
184
185 for agent_name in &swarm_config.agents {
186 let agent_config = match self.agents.get(agent_name) {
187 Some(cfg) => cfg.clone(),
188 None => {
189 return Ok(ToolResult {
190 success: false,
191 output: String::new(),
192 error: Some(format!("Swarm references unknown agent '{agent_name}'")),
193 });
194 }
195 };
196
197 let credential = agent_config
198 .api_key
199 .clone()
200 .or_else(|| self.fallback_credential.clone());
201
202 let provider = match providers::create_provider_with_options(
203 &agent_config.provider,
204 credential.as_deref(),
205 &self.provider_runtime_options,
206 ) {
207 Ok(p) => p,
208 Err(e) => {
209 return Ok(ToolResult {
210 success: false,
211 output: String::new(),
212 error: Some(format!(
213 "Failed to create provider for agent '{agent_name}': {e}"
214 )),
215 });
216 }
217 };
218
219 let name = agent_name.clone();
220 let prompt_clone = full_prompt.clone();
221 let timeout = swarm_config.timeout_secs;
222 let model = agent_config.model.clone();
223 let temperature = agent_config.temperature.unwrap_or(0.7);
224 let system_prompt = agent_config.system_prompt.clone();
225 let provider_name = agent_config.provider.clone();
226
227 join_set.spawn(async move {
228 let result = tokio::time::timeout(
229 Duration::from_secs(timeout),
230 provider.chat_with_system(
231 system_prompt.as_deref(),
232 &prompt_clone,
233 &model,
234 temperature,
235 ),
236 )
237 .await;
238
239 let output = match result {
240 Ok(Ok(text)) => {
241 if text.trim().is_empty() {
242 "[Empty response]".to_string()
243 } else {
244 text
245 }
246 }
247 Ok(Err(e)) => format!("[Error] {e}"),
248 Err(_) => format!("[Timed out after {timeout}s]"),
249 };
250
251 (name, provider_name, model, output)
252 });
253 }
254
255 let mut results = Vec::new();
256 while let Some(join_result) = join_set.join_next().await {
257 match join_result {
258 Ok((name, provider_name, model, output)) => {
259 results.push(format!("[{name} ({provider_name}/{model})]\n{output}"));
260 }
261 Err(e) => {
262 results.push(format!("[join error] {e}"));
263 }
264 }
265 }
266
267 Ok(ToolResult {
268 success: true,
269 output: format!(
270 "[Swarm parallel — {} agents]\n\n{}",
271 swarm_config.agents.len(),
272 results.join("\n\n---\n\n")
273 ),
274 error: None,
275 })
276 }
277
278 async fn execute_router(
279 &self,
280 swarm_config: &SwarmConfig,
281 prompt: &str,
282 context: &str,
283 ) -> anyhow::Result<ToolResult> {
284 if swarm_config.agents.is_empty() {
285 return Ok(ToolResult {
286 success: false,
287 output: String::new(),
288 error: Some("Router swarm has no agents to choose from".into()),
289 });
290 }
291
292 let agent_descriptions: Vec<String> = swarm_config
294 .agents
295 .iter()
296 .filter_map(|name| {
297 self.agents.get(name).map(|cfg| {
298 let desc = cfg
299 .system_prompt
300 .as_deref()
301 .unwrap_or("General purpose agent");
302 format!(
303 "- {name}: {desc} (provider: {}, model: {})",
304 cfg.provider, cfg.model
305 )
306 })
307 })
308 .collect();
309
310 let first_agent_name = &swarm_config.agents[0];
312 let first_agent_config = match self.agents.get(first_agent_name) {
313 Some(cfg) => cfg,
314 None => {
315 return Ok(ToolResult {
316 success: false,
317 output: String::new(),
318 error: Some(format!(
319 "Swarm references unknown agent '{first_agent_name}'"
320 )),
321 });
322 }
323 };
324
325 let router_provider = self
326 .create_provider_for_agent(first_agent_config, first_agent_name)
327 .map_err(|r| anyhow::anyhow!(r.error.unwrap_or_default()))?;
328
329 let base_router_prompt = swarm_config
330 .router_prompt
331 .as_deref()
332 .unwrap_or("Pick the single best agent for this task.");
333
334 let routing_prompt = format!(
335 "{base_router_prompt}\n\nAvailable agents:\n{}\n\nUser task: {prompt}\n\n\
336 Respond with ONLY the agent name, nothing else.",
337 agent_descriptions.join("\n")
338 );
339
340 let chosen = tokio::time::timeout(
341 Duration::from_secs(SWARM_AGENT_TIMEOUT_SECS),
342 router_provider.chat_with_system(
343 Some("You are a routing assistant. Respond with only the agent name."),
344 &routing_prompt,
345 &first_agent_config.model,
346 0.0,
347 ),
348 )
349 .await;
350
351 let chosen_name = match chosen {
352 Ok(Ok(name)) => name.trim().to_string(),
353 Ok(Err(e)) => {
354 return Ok(ToolResult {
355 success: false,
356 output: String::new(),
357 error: Some(format!("Router LLM call failed: {e}")),
358 });
359 }
360 Err(_) => {
361 return Ok(ToolResult {
362 success: false,
363 output: String::new(),
364 error: Some("Router LLM call timed out".into()),
365 });
366 }
367 };
368
369 let matched_name = swarm_config
371 .agents
372 .iter()
373 .find(|name| name.eq_ignore_ascii_case(&chosen_name))
374 .cloned()
375 .unwrap_or_else(|| swarm_config.agents[0].clone());
376
377 let agent_config = match self.agents.get(&matched_name) {
378 Some(cfg) => cfg,
379 None => {
380 return Ok(ToolResult {
381 success: false,
382 output: String::new(),
383 error: Some(format!("Router selected unknown agent '{matched_name}'")),
384 });
385 }
386 };
387
388 let full_prompt = if context.is_empty() {
389 prompt.to_string()
390 } else {
391 format!("[Context]\n{context}\n\n[Task]\n{prompt}")
392 };
393
394 match self
395 .call_agent(
396 &matched_name,
397 agent_config,
398 &full_prompt,
399 swarm_config.timeout_secs,
400 )
401 .await
402 {
403 Ok(output) => Ok(ToolResult {
404 success: true,
405 output: format!(
406 "[Swarm router — selected '{matched_name}' ({}/{})]\n{output}",
407 agent_config.provider, agent_config.model
408 ),
409 error: None,
410 }),
411 Err(e) => Ok(ToolResult {
412 success: false,
413 output: String::new(),
414 error: Some(e),
415 }),
416 }
417 }
418}
419
420#[async_trait]
421impl Tool for SwarmTool {
422 fn name(&self) -> &str {
423 "swarm"
424 }
425
426 fn description(&self) -> &str {
427 "Orchestrate a swarm of agents to collaboratively handle a task. Supports sequential \
428 (pipeline), parallel (fan-out/fan-in), and router (LLM-selected) strategies."
429 }
430
431 fn parameters_schema(&self) -> serde_json::Value {
432 let swarm_names: Vec<&str> = self.swarms.keys().map(String::as_str).collect();
433 json!({
434 "type": "object",
435 "additionalProperties": false,
436 "properties": {
437 "swarm": {
438 "type": "string",
439 "minLength": 1,
440 "description": format!(
441 "Name of the swarm to invoke. Available: {}",
442 if swarm_names.is_empty() {
443 "(none configured)".to_string()
444 } else {
445 swarm_names.join(", ")
446 }
447 )
448 },
449 "prompt": {
450 "type": "string",
451 "minLength": 1,
452 "description": "The task/prompt to send to the swarm"
453 },
454 "context": {
455 "type": "string",
456 "description": "Optional context to include (e.g. relevant code, prior findings)"
457 }
458 },
459 "required": ["swarm", "prompt"]
460 })
461 }
462
463 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
464 let swarm_name = args
465 .get("swarm")
466 .and_then(|v| v.as_str())
467 .map(str::trim)
468 .ok_or_else(|| anyhow::anyhow!("Missing 'swarm' parameter"))?;
469
470 if swarm_name.is_empty() {
471 return Ok(ToolResult {
472 success: false,
473 output: String::new(),
474 error: Some("'swarm' parameter must not be empty".into()),
475 });
476 }
477
478 let prompt = args
479 .get("prompt")
480 .and_then(|v| v.as_str())
481 .map(str::trim)
482 .ok_or_else(|| anyhow::anyhow!("Missing 'prompt' parameter"))?;
483
484 if prompt.is_empty() {
485 return Ok(ToolResult {
486 success: false,
487 output: String::new(),
488 error: Some("'prompt' parameter must not be empty".into()),
489 });
490 }
491
492 let context = args
493 .get("context")
494 .and_then(|v| v.as_str())
495 .map(str::trim)
496 .unwrap_or("");
497
498 let swarm_config = match self.swarms.get(swarm_name) {
499 Some(cfg) => cfg,
500 None => {
501 let available: Vec<&str> = self.swarms.keys().map(String::as_str).collect();
502 return Ok(ToolResult {
503 success: false,
504 output: String::new(),
505 error: Some(format!(
506 "Unknown swarm '{swarm_name}'. Available swarms: {}",
507 if available.is_empty() {
508 "(none configured)".to_string()
509 } else {
510 available.join(", ")
511 }
512 )),
513 });
514 }
515 };
516
517 if swarm_config.agents.is_empty() {
518 return Ok(ToolResult {
519 success: false,
520 output: String::new(),
521 error: Some(format!("Swarm '{swarm_name}' has no agents configured")),
522 });
523 }
524
525 if let Err(error) = self
526 .security
527 .enforce_tool_operation(ToolOperation::Act, "swarm")
528 {
529 return Ok(ToolResult {
530 success: false,
531 output: String::new(),
532 error: Some(error),
533 });
534 }
535
536 match swarm_config.strategy {
537 SwarmStrategy::Sequential => {
538 self.execute_sequential(swarm_config, prompt, context).await
539 }
540 SwarmStrategy::Parallel => self.execute_parallel(swarm_config, prompt, context).await,
541 SwarmStrategy::Router => self.execute_router(swarm_config, prompt, context).await,
542 }
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use crate::security::{AutonomyLevel, SecurityPolicy};
550
551 fn test_security() -> Arc<SecurityPolicy> {
552 Arc::new(SecurityPolicy::default())
553 }
554
555 fn sample_agents() -> HashMap<String, DelegateAgentConfig> {
556 let mut agents = HashMap::new();
557 agents.insert(
558 "researcher".to_string(),
559 DelegateAgentConfig {
560 provider: "ollama".to_string(),
561 model: "llama3".to_string(),
562 system_prompt: Some("You are a research assistant.".to_string()),
563 api_key: None,
564 temperature: Some(0.3),
565 max_depth: 3,
566 agentic: false,
567 allowed_tools: Vec::new(),
568 max_iterations: 10,
569 timeout_secs: None,
570 agentic_timeout_secs: None,
571 skills_directory: None,
572 },
573 );
574 agents.insert(
575 "writer".to_string(),
576 DelegateAgentConfig {
577 provider: "openrouter".to_string(),
578 model: "anthropic/claude-sonnet-4-20250514".to_string(),
579 system_prompt: Some("You are a technical writer.".to_string()),
580 api_key: Some("test-key".to_string()),
581 temperature: Some(0.5),
582 max_depth: 3,
583 agentic: false,
584 allowed_tools: Vec::new(),
585 max_iterations: 10,
586 timeout_secs: None,
587 agentic_timeout_secs: None,
588 skills_directory: None,
589 },
590 );
591 agents
592 }
593
594 fn sample_swarms() -> HashMap<String, SwarmConfig> {
595 let mut swarms = HashMap::new();
596 swarms.insert(
597 "pipeline".to_string(),
598 SwarmConfig {
599 agents: vec!["researcher".to_string(), "writer".to_string()],
600 strategy: SwarmStrategy::Sequential,
601 router_prompt: None,
602 description: Some("Research then write".to_string()),
603 timeout_secs: 300,
604 },
605 );
606 swarms.insert(
607 "fanout".to_string(),
608 SwarmConfig {
609 agents: vec!["researcher".to_string(), "writer".to_string()],
610 strategy: SwarmStrategy::Parallel,
611 router_prompt: None,
612 description: None,
613 timeout_secs: 300,
614 },
615 );
616 swarms.insert(
617 "router".to_string(),
618 SwarmConfig {
619 agents: vec!["researcher".to_string(), "writer".to_string()],
620 strategy: SwarmStrategy::Router,
621 router_prompt: Some("Pick the best agent.".to_string()),
622 description: None,
623 timeout_secs: 300,
624 },
625 );
626 swarms
627 }
628
629 #[test]
630 fn name_and_schema() {
631 let tool = SwarmTool::new(
632 sample_swarms(),
633 sample_agents(),
634 None,
635 test_security(),
636 providers::ProviderRuntimeOptions::default(),
637 );
638 assert_eq!(tool.name(), "swarm");
639 let schema = tool.parameters_schema();
640 assert!(schema["properties"]["swarm"].is_object());
641 assert!(schema["properties"]["prompt"].is_object());
642 assert!(schema["properties"]["context"].is_object());
643 let required = schema["required"].as_array().unwrap();
644 assert!(required.contains(&json!("swarm")));
645 assert!(required.contains(&json!("prompt")));
646 assert_eq!(schema["additionalProperties"], json!(false));
647 }
648
649 #[test]
650 fn description_not_empty() {
651 let tool = SwarmTool::new(
652 sample_swarms(),
653 sample_agents(),
654 None,
655 test_security(),
656 providers::ProviderRuntimeOptions::default(),
657 );
658 assert!(!tool.description().is_empty());
659 }
660
661 #[test]
662 fn schema_lists_swarm_names() {
663 let tool = SwarmTool::new(
664 sample_swarms(),
665 sample_agents(),
666 None,
667 test_security(),
668 providers::ProviderRuntimeOptions::default(),
669 );
670 let schema = tool.parameters_schema();
671 let desc = schema["properties"]["swarm"]["description"]
672 .as_str()
673 .unwrap();
674 assert!(desc.contains("pipeline") || desc.contains("fanout") || desc.contains("router"));
675 }
676
677 #[test]
678 fn empty_swarms_schema() {
679 let tool = SwarmTool::new(
680 HashMap::new(),
681 sample_agents(),
682 None,
683 test_security(),
684 providers::ProviderRuntimeOptions::default(),
685 );
686 let schema = tool.parameters_schema();
687 let desc = schema["properties"]["swarm"]["description"]
688 .as_str()
689 .unwrap();
690 assert!(desc.contains("none configured"));
691 }
692
693 #[tokio::test]
694 async fn unknown_swarm_returns_error() {
695 let tool = SwarmTool::new(
696 sample_swarms(),
697 sample_agents(),
698 None,
699 test_security(),
700 providers::ProviderRuntimeOptions::default(),
701 );
702 let result = tool
703 .execute(json!({"swarm": "nonexistent", "prompt": "test"}))
704 .await
705 .unwrap();
706 assert!(!result.success);
707 assert!(result.error.unwrap().contains("Unknown swarm"));
708 }
709
710 #[tokio::test]
711 async fn missing_swarm_param() {
712 let tool = SwarmTool::new(
713 sample_swarms(),
714 sample_agents(),
715 None,
716 test_security(),
717 providers::ProviderRuntimeOptions::default(),
718 );
719 let result = tool.execute(json!({"prompt": "test"})).await;
720 assert!(result.is_err());
721 }
722
723 #[tokio::test]
724 async fn missing_prompt_param() {
725 let tool = SwarmTool::new(
726 sample_swarms(),
727 sample_agents(),
728 None,
729 test_security(),
730 providers::ProviderRuntimeOptions::default(),
731 );
732 let result = tool.execute(json!({"swarm": "pipeline"})).await;
733 assert!(result.is_err());
734 }
735
736 #[tokio::test]
737 async fn blank_swarm_rejected() {
738 let tool = SwarmTool::new(
739 sample_swarms(),
740 sample_agents(),
741 None,
742 test_security(),
743 providers::ProviderRuntimeOptions::default(),
744 );
745 let result = tool
746 .execute(json!({"swarm": " ", "prompt": "test"}))
747 .await
748 .unwrap();
749 assert!(!result.success);
750 assert!(result.error.unwrap().contains("must not be empty"));
751 }
752
753 #[tokio::test]
754 async fn blank_prompt_rejected() {
755 let tool = SwarmTool::new(
756 sample_swarms(),
757 sample_agents(),
758 None,
759 test_security(),
760 providers::ProviderRuntimeOptions::default(),
761 );
762 let result = tool
763 .execute(json!({"swarm": "pipeline", "prompt": " "}))
764 .await
765 .unwrap();
766 assert!(!result.success);
767 assert!(result.error.unwrap().contains("must not be empty"));
768 }
769
770 #[tokio::test]
771 async fn swarm_with_missing_agent_returns_error() {
772 let mut swarms = HashMap::new();
773 swarms.insert(
774 "broken".to_string(),
775 SwarmConfig {
776 agents: vec!["nonexistent_agent".to_string()],
777 strategy: SwarmStrategy::Sequential,
778 router_prompt: None,
779 description: None,
780 timeout_secs: 60,
781 },
782 );
783 let tool = SwarmTool::new(
784 swarms,
785 sample_agents(),
786 None,
787 test_security(),
788 providers::ProviderRuntimeOptions::default(),
789 );
790 let result = tool
791 .execute(json!({"swarm": "broken", "prompt": "test"}))
792 .await
793 .unwrap();
794 assert!(!result.success);
795 assert!(result.error.unwrap().contains("unknown agent"));
796 }
797
798 #[tokio::test]
799 async fn swarm_with_empty_agents_returns_error() {
800 let mut swarms = HashMap::new();
801 swarms.insert(
802 "empty".to_string(),
803 SwarmConfig {
804 agents: Vec::new(),
805 strategy: SwarmStrategy::Parallel,
806 router_prompt: None,
807 description: None,
808 timeout_secs: 60,
809 },
810 );
811 let tool = SwarmTool::new(
812 swarms,
813 sample_agents(),
814 None,
815 test_security(),
816 providers::ProviderRuntimeOptions::default(),
817 );
818 let result = tool
819 .execute(json!({"swarm": "empty", "prompt": "test"}))
820 .await
821 .unwrap();
822 assert!(!result.success);
823 assert!(result.error.unwrap().contains("no agents configured"));
824 }
825
826 #[tokio::test]
827 async fn swarm_blocked_in_readonly_mode() {
828 let readonly = Arc::new(SecurityPolicy {
829 autonomy: AutonomyLevel::ReadOnly,
830 ..SecurityPolicy::default()
831 });
832 let tool = SwarmTool::new(
833 sample_swarms(),
834 sample_agents(),
835 None,
836 readonly,
837 providers::ProviderRuntimeOptions::default(),
838 );
839 let result = tool
840 .execute(json!({"swarm": "pipeline", "prompt": "test"}))
841 .await
842 .unwrap();
843 assert!(!result.success);
844 assert!(
845 result
846 .error
847 .as_deref()
848 .unwrap_or("")
849 .contains("read-only mode")
850 );
851 }
852
853 #[tokio::test]
854 async fn swarm_blocked_when_rate_limited() {
855 let limited = Arc::new(SecurityPolicy {
856 max_actions_per_hour: 0,
857 ..SecurityPolicy::default()
858 });
859 let tool = SwarmTool::new(
860 sample_swarms(),
861 sample_agents(),
862 None,
863 limited,
864 providers::ProviderRuntimeOptions::default(),
865 );
866 let result = tool
867 .execute(json!({"swarm": "pipeline", "prompt": "test"}))
868 .await
869 .unwrap();
870 assert!(!result.success);
871 assert!(
872 result
873 .error
874 .as_deref()
875 .unwrap_or("")
876 .contains("Rate limit exceeded")
877 );
878 }
879
880 #[tokio::test]
881 async fn sequential_invalid_provider_returns_error() {
882 let mut swarms = HashMap::new();
883 swarms.insert(
884 "seq".to_string(),
885 SwarmConfig {
886 agents: vec!["researcher".to_string()],
887 strategy: SwarmStrategy::Sequential,
888 router_prompt: None,
889 description: None,
890 timeout_secs: 60,
891 },
892 );
893 let tool = SwarmTool::new(
895 swarms,
896 sample_agents(),
897 None,
898 test_security(),
899 providers::ProviderRuntimeOptions::default(),
900 );
901 let result = tool
902 .execute(json!({"swarm": "seq", "prompt": "test"}))
903 .await
904 .unwrap();
905 assert!(!result.success);
907 }
908
909 #[tokio::test]
910 async fn parallel_invalid_provider_returns_error() {
911 let mut swarms = HashMap::new();
912 swarms.insert(
913 "par".to_string(),
914 SwarmConfig {
915 agents: vec!["researcher".to_string()],
916 strategy: SwarmStrategy::Parallel,
917 router_prompt: None,
918 description: None,
919 timeout_secs: 60,
920 },
921 );
922 let tool = SwarmTool::new(
923 swarms,
924 sample_agents(),
925 None,
926 test_security(),
927 providers::ProviderRuntimeOptions::default(),
928 );
929 let result = tool
930 .execute(json!({"swarm": "par", "prompt": "test"}))
931 .await
932 .unwrap();
933 assert!(result.success || result.error.is_some());
935 }
936
937 #[tokio::test]
938 async fn router_invalid_provider_returns_error() {
939 let mut swarms = HashMap::new();
940 swarms.insert(
941 "rout".to_string(),
942 SwarmConfig {
943 agents: vec!["researcher".to_string()],
944 strategy: SwarmStrategy::Router,
945 router_prompt: Some("Pick.".to_string()),
946 description: None,
947 timeout_secs: 60,
948 },
949 );
950 let tool = SwarmTool::new(
951 swarms,
952 sample_agents(),
953 None,
954 test_security(),
955 providers::ProviderRuntimeOptions::default(),
956 );
957 let result = tool
958 .execute(json!({"swarm": "rout", "prompt": "test"}))
959 .await
960 .unwrap();
961 assert!(!result.success);
962 }
963}