Fan `specs` out across the executor, bounded by its [`concurrency_hint`](AgentExecutor::concurrency_hint), preserving input order. A panicked branch becomes a failed [`StepOutcome`] without dropping the others. This is the barrier (`parallel`) primitive — it awaits every step. Later combinators (pipeline, phases) build on the same seam.
(
executor: Arc<dyn AgentExecutor>,
specs: Vec<AgentStepSpec>,
event_tx: Option<broadcast::Sender<AgentEvent>>,
)
| 156 | /// This is the barrier (`parallel`) primitive — it awaits every step. Later |
| 157 | /// combinators (pipeline, phases) build on the same seam. |
| 158 | pub async fn execute_steps_parallel( |
| 159 | executor: Arc<dyn AgentExecutor>, |
| 160 | specs: Vec<AgentStepSpec>, |
| 161 | event_tx: Option<broadcast::Sender<AgentEvent>>, |
| 162 | ) -> Vec<StepOutcome> { |
| 163 | let limit = executor.concurrency_hint(); |
| 164 | // Keep (task_id, agent) by index so a panicked branch still yields a |
| 165 | // correctly-labelled failed outcome (mirrors TaskExecutor's fallback). |
| 166 | let labels: Vec<(String, String)> = specs |
| 167 | .iter() |
| 168 | .map(|s| (s.task_id.clone(), s.agent.clone())) |
| 169 | .collect(); |
| 170 | |
| 171 | let results = run_ordered_parallel_with_limit(specs, limit, move |_idx, spec| { |
| 172 | let executor = Arc::clone(&executor); |
| 173 | let event_tx = event_tx.clone(); |
| 174 | async move { executor.execute_step(spec, event_tx).await } |
| 175 | }) |
| 176 | .await; |
| 177 | |
| 178 | results |
| 179 | .into_iter() |
| 180 | .map(|result| match result.output { |
| 181 | Ok(outcome) => outcome, |
| 182 | Err(error) => { |
| 183 | let (task_id, agent) = labels |
| 184 | .get(result.index) |
| 185 | .cloned() |
| 186 | .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string())); |
| 187 | StepOutcome::failed(task_id, agent, error.to_string()) |
| 188 | } |
| 189 | }) |
| 190 | .collect() |
| 191 | } |
| 192 | |
| 193 | #[cfg(test)] |
| 194 | mod tests { |