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,
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 self.check_run_budget(wave_budget)?;
161
162 let now = Utc::now();
163 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
164 Vec::with_capacity(steps.len());
165 let mut record_slots: Vec<usize> = Vec::with_capacity(steps.len());
167
168 for (slot, (name, config)) in steps.iter().enumerate() {
169 if slots[slot].is_some() {
170 continue;
171 }
172 let kind = config.kind();
173 let trace_id = step_trace_id(self.run_id, name, wave_position);
174 let step = self
175 .store
176 .create_step(NewStep {
177 run_id: self.run_id,
178 trace_id,
179 name: name.to_string(),
180 kind,
181 position: wave_position,
182 input: Some(to_value(config)?),
183 is_error_handler: false,
184 })
185 .await?;
186
187 self.start_step(step.id, now).await?;
188
189 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
192 self.fail_step(step.id, &err).await;
193 if !config.allow_failure() {
194 return Err(err);
195 }
196 self.has_allowed_failure = true;
197 info!(
198 run_id = %self.run_id,
199 step = %name,
200 error = %err,
201 "parallel step input preparation failed but allow_failure is set, skipping"
202 );
203 continue;
204 }
205
206 let mut config_with_trace = config.clone();
207 self.scope_step_config(&mut config_with_trace, name);
208 self.carry_capacity_wait_since(&mut config_with_trace, wave_position, name)
209 .await?;
210 self.assign_agent_session(&mut config_with_trace, step.id, wave_position, name)
211 .await?;
212 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
213 record_slots.push(slot);
214 }
215
216 let mut join_set = JoinSet::new();
217 let mut task_index: HashMap<Id, usize> = HashMap::new();
218 let parallel_timeout = self.guard_remaining_timeout();
219 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
220 let provider = self.provider.clone();
221 let interceptor = self.interceptor.clone();
224 let config = config.clone();
225 let step_log_sender = self
226 .log_sender
227 .as_ref()
228 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
229 let handle = join_set.spawn(async move {
230 let result = match parallel_timeout {
231 Some(dur) => {
232 match timeout(
233 dur,
234 execute_step_config_intercepted(
235 &config,
236 &provider,
237 step_log_sender,
238 interceptor.as_ref(),
239 ),
240 )
241 .await
242 {
243 Ok(r) => r,
244 Err(_elapsed) => {
245 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
246 elapsed_secs: 0,
247 max: 0,
248 }))
249 }
250 }
251 }
252 None => {
253 execute_step_config_intercepted(
254 &config,
255 &provider,
256 step_log_sender,
257 interceptor.as_ref(),
258 )
259 .await
260 }
261 };
262 (idx, result)
263 });
264 task_index.insert(handle.id(), idx);
265 }
266
267 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
269 vec![None; step_records.len()];
270 let mut first_error: Option<EngineError> = None;
271 let mut capacity_wait: Option<(Uuid, String, DateTime<Utc>)> = None;
274
275 while let Some(join_result) = join_set.join_next().await {
276 let (idx, step_result) = match join_result {
277 Ok(r) => r,
278 Err(e) => {
279 let error_msg = format!("join error: {e}");
280 if let Some(&idx) = task_index.get(&e.id()) {
281 let (step_id, _, step_name, _) = &step_records[idx];
282 let completed_at = Utc::now();
283 error!(
284 run_id = %self.run_id,
285 step = %step_name,
286 error = %error_msg,
287 "parallel step panicked or was cancelled"
288 );
289 if let Err(store_err) = self
290 .store
291 .update_step(
292 *step_id,
293 StepUpdate {
294 status: Some(StepStatus::Failed),
295 error: Some(error_msg.clone()),
296 completed_at: Some(completed_at),
297 ..StepUpdate::default()
298 },
299 )
300 .await
301 {
302 error!(
303 run_id = %self.run_id,
304 step_id = %step_id,
305 error = %store_err,
306 "failed to persist JoinError for step"
307 );
308 }
309 indexed_results[idx] = Some(Err(error_msg.clone()));
310 }
311 if first_error.is_none() {
312 first_error = Some(EngineError::StepConfig(error_msg));
313 }
314 if fail_fast {
315 join_set.abort_all();
316 }
317 continue;
318 }
319 };
320
321 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
322 let completed_at = Utc::now();
323
324 if let Err(err) = self
325 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
326 .await
327 {
328 self.fail_step(*step_id, &err).await;
329 indexed_results[idx] = Some(Err(err.to_string()));
330 if first_error.is_none() {
331 first_error = Some(err);
332 }
333 if fail_fast {
334 join_set.abort_all();
335 }
336 continue;
337 }
338
339 match step_result {
340 Ok(output) => {
341 self.total_cost_usd += output.cost_usd;
342 self.total_duration_ms += output.duration_ms;
343
344 if matches!(step_config, StepConfig::Agent(_)) {
346 let tokens = output.total_tokens();
347 if tokens > 0
348 && let Err(guard_err) = self.guard_record_tokens(tokens)
349 {
350 if first_error.is_none() {
351 first_error = Some(guard_err);
352 }
353 if fail_fast {
354 join_set.abort_all();
355 }
356 }
357 }
358
359 let debug_messages_json = output.debug_messages_json();
360
361 self.store
362 .update_step(
363 *step_id,
364 StepUpdate {
365 status: Some(StepStatus::Completed),
366 output: Some(output.output.clone()),
367 duration_ms: Some(output.duration_ms),
368 cost_usd: Some(output.cost_usd),
369 input_tokens: output.input_tokens,
370 cache_read_input_tokens: output.cache_read_input_tokens,
371 cache_creation_input_tokens: output.cache_creation_input_tokens,
372 output_tokens: output.output_tokens,
373 completed_at: Some(completed_at),
374 debug_messages: debug_messages_json,
375 account_id: output.account_id,
376 environment_id: output.environment_id.clone(),
377 ..StepUpdate::default()
378 },
379 )
380 .await?;
381
382 self.step_results.push(StepResult::from_success(
383 *step_trace,
384 step_name,
385 &output,
386 ));
387
388 if let Some(ref bus) = self.event_bus
389 && matches!(step_config, StepConfig::Agent(_))
390 {
391 let tokens = output.total_tokens();
392 bus.publish(
393 self.run_id,
394 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
395 step_name: step_name.clone(),
396 tokens,
397 cost_usd: output.cost_usd,
398 }),
399 );
400 }
401
402 info!(
403 run_id = %self.run_id,
404 step = %step_name,
405 trace_id = %step_trace,
406 duration_ms = output.duration_ms,
407 "parallel step completed"
408 );
409
410 indexed_results[idx] = Some(Ok(output));
411 }
412 Err(EngineError::Operation(OperationError::Agent(AgentError::CapacityWait {
413 kind,
414 wake_at,
415 }))) => {
416 self.park_capacity_step(*step_id).await?;
418 info!(
419 run_id = %self.run_id,
420 step = %step_name,
421 kind = %kind,
422 wake_at = %wake_at,
423 "no provider capacity, parallel step parked until the run wakes"
424 );
425 if capacity_wait
426 .as_ref()
427 .is_none_or(|(_, _, earliest)| wake_at < *earliest)
428 {
429 capacity_wait = Some((*step_id, kind, wake_at));
430 }
431 }
432 Err(err) => {
433 let err_msg = err.to_string();
434 let debug_messages_json = extract_debug_messages_from_error(&err);
435 let partial = extract_partial_usage_from_error(&err);
436 let raw_response_output = extract_raw_response_from_error(&err);
437
438 if let Some(ref usage) = partial {
439 if let Some(cost) = usage.cost_usd {
440 self.total_cost_usd += cost;
441 }
442 if let Some(dur) = usage.duration_ms {
443 self.total_duration_ms += dur;
444 }
445 }
446
447 if let Err(store_err) = self
448 .store
449 .update_step(
450 *step_id,
451 StepUpdate {
452 status: Some(StepStatus::Failed),
453 error: Some(err_msg.clone()),
454 output: raw_response_output.clone(),
455 completed_at: Some(completed_at),
456 debug_messages: debug_messages_json,
457 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
458 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
459 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
460 cache_read_input_tokens: partial
461 .as_ref()
462 .and_then(|p| p.cache_read_input_tokens),
463 cache_creation_input_tokens: partial
464 .as_ref()
465 .and_then(|p| p.cache_creation_input_tokens),
466 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
467 ..StepUpdate::default()
468 },
469 )
470 .await
471 {
472 error!(
473 step_id = %step_id,
474 error = %store_err,
475 "failed to persist parallel step failure"
476 );
477 }
478
479 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
480 let err_cost = partial
481 .as_ref()
482 .and_then(|p| p.cost_usd)
483 .unwrap_or(Decimal::ZERO);
484 self.step_results.push(StepResult::from_failure(
485 *step_trace,
486 step_name,
487 &err_msg,
488 err_duration,
489 err_cost,
490 ));
491
492 if step_config.allow_failure() {
493 self.has_allowed_failure = true;
494 info!(
495 run_id = %self.run_id,
496 step = %step_name,
497 error = %err_msg,
498 "parallel step failed but allow_failure is set, continuing"
499 );
500 indexed_results[idx] = Some(Ok(allowed_failure_output(
501 &err_msg,
502 raw_response_output,
503 partial.as_ref(),
504 )));
505 } else {
506 indexed_results[idx] = Some(Err(err_msg.clone()));
507
508 if first_error.is_none() {
509 first_error = Some(err);
510 }
511
512 if fail_fast {
513 join_set.abort_all();
514 }
515 }
516 }
517 }
518 }
519
520 if let Some(err) = first_error {
521 return Err(err);
522 }
523
524 if let Some((step_id, kind, wake_at)) = capacity_wait {
525 return Err(EngineError::CapacitySleeping {
526 run_id: self.run_id,
527 step_id,
528 kind,
529 wake_at,
530 });
531 }
532
533 self.persist_progress().await;
534
535 for (idx, (step_id, _trace_id, name, config)) in step_records.iter().enumerate() {
537 let mut output = match indexed_results[idx].take() {
538 Some(Ok(o)) => o,
539 _ => unreachable!("all steps succeeded if no error returned"),
540 };
541 output.artifacts = StepArtifacts::new(name, Some(*step_id), config.declared_outputs());
542 slots[record_slots[idx]] = Some(ParallelStepResult {
543 name: name.clone(),
544 output,
545 step_id: *step_id,
546 });
547 }
548 let results: Vec<ParallelStepResult> = slots.into_iter().flatten().collect();
549 self.last_step_ids = results.iter().map(|r| r.step_id).collect();
550
551 Ok(results)
552 }
553
554 fn replay_wave(
562 &mut self,
563 position: u32,
564 steps: &[(&str, StepConfig)],
565 ) -> Result<Vec<Option<ParallelStepResult>>, EngineError> {
566 let mut slots = Vec::with_capacity(steps.len());
567 for (name, config) in steps {
568 let Some(step) = self.replay_wave_steps.get(&(position, (*name).to_string())) else {
569 slots.push(None);
570 continue;
571 };
572 check_replay_identity(step, position, name, &config.kind())?;
573 if step.status.state != StepStatus::Completed {
574 slots.push(None);
575 continue;
576 }
577
578 let mut output = StepOutput::from(step);
579 let step_id = step.id;
580 self.total_duration_ms += output.duration_ms;
584 output.artifacts = StepArtifacts::new(name, Some(step_id), config.declared_outputs());
585
586 info!(
587 run_id = %self.run_id,
588 step = %name,
589 position,
590 "step replayed from previous execution"
591 );
592
593 slots.push(Some(ParallelStepResult {
594 name: (*name).to_string(),
595 output,
596 step_id,
597 }));
598 }
599 Ok(slots)
600 }
601}
602
603fn reject_duplicate_names(steps: &[(&str, StepConfig)]) -> Result<(), EngineError> {
609 let mut seen = HashSet::with_capacity(steps.len());
610 for (name, _) in steps {
611 if !seen.insert(*name) {
612 return Err(EngineError::StepConfig(format!(
613 "parallel wave has two steps named {name:?}; each step of a wave needs its own name"
614 )));
615 }
616 }
617 Ok(())
618}