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::{Span, 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, step_trace_id,
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, StepResult, 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 step_results: Vec<StepResult>,
127 event_bus: Option<crate::notify::WorkflowEventBus>,
129}
130
131struct OnErrorHandler {
133 name: String,
134 config: StepConfig,
135}
136
137impl WorkflowContext {
138 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
143 Self {
144 run_id,
145 store,
146 provider,
147 handler_resolver: None,
148 position: 0,
149 last_step_ids: Vec::new(),
150 total_cost_usd: Decimal::ZERO,
151 total_duration_ms: 0,
152 max_cost_usd: None,
153 inherited_cost_usd: Decimal::ZERO,
154 replay_steps: HashMap::new(),
155 granted_approvals: HashMap::new(),
156 attempt: 1,
157 carried_duration_ms: 0,
158 log_sender: None,
159 artifact_sink: None,
160 has_allowed_failure: false,
161 error_handlers: Vec::new(),
162 step_results: Vec::new(),
163 event_bus: None,
164 }
165 }
166
167 pub(crate) fn with_handler_resolver(
172 run_id: Uuid,
173 store: Arc<dyn Store>,
174 provider: Arc<dyn AgentProvider>,
175 resolver: HandlerResolver,
176 ) -> Self {
177 Self {
178 run_id,
179 store,
180 provider,
181 handler_resolver: Some(resolver),
182 position: 0,
183 last_step_ids: Vec::new(),
184 total_cost_usd: Decimal::ZERO,
185 total_duration_ms: 0,
186 max_cost_usd: None,
187 inherited_cost_usd: Decimal::ZERO,
188 replay_steps: HashMap::new(),
189 granted_approvals: HashMap::new(),
190 attempt: 1,
191 carried_duration_ms: 0,
192 log_sender: None,
193 artifact_sink: None,
194 has_allowed_failure: false,
195 error_handlers: Vec::new(),
196 step_results: Vec::new(),
197 event_bus: None,
198 }
199 }
200
201 pub fn set_log_sender(&mut self, sender: LogSender) {
203 self.log_sender = Some(sender);
204 }
205
206 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
226 self.artifact_sink = Some(sink);
227 }
228
229 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
235 self.event_bus = Some(bus);
236 }
237
238 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
240 self.artifact_sink.as_ref().ok_or_else(|| {
241 EngineError::ArtifactsUnavailable(
242 "no artifact storage is attached to this run".to_string(),
243 )
244 })
245 }
246
247 pub async fn put_artifact(
277 &self,
278 step_id: Uuid,
279 name: &str,
280 content_type: Option<&str>,
281 content: Vec<u8>,
282 ) -> Result<Artifact, EngineError> {
283 let sink = self.artifact_sink()?;
284 sink.put(
285 ArtifactUpload {
286 run_id: self.run_id,
287 step_id,
288 name: name.to_string(),
289 content_type: content_type
290 .map(str::to_string)
291 .unwrap_or_else(|| guess_content_type(name)),
292 },
293 stream_from_bytes(content),
294 )
295 .await
296 }
297
298 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
323 let sink = self.artifact_sink()?;
324
325 let artifact = self
326 .store
327 .find_artifact_for_input(ArtifactLookup {
328 run_id: self.run_id,
329 attempt: self.attempt,
330 before_position: self.position,
331 step_name: step.to_string(),
332 name: name.to_string(),
333 })
334 .await?
335 .ok_or_else(|| EngineError::ArtifactNotFound {
336 step: step.to_string(),
337 name: name.to_string(),
338 })?;
339
340 let mut content = sink.get(&artifact).await?;
341 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
342 while let Some(chunk) = content.next().await {
343 let chunk = chunk?;
344 buffer.extend_from_slice(chunk.as_ref());
345 }
346
347 Ok(buffer)
348 }
349
350 async fn prepare_step_inputs(
355 &self,
356 config: &StepConfig,
357 position: u32,
358 ) -> Result<(), EngineError> {
359 let StepConfig::Shell(shell) = config else {
360 return Ok(());
361 };
362 if shell.inputs.is_empty() {
363 return Ok(());
364 }
365
366 materialize_inputs(
367 self.artifact_sink()?,
368 &self.store,
369 shell,
370 StepLocation {
371 run_id: self.run_id,
372 attempt: self.attempt,
373 position,
374 },
375 )
376 .await
377 }
378
379 async fn store_step_outputs(
384 &self,
385 config: &StepConfig,
386 step_id: Uuid,
387 step_name: &str,
388 step_succeeded: bool,
389 ) -> Result<(), EngineError> {
390 let StepConfig::Shell(shell) = config else {
391 return Ok(());
392 };
393 if shell.outputs.is_empty() {
394 return Ok(());
395 }
396
397 let sink = match self.artifact_sink() {
398 Ok(sink) => sink,
399 Err(err) if step_succeeded => return Err(err),
400 Err(err) => {
401 warn!(
402 run_id = %self.run_id,
403 step = %step_name,
404 error = %err,
405 "cannot collect outputs of a failed step"
406 );
407 return Ok(());
408 }
409 };
410
411 let collected =
412 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
413
414 match collected {
415 Ok(()) => Ok(()),
416 Err(err) if step_succeeded => Err(err),
417 Err(err) => {
418 warn!(
419 run_id = %self.run_id,
420 step = %step_name,
421 error = %err,
422 "failed to collect outputs of a failed step"
423 );
424 Ok(())
425 }
426 }
427 }
428
429 pub(crate) fn carry_over_run_totals(
436 &mut self,
437 attempt: u32,
438 cost_usd: Decimal,
439 duration_ms: u64,
440 ) {
441 self.attempt = attempt;
442 self.total_cost_usd = cost_usd;
443 self.carried_duration_ms = duration_ms;
444 }
445
446 pub(crate) fn carried_duration_ms(&self) -> u64 {
448 self.carried_duration_ms
449 }
450
451 pub fn attempt(&self) -> u32 {
453 self.attempt
454 }
455
456 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
472 self.max_cost_usd = cap;
473 }
474
475 pub fn max_cost_usd(&self) -> Option<Decimal> {
477 self.max_cost_usd
478 }
479
480 pub fn charged_cost_usd(&self) -> Decimal {
485 self.inherited_cost_usd + self.total_cost_usd
486 }
487
488 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
499 let Some(limit) = self.max_cost_usd else {
500 return Ok(());
501 };
502
503 let spent = self.charged_cost_usd();
504 if spent + step_budget <= limit {
505 return Ok(());
506 }
507
508 error!(
509 run_id = %self.run_id,
510 limit_usd = %limit,
511 spent_usd = %spent,
512 step_budget_usd = %step_budget,
513 "run cost cap reached, refusing agent step"
514 );
515
516 Err(EngineError::RunBudgetExceeded {
517 run_id: self.run_id,
518 limit_usd: limit,
519 spent_usd: spent,
520 step_budget_usd: step_budget,
521 })
522 }
523
524 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
536 let steps = self.store.list_steps(self.run_id).await?;
537 for step in steps {
538 let dominated = matches!(
539 step.status.state,
540 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
541 );
542 if !dominated {
543 continue;
544 }
545
546 if step.attempt == self.attempt {
547 self.replay_steps.insert(step.position, step);
548 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
549 {
550 self.granted_approvals.insert(step.position, step.attempt);
551 }
552 }
553 Ok(())
554 }
555
556 pub fn run_id(&self) -> Uuid {
558 self.run_id
559 }
560
561 pub fn total_cost_usd(&self) -> Decimal {
563 self.total_cost_usd
564 }
565
566 pub fn has_allowed_failure(&self) -> bool {
568 self.has_allowed_failure
569 }
570
571 pub fn total_duration_ms(&self) -> u64 {
573 self.total_duration_ms
574 }
575
576 pub fn step_results(&self) -> &[StepResult] {
578 &self.step_results
579 }
580
581 async fn persist_progress(&self) {
587 if let Err(err) = self
588 .store
589 .update_run(
590 self.run_id,
591 RunUpdate {
592 cost_usd: Some(self.total_cost_usd),
593 duration_ms: Some(self.total_duration_ms),
594 ..RunUpdate::default()
595 },
596 )
597 .await
598 {
599 warn!(
600 run_id = %self.run_id,
601 error = %err,
602 "failed to persist run progress snapshot"
603 );
604 }
605 }
606
607 pub async fn parallel(
644 &mut self,
645 steps: Vec<(&str, StepConfig)>,
646 fail_fast: bool,
647 ) -> Result<Vec<ParallelStepResult>, EngineError> {
648 if steps.is_empty() {
649 return Ok(Vec::new());
650 }
651
652 let wave_budget: Decimal = steps
655 .iter()
656 .filter_map(|(_, config)| match config {
657 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
658 _ => None,
659 })
660 .map(step_budget_usd)
661 .sum();
662 self.check_run_budget(wave_budget)?;
663
664 let wave_position = self.position;
665 self.position += 1;
666
667 let now = Utc::now();
668 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
669 Vec::with_capacity(steps.len());
670
671 for (name, config) in &steps {
672 let kind = config.kind();
673 let trace_id = step_trace_id(self.run_id, name, wave_position);
674 let step = self
675 .store
676 .create_step(NewStep {
677 run_id: self.run_id,
678 trace_id,
679 name: name.to_string(),
680 kind,
681 position: wave_position,
682 input: Some(serde_json::to_value(config)?),
683 is_error_handler: false,
684 })
685 .await?;
686
687 self.start_step(step.id, now).await?;
688
689 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
692 self.fail_step(step.id, &err).await;
693 if !config.allow_failure() {
694 return Err(err);
695 }
696 self.has_allowed_failure = true;
697 info!(
698 run_id = %self.run_id,
699 step = %name,
700 error = %err,
701 "parallel step input preparation failed but allow_failure is set, skipping"
702 );
703 continue;
704 }
705
706 step_records.push((step.id, trace_id, name.to_string(), config.clone()));
707 }
708
709 let mut join_set = JoinSet::new();
710 let mut task_index: HashMap<Id, usize> = HashMap::new();
711 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
712 let provider = self.provider.clone();
713 let config = config.clone();
714 let step_log_sender = self
715 .log_sender
716 .as_ref()
717 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
718 let handle = join_set.spawn(async move {
719 (
720 idx,
721 execute_step_config(&config, &provider, step_log_sender).await,
722 )
723 });
724 task_index.insert(handle.id(), idx);
725 }
726
727 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
729 vec![None; step_records.len()];
730 let mut first_error: Option<EngineError> = None;
731
732 while let Some(join_result) = join_set.join_next().await {
733 let (idx, step_result) = match join_result {
734 Ok(r) => r,
735 Err(e) => {
736 let error_msg = format!("join error: {e}");
737 if let Some(&idx) = task_index.get(&e.id()) {
738 let (step_id, _, step_name, _) = &step_records[idx];
739 let completed_at = Utc::now();
740 error!(
741 run_id = %self.run_id,
742 step = %step_name,
743 error = %error_msg,
744 "parallel step panicked or was cancelled"
745 );
746 if let Err(store_err) = self
747 .store
748 .update_step(
749 *step_id,
750 StepUpdate {
751 status: Some(StepStatus::Failed),
752 error: Some(error_msg.clone()),
753 completed_at: Some(completed_at),
754 ..StepUpdate::default()
755 },
756 )
757 .await
758 {
759 error!(
760 run_id = %self.run_id,
761 step_id = %step_id,
762 error = %store_err,
763 "failed to persist JoinError for step"
764 );
765 }
766 indexed_results[idx] = Some(Err(error_msg.clone()));
767 }
768 if first_error.is_none() {
769 first_error = Some(EngineError::StepConfig(error_msg));
770 }
771 if fail_fast {
772 join_set.abort_all();
773 }
774 continue;
775 }
776 };
777
778 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
779 let completed_at = Utc::now();
780
781 if let Err(err) = self
782 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
783 .await
784 {
785 self.fail_step(*step_id, &err).await;
786 indexed_results[idx] = Some(Err(err.to_string()));
787 if first_error.is_none() {
788 first_error = Some(err);
789 }
790 if fail_fast {
791 join_set.abort_all();
792 }
793 continue;
794 }
795
796 match step_result {
797 Ok(output) => {
798 self.total_cost_usd += output.cost_usd;
799 self.total_duration_ms += output.duration_ms;
800
801 let debug_messages_json = output.debug_messages_json();
802
803 self.store
804 .update_step(
805 *step_id,
806 StepUpdate {
807 status: Some(StepStatus::Completed),
808 output: Some(output.output.clone()),
809 duration_ms: Some(output.duration_ms),
810 cost_usd: Some(output.cost_usd),
811 input_tokens: output.input_tokens,
812 output_tokens: output.output_tokens,
813 completed_at: Some(completed_at),
814 debug_messages: debug_messages_json,
815 ..StepUpdate::default()
816 },
817 )
818 .await?;
819
820 self.step_results.push(StepResult::from_success(
821 *step_trace,
822 step_name,
823 &output,
824 ));
825
826 info!(
827 run_id = %self.run_id,
828 step = %step_name,
829 trace_id = %step_trace,
830 duration_ms = output.duration_ms,
831 "parallel step completed"
832 );
833
834 indexed_results[idx] = Some(Ok(output));
835 }
836 Err(err) => {
837 let err_msg = err.to_string();
838 let debug_messages_json = extract_debug_messages_from_error(&err);
839 let partial = extract_partial_usage_from_error(&err);
840 let raw_response_output = extract_raw_response_from_error(&err);
841
842 if let Some(ref usage) = partial {
843 if let Some(cost) = usage.cost_usd {
844 self.total_cost_usd += cost;
845 }
846 if let Some(dur) = usage.duration_ms {
847 self.total_duration_ms += dur;
848 }
849 }
850
851 if let Err(store_err) = self
852 .store
853 .update_step(
854 *step_id,
855 StepUpdate {
856 status: Some(StepStatus::Failed),
857 error: Some(err_msg.clone()),
858 output: raw_response_output.clone(),
859 completed_at: Some(completed_at),
860 debug_messages: debug_messages_json,
861 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
862 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
863 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
864 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
865 ..StepUpdate::default()
866 },
867 )
868 .await
869 {
870 tracing::error!(
871 step_id = %step_id,
872 error = %store_err,
873 "failed to persist parallel step failure"
874 );
875 }
876
877 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
878 let err_cost = partial
879 .as_ref()
880 .and_then(|p| p.cost_usd)
881 .unwrap_or(Decimal::ZERO);
882 self.step_results.push(StepResult::from_failure(
883 *step_trace,
884 step_name,
885 &err_msg,
886 err_duration,
887 err_cost,
888 ));
889
890 if step_config.allow_failure() {
891 self.has_allowed_failure = true;
892 info!(
893 run_id = %self.run_id,
894 step = %step_name,
895 error = %err_msg,
896 "parallel step failed but allow_failure is set, continuing"
897 );
898 indexed_results[idx] = Some(Ok(allowed_failure_output(
899 &err_msg,
900 raw_response_output,
901 partial.as_ref(),
902 )));
903 } else {
904 indexed_results[idx] = Some(Err(err_msg.clone()));
905
906 if first_error.is_none() {
907 first_error = Some(err);
908 }
909
910 if fail_fast {
911 join_set.abort_all();
912 }
913 }
914 }
915 }
916 }
917
918 if let Some(err) = first_error {
919 return Err(err);
920 }
921
922 self.persist_progress().await;
923
924 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
925
926 let results: Vec<ParallelStepResult> = step_records
928 .iter()
929 .enumerate()
930 .map(|(idx, (step_id, _trace_id, name, _))| {
931 let output = match indexed_results[idx].take() {
932 Some(Ok(o)) => o,
933 _ => unreachable!("all steps succeeded if no error returned"),
934 };
935 ParallelStepResult {
936 name: name.clone(),
937 output,
938 step_id: *step_id,
939 }
940 })
941 .collect();
942
943 Ok(results)
944 }
945
946 pub async fn shell(
969 &mut self,
970 name: &str,
971 config: ShellConfig,
972 ) -> Result<StepOutput, EngineError> {
973 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
974 .await
975 }
976
977 pub async fn http(
997 &mut self,
998 name: &str,
999 config: HttpConfig,
1000 ) -> Result<StepOutput, EngineError> {
1001 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1002 .await
1003 }
1004
1005 pub async fn agent(
1025 &mut self,
1026 name: &str,
1027 config: impl Into<AgentStepConfig>,
1028 ) -> Result<StepOutput, EngineError> {
1029 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1030 .await
1031 }
1032
1033 pub async fn approval(
1064 &mut self,
1065 name: &str,
1066 config: ApprovalConfig,
1067 ) -> Result<(), EngineError> {
1068 let position = self.position;
1069 self.position += 1;
1070
1071 if let Some(existing) = self.replay_steps.get(&position)
1074 && existing.kind == StepKind::Approval
1075 {
1076 if existing.status.state == StepStatus::AwaitingApproval {
1077 self.store
1078 .update_step(
1079 existing.id,
1080 StepUpdate {
1081 status: Some(StepStatus::Completed),
1082 completed_at: Some(Utc::now()),
1083 ..StepUpdate::default()
1084 },
1085 )
1086 .await?;
1087 }
1088
1089 self.last_step_ids = vec![existing.id];
1090 info!(
1091 run_id = %self.run_id,
1092 step = %name,
1093 position,
1094 "approval step replayed (approved)"
1095 );
1096 return Ok(());
1097 }
1098
1099 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1103 let trace_id = step_trace_id(self.run_id, name, position);
1104 let step = self
1105 .store
1106 .create_step(NewStep {
1107 run_id: self.run_id,
1108 trace_id,
1109 name: name.to_string(),
1110 kind: StepKind::Approval,
1111 position,
1112 input: Some(serde_json::to_value(&config)?),
1113 is_error_handler: false,
1114 })
1115 .await?;
1116
1117 let now = Utc::now();
1118 self.start_step(step.id, now).await?;
1119 self.store
1120 .update_step(
1121 step.id,
1122 StepUpdate {
1123 status: Some(StepStatus::Completed),
1124 output: Some(json!({"approved_in_attempt": granted_in})),
1125 completed_at: Some(now),
1126 ..StepUpdate::default()
1127 },
1128 )
1129 .await?;
1130
1131 self.last_step_ids = vec![step.id];
1132 info!(
1133 run_id = %self.run_id,
1134 step = %name,
1135 position,
1136 granted_in_attempt = granted_in,
1137 attempt = self.attempt,
1138 "approval carried over from a previous attempt"
1139 );
1140 return Ok(());
1141 }
1142
1143 let trace_id = step_trace_id(self.run_id, name, position);
1145 let step = self
1146 .store
1147 .create_step(NewStep {
1148 run_id: self.run_id,
1149 trace_id,
1150 name: name.to_string(),
1151 kind: StepKind::Approval,
1152 position,
1153 input: Some(serde_json::to_value(&config)?),
1154 is_error_handler: false,
1155 })
1156 .await?;
1157
1158 self.start_step(step.id, Utc::now()).await?;
1159
1160 self.store
1163 .update_step(
1164 step.id,
1165 StepUpdate {
1166 status: Some(StepStatus::AwaitingApproval),
1167 ..StepUpdate::default()
1168 },
1169 )
1170 .await?;
1171
1172 self.last_step_ids = vec![step.id];
1173
1174 Err(EngineError::ApprovalRequired {
1175 run_id: self.run_id,
1176 step_id: step.id,
1177 message: config.message().to_string(),
1178 })
1179 }
1180
1181 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1210 let position = self.position;
1211 self.position += 1;
1212
1213 let trace_id = step_trace_id(self.run_id, name, position);
1214 let step = self
1215 .store
1216 .create_step(NewStep {
1217 run_id: self.run_id,
1218 trace_id,
1219 name: name.to_string(),
1220 kind: StepKind::Custom("skip".to_string()),
1221 position,
1222 input: None,
1223 is_error_handler: false,
1224 })
1225 .await?;
1226
1227 if !self.last_step_ids.is_empty() {
1228 let deps: Vec<NewStepDependency> = self
1229 .last_step_ids
1230 .iter()
1231 .map(|&depends_on| NewStepDependency {
1232 step_id: step.id,
1233 depends_on,
1234 })
1235 .collect();
1236 self.store.create_step_dependencies(deps).await?;
1237 }
1238
1239 let now = Utc::now();
1240 self.store
1241 .update_step(
1242 step.id,
1243 StepUpdate {
1244 status: Some(StepStatus::Skipped),
1245 output: Some(serde_json::json!({"reason": reason})),
1246 completed_at: Some(now),
1247 ..StepUpdate::default()
1248 },
1249 )
1250 .await?;
1251
1252 self.last_step_ids = vec![step.id];
1253
1254 info!(
1255 run_id = %self.run_id,
1256 step = %name,
1257 reason,
1258 "step skipped"
1259 );
1260
1261 Ok(())
1262 }
1263
1264 pub async fn operation(
1302 &mut self,
1303 name: &str,
1304 op: &dyn Operation,
1305 ) -> Result<StepOutput, EngineError> {
1306 let kind = StepKind::Custom(op.kind().to_string());
1307 let position = self.position;
1308 self.position += 1;
1309
1310 let trace_id = step_trace_id(self.run_id, name, position);
1311 let step = self
1312 .store
1313 .create_step(NewStep {
1314 run_id: self.run_id,
1315 trace_id,
1316 name: name.to_string(),
1317 kind,
1318 position,
1319 input: op.input(),
1320 is_error_handler: false,
1321 })
1322 .await?;
1323
1324 self.start_step(step.id, Utc::now()).await?;
1325
1326 let start = Instant::now();
1327
1328 match op.execute().await {
1329 Ok(output_value) => {
1330 let duration_ms = start.elapsed().as_millis() as u64;
1331 self.total_duration_ms += duration_ms;
1332
1333 let completed_at = Utc::now();
1334 self.store
1335 .update_step(
1336 step.id,
1337 StepUpdate {
1338 status: Some(StepStatus::Completed),
1339 output: Some(output_value.clone()),
1340 duration_ms: Some(duration_ms),
1341 cost_usd: Some(Decimal::ZERO),
1342 completed_at: Some(completed_at),
1343 ..StepUpdate::default()
1344 },
1345 )
1346 .await?;
1347
1348 info!(
1349 run_id = %self.run_id,
1350 step = %name,
1351 kind = op.kind(),
1352 duration_ms,
1353 "operation step completed"
1354 );
1355
1356 self.last_step_ids = vec![step.id];
1357
1358 Ok(StepOutput {
1359 output: output_value,
1360 duration_ms,
1361 cost_usd: Decimal::ZERO,
1362 input_tokens: None,
1363 output_tokens: None,
1364 model: None,
1365 debug_messages: None,
1366 })
1367 }
1368 Err(err) => {
1369 let completed_at = Utc::now();
1370 if let Err(store_err) = self
1371 .store
1372 .update_step(
1373 step.id,
1374 StepUpdate {
1375 status: Some(StepStatus::Failed),
1376 error: Some(err.to_string()),
1377 completed_at: Some(completed_at),
1378 ..StepUpdate::default()
1379 },
1380 )
1381 .await
1382 {
1383 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1384 }
1385
1386 Err(err)
1387 }
1388 }
1389 }
1390
1391 pub async fn workflow(
1418 &mut self,
1419 handler: &dyn WorkflowHandler,
1420 payload: Value,
1421 ) -> Result<StepOutput, EngineError> {
1422 let config = WorkflowStepConfig::new(handler.name(), payload);
1423 let position = self.position;
1424 self.position += 1;
1425
1426 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1427 let step = self
1428 .store
1429 .create_step(NewStep {
1430 run_id: self.run_id,
1431 trace_id,
1432 name: config.workflow_name.clone(),
1433 kind: StepKind::Workflow,
1434 position,
1435 input: Some(serde_json::to_value(&config)?),
1436 is_error_handler: false,
1437 })
1438 .await?;
1439
1440 self.start_step(step.id, Utc::now()).await?;
1441
1442 match self.execute_child_workflow(&config).await {
1443 Ok((output, child_had_allowed_failure)) => {
1444 self.total_cost_usd += output.cost_usd;
1445 self.total_duration_ms += output.duration_ms;
1446 if child_had_allowed_failure {
1447 self.has_allowed_failure = true;
1448 }
1449
1450 let completed_at = Utc::now();
1451 self.store
1452 .update_step(
1453 step.id,
1454 StepUpdate {
1455 status: Some(StepStatus::Completed),
1456 output: Some(output.output.clone()),
1457 duration_ms: Some(output.duration_ms),
1458 cost_usd: Some(output.cost_usd),
1459 completed_at: Some(completed_at),
1460 ..StepUpdate::default()
1461 },
1462 )
1463 .await?;
1464
1465 info!(
1466 run_id = %self.run_id,
1467 child_workflow = %config.workflow_name,
1468 duration_ms = output.duration_ms,
1469 "workflow step completed"
1470 );
1471
1472 self.last_step_ids = vec![step.id];
1473
1474 Ok(output)
1475 }
1476 Err(err) => {
1477 let completed_at = Utc::now();
1478 if let Err(store_err) = self
1479 .store
1480 .update_step(
1481 step.id,
1482 StepUpdate {
1483 status: Some(StepStatus::Failed),
1484 error: Some(err.to_string()),
1485 completed_at: Some(completed_at),
1486 ..StepUpdate::default()
1487 },
1488 )
1489 .await
1490 {
1491 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1492 }
1493
1494 Err(err)
1495 }
1496 }
1497 }
1498
1499 async fn execute_child_workflow(
1502 &self,
1503 config: &WorkflowStepConfig,
1504 ) -> Result<(StepOutput, bool), EngineError> {
1505 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1506 EngineError::InvalidWorkflow(
1507 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1508 )
1509 })?;
1510
1511 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1512 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1513 })?;
1514
1515 let parent = self.store.get_run(self.run_id).await?;
1518 let (parent_labels, parent_author) =
1519 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1520
1521 let child_run = self
1522 .store
1523 .create_run(NewRun {
1524 workflow_name: config.workflow_name.clone(),
1525 trigger: TriggerKind::Workflow,
1526 payload: config.payload.clone(),
1527 max_retries: 0,
1528 handler_version: None,
1529 labels: parent_labels,
1530 scheduled_at: None,
1531 created_by: parent_author,
1532 idempotency_key: None,
1533 max_cost_usd: self.max_cost_usd,
1535 })
1536 .await?
1537 .into_run();
1538
1539 let child_run_id = child_run.id;
1540 info!(
1541 parent_run_id = %self.run_id,
1542 child_run_id = %child_run_id,
1543 workflow = %config.workflow_name,
1544 "child run created"
1545 );
1546
1547 self.store
1548 .update_run_status(child_run_id, RunStatus::Running)
1549 .await?;
1550
1551 let run_start = Instant::now();
1552 let mut child_ctx = WorkflowContext {
1553 run_id: child_run_id,
1554 store: self.store.clone(),
1555 provider: self.provider.clone(),
1556 handler_resolver: self.handler_resolver.clone(),
1557 position: 0,
1558 last_step_ids: Vec::new(),
1559 total_cost_usd: Decimal::ZERO,
1560 total_duration_ms: 0,
1561 max_cost_usd: self.max_cost_usd,
1562 inherited_cost_usd: self.charged_cost_usd(),
1565 replay_steps: HashMap::new(),
1566 granted_approvals: HashMap::new(),
1567 attempt: 1,
1569 carried_duration_ms: 0,
1570 log_sender: self.log_sender.clone(),
1571 artifact_sink: self.artifact_sink.clone(),
1574 has_allowed_failure: false,
1575 error_handlers: Vec::new(),
1576 step_results: Vec::new(),
1577 event_bus: self.event_bus.clone(),
1578 };
1579
1580 let result = handler.execute(&mut child_ctx).await;
1581 let total_duration = run_start.elapsed().as_millis() as u64;
1582 let completed_at = Utc::now();
1583
1584 match result {
1585 Ok(()) => {
1586 let child_status = if child_ctx.has_allowed_failure {
1587 RunStatus::Warning
1588 } else {
1589 RunStatus::Completed
1590 };
1591 self.store
1592 .update_run(
1593 child_run_id,
1594 RunUpdate {
1595 status: Some(child_status),
1596 cost_usd: Some(child_ctx.total_cost_usd),
1597 duration_ms: Some(total_duration),
1598 completed_at: Some(completed_at),
1599 ..RunUpdate::default()
1600 },
1601 )
1602 .await?;
1603
1604 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1605 Ok((
1606 StepOutput {
1607 output: serde_json::json!({
1608 "run_id": child_run_id,
1609 "workflow_name": config.workflow_name,
1610 "status": child_status,
1611 "cost_usd": child_ctx.total_cost_usd,
1612 "duration_ms": total_duration,
1613 }),
1614 duration_ms: total_duration,
1615 cost_usd: child_ctx.total_cost_usd,
1616 input_tokens: None,
1617 output_tokens: None,
1618 model: None,
1619 debug_messages: None,
1620 },
1621 child_had_allowed_failure,
1622 ))
1623 }
1624 Err(err) => {
1625 if let Err(store_err) = self
1626 .store
1627 .update_run(
1628 child_run_id,
1629 RunUpdate {
1630 status: Some(RunStatus::Failed),
1631 error: Some(err.to_string()),
1632 cost_usd: Some(child_ctx.total_cost_usd),
1633 duration_ms: Some(total_duration),
1634 completed_at: Some(completed_at),
1635 ..RunUpdate::default()
1636 },
1637 )
1638 .await
1639 {
1640 error!(
1641 child_run_id = %child_run_id,
1642 store_error = %store_err,
1643 "failed to persist child run failure"
1644 );
1645 }
1646
1647 Err(err)
1648 }
1649 }
1650 }
1651
1652 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1657 let step = self.replay_steps.get(&position)?;
1658 if step.status.state != StepStatus::Completed {
1659 return None;
1660 }
1661 let output = StepOutput {
1662 output: step.output.clone().unwrap_or(Value::Null),
1663 duration_ms: step.duration_ms,
1664 cost_usd: step.cost_usd,
1665 input_tokens: step.input_tokens,
1666 output_tokens: step.output_tokens,
1667 model: None,
1668 debug_messages: None,
1669 };
1670 self.total_cost_usd += output.cost_usd;
1671 self.total_duration_ms += output.duration_ms;
1672 self.last_step_ids = vec![step.id];
1673 info!(
1674 run_id = %self.run_id,
1675 step = %step.name,
1676 position,
1677 "step replayed from previous execution"
1678 );
1679 Some(output)
1680 }
1681
1682 #[tracing::instrument(
1684 name = "context.execute_step",
1685 skip_all,
1686 fields(
1687 run_id = %self.run_id,
1688 step.name = %name,
1689 step.kind,
1690 step.position = self.position,
1691 step.trace_id,
1692 )
1693 )]
1694 async fn execute_step(
1695 &mut self,
1696 name: &str,
1697 kind: StepKind,
1698 config: StepConfig,
1699 ) -> Result<StepOutput, EngineError> {
1700 let kind_str: &'static str = match kind {
1701 StepKind::Shell => "shell",
1702 StepKind::Http => "http",
1703 StepKind::Agent => "agent",
1704 StepKind::Workflow => "workflow",
1705 StepKind::Approval => "approval",
1706 StepKind::Custom(_) => "custom",
1707 };
1708 Span::current().record("step.kind", kind_str);
1709
1710 let position = self.position;
1711 self.position += 1;
1712
1713 if let Some(output) = self.try_replay_step(position) {
1715 return Ok(output);
1716 }
1717
1718 if let StepConfig::Agent(ref agent_config) = config {
1721 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1722 }
1723
1724 let trace_id = step_trace_id(self.run_id, name, position);
1726 Span::current().record("step.trace_id", trace_id.to_string().as_str());
1727 let step = self
1728 .store
1729 .create_step(NewStep {
1730 run_id: self.run_id,
1731 trace_id,
1732 name: name.to_string(),
1733 kind,
1734 position,
1735 input: Some(serde_json::to_value(&config)?),
1736 is_error_handler: false,
1737 })
1738 .await?;
1739
1740 self.start_step(step.id, Utc::now()).await?;
1741
1742 if let Some(ref bus) = self.event_bus {
1743 bus.publish(
1744 self.run_id,
1745 crate::notify::WorkflowEvent::StepStarted {
1746 step_name: name.to_string(),
1747 step_index: position,
1748 timestamp: Utc::now(),
1749 },
1750 );
1751 }
1752
1753 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1756 self.fail_step(step.id, &err).await;
1757 if config.allow_failure() {
1758 self.has_allowed_failure = true;
1759 self.last_step_ids = vec![step.id];
1760 info!(
1761 run_id = %self.run_id,
1762 step = %name,
1763 error = %err,
1764 "step input preparation failed but allow_failure is set, continuing"
1765 );
1766 return Ok(StepOutput {
1767 output: json!({"error": err.to_string()}),
1768 duration_ms: 0,
1769 cost_usd: Decimal::ZERO,
1770 input_tokens: None,
1771 output_tokens: None,
1772 model: None,
1773 debug_messages: None,
1774 });
1775 }
1776 return Err(err);
1777 }
1778
1779 let step_log_sender = self
1780 .log_sender
1781 .as_ref()
1782 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1783
1784 let execution = execute_step_config(&config, &self.provider, step_log_sender).await;
1785
1786 let execution = self
1787 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
1788 .await;
1789
1790 if let Err(err) = self
1791 .store_step_outputs(&config, step.id, name, execution.is_ok())
1792 .await
1793 {
1794 self.fail_step(step.id, &err).await;
1795 return Err(err);
1796 }
1797
1798 match execution {
1799 Ok(output) => {
1800 self.total_cost_usd += output.cost_usd;
1801 self.total_duration_ms += output.duration_ms;
1802
1803 let debug_messages_json = output.debug_messages_json();
1804
1805 let completed_at = Utc::now();
1806 self.store
1807 .update_step(
1808 step.id,
1809 StepUpdate {
1810 status: Some(StepStatus::Completed),
1811 output: Some(output.output.clone()),
1812 duration_ms: Some(output.duration_ms),
1813 cost_usd: Some(output.cost_usd),
1814 input_tokens: output.input_tokens,
1815 output_tokens: output.output_tokens,
1816 completed_at: Some(completed_at),
1817 debug_messages: debug_messages_json,
1818 ..StepUpdate::default()
1819 },
1820 )
1821 .await?;
1822
1823 self.step_results
1824 .push(StepResult::from_success(trace_id, name, &output));
1825 self.persist_progress().await;
1826
1827 info!(
1828 run_id = %self.run_id,
1829 step = %name,
1830 trace_id = %trace_id,
1831 duration_ms = output.duration_ms,
1832 "step completed"
1833 );
1834
1835 if let Some(ref bus) = self.event_bus {
1836 bus.publish(
1837 self.run_id,
1838 crate::notify::WorkflowEvent::StepCompleted {
1839 step_name: name.to_string(),
1840 step_index: position,
1841 duration_ms: output.duration_ms,
1842 output_summary: None,
1843 },
1844 );
1845 }
1846
1847 self.last_step_ids = vec![step.id];
1848
1849 Ok(output)
1850 }
1851 Err(err) => {
1852 let completed_at = Utc::now();
1853 let debug_messages_json = extract_debug_messages_from_error(&err);
1854 let partial = extract_partial_usage_from_error(&err);
1855 let raw_response_output = extract_raw_response_from_error(&err);
1856
1857 if let Some(ref usage) = partial {
1858 if let Some(cost) = usage.cost_usd {
1859 self.total_cost_usd += cost;
1860 }
1861 if let Some(dur) = usage.duration_ms {
1862 self.total_duration_ms += dur;
1863 }
1864 }
1865
1866 if let Err(store_err) = self
1867 .store
1868 .update_step(
1869 step.id,
1870 StepUpdate {
1871 status: Some(StepStatus::Failed),
1872 error: Some(err.to_string()),
1873 output: raw_response_output.clone(),
1874 completed_at: Some(completed_at),
1875 debug_messages: debug_messages_json,
1876 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1877 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1878 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1879 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1880 ..StepUpdate::default()
1881 },
1882 )
1883 .await
1884 {
1885 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1886 }
1887
1888 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1889 let err_cost = partial
1890 .as_ref()
1891 .and_then(|p| p.cost_usd)
1892 .unwrap_or(Decimal::ZERO);
1893 self.step_results.push(StepResult::from_failure(
1894 trace_id,
1895 name,
1896 &err.to_string(),
1897 err_duration,
1898 err_cost,
1899 ));
1900 self.persist_progress().await;
1901
1902 if let Some(ref bus) = self.event_bus {
1903 bus.publish(
1904 self.run_id,
1905 crate::notify::WorkflowEvent::StepFailed {
1906 step_name: name.to_string(),
1907 step_index: position,
1908 error: err.to_string(),
1909 duration_ms: err_duration,
1910 },
1911 );
1912 }
1913
1914 self.fire_error_handlers(name, &err.to_string(), err_duration)
1915 .await;
1916
1917 if config.allow_failure() {
1918 self.has_allowed_failure = true;
1919 self.last_step_ids = vec![step.id];
1920 info!(
1921 run_id = %self.run_id,
1922 step = %name,
1923 error = %err,
1924 "step failed but allow_failure is set, continuing"
1925 );
1926 Ok(allowed_failure_output(
1927 &err.to_string(),
1928 raw_response_output,
1929 partial.as_ref(),
1930 ))
1931 } else {
1932 Err(err)
1933 }
1934 }
1935 }
1936 }
1937
1938 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
1944 async fn retry_step_if_configured(
1945 &self,
1946 name: &str,
1947 kind_str: &str,
1948 config: &StepConfig,
1949 step_id: Uuid,
1950 first_result: Result<StepOutput, EngineError>,
1951 ) -> Result<StepOutput, EngineError> {
1952 let policy = match config.retry() {
1953 Some(p) => p,
1954 None => return first_result,
1955 };
1956
1957 let mut last_result = match first_result {
1958 Ok(output) => return Ok(output),
1959 Err(err) if !is_step_retryable(&err) => return Err(err),
1960 Err(err) => Err(err),
1961 };
1962
1963 let step_log_sender = self
1964 .log_sender
1965 .as_ref()
1966 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
1967
1968 for attempt in 0..policy.max_retries() {
1969 if let StepConfig::Agent(agent_config) = config {
1970 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1971 }
1972
1973 let delay = policy.delay_for_attempt(attempt);
1974 info!(
1975 run_id = %self.run_id,
1976 step = %name,
1977 attempt = attempt + 1,
1978 max_retries = policy.max_retries(),
1979 delay_ms = delay.as_millis() as u64,
1980 "retrying step after transient failure"
1981 );
1982 tokio::time::sleep(delay).await;
1983
1984 record_retry_metric(kind_str, "retry");
1985
1986 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
1987 Ok(output) => return Ok(output),
1988 Err(err) if !is_step_retryable(&err) => return Err(err),
1989 err => last_result = err,
1990 }
1991 }
1992
1993 record_retry_metric(kind_str, "exhausted");
1994
1995 info!(
1996 run_id = %self.run_id,
1997 step = %name,
1998 max_retries = policy.max_retries(),
1999 "step retries exhausted"
2000 );
2001
2002 last_result
2003 }
2004
2005 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
2010 if !self.last_step_ids.is_empty() {
2011 let deps: Vec<NewStepDependency> = self
2012 .last_step_ids
2013 .iter()
2014 .map(|&depends_on| NewStepDependency {
2015 step_id,
2016 depends_on,
2017 })
2018 .collect();
2019 self.store.create_step_dependencies(deps).await?;
2020 }
2021
2022 self.store
2023 .update_step(
2024 step_id,
2025 StepUpdate {
2026 status: Some(StepStatus::Running),
2027 started_at: Some(now),
2028 ..StepUpdate::default()
2029 },
2030 )
2031 .await?;
2032
2033 Ok(())
2034 }
2035
2036 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2043 if let Err(store_err) = self
2044 .store
2045 .update_step(
2046 step_id,
2047 StepUpdate {
2048 status: Some(StepStatus::Failed),
2049 error: Some(err.to_string()),
2050 completed_at: Some(Utc::now()),
2051 ..StepUpdate::default()
2052 },
2053 )
2054 .await
2055 {
2056 error!(
2057 step_id = %step_id,
2058 error = %store_err,
2059 "failed to persist step failure"
2060 );
2061 }
2062 }
2063
2064 pub fn store(&self) -> &Arc<dyn Store> {
2066 &self.store
2067 }
2068
2069 pub async fn payload(&self) -> Result<Value, EngineError> {
2077 let run = self
2078 .store
2079 .get_run(self.run_id)
2080 .await?
2081 .ok_or(EngineError::Store(
2082 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2083 ))?;
2084 Ok(run.payload)
2085 }
2086
2087 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2115 let payload = self.payload().await?;
2116 serde_json::from_value(payload).map_err(EngineError::Serialization)
2117 }
2118
2119 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2142 self.error_handlers.push(OnErrorHandler {
2143 name: name.to_string(),
2144 config: config.into(),
2145 });
2146 }
2147
2148 pub fn clear_error_handlers(&mut self) {
2167 self.error_handlers.clear();
2168 }
2169
2170 async fn fire_error_handlers(
2176 &mut self,
2177 failed_step_name: &str,
2178 error_msg: &str,
2179 duration_ms: u64,
2180 ) {
2181 let handlers = std::mem::take(&mut self.error_handlers);
2182 if handlers.is_empty() {
2183 return;
2184 }
2185
2186 let error_context = json!({
2187 "failed_step": failed_step_name,
2188 "error": error_msg,
2189 "duration_ms": duration_ms,
2190 });
2191
2192 for handler in handlers {
2193 let mut config = handler.config.clone();
2194 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2195
2196 let position = self.position;
2197 self.position += 1;
2198
2199 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2200 let step = match self
2201 .store
2202 .create_step(NewStep {
2203 run_id: self.run_id,
2204 trace_id,
2205 name: handler.name.clone(),
2206 kind: config.kind(),
2207 position,
2208 input: Some(error_context.clone()),
2209 is_error_handler: true,
2210 })
2211 .await
2212 {
2213 Ok(step) => step,
2214 Err(err) => {
2215 warn!(
2216 run_id = %self.run_id,
2217 handler = %handler.name,
2218 error = %err,
2219 "failed to create error handler step"
2220 );
2221 continue;
2222 }
2223 };
2224
2225 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2226 warn!(
2227 run_id = %self.run_id,
2228 handler = %handler.name,
2229 error = %err,
2230 "failed to start error handler step"
2231 );
2232 continue;
2233 }
2234
2235 let step_log_sender = self
2236 .log_sender
2237 .as_ref()
2238 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2239
2240 let start = Instant::now();
2241 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2242 let handler_duration = start.elapsed().as_millis() as u64;
2243 let completed_at = Utc::now();
2244
2245 match result {
2246 Ok(output) => {
2247 if let Err(store_err) = self
2248 .store
2249 .update_step(
2250 step.id,
2251 StepUpdate {
2252 status: Some(StepStatus::Completed),
2253 output: Some(output.output),
2254 duration_ms: Some(handler_duration),
2255 cost_usd: Some(output.cost_usd),
2256 completed_at: Some(completed_at),
2257 ..StepUpdate::default()
2258 },
2259 )
2260 .await
2261 {
2262 warn!(
2263 run_id = %self.run_id,
2264 handler = %handler.name,
2265 error = %store_err,
2266 "failed to persist error handler completion"
2267 );
2268 }
2269
2270 info!(
2271 run_id = %self.run_id,
2272 handler = %handler.name,
2273 duration_ms = handler_duration,
2274 "error handler completed"
2275 );
2276 }
2277 Err(err) => {
2278 if let Err(store_err) = self
2279 .store
2280 .update_step(
2281 step.id,
2282 StepUpdate {
2283 status: Some(StepStatus::Failed),
2284 error: Some(err.to_string()),
2285 duration_ms: Some(handler_duration),
2286 completed_at: Some(completed_at),
2287 ..StepUpdate::default()
2288 },
2289 )
2290 .await
2291 {
2292 warn!(
2293 run_id = %self.run_id,
2294 handler = %handler.name,
2295 error = %store_err,
2296 "failed to persist error handler failure"
2297 );
2298 }
2299
2300 warn!(
2301 run_id = %self.run_id,
2302 handler = %handler.name,
2303 error = %err,
2304 "error handler failed (original error preserved)"
2305 );
2306 }
2307 }
2308 }
2309 }
2310}
2311
2312fn inject_error_context(
2314 config: &mut StepConfig,
2315 failed_step: &str,
2316 error_msg: &str,
2317 duration_ms: u64,
2318) {
2319 match config {
2320 StepConfig::Shell(shell) => {
2321 shell
2322 .env
2323 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2324 shell
2325 .env
2326 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2327 shell.env.push((
2328 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2329 duration_ms.to_string(),
2330 ));
2331 }
2332 StepConfig::Agent(agent) => {
2333 agent.prompt = format!(
2334 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2335 failed_step, duration_ms, error_msg, agent.prompt
2336 );
2337 }
2338 StepConfig::Http(http) => {
2339 http.headers
2340 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2341 http.headers.push((
2342 "X-Ironflow-Error-Message".to_string(),
2343 error_msg.to_string(),
2344 ));
2345 }
2346 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2347 }
2348}
2349
2350#[cfg(feature = "prometheus")]
2351fn record_retry_metric(kind: &str, outcome: &str) {
2352 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2353 use metrics::counter;
2354 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2355 .increment(1);
2356}
2357
2358#[cfg(not(feature = "prometheus"))]
2359fn record_retry_metric(_kind: &str, _outcome: &str) {}
2360
2361fn is_step_retryable(err: &EngineError) -> bool {
2365 use ironflow_core::error::{AgentError, OperationError};
2366
2367 match err {
2368 EngineError::Operation(op) => match op {
2369 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2370 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2371 OperationError::Deserialize { .. } => false,
2372 OperationError::Http {
2373 status: Some(code), ..
2374 } if (400..500).contains(code) && *code != 429 => false,
2375 _ => true,
2376 },
2377 _ => false,
2378 }
2379}
2380
2381fn allowed_failure_output(
2382 error_msg: &str,
2383 raw_response: Option<Value>,
2384 partial: Option<&StepPartialUsage>,
2385) -> StepOutput {
2386 StepOutput {
2387 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2388 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2389 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2390 input_tokens: partial.and_then(|p| p.input_tokens),
2391 output_tokens: partial.and_then(|p| p.output_tokens),
2392 model: None,
2393 debug_messages: None,
2394 }
2395}
2396
2397impl fmt::Debug for WorkflowContext {
2398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2399 f.debug_struct("WorkflowContext")
2400 .field("run_id", &self.run_id)
2401 .field("position", &self.position)
2402 .field("total_cost_usd", &self.total_cost_usd)
2403 .field("inherited_cost_usd", &self.inherited_cost_usd)
2404 .field("max_cost_usd", &self.max_cost_usd)
2405 .finish_non_exhaustive()
2406 }
2407}
2408
2409fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2412 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2413 debug_messages,
2414 ..
2415 })) = err
2416 && !debug_messages.is_empty()
2417 {
2418 return serde_json::to_value(debug_messages).ok();
2419 }
2420 None
2421}
2422
2423struct StepPartialUsage {
2429 cost_usd: Option<Decimal>,
2430 duration_ms: Option<u64>,
2431 input_tokens: Option<u64>,
2432 output_tokens: Option<u64>,
2433}
2434
2435fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2441 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2442 raw_response: Some(text),
2443 ..
2444 })) = err
2445 {
2446 return Some(Value::String(text.clone()));
2447 }
2448 None
2449}
2450
2451fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2452 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2453 partial_usage,
2454 ..
2455 })) = err
2456 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2457 {
2458 return Some(StepPartialUsage {
2459 cost_usd: partial_usage
2460 .cost_usd
2461 .and_then(|c| Decimal::try_from(c).ok()),
2462 duration_ms: partial_usage.duration_ms,
2463 input_tokens: partial_usage.input_tokens,
2464 output_tokens: partial_usage.output_tokens,
2465 });
2466 }
2467 None
2468}
2469
2470#[cfg(test)]
2471mod tests {
2472 use super::*;
2473 use ironflow_core::providers::claude::ClaudeCodeProvider;
2474 use ironflow_core::providers::record_replay::RecordReplayProvider;
2475 use ironflow_store::memory::InMemoryStore;
2476 use ironflow_store::models::{Run, RunActor, RunFilter};
2477 use ironflow_store::store::RunStore;
2478 use serde_json::json;
2479 use std::sync::Arc;
2480 use std::sync::atomic::{AtomicBool, Ordering};
2481 use uuid::Uuid;
2482
2483 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2485 let inner = ClaudeCodeProvider::new();
2486 Arc::new(RecordReplayProvider::replay(
2487 inner,
2488 "/tmp/ironflow-fixtures",
2489 ))
2490 }
2491
2492 fn create_test_context() -> WorkflowContext {
2494 let store = Arc::new(InMemoryStore::new());
2495 let provider = create_test_provider();
2496 let run_id = Uuid::now_v7();
2497 WorkflowContext::new(run_id, store, provider)
2498 }
2499
2500 #[test]
2501 fn context_new_initializes_correctly() {
2502 let ctx = create_test_context();
2503 assert_eq!(ctx.position, 0);
2504 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2505 assert_eq!(ctx.total_duration_ms, 0);
2506 assert!(ctx.last_step_ids.is_empty());
2507 assert!(ctx.replay_steps.is_empty());
2508 assert!(ctx.log_sender.is_none());
2509 }
2510
2511 #[test]
2512 fn context_run_id_returns_correct_id() {
2513 let run_id = Uuid::now_v7();
2514 let store = Arc::new(InMemoryStore::new());
2515 let provider = create_test_provider();
2516 let ctx = WorkflowContext::new(run_id, store, provider);
2517 assert_eq!(ctx.run_id(), run_id);
2518 }
2519
2520 #[test]
2521 fn context_total_cost_usd_initially_zero() {
2522 let ctx = create_test_context();
2523 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2524 }
2525
2526 #[test]
2527 fn context_total_duration_ms_initially_zero() {
2528 let ctx = create_test_context();
2529 assert_eq!(ctx.total_duration_ms(), 0);
2530 }
2531
2532 #[test]
2533 fn context_with_handler_resolver_creates_context_with_resolver() {
2534 let store = Arc::new(InMemoryStore::new());
2535 let provider = create_test_provider();
2536 let run_id = Uuid::now_v7();
2537
2538 let called = Arc::new(AtomicBool::new(false));
2539 let called_clone = called.clone();
2540
2541 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2542 called_clone.store(true, Ordering::SeqCst);
2543 None
2544 });
2545
2546 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2547
2548 assert_eq!(ctx.run_id(), run_id);
2549 assert!(ctx.handler_resolver.is_some());
2550 }
2551
2552 #[tokio::test]
2553 async fn context_set_log_sender_attaches_sender() {
2554 let mut ctx = create_test_context();
2555 let (sender, _receiver) = crate::log_sender::channel();
2556 ctx.set_log_sender(sender);
2557 assert!(ctx.log_sender.is_some());
2558 }
2559
2560 #[tokio::test]
2561 async fn context_skip_creates_skipped_step() {
2562 let store = Arc::new(InMemoryStore::new());
2563 let provider = create_test_provider();
2564
2565 store
2567 .create_run(NewRun {
2568 created_by: None,
2569 workflow_name: "test".to_string(),
2570 trigger: TriggerKind::Manual,
2571 payload: json!({}),
2572 max_retries: 0,
2573 handler_version: None,
2574 labels: Default::default(),
2575 scheduled_at: None,
2576 idempotency_key: None,
2577 max_cost_usd: None,
2578 })
2579 .await
2580 .expect("failed to create run")
2581 .into_run();
2582
2583 let runs = store
2585 .list_runs(RunFilter::default(), 1, 10)
2586 .await
2587 .expect("failed to list runs");
2588 let created_run_id = runs.items[0].id;
2589
2590 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2591 let initial_position = ctx.position;
2592
2593 ctx.skip("skip-step", "condition not met")
2594 .await
2595 .expect("skip failed");
2596
2597 assert_eq!(ctx.position, initial_position + 1);
2598 assert!(!ctx.last_step_ids.is_empty());
2599
2600 let steps = store
2602 .list_steps(created_run_id)
2603 .await
2604 .expect("failed to list steps");
2605 assert_eq!(steps.len(), 1);
2606 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2607 }
2608
2609 struct NoopSubWorkflow;
2612
2613 impl WorkflowHandler for NoopSubWorkflow {
2614 fn name(&self) -> &str {
2615 "noop-sub"
2616 }
2617
2618 fn execute<'a>(
2619 &'a self,
2620 _ctx: &'a mut WorkflowContext,
2621 ) -> crate::handler::HandlerFuture<'a> {
2622 Box::pin(async move { Ok(()) })
2623 }
2624 }
2625
2626 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2629 let store = Arc::new(InMemoryStore::new());
2630 let provider = create_test_provider();
2631
2632 let parent = store
2633 .create_run(NewRun {
2634 workflow_name: "parent".to_string(),
2635 trigger: TriggerKind::Api,
2636 payload: json!({}),
2637 max_retries: 0,
2638 handler_version: None,
2639 labels: Default::default(),
2640 scheduled_at: None,
2641 created_by,
2642 idempotency_key: None,
2643 max_cost_usd: None,
2644 })
2645 .await
2646 .expect("failed to create parent run")
2647 .into_run();
2648
2649 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2650 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2651 _ => None,
2652 });
2653
2654 let mut ctx =
2655 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2656 ctx.workflow(&NoopSubWorkflow, json!({}))
2657 .await
2658 .expect("sub-workflow failed");
2659
2660 let runs = store
2661 .list_runs(RunFilter::default(), 1, 10)
2662 .await
2663 .expect("failed to list runs");
2664 runs.items
2665 .into_iter()
2666 .find(|r| r.workflow_name == "noop-sub")
2667 .expect("child run was created")
2668 }
2669
2670 #[tokio::test]
2671 async fn child_run_inherits_the_parent_author() {
2672 let user_id = Uuid::now_v7();
2673 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2674
2675 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2676 }
2677
2678 #[tokio::test]
2679 async fn child_run_of_an_unattributed_parent_has_no_author() {
2680 let child = child_run_of_parent_authored_by(None).await;
2681
2682 assert!(child.created_by.is_none());
2683 }
2684
2685 #[tokio::test]
2686 async fn context_parallel_empty_steps_returns_empty_vec() {
2687 let mut ctx = create_test_context();
2688 let results = ctx
2689 .parallel(vec![], true)
2690 .await
2691 .expect("parallel should not fail on empty input");
2692 assert!(results.is_empty());
2693 }
2694
2695 #[tokio::test]
2696 async fn context_approval_first_execution_returns_error() {
2697 let store = Arc::new(InMemoryStore::new());
2698 let provider = create_test_provider();
2699
2700 store
2702 .create_run(NewRun {
2703 created_by: None,
2704 workflow_name: "test".to_string(),
2705 trigger: TriggerKind::Manual,
2706 payload: json!({}),
2707 max_retries: 0,
2708 handler_version: None,
2709 labels: Default::default(),
2710 scheduled_at: None,
2711 idempotency_key: None,
2712 max_cost_usd: None,
2713 })
2714 .await
2715 .expect("failed to create run")
2716 .into_run();
2717
2718 let runs = store
2720 .list_runs(RunFilter::default(), 1, 10)
2721 .await
2722 .expect("failed to list runs");
2723 let created_run_id = runs.items[0].id;
2724
2725 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2726
2727 let result = ctx
2728 .approval(
2729 "approve-step",
2730 crate::config::ApprovalConfig::new("Continue?"),
2731 )
2732 .await;
2733
2734 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2736
2737 assert_eq!(ctx.position, 1);
2739
2740 let steps = store
2742 .list_steps(created_run_id)
2743 .await
2744 .expect("failed to list steps");
2745 assert_eq!(steps.len(), 1);
2746 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2747 }
2748
2749 #[tokio::test]
2750 async fn context_approval_replay_returns_ok() {
2751 let store = Arc::new(InMemoryStore::new());
2752 let provider = create_test_provider();
2753
2754 store
2756 .create_run(NewRun {
2757 created_by: None,
2758 workflow_name: "test".to_string(),
2759 trigger: TriggerKind::Manual,
2760 payload: json!({}),
2761 max_retries: 0,
2762 handler_version: None,
2763 labels: Default::default(),
2764 scheduled_at: None,
2765 idempotency_key: None,
2766 max_cost_usd: None,
2767 })
2768 .await
2769 .expect("failed to create run")
2770 .into_run();
2771
2772 let runs = store
2774 .list_runs(RunFilter::default(), 1, 10)
2775 .await
2776 .expect("failed to list runs");
2777 let created_run_id = runs.items[0].id;
2778
2779 let step = store
2781 .create_step(NewStep {
2782 run_id: created_run_id,
2783 trace_id: step_trace_id(created_run_id, "approval", 0),
2784 name: "approval".to_string(),
2785 kind: StepKind::Approval,
2786 position: 0,
2787 input: None,
2788 is_error_handler: false,
2789 })
2790 .await
2791 .expect("failed to create step");
2792
2793 store
2795 .update_step(
2796 step.id,
2797 StepUpdate {
2798 status: Some(StepStatus::Running),
2799 started_at: Some(Utc::now()),
2800 ..StepUpdate::default()
2801 },
2802 )
2803 .await
2804 .expect("failed to update step to Running");
2805
2806 store
2807 .update_step(
2808 step.id,
2809 StepUpdate {
2810 status: Some(StepStatus::AwaitingApproval),
2811 ..StepUpdate::default()
2812 },
2813 )
2814 .await
2815 .expect("failed to update step to AwaitingApproval");
2816
2817 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2819 ctx.load_replay_steps()
2820 .await
2821 .expect("failed to load replay steps");
2822
2823 let result = ctx
2825 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2826 .await;
2827
2828 assert!(result.is_ok());
2829
2830 let steps = store
2832 .list_steps(created_run_id)
2833 .await
2834 .expect("failed to list steps");
2835 assert_eq!(steps.len(), 1);
2836 assert_eq!(steps[0].status.state, StepStatus::Completed);
2837 }
2838
2839 #[tokio::test]
2840 async fn context_load_replay_steps_loads_completed_steps() {
2841 let store = Arc::new(InMemoryStore::new());
2842 let provider = create_test_provider();
2843
2844 store
2846 .create_run(NewRun {
2847 created_by: None,
2848 workflow_name: "test".to_string(),
2849 trigger: TriggerKind::Manual,
2850 payload: json!({}),
2851 max_retries: 0,
2852 handler_version: None,
2853 labels: Default::default(),
2854 scheduled_at: None,
2855 idempotency_key: None,
2856 max_cost_usd: None,
2857 })
2858 .await
2859 .expect("failed to create run")
2860 .into_run();
2861
2862 let runs = store
2864 .list_runs(RunFilter::default(), 1, 10)
2865 .await
2866 .expect("failed to list runs");
2867 let created_run_id = runs.items[0].id;
2868
2869 let completed_step = store
2871 .create_step(NewStep {
2872 run_id: created_run_id,
2873 trace_id: step_trace_id(created_run_id, "completed", 0),
2874 name: "completed".to_string(),
2875 kind: StepKind::Shell,
2876 position: 0,
2877 input: None,
2878 is_error_handler: false,
2879 })
2880 .await
2881 .expect("failed to create step");
2882
2883 store
2885 .update_step(
2886 completed_step.id,
2887 StepUpdate {
2888 status: Some(StepStatus::Running),
2889 started_at: Some(Utc::now()),
2890 ..StepUpdate::default()
2891 },
2892 )
2893 .await
2894 .expect("failed to update step to Running");
2895
2896 store
2897 .update_step(
2898 completed_step.id,
2899 StepUpdate {
2900 status: Some(StepStatus::Completed),
2901 completed_at: Some(Utc::now()),
2902 ..StepUpdate::default()
2903 },
2904 )
2905 .await
2906 .expect("failed to update step to Completed");
2907
2908 let _pending_step = store
2909 .create_step(NewStep {
2910 run_id: created_run_id,
2911 trace_id: step_trace_id(created_run_id, "pending", 1),
2912 name: "pending".to_string(),
2913 kind: StepKind::Shell,
2914 position: 1,
2915 input: None,
2916 is_error_handler: false,
2917 })
2918 .await
2919 .expect("failed to create step");
2920
2921 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2923 ctx.load_replay_steps()
2924 .await
2925 .expect("failed to load replay steps");
2926
2927 assert_eq!(ctx.replay_steps.len(), 1);
2929 assert!(ctx.replay_steps.contains_key(&0));
2930 assert!(!ctx.replay_steps.contains_key(&1));
2931 }
2932
2933 #[tokio::test]
2934 async fn context_payload_returns_run_payload() {
2935 let store = Arc::new(InMemoryStore::new());
2936 let provider = create_test_provider();
2937 let test_payload = json!({"key": "value", "number": 42});
2938
2939 store
2941 .create_run(NewRun {
2942 created_by: None,
2943 workflow_name: "test".to_string(),
2944 trigger: TriggerKind::Manual,
2945 payload: test_payload.clone(),
2946 max_retries: 0,
2947 handler_version: None,
2948 labels: Default::default(),
2949 scheduled_at: None,
2950 idempotency_key: None,
2951 max_cost_usd: None,
2952 })
2953 .await
2954 .expect("failed to create run")
2955 .into_run();
2956
2957 let runs = store
2959 .list_runs(RunFilter::default(), 1, 10)
2960 .await
2961 .expect("failed to list runs");
2962 let created_run_id = runs.items[0].id;
2963
2964 let ctx = WorkflowContext::new(created_run_id, store, provider);
2965 let payload = ctx.payload().await.expect("failed to get payload");
2966
2967 assert_eq!(payload, test_payload);
2968 }
2969
2970 #[tokio::test]
2971 async fn context_payload_returns_error_for_nonexistent_run() {
2972 let store = Arc::new(InMemoryStore::new());
2973 let provider = create_test_provider();
2974 let run_id = Uuid::now_v7();
2975
2976 let ctx = WorkflowContext::new(run_id, store, provider);
2977 let result = ctx.payload().await;
2978
2979 assert!(result.is_err());
2980 }
2981
2982 #[tokio::test]
2983 async fn context_store_returns_reference() {
2984 let ctx = create_test_context();
2985 let _store = ctx.store();
2986 }
2988
2989 #[test]
2990 fn context_debug_formatting() {
2991 let ctx = create_test_context();
2992 let debug_str = format!("{:?}", ctx);
2993 assert!(debug_str.contains("WorkflowContext"));
2994 assert!(debug_str.contains("run_id"));
2995 }
2996
2997 #[tokio::test]
2998 async fn context_last_step_ids_tracks_executed_steps() {
2999 let store = Arc::new(InMemoryStore::new());
3000 let provider = create_test_provider();
3001
3002 store
3004 .create_run(NewRun {
3005 created_by: None,
3006 workflow_name: "test".to_string(),
3007 trigger: TriggerKind::Manual,
3008 payload: json!({}),
3009 max_retries: 0,
3010 handler_version: None,
3011 labels: Default::default(),
3012 scheduled_at: None,
3013 idempotency_key: None,
3014 max_cost_usd: None,
3015 })
3016 .await
3017 .expect("failed to create run")
3018 .into_run();
3019
3020 let runs = store
3022 .list_runs(RunFilter::default(), 1, 10)
3023 .await
3024 .expect("failed to list runs");
3025 let created_run_id = runs.items[0].id;
3026
3027 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
3028 assert!(ctx.last_step_ids.is_empty());
3029
3030 ctx.skip("step1", "reason").await.expect("skip failed");
3031
3032 assert_eq!(ctx.last_step_ids.len(), 1);
3033
3034 ctx.skip("step2", "reason").await.expect("skip failed");
3035
3036 assert_eq!(ctx.last_step_ids.len(), 1);
3038 }
3039}