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,
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 event_bus: Option<crate::notify::WorkflowEventBus>,
127}
128
129struct OnErrorHandler {
131 name: String,
132 config: StepConfig,
133}
134
135impl WorkflowContext {
136 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
141 Self {
142 run_id,
143 store,
144 provider,
145 handler_resolver: None,
146 position: 0,
147 last_step_ids: Vec::new(),
148 total_cost_usd: Decimal::ZERO,
149 total_duration_ms: 0,
150 max_cost_usd: None,
151 inherited_cost_usd: Decimal::ZERO,
152 replay_steps: HashMap::new(),
153 granted_approvals: HashMap::new(),
154 attempt: 1,
155 carried_duration_ms: 0,
156 log_sender: None,
157 artifact_sink: None,
158 has_allowed_failure: false,
159 error_handlers: Vec::new(),
160 event_bus: None,
161 }
162 }
163
164 pub(crate) fn with_handler_resolver(
169 run_id: Uuid,
170 store: Arc<dyn Store>,
171 provider: Arc<dyn AgentProvider>,
172 resolver: HandlerResolver,
173 ) -> Self {
174 Self {
175 run_id,
176 store,
177 provider,
178 handler_resolver: Some(resolver),
179 position: 0,
180 last_step_ids: Vec::new(),
181 total_cost_usd: Decimal::ZERO,
182 total_duration_ms: 0,
183 max_cost_usd: None,
184 inherited_cost_usd: Decimal::ZERO,
185 replay_steps: HashMap::new(),
186 granted_approvals: HashMap::new(),
187 attempt: 1,
188 carried_duration_ms: 0,
189 log_sender: None,
190 artifact_sink: None,
191 has_allowed_failure: false,
192 error_handlers: Vec::new(),
193 event_bus: None,
194 }
195 }
196
197 pub fn set_log_sender(&mut self, sender: LogSender) {
199 self.log_sender = Some(sender);
200 }
201
202 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
222 self.artifact_sink = Some(sink);
223 }
224
225 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
231 self.event_bus = Some(bus);
232 }
233
234 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
236 self.artifact_sink.as_ref().ok_or_else(|| {
237 EngineError::ArtifactsUnavailable(
238 "no artifact storage is attached to this run".to_string(),
239 )
240 })
241 }
242
243 pub async fn put_artifact(
273 &self,
274 step_id: Uuid,
275 name: &str,
276 content_type: Option<&str>,
277 content: Vec<u8>,
278 ) -> Result<Artifact, EngineError> {
279 let sink = self.artifact_sink()?;
280 sink.put(
281 ArtifactUpload {
282 run_id: self.run_id,
283 step_id,
284 name: name.to_string(),
285 content_type: content_type
286 .map(str::to_string)
287 .unwrap_or_else(|| guess_content_type(name)),
288 },
289 stream_from_bytes(content),
290 )
291 .await
292 }
293
294 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
319 let sink = self.artifact_sink()?;
320
321 let artifact = self
322 .store
323 .find_artifact_for_input(ArtifactLookup {
324 run_id: self.run_id,
325 attempt: self.attempt,
326 before_position: self.position,
327 step_name: step.to_string(),
328 name: name.to_string(),
329 })
330 .await?
331 .ok_or_else(|| EngineError::ArtifactNotFound {
332 step: step.to_string(),
333 name: name.to_string(),
334 })?;
335
336 let mut content = sink.get(&artifact).await?;
337 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
338 while let Some(chunk) = content.next().await {
339 let chunk = chunk?;
340 buffer.extend_from_slice(chunk.as_ref());
341 }
342
343 Ok(buffer)
344 }
345
346 async fn prepare_step_inputs(
351 &self,
352 config: &StepConfig,
353 position: u32,
354 ) -> Result<(), EngineError> {
355 let StepConfig::Shell(shell) = config else {
356 return Ok(());
357 };
358 if shell.inputs.is_empty() {
359 return Ok(());
360 }
361
362 materialize_inputs(
363 self.artifact_sink()?,
364 &self.store,
365 shell,
366 StepLocation {
367 run_id: self.run_id,
368 attempt: self.attempt,
369 position,
370 },
371 )
372 .await
373 }
374
375 async fn store_step_outputs(
380 &self,
381 config: &StepConfig,
382 step_id: Uuid,
383 step_name: &str,
384 step_succeeded: bool,
385 ) -> Result<(), EngineError> {
386 let StepConfig::Shell(shell) = config else {
387 return Ok(());
388 };
389 if shell.outputs.is_empty() {
390 return Ok(());
391 }
392
393 let sink = match self.artifact_sink() {
394 Ok(sink) => sink,
395 Err(err) if step_succeeded => return Err(err),
396 Err(err) => {
397 warn!(
398 run_id = %self.run_id,
399 step = %step_name,
400 error = %err,
401 "cannot collect outputs of a failed step"
402 );
403 return Ok(());
404 }
405 };
406
407 let collected =
408 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
409
410 match collected {
411 Ok(()) => Ok(()),
412 Err(err) if step_succeeded => Err(err),
413 Err(err) => {
414 warn!(
415 run_id = %self.run_id,
416 step = %step_name,
417 error = %err,
418 "failed to collect outputs of a failed step"
419 );
420 Ok(())
421 }
422 }
423 }
424
425 pub(crate) fn carry_over_run_totals(
432 &mut self,
433 attempt: u32,
434 cost_usd: Decimal,
435 duration_ms: u64,
436 ) {
437 self.attempt = attempt;
438 self.total_cost_usd = cost_usd;
439 self.carried_duration_ms = duration_ms;
440 }
441
442 pub(crate) fn carried_duration_ms(&self) -> u64 {
444 self.carried_duration_ms
445 }
446
447 pub fn attempt(&self) -> u32 {
449 self.attempt
450 }
451
452 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
468 self.max_cost_usd = cap;
469 }
470
471 pub fn max_cost_usd(&self) -> Option<Decimal> {
473 self.max_cost_usd
474 }
475
476 pub fn charged_cost_usd(&self) -> Decimal {
481 self.inherited_cost_usd + self.total_cost_usd
482 }
483
484 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
495 let Some(limit) = self.max_cost_usd else {
496 return Ok(());
497 };
498
499 let spent = self.charged_cost_usd();
500 if spent + step_budget <= limit {
501 return Ok(());
502 }
503
504 error!(
505 run_id = %self.run_id,
506 limit_usd = %limit,
507 spent_usd = %spent,
508 step_budget_usd = %step_budget,
509 "run cost cap reached, refusing agent step"
510 );
511
512 Err(EngineError::RunBudgetExceeded {
513 run_id: self.run_id,
514 limit_usd: limit,
515 spent_usd: spent,
516 step_budget_usd: step_budget,
517 })
518 }
519
520 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
532 let steps = self.store.list_steps(self.run_id).await?;
533 for step in steps {
534 let dominated = matches!(
535 step.status.state,
536 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
537 );
538 if !dominated {
539 continue;
540 }
541
542 if step.attempt == self.attempt {
543 self.replay_steps.insert(step.position, step);
544 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
545 {
546 self.granted_approvals.insert(step.position, step.attempt);
547 }
548 }
549 Ok(())
550 }
551
552 pub fn run_id(&self) -> Uuid {
554 self.run_id
555 }
556
557 pub fn total_cost_usd(&self) -> Decimal {
559 self.total_cost_usd
560 }
561
562 pub fn has_allowed_failure(&self) -> bool {
564 self.has_allowed_failure
565 }
566
567 pub fn total_duration_ms(&self) -> u64 {
569 self.total_duration_ms
570 }
571
572 pub async fn parallel(
609 &mut self,
610 steps: Vec<(&str, StepConfig)>,
611 fail_fast: bool,
612 ) -> Result<Vec<ParallelStepResult>, EngineError> {
613 if steps.is_empty() {
614 return Ok(Vec::new());
615 }
616
617 let wave_budget: Decimal = steps
620 .iter()
621 .filter_map(|(_, config)| match config {
622 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
623 _ => None,
624 })
625 .map(step_budget_usd)
626 .sum();
627 self.check_run_budget(wave_budget)?;
628
629 let wave_position = self.position;
630 self.position += 1;
631
632 let now = Utc::now();
633 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
634
635 for (name, config) in &steps {
636 let kind = config.kind();
637 let step = self
638 .store
639 .create_step(NewStep {
640 run_id: self.run_id,
641 name: name.to_string(),
642 kind,
643 position: wave_position,
644 input: Some(serde_json::to_value(config)?),
645 is_error_handler: false,
646 })
647 .await?;
648
649 self.start_step(step.id, now).await?;
650
651 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
654 self.fail_step(step.id, &err).await;
655 if !config.allow_failure() {
656 return Err(err);
657 }
658 self.has_allowed_failure = true;
659 info!(
660 run_id = %self.run_id,
661 step = %name,
662 error = %err,
663 "parallel step input preparation failed but allow_failure is set, skipping"
664 );
665 continue;
666 }
667
668 step_records.push((step.id, name.to_string(), config.clone()));
669 }
670
671 let mut join_set = JoinSet::new();
672 let mut task_index: HashMap<Id, usize> = HashMap::new();
673 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
674 let provider = self.provider.clone();
675 let config = config.clone();
676 let step_log_sender = self
677 .log_sender
678 .as_ref()
679 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
680 let handle = join_set.spawn(async move {
681 (
682 idx,
683 execute_step_config(&config, &provider, step_log_sender).await,
684 )
685 });
686 task_index.insert(handle.id(), idx);
687 }
688
689 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
691 vec![None; step_records.len()];
692 let mut first_error: Option<EngineError> = None;
693
694 while let Some(join_result) = join_set.join_next().await {
695 let (idx, step_result) = match join_result {
696 Ok(r) => r,
697 Err(e) => {
698 let error_msg = format!("join error: {e}");
699 if let Some(&idx) = task_index.get(&e.id()) {
700 let (step_id, step_name, _) = &step_records[idx];
701 let completed_at = Utc::now();
702 error!(
703 run_id = %self.run_id,
704 step = %step_name,
705 error = %error_msg,
706 "parallel step panicked or was cancelled"
707 );
708 if let Err(store_err) = self
709 .store
710 .update_step(
711 *step_id,
712 StepUpdate {
713 status: Some(StepStatus::Failed),
714 error: Some(error_msg.clone()),
715 completed_at: Some(completed_at),
716 ..StepUpdate::default()
717 },
718 )
719 .await
720 {
721 error!(
722 run_id = %self.run_id,
723 step_id = %step_id,
724 error = %store_err,
725 "failed to persist JoinError for step"
726 );
727 }
728 indexed_results[idx] = Some(Err(error_msg.clone()));
729 }
730 if first_error.is_none() {
731 first_error = Some(EngineError::StepConfig(error_msg));
732 }
733 if fail_fast {
734 join_set.abort_all();
735 }
736 continue;
737 }
738 };
739
740 let (step_id, step_name, step_config) = &step_records[idx];
741 let completed_at = Utc::now();
742
743 if let Err(err) = self
744 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
745 .await
746 {
747 self.fail_step(*step_id, &err).await;
748 indexed_results[idx] = Some(Err(err.to_string()));
749 if first_error.is_none() {
750 first_error = Some(err);
751 }
752 if fail_fast {
753 join_set.abort_all();
754 }
755 continue;
756 }
757
758 match step_result {
759 Ok(output) => {
760 self.total_cost_usd += output.cost_usd;
761 self.total_duration_ms += output.duration_ms;
762
763 let debug_messages_json = output.debug_messages_json();
764
765 self.store
766 .update_step(
767 *step_id,
768 StepUpdate {
769 status: Some(StepStatus::Completed),
770 output: Some(output.output.clone()),
771 duration_ms: Some(output.duration_ms),
772 cost_usd: Some(output.cost_usd),
773 input_tokens: output.input_tokens,
774 output_tokens: output.output_tokens,
775 completed_at: Some(completed_at),
776 debug_messages: debug_messages_json,
777 ..StepUpdate::default()
778 },
779 )
780 .await?;
781
782 info!(
783 run_id = %self.run_id,
784 step = %step_name,
785 duration_ms = output.duration_ms,
786 "parallel step completed"
787 );
788
789 indexed_results[idx] = Some(Ok(output));
790 }
791 Err(err) => {
792 let err_msg = err.to_string();
793 let debug_messages_json = extract_debug_messages_from_error(&err);
794 let partial = extract_partial_usage_from_error(&err);
795 let raw_response_output = extract_raw_response_from_error(&err);
796
797 if let Some(ref usage) = partial {
798 if let Some(cost) = usage.cost_usd {
799 self.total_cost_usd += cost;
800 }
801 if let Some(dur) = usage.duration_ms {
802 self.total_duration_ms += dur;
803 }
804 }
805
806 if let Err(store_err) = self
807 .store
808 .update_step(
809 *step_id,
810 StepUpdate {
811 status: Some(StepStatus::Failed),
812 error: Some(err_msg.clone()),
813 output: raw_response_output.clone(),
814 completed_at: Some(completed_at),
815 debug_messages: debug_messages_json,
816 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
817 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
818 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
819 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
820 ..StepUpdate::default()
821 },
822 )
823 .await
824 {
825 tracing::error!(
826 step_id = %step_id,
827 error = %store_err,
828 "failed to persist parallel step failure"
829 );
830 }
831
832 if step_config.allow_failure() {
833 self.has_allowed_failure = true;
834 info!(
835 run_id = %self.run_id,
836 step = %step_name,
837 error = %err_msg,
838 "parallel step failed but allow_failure is set, continuing"
839 );
840 indexed_results[idx] = Some(Ok(allowed_failure_output(
841 &err_msg,
842 raw_response_output,
843 partial.as_ref(),
844 )));
845 } else {
846 indexed_results[idx] = Some(Err(err_msg.clone()));
847
848 if first_error.is_none() {
849 first_error = Some(err);
850 }
851
852 if fail_fast {
853 join_set.abort_all();
854 }
855 }
856 }
857 }
858 }
859
860 if let Some(err) = first_error {
861 return Err(err);
862 }
863
864 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
865
866 let results: Vec<ParallelStepResult> = step_records
868 .iter()
869 .enumerate()
870 .map(|(idx, (step_id, name, _))| {
871 let output = match indexed_results[idx].take() {
872 Some(Ok(o)) => o,
873 _ => unreachable!("all steps succeeded if no error returned"),
874 };
875 ParallelStepResult {
876 name: name.clone(),
877 output,
878 step_id: *step_id,
879 }
880 })
881 .collect();
882
883 Ok(results)
884 }
885
886 pub async fn shell(
909 &mut self,
910 name: &str,
911 config: ShellConfig,
912 ) -> Result<StepOutput, EngineError> {
913 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
914 .await
915 }
916
917 pub async fn http(
937 &mut self,
938 name: &str,
939 config: HttpConfig,
940 ) -> Result<StepOutput, EngineError> {
941 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
942 .await
943 }
944
945 pub async fn agent(
965 &mut self,
966 name: &str,
967 config: impl Into<AgentStepConfig>,
968 ) -> Result<StepOutput, EngineError> {
969 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
970 .await
971 }
972
973 pub async fn approval(
1004 &mut self,
1005 name: &str,
1006 config: ApprovalConfig,
1007 ) -> Result<(), EngineError> {
1008 let position = self.position;
1009 self.position += 1;
1010
1011 if let Some(existing) = self.replay_steps.get(&position)
1014 && existing.kind == StepKind::Approval
1015 {
1016 if existing.status.state == StepStatus::AwaitingApproval {
1017 self.store
1018 .update_step(
1019 existing.id,
1020 StepUpdate {
1021 status: Some(StepStatus::Completed),
1022 completed_at: Some(Utc::now()),
1023 ..StepUpdate::default()
1024 },
1025 )
1026 .await?;
1027 }
1028
1029 self.last_step_ids = vec![existing.id];
1030 info!(
1031 run_id = %self.run_id,
1032 step = %name,
1033 position,
1034 "approval step replayed (approved)"
1035 );
1036 return Ok(());
1037 }
1038
1039 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1043 let step = self
1044 .store
1045 .create_step(NewStep {
1046 run_id: self.run_id,
1047 name: name.to_string(),
1048 kind: StepKind::Approval,
1049 position,
1050 input: Some(serde_json::to_value(&config)?),
1051 is_error_handler: false,
1052 })
1053 .await?;
1054
1055 let now = Utc::now();
1056 self.start_step(step.id, now).await?;
1057 self.store
1058 .update_step(
1059 step.id,
1060 StepUpdate {
1061 status: Some(StepStatus::Completed),
1062 output: Some(json!({"approved_in_attempt": granted_in})),
1063 completed_at: Some(now),
1064 ..StepUpdate::default()
1065 },
1066 )
1067 .await?;
1068
1069 self.last_step_ids = vec![step.id];
1070 info!(
1071 run_id = %self.run_id,
1072 step = %name,
1073 position,
1074 granted_in_attempt = granted_in,
1075 attempt = self.attempt,
1076 "approval carried over from a previous attempt"
1077 );
1078 return Ok(());
1079 }
1080
1081 let step = self
1083 .store
1084 .create_step(NewStep {
1085 run_id: self.run_id,
1086 name: name.to_string(),
1087 kind: StepKind::Approval,
1088 position,
1089 input: Some(serde_json::to_value(&config)?),
1090 is_error_handler: false,
1091 })
1092 .await?;
1093
1094 self.start_step(step.id, Utc::now()).await?;
1095
1096 self.store
1099 .update_step(
1100 step.id,
1101 StepUpdate {
1102 status: Some(StepStatus::AwaitingApproval),
1103 ..StepUpdate::default()
1104 },
1105 )
1106 .await?;
1107
1108 self.last_step_ids = vec![step.id];
1109
1110 Err(EngineError::ApprovalRequired {
1111 run_id: self.run_id,
1112 step_id: step.id,
1113 message: config.message().to_string(),
1114 })
1115 }
1116
1117 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1146 let position = self.position;
1147 self.position += 1;
1148
1149 let step = self
1150 .store
1151 .create_step(NewStep {
1152 run_id: self.run_id,
1153 name: name.to_string(),
1154 kind: StepKind::Custom("skip".to_string()),
1155 position,
1156 input: None,
1157 is_error_handler: false,
1158 })
1159 .await?;
1160
1161 if !self.last_step_ids.is_empty() {
1162 let deps: Vec<NewStepDependency> = self
1163 .last_step_ids
1164 .iter()
1165 .map(|&depends_on| NewStepDependency {
1166 step_id: step.id,
1167 depends_on,
1168 })
1169 .collect();
1170 self.store.create_step_dependencies(deps).await?;
1171 }
1172
1173 let now = Utc::now();
1174 self.store
1175 .update_step(
1176 step.id,
1177 StepUpdate {
1178 status: Some(StepStatus::Skipped),
1179 output: Some(serde_json::json!({"reason": reason})),
1180 completed_at: Some(now),
1181 ..StepUpdate::default()
1182 },
1183 )
1184 .await?;
1185
1186 self.last_step_ids = vec![step.id];
1187
1188 info!(
1189 run_id = %self.run_id,
1190 step = %name,
1191 reason,
1192 "step skipped"
1193 );
1194
1195 Ok(())
1196 }
1197
1198 pub async fn operation(
1236 &mut self,
1237 name: &str,
1238 op: &dyn Operation,
1239 ) -> Result<StepOutput, EngineError> {
1240 let kind = StepKind::Custom(op.kind().to_string());
1241 let position = self.position;
1242 self.position += 1;
1243
1244 let step = self
1245 .store
1246 .create_step(NewStep {
1247 run_id: self.run_id,
1248 name: name.to_string(),
1249 kind,
1250 position,
1251 input: op.input(),
1252 is_error_handler: false,
1253 })
1254 .await?;
1255
1256 self.start_step(step.id, Utc::now()).await?;
1257
1258 let start = Instant::now();
1259
1260 match op.execute().await {
1261 Ok(output_value) => {
1262 let duration_ms = start.elapsed().as_millis() as u64;
1263 self.total_duration_ms += duration_ms;
1264
1265 let completed_at = Utc::now();
1266 self.store
1267 .update_step(
1268 step.id,
1269 StepUpdate {
1270 status: Some(StepStatus::Completed),
1271 output: Some(output_value.clone()),
1272 duration_ms: Some(duration_ms),
1273 cost_usd: Some(Decimal::ZERO),
1274 completed_at: Some(completed_at),
1275 ..StepUpdate::default()
1276 },
1277 )
1278 .await?;
1279
1280 info!(
1281 run_id = %self.run_id,
1282 step = %name,
1283 kind = op.kind(),
1284 duration_ms,
1285 "operation step completed"
1286 );
1287
1288 self.last_step_ids = vec![step.id];
1289
1290 Ok(StepOutput {
1291 output: output_value,
1292 duration_ms,
1293 cost_usd: Decimal::ZERO,
1294 input_tokens: None,
1295 output_tokens: None,
1296 model: None,
1297 debug_messages: None,
1298 })
1299 }
1300 Err(err) => {
1301 let completed_at = Utc::now();
1302 if let Err(store_err) = self
1303 .store
1304 .update_step(
1305 step.id,
1306 StepUpdate {
1307 status: Some(StepStatus::Failed),
1308 error: Some(err.to_string()),
1309 completed_at: Some(completed_at),
1310 ..StepUpdate::default()
1311 },
1312 )
1313 .await
1314 {
1315 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1316 }
1317
1318 Err(err)
1319 }
1320 }
1321 }
1322
1323 pub async fn workflow(
1350 &mut self,
1351 handler: &dyn WorkflowHandler,
1352 payload: Value,
1353 ) -> Result<StepOutput, EngineError> {
1354 let config = WorkflowStepConfig::new(handler.name(), payload);
1355 let position = self.position;
1356 self.position += 1;
1357
1358 let step = self
1359 .store
1360 .create_step(NewStep {
1361 run_id: self.run_id,
1362 name: config.workflow_name.clone(),
1363 kind: StepKind::Workflow,
1364 position,
1365 input: Some(serde_json::to_value(&config)?),
1366 is_error_handler: false,
1367 })
1368 .await?;
1369
1370 self.start_step(step.id, Utc::now()).await?;
1371
1372 match self.execute_child_workflow(&config).await {
1373 Ok((output, child_had_allowed_failure)) => {
1374 self.total_cost_usd += output.cost_usd;
1375 self.total_duration_ms += output.duration_ms;
1376 if child_had_allowed_failure {
1377 self.has_allowed_failure = true;
1378 }
1379
1380 let completed_at = Utc::now();
1381 self.store
1382 .update_step(
1383 step.id,
1384 StepUpdate {
1385 status: Some(StepStatus::Completed),
1386 output: Some(output.output.clone()),
1387 duration_ms: Some(output.duration_ms),
1388 cost_usd: Some(output.cost_usd),
1389 completed_at: Some(completed_at),
1390 ..StepUpdate::default()
1391 },
1392 )
1393 .await?;
1394
1395 info!(
1396 run_id = %self.run_id,
1397 child_workflow = %config.workflow_name,
1398 duration_ms = output.duration_ms,
1399 "workflow step completed"
1400 );
1401
1402 self.last_step_ids = vec![step.id];
1403
1404 Ok(output)
1405 }
1406 Err(err) => {
1407 let completed_at = Utc::now();
1408 if let Err(store_err) = self
1409 .store
1410 .update_step(
1411 step.id,
1412 StepUpdate {
1413 status: Some(StepStatus::Failed),
1414 error: Some(err.to_string()),
1415 completed_at: Some(completed_at),
1416 ..StepUpdate::default()
1417 },
1418 )
1419 .await
1420 {
1421 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1422 }
1423
1424 Err(err)
1425 }
1426 }
1427 }
1428
1429 async fn execute_child_workflow(
1432 &self,
1433 config: &WorkflowStepConfig,
1434 ) -> Result<(StepOutput, bool), EngineError> {
1435 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1436 EngineError::InvalidWorkflow(
1437 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1438 )
1439 })?;
1440
1441 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1442 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1443 })?;
1444
1445 let parent = self.store.get_run(self.run_id).await?;
1448 let (parent_labels, parent_author) =
1449 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1450
1451 let child_run = self
1452 .store
1453 .create_run(NewRun {
1454 workflow_name: config.workflow_name.clone(),
1455 trigger: TriggerKind::Workflow,
1456 payload: config.payload.clone(),
1457 max_retries: 0,
1458 handler_version: None,
1459 labels: parent_labels,
1460 scheduled_at: None,
1461 created_by: parent_author,
1462 idempotency_key: None,
1463 max_cost_usd: self.max_cost_usd,
1465 })
1466 .await?
1467 .into_run();
1468
1469 let child_run_id = child_run.id;
1470 info!(
1471 parent_run_id = %self.run_id,
1472 child_run_id = %child_run_id,
1473 workflow = %config.workflow_name,
1474 "child run created"
1475 );
1476
1477 self.store
1478 .update_run_status(child_run_id, RunStatus::Running)
1479 .await?;
1480
1481 let run_start = Instant::now();
1482 let mut child_ctx = WorkflowContext {
1483 run_id: child_run_id,
1484 store: self.store.clone(),
1485 provider: self.provider.clone(),
1486 handler_resolver: self.handler_resolver.clone(),
1487 position: 0,
1488 last_step_ids: Vec::new(),
1489 total_cost_usd: Decimal::ZERO,
1490 total_duration_ms: 0,
1491 max_cost_usd: self.max_cost_usd,
1492 inherited_cost_usd: self.charged_cost_usd(),
1495 replay_steps: HashMap::new(),
1496 granted_approvals: HashMap::new(),
1497 attempt: 1,
1499 carried_duration_ms: 0,
1500 log_sender: self.log_sender.clone(),
1501 artifact_sink: self.artifact_sink.clone(),
1504 has_allowed_failure: false,
1505 error_handlers: Vec::new(),
1506 event_bus: self.event_bus.clone(),
1507 };
1508
1509 let result = handler.execute(&mut child_ctx).await;
1510 let total_duration = run_start.elapsed().as_millis() as u64;
1511 let completed_at = Utc::now();
1512
1513 match result {
1514 Ok(()) => {
1515 let child_status = if child_ctx.has_allowed_failure {
1516 RunStatus::Warning
1517 } else {
1518 RunStatus::Completed
1519 };
1520 self.store
1521 .update_run(
1522 child_run_id,
1523 RunUpdate {
1524 status: Some(child_status),
1525 cost_usd: Some(child_ctx.total_cost_usd),
1526 duration_ms: Some(total_duration),
1527 completed_at: Some(completed_at),
1528 ..RunUpdate::default()
1529 },
1530 )
1531 .await?;
1532
1533 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1534 Ok((
1535 StepOutput {
1536 output: serde_json::json!({
1537 "run_id": child_run_id,
1538 "workflow_name": config.workflow_name,
1539 "status": child_status,
1540 "cost_usd": child_ctx.total_cost_usd,
1541 "duration_ms": total_duration,
1542 }),
1543 duration_ms: total_duration,
1544 cost_usd: child_ctx.total_cost_usd,
1545 input_tokens: None,
1546 output_tokens: None,
1547 model: None,
1548 debug_messages: None,
1549 },
1550 child_had_allowed_failure,
1551 ))
1552 }
1553 Err(err) => {
1554 if let Err(store_err) = self
1555 .store
1556 .update_run(
1557 child_run_id,
1558 RunUpdate {
1559 status: Some(RunStatus::Failed),
1560 error: Some(err.to_string()),
1561 cost_usd: Some(child_ctx.total_cost_usd),
1562 duration_ms: Some(total_duration),
1563 completed_at: Some(completed_at),
1564 ..RunUpdate::default()
1565 },
1566 )
1567 .await
1568 {
1569 error!(
1570 child_run_id = %child_run_id,
1571 store_error = %store_err,
1572 "failed to persist child run failure"
1573 );
1574 }
1575
1576 Err(err)
1577 }
1578 }
1579 }
1580
1581 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1586 let step = self.replay_steps.get(&position)?;
1587 if step.status.state != StepStatus::Completed {
1588 return None;
1589 }
1590 let output = StepOutput {
1591 output: step.output.clone().unwrap_or(Value::Null),
1592 duration_ms: step.duration_ms,
1593 cost_usd: step.cost_usd,
1594 input_tokens: step.input_tokens,
1595 output_tokens: step.output_tokens,
1596 model: None,
1597 debug_messages: None,
1598 };
1599 self.total_cost_usd += output.cost_usd;
1600 self.total_duration_ms += output.duration_ms;
1601 self.last_step_ids = vec![step.id];
1602 info!(
1603 run_id = %self.run_id,
1604 step = %step.name,
1605 position,
1606 "step replayed from previous execution"
1607 );
1608 Some(output)
1609 }
1610
1611 #[tracing::instrument(
1613 name = "context.execute_step",
1614 skip_all,
1615 fields(
1616 run_id = %self.run_id,
1617 step.name = %name,
1618 step.kind,
1619 step.position = self.position,
1620 )
1621 )]
1622 async fn execute_step(
1623 &mut self,
1624 name: &str,
1625 kind: StepKind,
1626 config: StepConfig,
1627 ) -> Result<StepOutput, EngineError> {
1628 let kind_str: &'static str = match kind {
1629 StepKind::Shell => "shell",
1630 StepKind::Http => "http",
1631 StepKind::Agent => "agent",
1632 StepKind::Workflow => "workflow",
1633 StepKind::Approval => "approval",
1634 StepKind::Custom(_) => "custom",
1635 };
1636 Span::current().record("step.kind", kind_str);
1637
1638 let position = self.position;
1639 self.position += 1;
1640
1641 if let Some(output) = self.try_replay_step(position) {
1643 return Ok(output);
1644 }
1645
1646 if let StepConfig::Agent(ref agent_config) = config {
1649 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1650 }
1651
1652 let step = self
1654 .store
1655 .create_step(NewStep {
1656 run_id: self.run_id,
1657 name: name.to_string(),
1658 kind,
1659 position,
1660 input: Some(serde_json::to_value(&config)?),
1661 is_error_handler: false,
1662 })
1663 .await?;
1664
1665 self.start_step(step.id, Utc::now()).await?;
1666
1667 if let Some(ref bus) = self.event_bus {
1668 bus.publish(
1669 self.run_id,
1670 crate::notify::WorkflowEvent::StepStarted {
1671 step_name: name.to_string(),
1672 step_index: position,
1673 timestamp: Utc::now(),
1674 },
1675 );
1676 }
1677
1678 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1681 self.fail_step(step.id, &err).await;
1682 if config.allow_failure() {
1683 self.has_allowed_failure = true;
1684 self.last_step_ids = vec![step.id];
1685 info!(
1686 run_id = %self.run_id,
1687 step = %name,
1688 error = %err,
1689 "step input preparation failed but allow_failure is set, continuing"
1690 );
1691 return Ok(StepOutput {
1692 output: json!({"error": err.to_string()}),
1693 duration_ms: 0,
1694 cost_usd: Decimal::ZERO,
1695 input_tokens: None,
1696 output_tokens: None,
1697 model: None,
1698 debug_messages: None,
1699 });
1700 }
1701 return Err(err);
1702 }
1703
1704 let step_log_sender = self
1705 .log_sender
1706 .as_ref()
1707 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1708
1709 let execution = execute_step_config(&config, &self.provider, step_log_sender).await;
1710
1711 let execution = self
1712 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
1713 .await;
1714
1715 if let Err(err) = self
1716 .store_step_outputs(&config, step.id, name, execution.is_ok())
1717 .await
1718 {
1719 self.fail_step(step.id, &err).await;
1720 return Err(err);
1721 }
1722
1723 match execution {
1724 Ok(output) => {
1725 self.total_cost_usd += output.cost_usd;
1726 self.total_duration_ms += output.duration_ms;
1727
1728 let debug_messages_json = output.debug_messages_json();
1729
1730 let completed_at = Utc::now();
1731 self.store
1732 .update_step(
1733 step.id,
1734 StepUpdate {
1735 status: Some(StepStatus::Completed),
1736 output: Some(output.output.clone()),
1737 duration_ms: Some(output.duration_ms),
1738 cost_usd: Some(output.cost_usd),
1739 input_tokens: output.input_tokens,
1740 output_tokens: output.output_tokens,
1741 completed_at: Some(completed_at),
1742 debug_messages: debug_messages_json,
1743 ..StepUpdate::default()
1744 },
1745 )
1746 .await?;
1747
1748 info!(
1749 run_id = %self.run_id,
1750 step = %name,
1751 duration_ms = output.duration_ms,
1752 "step completed"
1753 );
1754
1755 if let Some(ref bus) = self.event_bus {
1756 bus.publish(
1757 self.run_id,
1758 crate::notify::WorkflowEvent::StepCompleted {
1759 step_name: name.to_string(),
1760 step_index: position,
1761 duration_ms: output.duration_ms,
1762 output_summary: None,
1763 },
1764 );
1765 }
1766
1767 self.last_step_ids = vec![step.id];
1768
1769 Ok(output)
1770 }
1771 Err(err) => {
1772 let completed_at = Utc::now();
1773 let debug_messages_json = extract_debug_messages_from_error(&err);
1774 let partial = extract_partial_usage_from_error(&err);
1775 let raw_response_output = extract_raw_response_from_error(&err);
1776
1777 if let Some(ref usage) = partial {
1778 if let Some(cost) = usage.cost_usd {
1779 self.total_cost_usd += cost;
1780 }
1781 if let Some(dur) = usage.duration_ms {
1782 self.total_duration_ms += dur;
1783 }
1784 }
1785
1786 if let Err(store_err) = self
1787 .store
1788 .update_step(
1789 step.id,
1790 StepUpdate {
1791 status: Some(StepStatus::Failed),
1792 error: Some(err.to_string()),
1793 output: raw_response_output.clone(),
1794 completed_at: Some(completed_at),
1795 debug_messages: debug_messages_json,
1796 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1797 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1798 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1799 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1800 ..StepUpdate::default()
1801 },
1802 )
1803 .await
1804 {
1805 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1806 }
1807
1808 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1809
1810 if let Some(ref bus) = self.event_bus {
1811 bus.publish(
1812 self.run_id,
1813 crate::notify::WorkflowEvent::StepFailed {
1814 step_name: name.to_string(),
1815 step_index: position,
1816 error: err.to_string(),
1817 duration_ms: err_duration,
1818 },
1819 );
1820 }
1821
1822 self.fire_error_handlers(name, &err.to_string(), err_duration)
1823 .await;
1824
1825 if config.allow_failure() {
1826 self.has_allowed_failure = true;
1827 self.last_step_ids = vec![step.id];
1828 info!(
1829 run_id = %self.run_id,
1830 step = %name,
1831 error = %err,
1832 "step failed but allow_failure is set, continuing"
1833 );
1834 Ok(allowed_failure_output(
1835 &err.to_string(),
1836 raw_response_output,
1837 partial.as_ref(),
1838 ))
1839 } else {
1840 Err(err)
1841 }
1842 }
1843 }
1844 }
1845
1846 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
1852 async fn retry_step_if_configured(
1853 &self,
1854 name: &str,
1855 kind_str: &str,
1856 config: &StepConfig,
1857 step_id: Uuid,
1858 first_result: Result<StepOutput, EngineError>,
1859 ) -> Result<StepOutput, EngineError> {
1860 let policy = match config.retry() {
1861 Some(p) => p,
1862 None => return first_result,
1863 };
1864
1865 let mut last_result = match first_result {
1866 Ok(output) => return Ok(output),
1867 Err(err) if !is_step_retryable(&err) => return Err(err),
1868 Err(err) => Err(err),
1869 };
1870
1871 let step_log_sender = self
1872 .log_sender
1873 .as_ref()
1874 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
1875
1876 for attempt in 0..policy.max_retries() {
1877 if let StepConfig::Agent(agent_config) = config {
1878 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1879 }
1880
1881 let delay = policy.delay_for_attempt(attempt);
1882 info!(
1883 run_id = %self.run_id,
1884 step = %name,
1885 attempt = attempt + 1,
1886 max_retries = policy.max_retries(),
1887 delay_ms = delay.as_millis() as u64,
1888 "retrying step after transient failure"
1889 );
1890 tokio::time::sleep(delay).await;
1891
1892 record_retry_metric(kind_str, "retry");
1893
1894 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
1895 Ok(output) => return Ok(output),
1896 Err(err) if !is_step_retryable(&err) => return Err(err),
1897 err => last_result = err,
1898 }
1899 }
1900
1901 record_retry_metric(kind_str, "exhausted");
1902
1903 info!(
1904 run_id = %self.run_id,
1905 step = %name,
1906 max_retries = policy.max_retries(),
1907 "step retries exhausted"
1908 );
1909
1910 last_result
1911 }
1912
1913 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1918 if !self.last_step_ids.is_empty() {
1919 let deps: Vec<NewStepDependency> = self
1920 .last_step_ids
1921 .iter()
1922 .map(|&depends_on| NewStepDependency {
1923 step_id,
1924 depends_on,
1925 })
1926 .collect();
1927 self.store.create_step_dependencies(deps).await?;
1928 }
1929
1930 self.store
1931 .update_step(
1932 step_id,
1933 StepUpdate {
1934 status: Some(StepStatus::Running),
1935 started_at: Some(now),
1936 ..StepUpdate::default()
1937 },
1938 )
1939 .await?;
1940
1941 Ok(())
1942 }
1943
1944 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
1951 if let Err(store_err) = self
1952 .store
1953 .update_step(
1954 step_id,
1955 StepUpdate {
1956 status: Some(StepStatus::Failed),
1957 error: Some(err.to_string()),
1958 completed_at: Some(Utc::now()),
1959 ..StepUpdate::default()
1960 },
1961 )
1962 .await
1963 {
1964 error!(
1965 step_id = %step_id,
1966 error = %store_err,
1967 "failed to persist step failure"
1968 );
1969 }
1970 }
1971
1972 pub fn store(&self) -> &Arc<dyn Store> {
1974 &self.store
1975 }
1976
1977 pub async fn payload(&self) -> Result<Value, EngineError> {
1985 let run = self
1986 .store
1987 .get_run(self.run_id)
1988 .await?
1989 .ok_or(EngineError::Store(
1990 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1991 ))?;
1992 Ok(run.payload)
1993 }
1994
1995 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2023 let payload = self.payload().await?;
2024 serde_json::from_value(payload).map_err(EngineError::Serialization)
2025 }
2026
2027 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2050 self.error_handlers.push(OnErrorHandler {
2051 name: name.to_string(),
2052 config: config.into(),
2053 });
2054 }
2055
2056 pub fn clear_error_handlers(&mut self) {
2075 self.error_handlers.clear();
2076 }
2077
2078 async fn fire_error_handlers(
2084 &mut self,
2085 failed_step_name: &str,
2086 error_msg: &str,
2087 duration_ms: u64,
2088 ) {
2089 let handlers = std::mem::take(&mut self.error_handlers);
2090 if handlers.is_empty() {
2091 return;
2092 }
2093
2094 let error_context = json!({
2095 "failed_step": failed_step_name,
2096 "error": error_msg,
2097 "duration_ms": duration_ms,
2098 });
2099
2100 for handler in handlers {
2101 let mut config = handler.config.clone();
2102 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2103
2104 let position = self.position;
2105 self.position += 1;
2106
2107 let step = match self
2108 .store
2109 .create_step(NewStep {
2110 run_id: self.run_id,
2111 name: handler.name.clone(),
2112 kind: config.kind(),
2113 position,
2114 input: Some(error_context.clone()),
2115 is_error_handler: true,
2116 })
2117 .await
2118 {
2119 Ok(step) => step,
2120 Err(err) => {
2121 warn!(
2122 run_id = %self.run_id,
2123 handler = %handler.name,
2124 error = %err,
2125 "failed to create error handler step"
2126 );
2127 continue;
2128 }
2129 };
2130
2131 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2132 warn!(
2133 run_id = %self.run_id,
2134 handler = %handler.name,
2135 error = %err,
2136 "failed to start error handler step"
2137 );
2138 continue;
2139 }
2140
2141 let step_log_sender = self
2142 .log_sender
2143 .as_ref()
2144 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2145
2146 let start = Instant::now();
2147 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2148 let handler_duration = start.elapsed().as_millis() as u64;
2149 let completed_at = Utc::now();
2150
2151 match result {
2152 Ok(output) => {
2153 if let Err(store_err) = self
2154 .store
2155 .update_step(
2156 step.id,
2157 StepUpdate {
2158 status: Some(StepStatus::Completed),
2159 output: Some(output.output),
2160 duration_ms: Some(handler_duration),
2161 cost_usd: Some(output.cost_usd),
2162 completed_at: Some(completed_at),
2163 ..StepUpdate::default()
2164 },
2165 )
2166 .await
2167 {
2168 warn!(
2169 run_id = %self.run_id,
2170 handler = %handler.name,
2171 error = %store_err,
2172 "failed to persist error handler completion"
2173 );
2174 }
2175
2176 info!(
2177 run_id = %self.run_id,
2178 handler = %handler.name,
2179 duration_ms = handler_duration,
2180 "error handler completed"
2181 );
2182 }
2183 Err(err) => {
2184 if let Err(store_err) = self
2185 .store
2186 .update_step(
2187 step.id,
2188 StepUpdate {
2189 status: Some(StepStatus::Failed),
2190 error: Some(err.to_string()),
2191 duration_ms: Some(handler_duration),
2192 completed_at: Some(completed_at),
2193 ..StepUpdate::default()
2194 },
2195 )
2196 .await
2197 {
2198 warn!(
2199 run_id = %self.run_id,
2200 handler = %handler.name,
2201 error = %store_err,
2202 "failed to persist error handler failure"
2203 );
2204 }
2205
2206 warn!(
2207 run_id = %self.run_id,
2208 handler = %handler.name,
2209 error = %err,
2210 "error handler failed (original error preserved)"
2211 );
2212 }
2213 }
2214 }
2215 }
2216}
2217
2218fn inject_error_context(
2220 config: &mut StepConfig,
2221 failed_step: &str,
2222 error_msg: &str,
2223 duration_ms: u64,
2224) {
2225 match config {
2226 StepConfig::Shell(shell) => {
2227 shell
2228 .env
2229 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2230 shell
2231 .env
2232 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2233 shell.env.push((
2234 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2235 duration_ms.to_string(),
2236 ));
2237 }
2238 StepConfig::Agent(agent) => {
2239 agent.prompt = format!(
2240 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2241 failed_step, duration_ms, error_msg, agent.prompt
2242 );
2243 }
2244 StepConfig::Http(http) => {
2245 http.headers
2246 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2247 http.headers.push((
2248 "X-Ironflow-Error-Message".to_string(),
2249 error_msg.to_string(),
2250 ));
2251 }
2252 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2253 }
2254}
2255
2256#[cfg(feature = "prometheus")]
2257fn record_retry_metric(kind: &str, outcome: &str) {
2258 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2259 use metrics::counter;
2260 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2261 .increment(1);
2262}
2263
2264#[cfg(not(feature = "prometheus"))]
2265fn record_retry_metric(_kind: &str, _outcome: &str) {}
2266
2267fn is_step_retryable(err: &EngineError) -> bool {
2271 use ironflow_core::error::{AgentError, OperationError};
2272
2273 match err {
2274 EngineError::Operation(op) => match op {
2275 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2276 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2277 OperationError::Deserialize { .. } => false,
2278 OperationError::Http {
2279 status: Some(code), ..
2280 } if (400..500).contains(code) && *code != 429 => false,
2281 _ => true,
2282 },
2283 _ => false,
2284 }
2285}
2286
2287fn allowed_failure_output(
2288 error_msg: &str,
2289 raw_response: Option<Value>,
2290 partial: Option<&StepPartialUsage>,
2291) -> StepOutput {
2292 StepOutput {
2293 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2294 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2295 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2296 input_tokens: partial.and_then(|p| p.input_tokens),
2297 output_tokens: partial.and_then(|p| p.output_tokens),
2298 model: None,
2299 debug_messages: None,
2300 }
2301}
2302
2303impl fmt::Debug for WorkflowContext {
2304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2305 f.debug_struct("WorkflowContext")
2306 .field("run_id", &self.run_id)
2307 .field("position", &self.position)
2308 .field("total_cost_usd", &self.total_cost_usd)
2309 .field("inherited_cost_usd", &self.inherited_cost_usd)
2310 .field("max_cost_usd", &self.max_cost_usd)
2311 .finish_non_exhaustive()
2312 }
2313}
2314
2315fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2318 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2319 debug_messages,
2320 ..
2321 })) = err
2322 && !debug_messages.is_empty()
2323 {
2324 return serde_json::to_value(debug_messages).ok();
2325 }
2326 None
2327}
2328
2329struct StepPartialUsage {
2335 cost_usd: Option<Decimal>,
2336 duration_ms: Option<u64>,
2337 input_tokens: Option<u64>,
2338 output_tokens: Option<u64>,
2339}
2340
2341fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2347 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2348 raw_response: Some(text),
2349 ..
2350 })) = err
2351 {
2352 return Some(Value::String(text.clone()));
2353 }
2354 None
2355}
2356
2357fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2358 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2359 partial_usage,
2360 ..
2361 })) = err
2362 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2363 {
2364 return Some(StepPartialUsage {
2365 cost_usd: partial_usage
2366 .cost_usd
2367 .and_then(|c| Decimal::try_from(c).ok()),
2368 duration_ms: partial_usage.duration_ms,
2369 input_tokens: partial_usage.input_tokens,
2370 output_tokens: partial_usage.output_tokens,
2371 });
2372 }
2373 None
2374}
2375
2376#[cfg(test)]
2377mod tests {
2378 use super::*;
2379 use ironflow_core::providers::claude::ClaudeCodeProvider;
2380 use ironflow_core::providers::record_replay::RecordReplayProvider;
2381 use ironflow_store::memory::InMemoryStore;
2382 use ironflow_store::models::{Run, RunActor, RunFilter};
2383 use ironflow_store::store::RunStore;
2384 use serde_json::json;
2385 use std::sync::Arc;
2386 use std::sync::atomic::{AtomicBool, Ordering};
2387 use uuid::Uuid;
2388
2389 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2391 let inner = ClaudeCodeProvider::new();
2392 Arc::new(RecordReplayProvider::replay(
2393 inner,
2394 "/tmp/ironflow-fixtures",
2395 ))
2396 }
2397
2398 fn create_test_context() -> WorkflowContext {
2400 let store = Arc::new(InMemoryStore::new());
2401 let provider = create_test_provider();
2402 let run_id = Uuid::now_v7();
2403 WorkflowContext::new(run_id, store, provider)
2404 }
2405
2406 #[test]
2407 fn context_new_initializes_correctly() {
2408 let ctx = create_test_context();
2409 assert_eq!(ctx.position, 0);
2410 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2411 assert_eq!(ctx.total_duration_ms, 0);
2412 assert!(ctx.last_step_ids.is_empty());
2413 assert!(ctx.replay_steps.is_empty());
2414 assert!(ctx.log_sender.is_none());
2415 }
2416
2417 #[test]
2418 fn context_run_id_returns_correct_id() {
2419 let run_id = Uuid::now_v7();
2420 let store = Arc::new(InMemoryStore::new());
2421 let provider = create_test_provider();
2422 let ctx = WorkflowContext::new(run_id, store, provider);
2423 assert_eq!(ctx.run_id(), run_id);
2424 }
2425
2426 #[test]
2427 fn context_total_cost_usd_initially_zero() {
2428 let ctx = create_test_context();
2429 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2430 }
2431
2432 #[test]
2433 fn context_total_duration_ms_initially_zero() {
2434 let ctx = create_test_context();
2435 assert_eq!(ctx.total_duration_ms(), 0);
2436 }
2437
2438 #[test]
2439 fn context_with_handler_resolver_creates_context_with_resolver() {
2440 let store = Arc::new(InMemoryStore::new());
2441 let provider = create_test_provider();
2442 let run_id = Uuid::now_v7();
2443
2444 let called = Arc::new(AtomicBool::new(false));
2445 let called_clone = called.clone();
2446
2447 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2448 called_clone.store(true, Ordering::SeqCst);
2449 None
2450 });
2451
2452 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2453
2454 assert_eq!(ctx.run_id(), run_id);
2455 assert!(ctx.handler_resolver.is_some());
2456 }
2457
2458 #[tokio::test]
2459 async fn context_set_log_sender_attaches_sender() {
2460 let mut ctx = create_test_context();
2461 let (sender, _receiver) = crate::log_sender::channel();
2462 ctx.set_log_sender(sender);
2463 assert!(ctx.log_sender.is_some());
2464 }
2465
2466 #[tokio::test]
2467 async fn context_skip_creates_skipped_step() {
2468 let store = Arc::new(InMemoryStore::new());
2469 let provider = create_test_provider();
2470
2471 store
2473 .create_run(NewRun {
2474 created_by: None,
2475 workflow_name: "test".to_string(),
2476 trigger: TriggerKind::Manual,
2477 payload: json!({}),
2478 max_retries: 0,
2479 handler_version: None,
2480 labels: Default::default(),
2481 scheduled_at: None,
2482 idempotency_key: None,
2483 max_cost_usd: None,
2484 })
2485 .await
2486 .expect("failed to create run")
2487 .into_run();
2488
2489 let runs = store
2491 .list_runs(RunFilter::default(), 1, 10)
2492 .await
2493 .expect("failed to list runs");
2494 let created_run_id = runs.items[0].id;
2495
2496 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2497 let initial_position = ctx.position;
2498
2499 ctx.skip("skip-step", "condition not met")
2500 .await
2501 .expect("skip failed");
2502
2503 assert_eq!(ctx.position, initial_position + 1);
2504 assert!(!ctx.last_step_ids.is_empty());
2505
2506 let steps = store
2508 .list_steps(created_run_id)
2509 .await
2510 .expect("failed to list steps");
2511 assert_eq!(steps.len(), 1);
2512 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2513 }
2514
2515 struct NoopSubWorkflow;
2518
2519 impl WorkflowHandler for NoopSubWorkflow {
2520 fn name(&self) -> &str {
2521 "noop-sub"
2522 }
2523
2524 fn execute<'a>(
2525 &'a self,
2526 _ctx: &'a mut WorkflowContext,
2527 ) -> crate::handler::HandlerFuture<'a> {
2528 Box::pin(async move { Ok(()) })
2529 }
2530 }
2531
2532 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2535 let store = Arc::new(InMemoryStore::new());
2536 let provider = create_test_provider();
2537
2538 let parent = store
2539 .create_run(NewRun {
2540 workflow_name: "parent".to_string(),
2541 trigger: TriggerKind::Api,
2542 payload: json!({}),
2543 max_retries: 0,
2544 handler_version: None,
2545 labels: Default::default(),
2546 scheduled_at: None,
2547 created_by,
2548 idempotency_key: None,
2549 max_cost_usd: None,
2550 })
2551 .await
2552 .expect("failed to create parent run")
2553 .into_run();
2554
2555 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2556 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2557 _ => None,
2558 });
2559
2560 let mut ctx =
2561 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2562 ctx.workflow(&NoopSubWorkflow, json!({}))
2563 .await
2564 .expect("sub-workflow failed");
2565
2566 let runs = store
2567 .list_runs(RunFilter::default(), 1, 10)
2568 .await
2569 .expect("failed to list runs");
2570 runs.items
2571 .into_iter()
2572 .find(|r| r.workflow_name == "noop-sub")
2573 .expect("child run was created")
2574 }
2575
2576 #[tokio::test]
2577 async fn child_run_inherits_the_parent_author() {
2578 let user_id = Uuid::now_v7();
2579 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2580
2581 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2582 }
2583
2584 #[tokio::test]
2585 async fn child_run_of_an_unattributed_parent_has_no_author() {
2586 let child = child_run_of_parent_authored_by(None).await;
2587
2588 assert!(child.created_by.is_none());
2589 }
2590
2591 #[tokio::test]
2592 async fn context_parallel_empty_steps_returns_empty_vec() {
2593 let mut ctx = create_test_context();
2594 let results = ctx
2595 .parallel(vec![], true)
2596 .await
2597 .expect("parallel should not fail on empty input");
2598 assert!(results.is_empty());
2599 }
2600
2601 #[tokio::test]
2602 async fn context_approval_first_execution_returns_error() {
2603 let store = Arc::new(InMemoryStore::new());
2604 let provider = create_test_provider();
2605
2606 store
2608 .create_run(NewRun {
2609 created_by: None,
2610 workflow_name: "test".to_string(),
2611 trigger: TriggerKind::Manual,
2612 payload: json!({}),
2613 max_retries: 0,
2614 handler_version: None,
2615 labels: Default::default(),
2616 scheduled_at: None,
2617 idempotency_key: None,
2618 max_cost_usd: None,
2619 })
2620 .await
2621 .expect("failed to create run")
2622 .into_run();
2623
2624 let runs = store
2626 .list_runs(RunFilter::default(), 1, 10)
2627 .await
2628 .expect("failed to list runs");
2629 let created_run_id = runs.items[0].id;
2630
2631 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2632
2633 let result = ctx
2634 .approval(
2635 "approve-step",
2636 crate::config::ApprovalConfig::new("Continue?"),
2637 )
2638 .await;
2639
2640 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2642
2643 assert_eq!(ctx.position, 1);
2645
2646 let steps = store
2648 .list_steps(created_run_id)
2649 .await
2650 .expect("failed to list steps");
2651 assert_eq!(steps.len(), 1);
2652 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2653 }
2654
2655 #[tokio::test]
2656 async fn context_approval_replay_returns_ok() {
2657 let store = Arc::new(InMemoryStore::new());
2658 let provider = create_test_provider();
2659
2660 store
2662 .create_run(NewRun {
2663 created_by: None,
2664 workflow_name: "test".to_string(),
2665 trigger: TriggerKind::Manual,
2666 payload: json!({}),
2667 max_retries: 0,
2668 handler_version: None,
2669 labels: Default::default(),
2670 scheduled_at: None,
2671 idempotency_key: None,
2672 max_cost_usd: None,
2673 })
2674 .await
2675 .expect("failed to create run")
2676 .into_run();
2677
2678 let runs = store
2680 .list_runs(RunFilter::default(), 1, 10)
2681 .await
2682 .expect("failed to list runs");
2683 let created_run_id = runs.items[0].id;
2684
2685 let step = store
2687 .create_step(NewStep {
2688 run_id: created_run_id,
2689 name: "approval".to_string(),
2690 kind: StepKind::Approval,
2691 position: 0,
2692 input: None,
2693 is_error_handler: false,
2694 })
2695 .await
2696 .expect("failed to create step");
2697
2698 store
2700 .update_step(
2701 step.id,
2702 StepUpdate {
2703 status: Some(StepStatus::Running),
2704 started_at: Some(Utc::now()),
2705 ..StepUpdate::default()
2706 },
2707 )
2708 .await
2709 .expect("failed to update step to Running");
2710
2711 store
2712 .update_step(
2713 step.id,
2714 StepUpdate {
2715 status: Some(StepStatus::AwaitingApproval),
2716 ..StepUpdate::default()
2717 },
2718 )
2719 .await
2720 .expect("failed to update step to AwaitingApproval");
2721
2722 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2724 ctx.load_replay_steps()
2725 .await
2726 .expect("failed to load replay steps");
2727
2728 let result = ctx
2730 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2731 .await;
2732
2733 assert!(result.is_ok());
2734
2735 let steps = store
2737 .list_steps(created_run_id)
2738 .await
2739 .expect("failed to list steps");
2740 assert_eq!(steps.len(), 1);
2741 assert_eq!(steps[0].status.state, StepStatus::Completed);
2742 }
2743
2744 #[tokio::test]
2745 async fn context_load_replay_steps_loads_completed_steps() {
2746 let store = Arc::new(InMemoryStore::new());
2747 let provider = create_test_provider();
2748
2749 store
2751 .create_run(NewRun {
2752 created_by: None,
2753 workflow_name: "test".to_string(),
2754 trigger: TriggerKind::Manual,
2755 payload: json!({}),
2756 max_retries: 0,
2757 handler_version: None,
2758 labels: Default::default(),
2759 scheduled_at: None,
2760 idempotency_key: None,
2761 max_cost_usd: None,
2762 })
2763 .await
2764 .expect("failed to create run")
2765 .into_run();
2766
2767 let runs = store
2769 .list_runs(RunFilter::default(), 1, 10)
2770 .await
2771 .expect("failed to list runs");
2772 let created_run_id = runs.items[0].id;
2773
2774 let completed_step = store
2776 .create_step(NewStep {
2777 run_id: created_run_id,
2778 name: "completed".to_string(),
2779 kind: StepKind::Shell,
2780 position: 0,
2781 input: None,
2782 is_error_handler: false,
2783 })
2784 .await
2785 .expect("failed to create step");
2786
2787 store
2789 .update_step(
2790 completed_step.id,
2791 StepUpdate {
2792 status: Some(StepStatus::Running),
2793 started_at: Some(Utc::now()),
2794 ..StepUpdate::default()
2795 },
2796 )
2797 .await
2798 .expect("failed to update step to Running");
2799
2800 store
2801 .update_step(
2802 completed_step.id,
2803 StepUpdate {
2804 status: Some(StepStatus::Completed),
2805 completed_at: Some(Utc::now()),
2806 ..StepUpdate::default()
2807 },
2808 )
2809 .await
2810 .expect("failed to update step to Completed");
2811
2812 let _pending_step = store
2813 .create_step(NewStep {
2814 run_id: created_run_id,
2815 name: "pending".to_string(),
2816 kind: StepKind::Shell,
2817 position: 1,
2818 input: None,
2819 is_error_handler: false,
2820 })
2821 .await
2822 .expect("failed to create step");
2823
2824 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2826 ctx.load_replay_steps()
2827 .await
2828 .expect("failed to load replay steps");
2829
2830 assert_eq!(ctx.replay_steps.len(), 1);
2832 assert!(ctx.replay_steps.contains_key(&0));
2833 assert!(!ctx.replay_steps.contains_key(&1));
2834 }
2835
2836 #[tokio::test]
2837 async fn context_payload_returns_run_payload() {
2838 let store = Arc::new(InMemoryStore::new());
2839 let provider = create_test_provider();
2840 let test_payload = json!({"key": "value", "number": 42});
2841
2842 store
2844 .create_run(NewRun {
2845 created_by: None,
2846 workflow_name: "test".to_string(),
2847 trigger: TriggerKind::Manual,
2848 payload: test_payload.clone(),
2849 max_retries: 0,
2850 handler_version: None,
2851 labels: Default::default(),
2852 scheduled_at: None,
2853 idempotency_key: None,
2854 max_cost_usd: None,
2855 })
2856 .await
2857 .expect("failed to create run")
2858 .into_run();
2859
2860 let runs = store
2862 .list_runs(RunFilter::default(), 1, 10)
2863 .await
2864 .expect("failed to list runs");
2865 let created_run_id = runs.items[0].id;
2866
2867 let ctx = WorkflowContext::new(created_run_id, store, provider);
2868 let payload = ctx.payload().await.expect("failed to get payload");
2869
2870 assert_eq!(payload, test_payload);
2871 }
2872
2873 #[tokio::test]
2874 async fn context_payload_returns_error_for_nonexistent_run() {
2875 let store = Arc::new(InMemoryStore::new());
2876 let provider = create_test_provider();
2877 let run_id = Uuid::now_v7();
2878
2879 let ctx = WorkflowContext::new(run_id, store, provider);
2880 let result = ctx.payload().await;
2881
2882 assert!(result.is_err());
2883 }
2884
2885 #[tokio::test]
2886 async fn context_store_returns_reference() {
2887 let ctx = create_test_context();
2888 let _store = ctx.store();
2889 }
2891
2892 #[test]
2893 fn context_debug_formatting() {
2894 let ctx = create_test_context();
2895 let debug_str = format!("{:?}", ctx);
2896 assert!(debug_str.contains("WorkflowContext"));
2897 assert!(debug_str.contains("run_id"));
2898 }
2899
2900 #[tokio::test]
2901 async fn context_last_step_ids_tracks_executed_steps() {
2902 let store = Arc::new(InMemoryStore::new());
2903 let provider = create_test_provider();
2904
2905 store
2907 .create_run(NewRun {
2908 created_by: None,
2909 workflow_name: "test".to_string(),
2910 trigger: TriggerKind::Manual,
2911 payload: json!({}),
2912 max_retries: 0,
2913 handler_version: None,
2914 labels: Default::default(),
2915 scheduled_at: None,
2916 idempotency_key: None,
2917 max_cost_usd: None,
2918 })
2919 .await
2920 .expect("failed to create run")
2921 .into_run();
2922
2923 let runs = store
2925 .list_runs(RunFilter::default(), 1, 10)
2926 .await
2927 .expect("failed to list runs");
2928 let created_run_id = runs.items[0].id;
2929
2930 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2931 assert!(ctx.last_step_ids.is_empty());
2932
2933 ctx.skip("step1", "reason").await.expect("skip failed");
2934
2935 assert_eq!(ctx.last_step_ids.len(), 1);
2936
2937 ctx.skip("step2", "reason").await.expect("skip failed");
2938
2939 assert_eq!(ctx.last_step_ids.len(), 1);
2941 }
2942}