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