1use std::collections::HashMap;
27use std::fmt;
28use std::sync::Arc;
29use std::time::Instant;
30
31use chrono::{DateTime, Utc};
32use futures_util::StreamExt;
33use rust_decimal::Decimal;
34use serde_json::{Value, json};
35use tokio::task::{Id, JoinSet};
36use tracing::{error, info, warn};
37use uuid::Uuid;
38
39use ironflow_core::error::{AgentError, OperationError};
40use ironflow_core::provider::AgentProvider;
41use ironflow_store::models::{
42 ArtifactLookup, NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind,
43 StepStatus, StepUpdate, TriggerKind,
44};
45use ironflow_store::store::Store;
46
47use ironflow_artifacts::name::guess_content_type;
48use ironflow_artifacts::stream_from_bytes;
49use ironflow_store::entities::Artifact;
50
51use crate::artifact::{
52 ArtifactSink, ArtifactUpload, StepLocation, collect_outputs, materialize_inputs,
53};
54use crate::budget::step_budget_usd;
55use crate::config::{
56 AgentStepConfig, ApprovalConfig, HttpConfig, ShellConfig, StepConfig, WorkflowStepConfig,
57};
58use crate::error::EngineError;
59use crate::executor::{ParallelStepResult, StepOutput, execute_step_config};
60use crate::handler::WorkflowHandler;
61use crate::log_sender::{LogSender, StepLogSender};
62use crate::operation::Operation;
63
64pub(crate) type HandlerResolver =
66 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
67
68pub struct WorkflowContext {
87 run_id: Uuid,
88 store: Arc<dyn Store>,
89 provider: Arc<dyn AgentProvider>,
90 handler_resolver: Option<HandlerResolver>,
91 position: u32,
92 last_step_ids: Vec<Uuid>,
94 total_cost_usd: Decimal,
96 total_duration_ms: u64,
98 max_cost_usd: Option<Decimal>,
100 inherited_cost_usd: Decimal,
103 replay_steps: HashMap<u32, Step>,
106 granted_approvals: HashMap<u32, u32>,
110 attempt: u32,
112 carried_duration_ms: u64,
115 log_sender: Option<LogSender>,
117 artifact_sink: Option<Arc<dyn ArtifactSink>>,
121 has_allowed_failure: bool,
123 error_handlers: Vec<OnErrorHandler>,
125}
126
127struct OnErrorHandler {
129 name: String,
130 config: StepConfig,
131}
132
133impl WorkflowContext {
134 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
139 Self {
140 run_id,
141 store,
142 provider,
143 handler_resolver: None,
144 position: 0,
145 last_step_ids: Vec::new(),
146 total_cost_usd: Decimal::ZERO,
147 total_duration_ms: 0,
148 max_cost_usd: None,
149 inherited_cost_usd: Decimal::ZERO,
150 replay_steps: HashMap::new(),
151 granted_approvals: HashMap::new(),
152 attempt: 1,
153 carried_duration_ms: 0,
154 log_sender: None,
155 artifact_sink: None,
156 has_allowed_failure: false,
157 error_handlers: Vec::new(),
158 }
159 }
160
161 pub(crate) fn with_handler_resolver(
166 run_id: Uuid,
167 store: Arc<dyn Store>,
168 provider: Arc<dyn AgentProvider>,
169 resolver: HandlerResolver,
170 ) -> Self {
171 Self {
172 run_id,
173 store,
174 provider,
175 handler_resolver: Some(resolver),
176 position: 0,
177 last_step_ids: Vec::new(),
178 total_cost_usd: Decimal::ZERO,
179 total_duration_ms: 0,
180 max_cost_usd: None,
181 inherited_cost_usd: Decimal::ZERO,
182 replay_steps: HashMap::new(),
183 granted_approvals: HashMap::new(),
184 attempt: 1,
185 carried_duration_ms: 0,
186 log_sender: None,
187 artifact_sink: None,
188 has_allowed_failure: false,
189 error_handlers: Vec::new(),
190 }
191 }
192
193 pub fn set_log_sender(&mut self, sender: LogSender) {
195 self.log_sender = Some(sender);
196 }
197
198 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
218 self.artifact_sink = Some(sink);
219 }
220
221 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
223 self.artifact_sink.as_ref().ok_or_else(|| {
224 EngineError::ArtifactsUnavailable(
225 "no artifact storage is attached to this run".to_string(),
226 )
227 })
228 }
229
230 pub async fn put_artifact(
260 &self,
261 step_id: Uuid,
262 name: &str,
263 content_type: Option<&str>,
264 content: Vec<u8>,
265 ) -> Result<Artifact, EngineError> {
266 let sink = self.artifact_sink()?;
267 sink.put(
268 ArtifactUpload {
269 run_id: self.run_id,
270 step_id,
271 name: name.to_string(),
272 content_type: content_type
273 .map(str::to_string)
274 .unwrap_or_else(|| guess_content_type(name)),
275 },
276 stream_from_bytes(content),
277 )
278 .await
279 }
280
281 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
306 let sink = self.artifact_sink()?;
307
308 let artifact = self
309 .store
310 .find_artifact_for_input(ArtifactLookup {
311 run_id: self.run_id,
312 attempt: self.attempt,
313 before_position: self.position,
314 step_name: step.to_string(),
315 name: name.to_string(),
316 })
317 .await?
318 .ok_or_else(|| EngineError::ArtifactNotFound {
319 step: step.to_string(),
320 name: name.to_string(),
321 })?;
322
323 let mut content = sink.get(&artifact).await?;
324 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
325 while let Some(chunk) = content.next().await {
326 let chunk = chunk?;
327 buffer.extend_from_slice(chunk.as_ref());
328 }
329
330 Ok(buffer)
331 }
332
333 async fn prepare_step_inputs(
338 &self,
339 config: &StepConfig,
340 position: u32,
341 ) -> Result<(), EngineError> {
342 let StepConfig::Shell(shell) = config else {
343 return Ok(());
344 };
345 if shell.inputs.is_empty() {
346 return Ok(());
347 }
348
349 materialize_inputs(
350 self.artifact_sink()?,
351 &self.store,
352 shell,
353 StepLocation {
354 run_id: self.run_id,
355 attempt: self.attempt,
356 position,
357 },
358 )
359 .await
360 }
361
362 async fn store_step_outputs(
367 &self,
368 config: &StepConfig,
369 step_id: Uuid,
370 step_name: &str,
371 step_succeeded: bool,
372 ) -> Result<(), EngineError> {
373 let StepConfig::Shell(shell) = config else {
374 return Ok(());
375 };
376 if shell.outputs.is_empty() {
377 return Ok(());
378 }
379
380 let sink = match self.artifact_sink() {
381 Ok(sink) => sink,
382 Err(err) if step_succeeded => return Err(err),
383 Err(err) => {
384 warn!(
385 run_id = %self.run_id,
386 step = %step_name,
387 error = %err,
388 "cannot collect outputs of a failed step"
389 );
390 return Ok(());
391 }
392 };
393
394 let collected =
395 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
396
397 match collected {
398 Ok(()) => Ok(()),
399 Err(err) if step_succeeded => Err(err),
400 Err(err) => {
401 warn!(
402 run_id = %self.run_id,
403 step = %step_name,
404 error = %err,
405 "failed to collect outputs of a failed step"
406 );
407 Ok(())
408 }
409 }
410 }
411
412 pub(crate) fn carry_over_run_totals(
419 &mut self,
420 attempt: u32,
421 cost_usd: Decimal,
422 duration_ms: u64,
423 ) {
424 self.attempt = attempt;
425 self.total_cost_usd = cost_usd;
426 self.carried_duration_ms = duration_ms;
427 }
428
429 pub(crate) fn carried_duration_ms(&self) -> u64 {
431 self.carried_duration_ms
432 }
433
434 pub fn attempt(&self) -> u32 {
436 self.attempt
437 }
438
439 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
455 self.max_cost_usd = cap;
456 }
457
458 pub fn max_cost_usd(&self) -> Option<Decimal> {
460 self.max_cost_usd
461 }
462
463 pub fn charged_cost_usd(&self) -> Decimal {
468 self.inherited_cost_usd + self.total_cost_usd
469 }
470
471 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
482 let Some(limit) = self.max_cost_usd else {
483 return Ok(());
484 };
485
486 let spent = self.charged_cost_usd();
487 if spent + step_budget <= limit {
488 return Ok(());
489 }
490
491 error!(
492 run_id = %self.run_id,
493 limit_usd = %limit,
494 spent_usd = %spent,
495 step_budget_usd = %step_budget,
496 "run cost cap reached, refusing agent step"
497 );
498
499 Err(EngineError::RunBudgetExceeded {
500 run_id: self.run_id,
501 limit_usd: limit,
502 spent_usd: spent,
503 step_budget_usd: step_budget,
504 })
505 }
506
507 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
519 let steps = self.store.list_steps(self.run_id).await?;
520 for step in steps {
521 let dominated = matches!(
522 step.status.state,
523 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
524 );
525 if !dominated {
526 continue;
527 }
528
529 if step.attempt == self.attempt {
530 self.replay_steps.insert(step.position, step);
531 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
532 {
533 self.granted_approvals.insert(step.position, step.attempt);
534 }
535 }
536 Ok(())
537 }
538
539 pub fn run_id(&self) -> Uuid {
541 self.run_id
542 }
543
544 pub fn total_cost_usd(&self) -> Decimal {
546 self.total_cost_usd
547 }
548
549 pub fn has_allowed_failure(&self) -> bool {
551 self.has_allowed_failure
552 }
553
554 pub fn total_duration_ms(&self) -> u64 {
556 self.total_duration_ms
557 }
558
559 pub async fn parallel(
596 &mut self,
597 steps: Vec<(&str, StepConfig)>,
598 fail_fast: bool,
599 ) -> Result<Vec<ParallelStepResult>, EngineError> {
600 if steps.is_empty() {
601 return Ok(Vec::new());
602 }
603
604 let wave_budget: Decimal = steps
607 .iter()
608 .filter_map(|(_, config)| match config {
609 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
610 _ => None,
611 })
612 .map(step_budget_usd)
613 .sum();
614 self.check_run_budget(wave_budget)?;
615
616 let wave_position = self.position;
617 self.position += 1;
618
619 let now = Utc::now();
620 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
621
622 for (name, config) in &steps {
623 let kind = config.kind();
624 let step = self
625 .store
626 .create_step(NewStep {
627 run_id: self.run_id,
628 name: name.to_string(),
629 kind,
630 position: wave_position,
631 input: Some(serde_json::to_value(config)?),
632 is_error_handler: false,
633 })
634 .await?;
635
636 self.start_step(step.id, now).await?;
637
638 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
641 self.fail_step(step.id, &err).await;
642 if !config.allow_failure() {
643 return Err(err);
644 }
645 self.has_allowed_failure = true;
646 info!(
647 run_id = %self.run_id,
648 step = %name,
649 error = %err,
650 "parallel step input preparation failed but allow_failure is set, skipping"
651 );
652 continue;
653 }
654
655 step_records.push((step.id, name.to_string(), config.clone()));
656 }
657
658 let mut join_set = JoinSet::new();
659 let mut task_index: HashMap<Id, usize> = HashMap::new();
660 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
661 let provider = self.provider.clone();
662 let config = config.clone();
663 let step_log_sender = self
664 .log_sender
665 .as_ref()
666 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
667 let handle = join_set.spawn(async move {
668 (
669 idx,
670 execute_step_config(&config, &provider, step_log_sender).await,
671 )
672 });
673 task_index.insert(handle.id(), idx);
674 }
675
676 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
678 vec![None; step_records.len()];
679 let mut first_error: Option<EngineError> = None;
680
681 while let Some(join_result) = join_set.join_next().await {
682 let (idx, step_result) = match join_result {
683 Ok(r) => r,
684 Err(e) => {
685 let error_msg = format!("join error: {e}");
686 if let Some(&idx) = task_index.get(&e.id()) {
687 let (step_id, step_name, _) = &step_records[idx];
688 let completed_at = Utc::now();
689 error!(
690 run_id = %self.run_id,
691 step = %step_name,
692 error = %error_msg,
693 "parallel step panicked or was cancelled"
694 );
695 if let Err(store_err) = self
696 .store
697 .update_step(
698 *step_id,
699 StepUpdate {
700 status: Some(StepStatus::Failed),
701 error: Some(error_msg.clone()),
702 completed_at: Some(completed_at),
703 ..StepUpdate::default()
704 },
705 )
706 .await
707 {
708 error!(
709 run_id = %self.run_id,
710 step_id = %step_id,
711 error = %store_err,
712 "failed to persist JoinError for step"
713 );
714 }
715 indexed_results[idx] = Some(Err(error_msg.clone()));
716 }
717 if first_error.is_none() {
718 first_error = Some(EngineError::StepConfig(error_msg));
719 }
720 if fail_fast {
721 join_set.abort_all();
722 }
723 continue;
724 }
725 };
726
727 let (step_id, step_name, step_config) = &step_records[idx];
728 let completed_at = Utc::now();
729
730 if let Err(err) = self
731 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
732 .await
733 {
734 self.fail_step(*step_id, &err).await;
735 indexed_results[idx] = Some(Err(err.to_string()));
736 if first_error.is_none() {
737 first_error = Some(err);
738 }
739 if fail_fast {
740 join_set.abort_all();
741 }
742 continue;
743 }
744
745 match step_result {
746 Ok(output) => {
747 self.total_cost_usd += output.cost_usd;
748 self.total_duration_ms += output.duration_ms;
749
750 let debug_messages_json = output.debug_messages_json();
751
752 self.store
753 .update_step(
754 *step_id,
755 StepUpdate {
756 status: Some(StepStatus::Completed),
757 output: Some(output.output.clone()),
758 duration_ms: Some(output.duration_ms),
759 cost_usd: Some(output.cost_usd),
760 input_tokens: output.input_tokens,
761 output_tokens: output.output_tokens,
762 completed_at: Some(completed_at),
763 debug_messages: debug_messages_json,
764 ..StepUpdate::default()
765 },
766 )
767 .await?;
768
769 info!(
770 run_id = %self.run_id,
771 step = %step_name,
772 duration_ms = output.duration_ms,
773 "parallel step completed"
774 );
775
776 indexed_results[idx] = Some(Ok(output));
777 }
778 Err(err) => {
779 let err_msg = err.to_string();
780 let debug_messages_json = extract_debug_messages_from_error(&err);
781 let partial = extract_partial_usage_from_error(&err);
782 let raw_response_output = extract_raw_response_from_error(&err);
783
784 if let Some(ref usage) = partial {
785 if let Some(cost) = usage.cost_usd {
786 self.total_cost_usd += cost;
787 }
788 if let Some(dur) = usage.duration_ms {
789 self.total_duration_ms += dur;
790 }
791 }
792
793 if let Err(store_err) = self
794 .store
795 .update_step(
796 *step_id,
797 StepUpdate {
798 status: Some(StepStatus::Failed),
799 error: Some(err_msg.clone()),
800 output: raw_response_output.clone(),
801 completed_at: Some(completed_at),
802 debug_messages: debug_messages_json,
803 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
804 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
805 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
806 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
807 ..StepUpdate::default()
808 },
809 )
810 .await
811 {
812 tracing::error!(
813 step_id = %step_id,
814 error = %store_err,
815 "failed to persist parallel step failure"
816 );
817 }
818
819 if step_config.allow_failure() {
820 self.has_allowed_failure = true;
821 info!(
822 run_id = %self.run_id,
823 step = %step_name,
824 error = %err_msg,
825 "parallel step failed but allow_failure is set, continuing"
826 );
827 indexed_results[idx] = Some(Ok(allowed_failure_output(
828 &err_msg,
829 raw_response_output,
830 partial.as_ref(),
831 )));
832 } else {
833 indexed_results[idx] = Some(Err(err_msg.clone()));
834
835 if first_error.is_none() {
836 first_error = Some(err);
837 }
838
839 if fail_fast {
840 join_set.abort_all();
841 }
842 }
843 }
844 }
845 }
846
847 if let Some(err) = first_error {
848 return Err(err);
849 }
850
851 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
852
853 let results: Vec<ParallelStepResult> = step_records
855 .iter()
856 .enumerate()
857 .map(|(idx, (step_id, name, _))| {
858 let output = match indexed_results[idx].take() {
859 Some(Ok(o)) => o,
860 _ => unreachable!("all steps succeeded if no error returned"),
861 };
862 ParallelStepResult {
863 name: name.clone(),
864 output,
865 step_id: *step_id,
866 }
867 })
868 .collect();
869
870 Ok(results)
871 }
872
873 pub async fn shell(
896 &mut self,
897 name: &str,
898 config: ShellConfig,
899 ) -> Result<StepOutput, EngineError> {
900 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
901 .await
902 }
903
904 pub async fn http(
924 &mut self,
925 name: &str,
926 config: HttpConfig,
927 ) -> Result<StepOutput, EngineError> {
928 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
929 .await
930 }
931
932 pub async fn agent(
952 &mut self,
953 name: &str,
954 config: impl Into<AgentStepConfig>,
955 ) -> Result<StepOutput, EngineError> {
956 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
957 .await
958 }
959
960 pub async fn approval(
991 &mut self,
992 name: &str,
993 config: ApprovalConfig,
994 ) -> Result<(), EngineError> {
995 let position = self.position;
996 self.position += 1;
997
998 if let Some(existing) = self.replay_steps.get(&position)
1001 && existing.kind == StepKind::Approval
1002 {
1003 if existing.status.state == StepStatus::AwaitingApproval {
1004 self.store
1005 .update_step(
1006 existing.id,
1007 StepUpdate {
1008 status: Some(StepStatus::Completed),
1009 completed_at: Some(Utc::now()),
1010 ..StepUpdate::default()
1011 },
1012 )
1013 .await?;
1014 }
1015
1016 self.last_step_ids = vec![existing.id];
1017 info!(
1018 run_id = %self.run_id,
1019 step = %name,
1020 position,
1021 "approval step replayed (approved)"
1022 );
1023 return Ok(());
1024 }
1025
1026 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1030 let step = self
1031 .store
1032 .create_step(NewStep {
1033 run_id: self.run_id,
1034 name: name.to_string(),
1035 kind: StepKind::Approval,
1036 position,
1037 input: Some(serde_json::to_value(&config)?),
1038 is_error_handler: false,
1039 })
1040 .await?;
1041
1042 let now = Utc::now();
1043 self.start_step(step.id, now).await?;
1044 self.store
1045 .update_step(
1046 step.id,
1047 StepUpdate {
1048 status: Some(StepStatus::Completed),
1049 output: Some(json!({"approved_in_attempt": granted_in})),
1050 completed_at: Some(now),
1051 ..StepUpdate::default()
1052 },
1053 )
1054 .await?;
1055
1056 self.last_step_ids = vec![step.id];
1057 info!(
1058 run_id = %self.run_id,
1059 step = %name,
1060 position,
1061 granted_in_attempt = granted_in,
1062 attempt = self.attempt,
1063 "approval carried over from a previous attempt"
1064 );
1065 return Ok(());
1066 }
1067
1068 let step = self
1070 .store
1071 .create_step(NewStep {
1072 run_id: self.run_id,
1073 name: name.to_string(),
1074 kind: StepKind::Approval,
1075 position,
1076 input: Some(serde_json::to_value(&config)?),
1077 is_error_handler: false,
1078 })
1079 .await?;
1080
1081 self.start_step(step.id, Utc::now()).await?;
1082
1083 self.store
1086 .update_step(
1087 step.id,
1088 StepUpdate {
1089 status: Some(StepStatus::AwaitingApproval),
1090 ..StepUpdate::default()
1091 },
1092 )
1093 .await?;
1094
1095 self.last_step_ids = vec![step.id];
1096
1097 Err(EngineError::ApprovalRequired {
1098 run_id: self.run_id,
1099 step_id: step.id,
1100 message: config.message().to_string(),
1101 })
1102 }
1103
1104 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1133 let position = self.position;
1134 self.position += 1;
1135
1136 let step = self
1137 .store
1138 .create_step(NewStep {
1139 run_id: self.run_id,
1140 name: name.to_string(),
1141 kind: StepKind::Custom("skip".to_string()),
1142 position,
1143 input: None,
1144 is_error_handler: false,
1145 })
1146 .await?;
1147
1148 if !self.last_step_ids.is_empty() {
1149 let deps: Vec<NewStepDependency> = self
1150 .last_step_ids
1151 .iter()
1152 .map(|&depends_on| NewStepDependency {
1153 step_id: step.id,
1154 depends_on,
1155 })
1156 .collect();
1157 self.store.create_step_dependencies(deps).await?;
1158 }
1159
1160 let now = Utc::now();
1161 self.store
1162 .update_step(
1163 step.id,
1164 StepUpdate {
1165 status: Some(StepStatus::Skipped),
1166 output: Some(serde_json::json!({"reason": reason})),
1167 completed_at: Some(now),
1168 ..StepUpdate::default()
1169 },
1170 )
1171 .await?;
1172
1173 self.last_step_ids = vec![step.id];
1174
1175 info!(
1176 run_id = %self.run_id,
1177 step = %name,
1178 reason,
1179 "step skipped"
1180 );
1181
1182 Ok(())
1183 }
1184
1185 pub async fn operation(
1223 &mut self,
1224 name: &str,
1225 op: &dyn Operation,
1226 ) -> Result<StepOutput, EngineError> {
1227 let kind = StepKind::Custom(op.kind().to_string());
1228 let position = self.position;
1229 self.position += 1;
1230
1231 let step = self
1232 .store
1233 .create_step(NewStep {
1234 run_id: self.run_id,
1235 name: name.to_string(),
1236 kind,
1237 position,
1238 input: op.input(),
1239 is_error_handler: false,
1240 })
1241 .await?;
1242
1243 self.start_step(step.id, Utc::now()).await?;
1244
1245 let start = Instant::now();
1246
1247 match op.execute().await {
1248 Ok(output_value) => {
1249 let duration_ms = start.elapsed().as_millis() as u64;
1250 self.total_duration_ms += duration_ms;
1251
1252 let completed_at = Utc::now();
1253 self.store
1254 .update_step(
1255 step.id,
1256 StepUpdate {
1257 status: Some(StepStatus::Completed),
1258 output: Some(output_value.clone()),
1259 duration_ms: Some(duration_ms),
1260 cost_usd: Some(Decimal::ZERO),
1261 completed_at: Some(completed_at),
1262 ..StepUpdate::default()
1263 },
1264 )
1265 .await?;
1266
1267 info!(
1268 run_id = %self.run_id,
1269 step = %name,
1270 kind = op.kind(),
1271 duration_ms,
1272 "operation step completed"
1273 );
1274
1275 self.last_step_ids = vec![step.id];
1276
1277 Ok(StepOutput {
1278 output: output_value,
1279 duration_ms,
1280 cost_usd: Decimal::ZERO,
1281 input_tokens: None,
1282 output_tokens: None,
1283 model: None,
1284 debug_messages: None,
1285 })
1286 }
1287 Err(err) => {
1288 let completed_at = Utc::now();
1289 if let Err(store_err) = self
1290 .store
1291 .update_step(
1292 step.id,
1293 StepUpdate {
1294 status: Some(StepStatus::Failed),
1295 error: Some(err.to_string()),
1296 completed_at: Some(completed_at),
1297 ..StepUpdate::default()
1298 },
1299 )
1300 .await
1301 {
1302 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1303 }
1304
1305 Err(err)
1306 }
1307 }
1308 }
1309
1310 pub async fn workflow(
1337 &mut self,
1338 handler: &dyn WorkflowHandler,
1339 payload: Value,
1340 ) -> Result<StepOutput, EngineError> {
1341 let config = WorkflowStepConfig::new(handler.name(), payload);
1342 let position = self.position;
1343 self.position += 1;
1344
1345 let step = self
1346 .store
1347 .create_step(NewStep {
1348 run_id: self.run_id,
1349 name: config.workflow_name.clone(),
1350 kind: StepKind::Workflow,
1351 position,
1352 input: Some(serde_json::to_value(&config)?),
1353 is_error_handler: false,
1354 })
1355 .await?;
1356
1357 self.start_step(step.id, Utc::now()).await?;
1358
1359 match self.execute_child_workflow(&config).await {
1360 Ok((output, child_had_allowed_failure)) => {
1361 self.total_cost_usd += output.cost_usd;
1362 self.total_duration_ms += output.duration_ms;
1363 if child_had_allowed_failure {
1364 self.has_allowed_failure = true;
1365 }
1366
1367 let completed_at = Utc::now();
1368 self.store
1369 .update_step(
1370 step.id,
1371 StepUpdate {
1372 status: Some(StepStatus::Completed),
1373 output: Some(output.output.clone()),
1374 duration_ms: Some(output.duration_ms),
1375 cost_usd: Some(output.cost_usd),
1376 completed_at: Some(completed_at),
1377 ..StepUpdate::default()
1378 },
1379 )
1380 .await?;
1381
1382 info!(
1383 run_id = %self.run_id,
1384 child_workflow = %config.workflow_name,
1385 duration_ms = output.duration_ms,
1386 "workflow step completed"
1387 );
1388
1389 self.last_step_ids = vec![step.id];
1390
1391 Ok(output)
1392 }
1393 Err(err) => {
1394 let completed_at = Utc::now();
1395 if let Err(store_err) = self
1396 .store
1397 .update_step(
1398 step.id,
1399 StepUpdate {
1400 status: Some(StepStatus::Failed),
1401 error: Some(err.to_string()),
1402 completed_at: Some(completed_at),
1403 ..StepUpdate::default()
1404 },
1405 )
1406 .await
1407 {
1408 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1409 }
1410
1411 Err(err)
1412 }
1413 }
1414 }
1415
1416 async fn execute_child_workflow(
1419 &self,
1420 config: &WorkflowStepConfig,
1421 ) -> Result<(StepOutput, bool), EngineError> {
1422 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1423 EngineError::InvalidWorkflow(
1424 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1425 )
1426 })?;
1427
1428 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1429 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1430 })?;
1431
1432 let parent = self.store.get_run(self.run_id).await?;
1435 let (parent_labels, parent_author) =
1436 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1437
1438 let child_run = self
1439 .store
1440 .create_run(NewRun {
1441 workflow_name: config.workflow_name.clone(),
1442 trigger: TriggerKind::Workflow,
1443 payload: config.payload.clone(),
1444 max_retries: 0,
1445 handler_version: None,
1446 labels: parent_labels,
1447 scheduled_at: None,
1448 created_by: parent_author,
1449 idempotency_key: None,
1450 max_cost_usd: self.max_cost_usd,
1452 })
1453 .await?
1454 .into_run();
1455
1456 let child_run_id = child_run.id;
1457 info!(
1458 parent_run_id = %self.run_id,
1459 child_run_id = %child_run_id,
1460 workflow = %config.workflow_name,
1461 "child run created"
1462 );
1463
1464 self.store
1465 .update_run_status(child_run_id, RunStatus::Running)
1466 .await?;
1467
1468 let run_start = Instant::now();
1469 let mut child_ctx = WorkflowContext {
1470 run_id: child_run_id,
1471 store: self.store.clone(),
1472 provider: self.provider.clone(),
1473 handler_resolver: self.handler_resolver.clone(),
1474 position: 0,
1475 last_step_ids: Vec::new(),
1476 total_cost_usd: Decimal::ZERO,
1477 total_duration_ms: 0,
1478 max_cost_usd: self.max_cost_usd,
1479 inherited_cost_usd: self.charged_cost_usd(),
1482 replay_steps: HashMap::new(),
1483 granted_approvals: HashMap::new(),
1484 attempt: 1,
1486 carried_duration_ms: 0,
1487 log_sender: self.log_sender.clone(),
1488 artifact_sink: self.artifact_sink.clone(),
1491 has_allowed_failure: false,
1492 error_handlers: Vec::new(),
1493 };
1494
1495 let result = handler.execute(&mut child_ctx).await;
1496 let total_duration = run_start.elapsed().as_millis() as u64;
1497 let completed_at = Utc::now();
1498
1499 match result {
1500 Ok(()) => {
1501 let child_status = if child_ctx.has_allowed_failure {
1502 RunStatus::Warning
1503 } else {
1504 RunStatus::Completed
1505 };
1506 self.store
1507 .update_run(
1508 child_run_id,
1509 RunUpdate {
1510 status: Some(child_status),
1511 cost_usd: Some(child_ctx.total_cost_usd),
1512 duration_ms: Some(total_duration),
1513 completed_at: Some(completed_at),
1514 ..RunUpdate::default()
1515 },
1516 )
1517 .await?;
1518
1519 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1520 Ok((
1521 StepOutput {
1522 output: serde_json::json!({
1523 "run_id": child_run_id,
1524 "workflow_name": config.workflow_name,
1525 "status": child_status,
1526 "cost_usd": child_ctx.total_cost_usd,
1527 "duration_ms": total_duration,
1528 }),
1529 duration_ms: total_duration,
1530 cost_usd: child_ctx.total_cost_usd,
1531 input_tokens: None,
1532 output_tokens: None,
1533 model: None,
1534 debug_messages: None,
1535 },
1536 child_had_allowed_failure,
1537 ))
1538 }
1539 Err(err) => {
1540 if let Err(store_err) = self
1541 .store
1542 .update_run(
1543 child_run_id,
1544 RunUpdate {
1545 status: Some(RunStatus::Failed),
1546 error: Some(err.to_string()),
1547 cost_usd: Some(child_ctx.total_cost_usd),
1548 duration_ms: Some(total_duration),
1549 completed_at: Some(completed_at),
1550 ..RunUpdate::default()
1551 },
1552 )
1553 .await
1554 {
1555 error!(
1556 child_run_id = %child_run_id,
1557 store_error = %store_err,
1558 "failed to persist child run failure"
1559 );
1560 }
1561
1562 Err(err)
1563 }
1564 }
1565 }
1566
1567 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1572 let step = self.replay_steps.get(&position)?;
1573 if step.status.state != StepStatus::Completed {
1574 return None;
1575 }
1576 let output = StepOutput {
1577 output: step.output.clone().unwrap_or(Value::Null),
1578 duration_ms: step.duration_ms,
1579 cost_usd: step.cost_usd,
1580 input_tokens: step.input_tokens,
1581 output_tokens: step.output_tokens,
1582 model: None,
1583 debug_messages: None,
1584 };
1585 self.total_cost_usd += output.cost_usd;
1586 self.total_duration_ms += output.duration_ms;
1587 self.last_step_ids = vec![step.id];
1588 info!(
1589 run_id = %self.run_id,
1590 step = %step.name,
1591 position,
1592 "step replayed from previous execution"
1593 );
1594 Some(output)
1595 }
1596
1597 async fn execute_step(
1599 &mut self,
1600 name: &str,
1601 kind: StepKind,
1602 config: StepConfig,
1603 ) -> Result<StepOutput, EngineError> {
1604 let position = self.position;
1605 self.position += 1;
1606
1607 if let Some(output) = self.try_replay_step(position) {
1609 return Ok(output);
1610 }
1611
1612 if let StepConfig::Agent(ref agent_config) = config {
1615 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1616 }
1617
1618 let step = self
1620 .store
1621 .create_step(NewStep {
1622 run_id: self.run_id,
1623 name: name.to_string(),
1624 kind,
1625 position,
1626 input: Some(serde_json::to_value(&config)?),
1627 is_error_handler: false,
1628 })
1629 .await?;
1630
1631 self.start_step(step.id, Utc::now()).await?;
1632
1633 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1636 self.fail_step(step.id, &err).await;
1637 if config.allow_failure() {
1638 self.has_allowed_failure = true;
1639 self.last_step_ids = vec![step.id];
1640 info!(
1641 run_id = %self.run_id,
1642 step = %name,
1643 error = %err,
1644 "step input preparation failed but allow_failure is set, continuing"
1645 );
1646 return Ok(StepOutput {
1647 output: json!({"error": err.to_string()}),
1648 duration_ms: 0,
1649 cost_usd: Decimal::ZERO,
1650 input_tokens: None,
1651 output_tokens: None,
1652 model: None,
1653 debug_messages: None,
1654 });
1655 }
1656 return Err(err);
1657 }
1658
1659 let step_log_sender = self
1660 .log_sender
1661 .as_ref()
1662 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1663
1664 let execution = execute_step_config(&config, &self.provider, step_log_sender).await;
1665
1666 if let Err(err) = self
1667 .store_step_outputs(&config, step.id, name, execution.is_ok())
1668 .await
1669 {
1670 self.fail_step(step.id, &err).await;
1671 return Err(err);
1672 }
1673
1674 match execution {
1675 Ok(output) => {
1676 self.total_cost_usd += output.cost_usd;
1677 self.total_duration_ms += output.duration_ms;
1678
1679 let debug_messages_json = output.debug_messages_json();
1680
1681 let completed_at = Utc::now();
1682 self.store
1683 .update_step(
1684 step.id,
1685 StepUpdate {
1686 status: Some(StepStatus::Completed),
1687 output: Some(output.output.clone()),
1688 duration_ms: Some(output.duration_ms),
1689 cost_usd: Some(output.cost_usd),
1690 input_tokens: output.input_tokens,
1691 output_tokens: output.output_tokens,
1692 completed_at: Some(completed_at),
1693 debug_messages: debug_messages_json,
1694 ..StepUpdate::default()
1695 },
1696 )
1697 .await?;
1698
1699 info!(
1700 run_id = %self.run_id,
1701 step = %name,
1702 duration_ms = output.duration_ms,
1703 "step completed"
1704 );
1705
1706 self.last_step_ids = vec![step.id];
1707
1708 Ok(output)
1709 }
1710 Err(err) => {
1711 let completed_at = Utc::now();
1712 let debug_messages_json = extract_debug_messages_from_error(&err);
1713 let partial = extract_partial_usage_from_error(&err);
1714 let raw_response_output = extract_raw_response_from_error(&err);
1715
1716 if let Some(ref usage) = partial {
1717 if let Some(cost) = usage.cost_usd {
1718 self.total_cost_usd += cost;
1719 }
1720 if let Some(dur) = usage.duration_ms {
1721 self.total_duration_ms += dur;
1722 }
1723 }
1724
1725 if let Err(store_err) = self
1726 .store
1727 .update_step(
1728 step.id,
1729 StepUpdate {
1730 status: Some(StepStatus::Failed),
1731 error: Some(err.to_string()),
1732 output: raw_response_output.clone(),
1733 completed_at: Some(completed_at),
1734 debug_messages: debug_messages_json,
1735 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1736 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1737 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1738 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1739 ..StepUpdate::default()
1740 },
1741 )
1742 .await
1743 {
1744 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1745 }
1746
1747 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1748 self.fire_error_handlers(name, &err.to_string(), err_duration)
1749 .await;
1750
1751 if config.allow_failure() {
1752 self.has_allowed_failure = true;
1753 self.last_step_ids = vec![step.id];
1754 info!(
1755 run_id = %self.run_id,
1756 step = %name,
1757 error = %err,
1758 "step failed but allow_failure is set, continuing"
1759 );
1760 Ok(allowed_failure_output(
1761 &err.to_string(),
1762 raw_response_output,
1763 partial.as_ref(),
1764 ))
1765 } else {
1766 Err(err)
1767 }
1768 }
1769 }
1770 }
1771
1772 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1777 if !self.last_step_ids.is_empty() {
1778 let deps: Vec<NewStepDependency> = self
1779 .last_step_ids
1780 .iter()
1781 .map(|&depends_on| NewStepDependency {
1782 step_id,
1783 depends_on,
1784 })
1785 .collect();
1786 self.store.create_step_dependencies(deps).await?;
1787 }
1788
1789 self.store
1790 .update_step(
1791 step_id,
1792 StepUpdate {
1793 status: Some(StepStatus::Running),
1794 started_at: Some(now),
1795 ..StepUpdate::default()
1796 },
1797 )
1798 .await?;
1799
1800 Ok(())
1801 }
1802
1803 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
1810 if let Err(store_err) = self
1811 .store
1812 .update_step(
1813 step_id,
1814 StepUpdate {
1815 status: Some(StepStatus::Failed),
1816 error: Some(err.to_string()),
1817 completed_at: Some(Utc::now()),
1818 ..StepUpdate::default()
1819 },
1820 )
1821 .await
1822 {
1823 error!(
1824 step_id = %step_id,
1825 error = %store_err,
1826 "failed to persist step failure"
1827 );
1828 }
1829 }
1830
1831 pub fn store(&self) -> &Arc<dyn Store> {
1833 &self.store
1834 }
1835
1836 pub async fn payload(&self) -> Result<Value, EngineError> {
1844 let run = self
1845 .store
1846 .get_run(self.run_id)
1847 .await?
1848 .ok_or(EngineError::Store(
1849 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1850 ))?;
1851 Ok(run.payload)
1852 }
1853
1854 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
1877 self.error_handlers.push(OnErrorHandler {
1878 name: name.to_string(),
1879 config: config.into(),
1880 });
1881 }
1882
1883 pub fn clear_error_handlers(&mut self) {
1902 self.error_handlers.clear();
1903 }
1904
1905 async fn fire_error_handlers(
1911 &mut self,
1912 failed_step_name: &str,
1913 error_msg: &str,
1914 duration_ms: u64,
1915 ) {
1916 let handlers = std::mem::take(&mut self.error_handlers);
1917 if handlers.is_empty() {
1918 return;
1919 }
1920
1921 let error_context = json!({
1922 "failed_step": failed_step_name,
1923 "error": error_msg,
1924 "duration_ms": duration_ms,
1925 });
1926
1927 for handler in handlers {
1928 let mut config = handler.config.clone();
1929 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
1930
1931 let position = self.position;
1932 self.position += 1;
1933
1934 let step = match self
1935 .store
1936 .create_step(NewStep {
1937 run_id: self.run_id,
1938 name: handler.name.clone(),
1939 kind: config.kind(),
1940 position,
1941 input: Some(error_context.clone()),
1942 is_error_handler: true,
1943 })
1944 .await
1945 {
1946 Ok(step) => step,
1947 Err(err) => {
1948 warn!(
1949 run_id = %self.run_id,
1950 handler = %handler.name,
1951 error = %err,
1952 "failed to create error handler step"
1953 );
1954 continue;
1955 }
1956 };
1957
1958 if let Err(err) = self.start_step(step.id, Utc::now()).await {
1959 warn!(
1960 run_id = %self.run_id,
1961 handler = %handler.name,
1962 error = %err,
1963 "failed to start error handler step"
1964 );
1965 continue;
1966 }
1967
1968 let step_log_sender = self
1969 .log_sender
1970 .as_ref()
1971 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
1972
1973 let start = Instant::now();
1974 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
1975 let handler_duration = start.elapsed().as_millis() as u64;
1976 let completed_at = Utc::now();
1977
1978 match result {
1979 Ok(output) => {
1980 if let Err(store_err) = self
1981 .store
1982 .update_step(
1983 step.id,
1984 StepUpdate {
1985 status: Some(StepStatus::Completed),
1986 output: Some(output.output),
1987 duration_ms: Some(handler_duration),
1988 cost_usd: Some(output.cost_usd),
1989 completed_at: Some(completed_at),
1990 ..StepUpdate::default()
1991 },
1992 )
1993 .await
1994 {
1995 warn!(
1996 run_id = %self.run_id,
1997 handler = %handler.name,
1998 error = %store_err,
1999 "failed to persist error handler completion"
2000 );
2001 }
2002
2003 info!(
2004 run_id = %self.run_id,
2005 handler = %handler.name,
2006 duration_ms = handler_duration,
2007 "error handler completed"
2008 );
2009 }
2010 Err(err) => {
2011 if let Err(store_err) = self
2012 .store
2013 .update_step(
2014 step.id,
2015 StepUpdate {
2016 status: Some(StepStatus::Failed),
2017 error: Some(err.to_string()),
2018 duration_ms: Some(handler_duration),
2019 completed_at: Some(completed_at),
2020 ..StepUpdate::default()
2021 },
2022 )
2023 .await
2024 {
2025 warn!(
2026 run_id = %self.run_id,
2027 handler = %handler.name,
2028 error = %store_err,
2029 "failed to persist error handler failure"
2030 );
2031 }
2032
2033 warn!(
2034 run_id = %self.run_id,
2035 handler = %handler.name,
2036 error = %err,
2037 "error handler failed (original error preserved)"
2038 );
2039 }
2040 }
2041 }
2042 }
2043}
2044
2045fn inject_error_context(
2047 config: &mut StepConfig,
2048 failed_step: &str,
2049 error_msg: &str,
2050 duration_ms: u64,
2051) {
2052 match config {
2053 StepConfig::Shell(shell) => {
2054 shell
2055 .env
2056 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2057 shell
2058 .env
2059 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2060 shell.env.push((
2061 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2062 duration_ms.to_string(),
2063 ));
2064 }
2065 StepConfig::Agent(agent) => {
2066 agent.prompt = format!(
2067 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2068 failed_step, duration_ms, error_msg, agent.prompt
2069 );
2070 }
2071 StepConfig::Http(http) => {
2072 http.headers
2073 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2074 http.headers.push((
2075 "X-Ironflow-Error-Message".to_string(),
2076 error_msg.to_string(),
2077 ));
2078 }
2079 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2080 }
2081}
2082
2083fn allowed_failure_output(
2084 error_msg: &str,
2085 raw_response: Option<Value>,
2086 partial: Option<&StepPartialUsage>,
2087) -> StepOutput {
2088 StepOutput {
2089 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2090 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2091 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2092 input_tokens: partial.and_then(|p| p.input_tokens),
2093 output_tokens: partial.and_then(|p| p.output_tokens),
2094 model: None,
2095 debug_messages: None,
2096 }
2097}
2098
2099impl fmt::Debug for WorkflowContext {
2100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2101 f.debug_struct("WorkflowContext")
2102 .field("run_id", &self.run_id)
2103 .field("position", &self.position)
2104 .field("total_cost_usd", &self.total_cost_usd)
2105 .field("inherited_cost_usd", &self.inherited_cost_usd)
2106 .field("max_cost_usd", &self.max_cost_usd)
2107 .finish_non_exhaustive()
2108 }
2109}
2110
2111fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2114 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2115 debug_messages,
2116 ..
2117 })) = err
2118 && !debug_messages.is_empty()
2119 {
2120 return serde_json::to_value(debug_messages).ok();
2121 }
2122 None
2123}
2124
2125struct StepPartialUsage {
2131 cost_usd: Option<Decimal>,
2132 duration_ms: Option<u64>,
2133 input_tokens: Option<u64>,
2134 output_tokens: Option<u64>,
2135}
2136
2137fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2143 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2144 raw_response: Some(text),
2145 ..
2146 })) = err
2147 {
2148 return Some(Value::String(text.clone()));
2149 }
2150 None
2151}
2152
2153fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2154 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2155 partial_usage,
2156 ..
2157 })) = err
2158 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2159 {
2160 return Some(StepPartialUsage {
2161 cost_usd: partial_usage
2162 .cost_usd
2163 .and_then(|c| Decimal::try_from(c).ok()),
2164 duration_ms: partial_usage.duration_ms,
2165 input_tokens: partial_usage.input_tokens,
2166 output_tokens: partial_usage.output_tokens,
2167 });
2168 }
2169 None
2170}
2171
2172#[cfg(test)]
2173mod tests {
2174 use super::*;
2175 use ironflow_core::providers::claude::ClaudeCodeProvider;
2176 use ironflow_core::providers::record_replay::RecordReplayProvider;
2177 use ironflow_store::memory::InMemoryStore;
2178 use ironflow_store::models::{Run, RunActor, RunFilter};
2179 use ironflow_store::store::RunStore;
2180 use serde_json::json;
2181 use std::sync::Arc;
2182 use std::sync::atomic::{AtomicBool, Ordering};
2183 use uuid::Uuid;
2184
2185 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2187 let inner = ClaudeCodeProvider::new();
2188 Arc::new(RecordReplayProvider::replay(
2189 inner,
2190 "/tmp/ironflow-fixtures",
2191 ))
2192 }
2193
2194 fn create_test_context() -> WorkflowContext {
2196 let store = Arc::new(InMemoryStore::new());
2197 let provider = create_test_provider();
2198 let run_id = Uuid::now_v7();
2199 WorkflowContext::new(run_id, store, provider)
2200 }
2201
2202 #[test]
2203 fn context_new_initializes_correctly() {
2204 let ctx = create_test_context();
2205 assert_eq!(ctx.position, 0);
2206 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2207 assert_eq!(ctx.total_duration_ms, 0);
2208 assert!(ctx.last_step_ids.is_empty());
2209 assert!(ctx.replay_steps.is_empty());
2210 assert!(ctx.log_sender.is_none());
2211 }
2212
2213 #[test]
2214 fn context_run_id_returns_correct_id() {
2215 let run_id = Uuid::now_v7();
2216 let store = Arc::new(InMemoryStore::new());
2217 let provider = create_test_provider();
2218 let ctx = WorkflowContext::new(run_id, store, provider);
2219 assert_eq!(ctx.run_id(), run_id);
2220 }
2221
2222 #[test]
2223 fn context_total_cost_usd_initially_zero() {
2224 let ctx = create_test_context();
2225 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2226 }
2227
2228 #[test]
2229 fn context_total_duration_ms_initially_zero() {
2230 let ctx = create_test_context();
2231 assert_eq!(ctx.total_duration_ms(), 0);
2232 }
2233
2234 #[test]
2235 fn context_with_handler_resolver_creates_context_with_resolver() {
2236 let store = Arc::new(InMemoryStore::new());
2237 let provider = create_test_provider();
2238 let run_id = Uuid::now_v7();
2239
2240 let called = Arc::new(AtomicBool::new(false));
2241 let called_clone = called.clone();
2242
2243 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2244 called_clone.store(true, Ordering::SeqCst);
2245 None
2246 });
2247
2248 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2249
2250 assert_eq!(ctx.run_id(), run_id);
2251 assert!(ctx.handler_resolver.is_some());
2252 }
2253
2254 #[tokio::test]
2255 async fn context_set_log_sender_attaches_sender() {
2256 let mut ctx = create_test_context();
2257 let (sender, _receiver) = crate::log_sender::channel();
2258 ctx.set_log_sender(sender);
2259 assert!(ctx.log_sender.is_some());
2260 }
2261
2262 #[tokio::test]
2263 async fn context_skip_creates_skipped_step() {
2264 let store = Arc::new(InMemoryStore::new());
2265 let provider = create_test_provider();
2266
2267 store
2269 .create_run(NewRun {
2270 created_by: None,
2271 workflow_name: "test".to_string(),
2272 trigger: TriggerKind::Manual,
2273 payload: json!({}),
2274 max_retries: 0,
2275 handler_version: None,
2276 labels: Default::default(),
2277 scheduled_at: None,
2278 idempotency_key: None,
2279 max_cost_usd: None,
2280 })
2281 .await
2282 .expect("failed to create run")
2283 .into_run();
2284
2285 let runs = store
2287 .list_runs(RunFilter::default(), 1, 10)
2288 .await
2289 .expect("failed to list runs");
2290 let created_run_id = runs.items[0].id;
2291
2292 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2293 let initial_position = ctx.position;
2294
2295 ctx.skip("skip-step", "condition not met")
2296 .await
2297 .expect("skip failed");
2298
2299 assert_eq!(ctx.position, initial_position + 1);
2300 assert!(!ctx.last_step_ids.is_empty());
2301
2302 let steps = store
2304 .list_steps(created_run_id)
2305 .await
2306 .expect("failed to list steps");
2307 assert_eq!(steps.len(), 1);
2308 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2309 }
2310
2311 struct NoopSubWorkflow;
2314
2315 impl WorkflowHandler for NoopSubWorkflow {
2316 fn name(&self) -> &str {
2317 "noop-sub"
2318 }
2319
2320 fn execute<'a>(
2321 &'a self,
2322 _ctx: &'a mut WorkflowContext,
2323 ) -> crate::handler::HandlerFuture<'a> {
2324 Box::pin(async move { Ok(()) })
2325 }
2326 }
2327
2328 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2331 let store = Arc::new(InMemoryStore::new());
2332 let provider = create_test_provider();
2333
2334 let parent = store
2335 .create_run(NewRun {
2336 workflow_name: "parent".to_string(),
2337 trigger: TriggerKind::Api,
2338 payload: json!({}),
2339 max_retries: 0,
2340 handler_version: None,
2341 labels: Default::default(),
2342 scheduled_at: None,
2343 created_by,
2344 idempotency_key: None,
2345 max_cost_usd: None,
2346 })
2347 .await
2348 .expect("failed to create parent run")
2349 .into_run();
2350
2351 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2352 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2353 _ => None,
2354 });
2355
2356 let mut ctx =
2357 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2358 ctx.workflow(&NoopSubWorkflow, json!({}))
2359 .await
2360 .expect("sub-workflow failed");
2361
2362 let runs = store
2363 .list_runs(RunFilter::default(), 1, 10)
2364 .await
2365 .expect("failed to list runs");
2366 runs.items
2367 .into_iter()
2368 .find(|r| r.workflow_name == "noop-sub")
2369 .expect("child run was created")
2370 }
2371
2372 #[tokio::test]
2373 async fn child_run_inherits_the_parent_author() {
2374 let user_id = Uuid::now_v7();
2375 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2376
2377 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2378 }
2379
2380 #[tokio::test]
2381 async fn child_run_of_an_unattributed_parent_has_no_author() {
2382 let child = child_run_of_parent_authored_by(None).await;
2383
2384 assert!(child.created_by.is_none());
2385 }
2386
2387 #[tokio::test]
2388 async fn context_parallel_empty_steps_returns_empty_vec() {
2389 let mut ctx = create_test_context();
2390 let results = ctx
2391 .parallel(vec![], true)
2392 .await
2393 .expect("parallel should not fail on empty input");
2394 assert!(results.is_empty());
2395 }
2396
2397 #[tokio::test]
2398 async fn context_approval_first_execution_returns_error() {
2399 let store = Arc::new(InMemoryStore::new());
2400 let provider = create_test_provider();
2401
2402 store
2404 .create_run(NewRun {
2405 created_by: None,
2406 workflow_name: "test".to_string(),
2407 trigger: TriggerKind::Manual,
2408 payload: json!({}),
2409 max_retries: 0,
2410 handler_version: None,
2411 labels: Default::default(),
2412 scheduled_at: None,
2413 idempotency_key: None,
2414 max_cost_usd: None,
2415 })
2416 .await
2417 .expect("failed to create run")
2418 .into_run();
2419
2420 let runs = store
2422 .list_runs(RunFilter::default(), 1, 10)
2423 .await
2424 .expect("failed to list runs");
2425 let created_run_id = runs.items[0].id;
2426
2427 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2428
2429 let result = ctx
2430 .approval(
2431 "approve-step",
2432 crate::config::ApprovalConfig::new("Continue?"),
2433 )
2434 .await;
2435
2436 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2438
2439 assert_eq!(ctx.position, 1);
2441
2442 let steps = store
2444 .list_steps(created_run_id)
2445 .await
2446 .expect("failed to list steps");
2447 assert_eq!(steps.len(), 1);
2448 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2449 }
2450
2451 #[tokio::test]
2452 async fn context_approval_replay_returns_ok() {
2453 let store = Arc::new(InMemoryStore::new());
2454 let provider = create_test_provider();
2455
2456 store
2458 .create_run(NewRun {
2459 created_by: None,
2460 workflow_name: "test".to_string(),
2461 trigger: TriggerKind::Manual,
2462 payload: json!({}),
2463 max_retries: 0,
2464 handler_version: None,
2465 labels: Default::default(),
2466 scheduled_at: None,
2467 idempotency_key: None,
2468 max_cost_usd: None,
2469 })
2470 .await
2471 .expect("failed to create run")
2472 .into_run();
2473
2474 let runs = store
2476 .list_runs(RunFilter::default(), 1, 10)
2477 .await
2478 .expect("failed to list runs");
2479 let created_run_id = runs.items[0].id;
2480
2481 let step = store
2483 .create_step(NewStep {
2484 run_id: created_run_id,
2485 name: "approval".to_string(),
2486 kind: StepKind::Approval,
2487 position: 0,
2488 input: None,
2489 is_error_handler: false,
2490 })
2491 .await
2492 .expect("failed to create step");
2493
2494 store
2496 .update_step(
2497 step.id,
2498 StepUpdate {
2499 status: Some(StepStatus::Running),
2500 started_at: Some(Utc::now()),
2501 ..StepUpdate::default()
2502 },
2503 )
2504 .await
2505 .expect("failed to update step to Running");
2506
2507 store
2508 .update_step(
2509 step.id,
2510 StepUpdate {
2511 status: Some(StepStatus::AwaitingApproval),
2512 ..StepUpdate::default()
2513 },
2514 )
2515 .await
2516 .expect("failed to update step to AwaitingApproval");
2517
2518 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2520 ctx.load_replay_steps()
2521 .await
2522 .expect("failed to load replay steps");
2523
2524 let result = ctx
2526 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2527 .await;
2528
2529 assert!(result.is_ok());
2530
2531 let steps = store
2533 .list_steps(created_run_id)
2534 .await
2535 .expect("failed to list steps");
2536 assert_eq!(steps.len(), 1);
2537 assert_eq!(steps[0].status.state, StepStatus::Completed);
2538 }
2539
2540 #[tokio::test]
2541 async fn context_load_replay_steps_loads_completed_steps() {
2542 let store = Arc::new(InMemoryStore::new());
2543 let provider = create_test_provider();
2544
2545 store
2547 .create_run(NewRun {
2548 created_by: None,
2549 workflow_name: "test".to_string(),
2550 trigger: TriggerKind::Manual,
2551 payload: json!({}),
2552 max_retries: 0,
2553 handler_version: None,
2554 labels: Default::default(),
2555 scheduled_at: None,
2556 idempotency_key: None,
2557 max_cost_usd: None,
2558 })
2559 .await
2560 .expect("failed to create run")
2561 .into_run();
2562
2563 let runs = store
2565 .list_runs(RunFilter::default(), 1, 10)
2566 .await
2567 .expect("failed to list runs");
2568 let created_run_id = runs.items[0].id;
2569
2570 let completed_step = store
2572 .create_step(NewStep {
2573 run_id: created_run_id,
2574 name: "completed".to_string(),
2575 kind: StepKind::Shell,
2576 position: 0,
2577 input: None,
2578 is_error_handler: false,
2579 })
2580 .await
2581 .expect("failed to create step");
2582
2583 store
2585 .update_step(
2586 completed_step.id,
2587 StepUpdate {
2588 status: Some(StepStatus::Running),
2589 started_at: Some(Utc::now()),
2590 ..StepUpdate::default()
2591 },
2592 )
2593 .await
2594 .expect("failed to update step to Running");
2595
2596 store
2597 .update_step(
2598 completed_step.id,
2599 StepUpdate {
2600 status: Some(StepStatus::Completed),
2601 completed_at: Some(Utc::now()),
2602 ..StepUpdate::default()
2603 },
2604 )
2605 .await
2606 .expect("failed to update step to Completed");
2607
2608 let _pending_step = store
2609 .create_step(NewStep {
2610 run_id: created_run_id,
2611 name: "pending".to_string(),
2612 kind: StepKind::Shell,
2613 position: 1,
2614 input: None,
2615 is_error_handler: false,
2616 })
2617 .await
2618 .expect("failed to create step");
2619
2620 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2622 ctx.load_replay_steps()
2623 .await
2624 .expect("failed to load replay steps");
2625
2626 assert_eq!(ctx.replay_steps.len(), 1);
2628 assert!(ctx.replay_steps.contains_key(&0));
2629 assert!(!ctx.replay_steps.contains_key(&1));
2630 }
2631
2632 #[tokio::test]
2633 async fn context_payload_returns_run_payload() {
2634 let store = Arc::new(InMemoryStore::new());
2635 let provider = create_test_provider();
2636 let test_payload = json!({"key": "value", "number": 42});
2637
2638 store
2640 .create_run(NewRun {
2641 created_by: None,
2642 workflow_name: "test".to_string(),
2643 trigger: TriggerKind::Manual,
2644 payload: test_payload.clone(),
2645 max_retries: 0,
2646 handler_version: None,
2647 labels: Default::default(),
2648 scheduled_at: None,
2649 idempotency_key: None,
2650 max_cost_usd: None,
2651 })
2652 .await
2653 .expect("failed to create run")
2654 .into_run();
2655
2656 let runs = store
2658 .list_runs(RunFilter::default(), 1, 10)
2659 .await
2660 .expect("failed to list runs");
2661 let created_run_id = runs.items[0].id;
2662
2663 let ctx = WorkflowContext::new(created_run_id, store, provider);
2664 let payload = ctx.payload().await.expect("failed to get payload");
2665
2666 assert_eq!(payload, test_payload);
2667 }
2668
2669 #[tokio::test]
2670 async fn context_payload_returns_error_for_nonexistent_run() {
2671 let store = Arc::new(InMemoryStore::new());
2672 let provider = create_test_provider();
2673 let run_id = Uuid::now_v7();
2674
2675 let ctx = WorkflowContext::new(run_id, store, provider);
2676 let result = ctx.payload().await;
2677
2678 assert!(result.is_err());
2679 }
2680
2681 #[tokio::test]
2682 async fn context_store_returns_reference() {
2683 let ctx = create_test_context();
2684 let _store = ctx.store();
2685 }
2687
2688 #[test]
2689 fn context_debug_formatting() {
2690 let ctx = create_test_context();
2691 let debug_str = format!("{:?}", ctx);
2692 assert!(debug_str.contains("WorkflowContext"));
2693 assert!(debug_str.contains("run_id"));
2694 }
2695
2696 #[tokio::test]
2697 async fn context_last_step_ids_tracks_executed_steps() {
2698 let store = Arc::new(InMemoryStore::new());
2699 let provider = create_test_provider();
2700
2701 store
2703 .create_run(NewRun {
2704 created_by: None,
2705 workflow_name: "test".to_string(),
2706 trigger: TriggerKind::Manual,
2707 payload: json!({}),
2708 max_retries: 0,
2709 handler_version: None,
2710 labels: Default::default(),
2711 scheduled_at: None,
2712 idempotency_key: None,
2713 max_cost_usd: None,
2714 })
2715 .await
2716 .expect("failed to create run")
2717 .into_run();
2718
2719 let runs = store
2721 .list_runs(RunFilter::default(), 1, 10)
2722 .await
2723 .expect("failed to list runs");
2724 let created_run_id = runs.items[0].id;
2725
2726 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2727 assert!(ctx.last_step_ids.is_empty());
2728
2729 ctx.skip("step1", "reason").await.expect("skip failed");
2730
2731 assert_eq!(ctx.last_step_ids.len(), 1);
2732
2733 ctx.skip("step2", "reason").await.expect("skip failed");
2734
2735 assert_eq!(ctx.last_step_ids.len(), 1);
2737 }
2738}