ai_agents_runtime/orchestration/
concurrent.rs1use std::collections::HashMap;
2use std::time::Instant;
3
4use ai_agents_core::{AgentError, Result};
5use ai_agents_llm::LLMProvider;
6use ai_agents_observability::{current_observation_context, with_observation_context};
7use ai_agents_state::{AggregationConfig, ConcurrentAgentRef, PartialFailureAction};
8use tokio::task::JoinSet;
9use tracing::{info, warn};
10
11use super::aggregation;
12use super::types::{AgentResult, ConcurrentResult};
13use crate::Agent;
14use crate::runtime::{current_runtime_gate_identity_stack, scope_runtime_gate_identity_stack};
15use crate::spawner::AgentRegistry;
16use crate::turn_context::current_turn_actor_context;
17
18#[allow(clippy::too_many_arguments)]
22pub async fn concurrent(
23 registry: &AgentRegistry,
24 input: &str,
25 agents: &[ConcurrentAgentRef],
26 aggregation_config: &AggregationConfig,
27 llm: Option<&dyn LLMProvider>,
28 min_required: Option<usize>,
29 timeout_ms: Option<u64>,
30 on_partial_failure: PartialFailureAction,
31 vote_parallelism: Option<usize>,
32) -> Result<ConcurrentResult> {
33 if agents.is_empty() {
34 return Err(AgentError::Config(
35 "No agents for concurrent execution".into(),
36 ));
37 }
38
39 let start = Instant::now();
40 let mut join_set = JoinSet::new();
41 let gate_identity_stack = current_runtime_gate_identity_stack();
45
46 for (agent_index, agent_ref) in agents.iter().enumerate() {
47 let agent_id = agent_ref.id().to_string();
48 let agent = registry.get(&agent_id).ok_or_else(|| {
49 AgentError::Other(format!("Agent not found in registry: {}", agent_id))
50 })?;
51 let input_owned = input.to_string();
52 let timeout = timeout_ms;
53 let actor_context = current_turn_actor_context();
54 let observation_context = current_observation_context();
55 let gate_identity_stack = gate_identity_stack.clone();
56
57 join_set.spawn(async move {
58 scope_runtime_gate_identity_stack(&gate_identity_stack, async move {
59 let agent_start = Instant::now();
60 let run = async {
61 if let Some(context) = actor_context {
62 agent.chat_with_actor_context(&input_owned, context).await
63 } else {
64 agent.chat(&input_owned).await
65 }
66 };
67 let result = if let Some(t) = timeout {
68 match tokio::time::timeout(tokio::time::Duration::from_millis(t), async {
69 if let Some(context) = observation_context.clone() {
70 with_observation_context(context, run).await
71 } else {
72 run.await
73 }
74 })
75 .await
76 {
77 Ok(r) => r,
78 Err(_) => Err(AgentError::Other(format!(
79 "Agent {} timed out after {}ms",
80 agent_id, t
81 ))),
82 }
83 } else if let Some(context) = observation_context {
84 with_observation_context(context, run).await
85 } else {
86 run.await
87 };
88
89 let duration_ms = agent_start.elapsed().as_millis() as u64;
90 match result {
91 Ok(response) => AgentResult {
92 agent_index,
93 agent_id,
94 response: Some(response),
95 duration_ms,
96 success: true,
97 error: None,
98 },
99 Err(e) => AgentResult {
100 agent_index,
101 agent_id,
102 response: None,
103 duration_ms,
104 success: false,
105 error: Some(e.to_string()),
106 },
107 }
108 })
109 .await
110 });
111 }
112
113 let mut results = Vec::with_capacity(agents.len());
114 while let Some(join_result) = join_set.join_next().await {
115 match join_result {
116 Ok(agent_result) => results.push(agent_result),
117 Err(e) => {
118 warn!(error = %e, "Concurrent task panicked");
119 }
120 }
121 }
122
123 results.sort_by_key(|result| result.agent_index);
124
125 let success_count = results.iter().filter(|r| r.success).count();
126 let failed_count = results.len() - success_count;
127
128 if failed_count > 0 && matches!(on_partial_failure, PartialFailureAction::Abort) {
130 let failed_agents: Vec<_> = results
131 .iter()
132 .filter(|r| !r.success)
133 .map(|r| r.agent_id.as_str())
134 .collect();
135 return Err(AgentError::Other(format!(
136 "Concurrent execution aborted: {} agent(s) failed [{}]",
137 failed_count,
138 failed_agents.join(", ")
139 )));
140 }
141
142 if let Some(min) = min_required
144 && success_count < min
145 {
146 return Err(AgentError::Other(format!(
147 "Only {} of {} required agents succeeded",
148 success_count, min
149 )));
150 }
151
152 let agent_weights: HashMap<String, f64> = agents
154 .iter()
155 .map(|a| (a.id().to_string(), a.weight()))
156 .collect();
157
158 let strategy_name = format!("{:?}", aggregation_config.strategy);
159 let response = aggregation::aggregate(
160 &results,
161 aggregation_config,
162 llm,
163 &agent_weights,
164 vote_parallelism,
165 )
166 .await?;
167
168 info!(
169 agents = results.len(),
170 successes = success_count,
171 duration_ms = start.elapsed().as_millis() as u64,
172 "Concurrent execution completed"
173 );
174
175 Ok(ConcurrentResult {
176 response,
177 agent_results: results,
178 aggregation_strategy: strategy_name,
179 })
180}