1use std::collections::{HashMap, HashSet};
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde_json::to_value;
8use tokio::task::{Id, JoinSet};
9use tokio::time::timeout;
10use tracing::{error, info};
11use uuid::Uuid;
12
13use ironflow_core::error::{AgentError, OperationError};
14use ironflow_store::models::{NewStep, StepStatus, StepUpdate, step_trace_id};
15
16use crate::budget::step_budget_usd;
17use crate::config::StepConfig;
18use crate::context::WorkflowContext;
19use crate::context::failure::{
20 allowed_failure_output, extract_debug_messages_from_error, extract_partial_usage_from_error,
21 extract_raw_response_from_error, replayed_failure_output,
22};
23use crate::context::lifecycle::check_replay_identity;
24use crate::error::EngineError;
25use crate::executor::{
26 ParallelStepResult, StepArtifacts, StepOutput, StepResult, execute_step_config_intercepted,
27};
28use crate::guard::WorkflowRejection;
29use crate::log_sender::StepLogSender;
30use crate::notify::{WorkflowAgentStepTokensUsedEvent, WorkflowEvent};
31use crate::plan::{lock_plan, planned_output};
32
33impl WorkflowContext {
34 pub async fn parallel(
83 &mut self,
84 steps: Vec<(&str, StepConfig)>,
85 fail_fast: bool,
86 ) -> Result<Vec<ParallelStepResult>, EngineError> {
87 if steps.is_empty() {
88 return Ok(Vec::new());
89 }
90 reject_duplicate_names(&steps)?;
91
92 if let Some(plan) = self.plan().cloned() {
95 self.position += 1;
96 let mut results = Vec::with_capacity(steps.len());
97 let mut names = Vec::with_capacity(steps.len());
98 let capped = {
99 let mut recorder = lock_plan(&plan);
100 let group = recorder.next_group();
101 let mut capped = None;
102 for (name, config) in &steps {
103 let wave = Some(group.clone());
104 if let Err(err) =
105 recorder.record_or_stop(name, config.kind(), &self.workflow_name, wave)
106 {
107 capped = Some(err);
108 break;
109 }
110 names.push((*name).to_string());
111 let estimate = recorder.estimate_for(name);
112 let mut output = planned_output(config, estimate);
113 output.artifacts = StepArtifacts::new(name, None, config.declared_outputs());
114 results.push(ParallelStepResult {
115 name: (*name).to_string(),
116 output,
117 step_id: Uuid::now_v7(),
118 });
119 }
120 recorder.set_last(names);
121 capped
122 };
123 return match capped {
125 Some(err) => Err(err),
126 None => Ok(results),
127 };
128 }
129
130 self.check_guard_timeout()?;
132
133 let wave_position = self.position;
134 self.position += 1;
135
136 let mut slots = self.replay_wave(wave_position, &steps)?;
142 if slots.iter().all(Option::is_some) {
143 let results: Vec<ParallelStepResult> = slots.into_iter().flatten().collect();
144 self.last_step_ids = results.iter().map(|r| r.step_id).collect();
145 return Ok(results);
146 }
147
148 let wave_budget: Decimal = steps
151 .iter()
152 .zip(&slots)
153 .filter(|(_, slot)| slot.is_none())
154 .filter_map(|((_, config), _)| match config {
155 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
156 _ => None,
157 })
158 .map(step_budget_usd)
159 .sum();
160 let _reservation = self.check_run_budget(wave_budget)?;
162
163 let now = Utc::now();
164 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
165 Vec::with_capacity(steps.len());
166 let mut record_slots: Vec<usize> = Vec::with_capacity(steps.len());
168
169 for (slot, (name, config)) in steps.iter().enumerate() {
170 if slots[slot].is_some() {
171 continue;
172 }
173 let kind = config.kind();
174 let trace_id = step_trace_id(self.run_id, name, wave_position);
175 let step = self
176 .store
177 .create_step(NewStep {
178 run_id: self.run_id,
179 trace_id,
180 name: name.to_string(),
181 kind,
182 position: wave_position,
183 input: Some(to_value(config)?),
184 is_error_handler: false,
185 })
186 .await?;
187
188 self.start_step(step.id, now).await?;
189
190 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
193 self.fail_step(step.id, &err).await;
194 if !config.allow_failure() {
195 return Err(err);
196 }
197 self.has_allowed_failure = true;
198 info!(
199 run_id = %self.run_id,
200 step = %name,
201 error = %err,
202 "parallel step input preparation failed but allow_failure is set, skipping"
203 );
204 continue;
205 }
206
207 let mut config_with_trace = config.clone();
208 self.scope_step_config(&mut config_with_trace, name);
209 self.carry_capacity_wait_since(&mut config_with_trace, wave_position, name)
210 .await?;
211 self.assign_agent_session(&mut config_with_trace, step.id, wave_position, name)
212 .await?;
213 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
214 record_slots.push(slot);
215 }
216
217 let mut join_set = JoinSet::new();
218 let mut task_index: HashMap<Id, usize> = HashMap::new();
219 let parallel_timeout = self.guard_remaining_timeout();
220 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
221 let provider = self.provider.clone();
222 let interceptor = self.interceptor.clone();
225 let config = config.clone();
226 let step_log_sender = self
227 .log_sender
228 .as_ref()
229 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
230 let handle = join_set.spawn(async move {
231 let result = match parallel_timeout {
232 Some(dur) => {
233 match timeout(
234 dur,
235 execute_step_config_intercepted(
236 &config,
237 &provider,
238 step_log_sender,
239 interceptor.as_ref(),
240 ),
241 )
242 .await
243 {
244 Ok(r) => r,
245 Err(_elapsed) => {
246 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
247 elapsed_secs: 0,
248 max: 0,
249 }))
250 }
251 }
252 }
253 None => {
254 execute_step_config_intercepted(
255 &config,
256 &provider,
257 step_log_sender,
258 interceptor.as_ref(),
259 )
260 .await
261 }
262 };
263 (idx, result)
264 });
265 task_index.insert(handle.id(), idx);
266 }
267
268 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
270 vec![None; step_records.len()];
271 let mut first_error: Option<EngineError> = None;
272 let mut capacity_wait: Option<(Uuid, String, DateTime<Utc>)> = None;
275
276 while let Some(join_result) = join_set.join_next().await {
277 let (idx, step_result) = match join_result {
278 Ok(r) => r,
279 Err(e) => {
280 let error_msg = format!("join error: {e}");
281 if let Some(&idx) = task_index.get(&e.id()) {
282 let (step_id, _, step_name, _) = &step_records[idx];
283 let completed_at = Utc::now();
284 error!(
285 run_id = %self.run_id,
286 step = %step_name,
287 error = %error_msg,
288 "parallel step panicked or was cancelled"
289 );
290 if let Err(store_err) = self
291 .store
292 .update_step(
293 *step_id,
294 StepUpdate {
295 status: Some(StepStatus::Failed),
296 error: Some(error_msg.clone()),
297 completed_at: Some(completed_at),
298 ..StepUpdate::default()
299 },
300 )
301 .await
302 {
303 error!(
304 run_id = %self.run_id,
305 step_id = %step_id,
306 error = %store_err,
307 "failed to persist JoinError for step"
308 );
309 }
310 indexed_results[idx] = Some(Err(error_msg.clone()));
311 }
312 if first_error.is_none() {
313 first_error = Some(EngineError::StepConfig(error_msg));
314 }
315 if fail_fast {
316 join_set.abort_all();
317 }
318 continue;
319 }
320 };
321
322 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
323 let completed_at = Utc::now();
324
325 if let Err(err) = self
326 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
327 .await
328 {
329 self.fail_step(*step_id, &err).await;
330 indexed_results[idx] = Some(Err(err.to_string()));
331 if first_error.is_none() {
332 first_error = Some(err);
333 }
334 if fail_fast {
335 join_set.abort_all();
336 }
337 continue;
338 }
339
340 match step_result {
341 Ok(output) => {
342 self.charge_cost(output.cost_usd);
343 self.total_duration_ms += output.duration_ms;
344
345 if matches!(step_config, StepConfig::Agent(_)) {
347 let tokens = output.total_tokens();
348 if tokens > 0
349 && let Err(guard_err) = self.guard_record_tokens(tokens)
350 {
351 if first_error.is_none() {
352 first_error = Some(guard_err);
353 }
354 if fail_fast {
355 join_set.abort_all();
356 }
357 }
358 }
359
360 let debug_messages_json = output.debug_messages_json();
361
362 self.store
363 .update_step(
364 *step_id,
365 StepUpdate {
366 status: Some(StepStatus::Completed),
367 output: Some(output.output.clone()),
368 duration_ms: Some(output.duration_ms),
369 cost_usd: Some(output.cost_usd),
370 input_tokens: output.input_tokens,
371 cache_read_input_tokens: output.cache_read_input_tokens,
372 cache_creation_input_tokens: output.cache_creation_input_tokens,
373 output_tokens: output.output_tokens,
374 completed_at: Some(completed_at),
375 debug_messages: debug_messages_json,
376 account_id: output.account_id,
377 environment_id: output.environment_id.clone(),
378 ..StepUpdate::default()
379 },
380 )
381 .await?;
382
383 self.step_results.push(StepResult::from_success(
384 *step_trace,
385 step_name,
386 &output,
387 ));
388
389 if let Some(ref bus) = self.event_bus
390 && matches!(step_config, StepConfig::Agent(_))
391 {
392 let tokens = output.total_tokens();
393 bus.publish(
394 self.run_id,
395 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
396 step_name: step_name.clone(),
397 tokens,
398 cost_usd: output.cost_usd,
399 }),
400 );
401 }
402
403 info!(
404 run_id = %self.run_id,
405 step = %step_name,
406 trace_id = %step_trace,
407 duration_ms = output.duration_ms,
408 "parallel step completed"
409 );
410
411 indexed_results[idx] = Some(Ok(output));
412 }
413 Err(EngineError::Operation(OperationError::Agent(AgentError::CapacityWait {
414 kind,
415 wake_at,
416 }))) => {
417 self.park_capacity_step(*step_id).await?;
419 info!(
420 run_id = %self.run_id,
421 step = %step_name,
422 kind = %kind,
423 wake_at = %wake_at,
424 "no provider capacity, parallel step parked until the run wakes"
425 );
426 if capacity_wait
427 .as_ref()
428 .is_none_or(|(_, _, earliest)| wake_at < *earliest)
429 {
430 capacity_wait = Some((*step_id, kind, wake_at));
431 }
432 }
433 Err(err) => {
434 let err_msg = err.to_string();
435 let debug_messages_json = extract_debug_messages_from_error(&err);
436 let partial = extract_partial_usage_from_error(&err);
437 let raw_response_output = extract_raw_response_from_error(&err);
438
439 if let Some(ref usage) = partial {
440 if let Some(cost) = usage.cost_usd {
441 self.charge_cost(cost);
442 }
443 if let Some(dur) = usage.duration_ms {
444 self.total_duration_ms += dur;
445 }
446 }
447
448 if let Err(store_err) = self
449 .store
450 .update_step(
451 *step_id,
452 StepUpdate {
453 status: Some(StepStatus::Failed),
454 error: Some(err_msg.clone()),
455 output: raw_response_output.clone(),
456 completed_at: Some(completed_at),
457 debug_messages: debug_messages_json,
458 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
459 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
460 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
461 cache_read_input_tokens: partial
462 .as_ref()
463 .and_then(|p| p.cache_read_input_tokens),
464 cache_creation_input_tokens: partial
465 .as_ref()
466 .and_then(|p| p.cache_creation_input_tokens),
467 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
468 ..StepUpdate::default()
469 },
470 )
471 .await
472 {
473 error!(
474 step_id = %step_id,
475 error = %store_err,
476 "failed to persist parallel step failure"
477 );
478 }
479
480 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
481 let err_cost = partial
482 .as_ref()
483 .and_then(|p| p.cost_usd)
484 .unwrap_or(Decimal::ZERO);
485 self.step_results.push(StepResult::from_failure(
486 *step_trace,
487 step_name,
488 &err_msg,
489 err_duration,
490 err_cost,
491 ));
492
493 if step_config.allow_failure() {
494 self.has_allowed_failure = true;
495 info!(
496 run_id = %self.run_id,
497 step = %step_name,
498 error = %err_msg,
499 "parallel step failed but allow_failure is set, continuing"
500 );
501 indexed_results[idx] = Some(Ok(allowed_failure_output(
502 &err_msg,
503 raw_response_output,
504 partial.as_ref(),
505 )));
506 } else {
507 indexed_results[idx] = Some(Err(err_msg.clone()));
508
509 if first_error.is_none() {
510 first_error = Some(err);
511 }
512
513 if fail_fast {
514 join_set.abort_all();
515 }
516 }
517 }
518 }
519 }
520
521 if let Some(err) = first_error {
522 return Err(err);
523 }
524
525 if let Some((step_id, kind, wake_at)) = capacity_wait {
526 return Err(EngineError::CapacitySleeping {
527 run_id: self.run_id,
528 step_id,
529 kind,
530 wake_at,
531 });
532 }
533
534 self.persist_progress().await;
535
536 for (idx, (step_id, _trace_id, name, config)) in step_records.iter().enumerate() {
538 let mut output = match indexed_results[idx].take() {
539 Some(Ok(o)) => o,
540 _ => unreachable!("all steps succeeded if no error returned"),
541 };
542 output.artifacts = StepArtifacts::new(name, Some(*step_id), config.declared_outputs());
543 slots[record_slots[idx]] = Some(ParallelStepResult {
544 name: name.clone(),
545 output,
546 step_id: *step_id,
547 });
548 }
549 let results: Vec<ParallelStepResult> = slots.into_iter().flatten().collect();
550 self.last_step_ids = results.iter().map(|r| r.step_id).collect();
551
552 Ok(results)
553 }
554
555 fn replay_wave(
564 &mut self,
565 position: u32,
566 steps: &[(&str, StepConfig)],
567 ) -> Result<Vec<Option<ParallelStepResult>>, EngineError> {
568 let mut slots = Vec::with_capacity(steps.len());
569 for (name, config) in steps {
570 let Some(step) = self.replay_wave_steps.get(&(position, (*name).to_string())) else {
571 slots.push(None);
572 continue;
573 };
574 check_replay_identity(step, position, name, &config.kind())?;
575 let mut output = match step.status.state {
576 StepStatus::Completed => StepOutput::from(step),
577 StepStatus::Failed if config.allow_failure() => {
578 let Some(output) = replayed_failure_output(step) else {
579 slots.push(None);
580 continue;
581 };
582 self.has_allowed_failure = true;
583 output
584 }
585 _ => {
586 slots.push(None);
587 continue;
588 }
589 };
590 let step_id = step.id;
591 self.total_duration_ms += output.duration_ms;
595 output.artifacts = StepArtifacts::new(name, Some(step_id), config.declared_outputs());
596
597 info!(
598 run_id = %self.run_id,
599 step = %name,
600 position,
601 "step replayed from previous execution"
602 );
603
604 slots.push(Some(ParallelStepResult {
605 name: (*name).to_string(),
606 output,
607 step_id,
608 }));
609 }
610 Ok(slots)
611 }
612}
613
614fn reject_duplicate_names(steps: &[(&str, StepConfig)]) -> Result<(), EngineError> {
620 let mut seen = HashSet::with_capacity(steps.len());
621 for (name, _) in steps {
622 if !seen.insert(*name) {
623 return Err(EngineError::StepConfig(format!(
624 "parallel wave has two steps named {name:?}; each step of a wave needs its own name"
625 )));
626 }
627 }
628 Ok(())
629}