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 = 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(name) => name.as_str(),
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 if let Err(err) = self
1687 .store_step_outputs(&config, step.id, name, execution.is_ok())
1688 .await
1689 {
1690 self.fail_step(step.id, &err).await;
1691 return Err(err);
1692 }
1693
1694 match execution {
1695 Ok(output) => {
1696 self.total_cost_usd += output.cost_usd;
1697 self.total_duration_ms += output.duration_ms;
1698
1699 let debug_messages_json = output.debug_messages_json();
1700
1701 let completed_at = Utc::now();
1702 self.store
1703 .update_step(
1704 step.id,
1705 StepUpdate {
1706 status: Some(StepStatus::Completed),
1707 output: Some(output.output.clone()),
1708 duration_ms: Some(output.duration_ms),
1709 cost_usd: Some(output.cost_usd),
1710 input_tokens: output.input_tokens,
1711 output_tokens: output.output_tokens,
1712 completed_at: Some(completed_at),
1713 debug_messages: debug_messages_json,
1714 ..StepUpdate::default()
1715 },
1716 )
1717 .await?;
1718
1719 info!(
1720 run_id = %self.run_id,
1721 step = %name,
1722 duration_ms = output.duration_ms,
1723 "step completed"
1724 );
1725
1726 self.last_step_ids = vec![step.id];
1727
1728 Ok(output)
1729 }
1730 Err(err) => {
1731 let completed_at = Utc::now();
1732 let debug_messages_json = extract_debug_messages_from_error(&err);
1733 let partial = extract_partial_usage_from_error(&err);
1734 let raw_response_output = extract_raw_response_from_error(&err);
1735
1736 if let Some(ref usage) = partial {
1737 if let Some(cost) = usage.cost_usd {
1738 self.total_cost_usd += cost;
1739 }
1740 if let Some(dur) = usage.duration_ms {
1741 self.total_duration_ms += dur;
1742 }
1743 }
1744
1745 if let Err(store_err) = self
1746 .store
1747 .update_step(
1748 step.id,
1749 StepUpdate {
1750 status: Some(StepStatus::Failed),
1751 error: Some(err.to_string()),
1752 output: raw_response_output.clone(),
1753 completed_at: Some(completed_at),
1754 debug_messages: debug_messages_json,
1755 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1756 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1757 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1758 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1759 ..StepUpdate::default()
1760 },
1761 )
1762 .await
1763 {
1764 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1765 }
1766
1767 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1768 self.fire_error_handlers(name, &err.to_string(), err_duration)
1769 .await;
1770
1771 if config.allow_failure() {
1772 self.has_allowed_failure = true;
1773 self.last_step_ids = vec![step.id];
1774 info!(
1775 run_id = %self.run_id,
1776 step = %name,
1777 error = %err,
1778 "step failed but allow_failure is set, continuing"
1779 );
1780 Ok(allowed_failure_output(
1781 &err.to_string(),
1782 raw_response_output,
1783 partial.as_ref(),
1784 ))
1785 } else {
1786 Err(err)
1787 }
1788 }
1789 }
1790 }
1791
1792 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1797 if !self.last_step_ids.is_empty() {
1798 let deps: Vec<NewStepDependency> = self
1799 .last_step_ids
1800 .iter()
1801 .map(|&depends_on| NewStepDependency {
1802 step_id,
1803 depends_on,
1804 })
1805 .collect();
1806 self.store.create_step_dependencies(deps).await?;
1807 }
1808
1809 self.store
1810 .update_step(
1811 step_id,
1812 StepUpdate {
1813 status: Some(StepStatus::Running),
1814 started_at: Some(now),
1815 ..StepUpdate::default()
1816 },
1817 )
1818 .await?;
1819
1820 Ok(())
1821 }
1822
1823 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
1830 if let Err(store_err) = self
1831 .store
1832 .update_step(
1833 step_id,
1834 StepUpdate {
1835 status: Some(StepStatus::Failed),
1836 error: Some(err.to_string()),
1837 completed_at: Some(Utc::now()),
1838 ..StepUpdate::default()
1839 },
1840 )
1841 .await
1842 {
1843 error!(
1844 step_id = %step_id,
1845 error = %store_err,
1846 "failed to persist step failure"
1847 );
1848 }
1849 }
1850
1851 pub fn store(&self) -> &Arc<dyn Store> {
1853 &self.store
1854 }
1855
1856 pub async fn payload(&self) -> Result<Value, EngineError> {
1864 let run = self
1865 .store
1866 .get_run(self.run_id)
1867 .await?
1868 .ok_or(EngineError::Store(
1869 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1870 ))?;
1871 Ok(run.payload)
1872 }
1873
1874 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
1902 let payload = self.payload().await?;
1903 serde_json::from_value(payload).map_err(EngineError::Serialization)
1904 }
1905
1906 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
1929 self.error_handlers.push(OnErrorHandler {
1930 name: name.to_string(),
1931 config: config.into(),
1932 });
1933 }
1934
1935 pub fn clear_error_handlers(&mut self) {
1954 self.error_handlers.clear();
1955 }
1956
1957 async fn fire_error_handlers(
1963 &mut self,
1964 failed_step_name: &str,
1965 error_msg: &str,
1966 duration_ms: u64,
1967 ) {
1968 let handlers = std::mem::take(&mut self.error_handlers);
1969 if handlers.is_empty() {
1970 return;
1971 }
1972
1973 let error_context = json!({
1974 "failed_step": failed_step_name,
1975 "error": error_msg,
1976 "duration_ms": duration_ms,
1977 });
1978
1979 for handler in handlers {
1980 let mut config = handler.config.clone();
1981 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
1982
1983 let position = self.position;
1984 self.position += 1;
1985
1986 let step = match self
1987 .store
1988 .create_step(NewStep {
1989 run_id: self.run_id,
1990 name: handler.name.clone(),
1991 kind: config.kind(),
1992 position,
1993 input: Some(error_context.clone()),
1994 is_error_handler: true,
1995 })
1996 .await
1997 {
1998 Ok(step) => step,
1999 Err(err) => {
2000 warn!(
2001 run_id = %self.run_id,
2002 handler = %handler.name,
2003 error = %err,
2004 "failed to create error handler step"
2005 );
2006 continue;
2007 }
2008 };
2009
2010 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2011 warn!(
2012 run_id = %self.run_id,
2013 handler = %handler.name,
2014 error = %err,
2015 "failed to start error handler step"
2016 );
2017 continue;
2018 }
2019
2020 let step_log_sender = self
2021 .log_sender
2022 .as_ref()
2023 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2024
2025 let start = Instant::now();
2026 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2027 let handler_duration = start.elapsed().as_millis() as u64;
2028 let completed_at = Utc::now();
2029
2030 match result {
2031 Ok(output) => {
2032 if let Err(store_err) = self
2033 .store
2034 .update_step(
2035 step.id,
2036 StepUpdate {
2037 status: Some(StepStatus::Completed),
2038 output: Some(output.output),
2039 duration_ms: Some(handler_duration),
2040 cost_usd: Some(output.cost_usd),
2041 completed_at: Some(completed_at),
2042 ..StepUpdate::default()
2043 },
2044 )
2045 .await
2046 {
2047 warn!(
2048 run_id = %self.run_id,
2049 handler = %handler.name,
2050 error = %store_err,
2051 "failed to persist error handler completion"
2052 );
2053 }
2054
2055 info!(
2056 run_id = %self.run_id,
2057 handler = %handler.name,
2058 duration_ms = handler_duration,
2059 "error handler completed"
2060 );
2061 }
2062 Err(err) => {
2063 if let Err(store_err) = self
2064 .store
2065 .update_step(
2066 step.id,
2067 StepUpdate {
2068 status: Some(StepStatus::Failed),
2069 error: Some(err.to_string()),
2070 duration_ms: Some(handler_duration),
2071 completed_at: Some(completed_at),
2072 ..StepUpdate::default()
2073 },
2074 )
2075 .await
2076 {
2077 warn!(
2078 run_id = %self.run_id,
2079 handler = %handler.name,
2080 error = %store_err,
2081 "failed to persist error handler failure"
2082 );
2083 }
2084
2085 warn!(
2086 run_id = %self.run_id,
2087 handler = %handler.name,
2088 error = %err,
2089 "error handler failed (original error preserved)"
2090 );
2091 }
2092 }
2093 }
2094 }
2095}
2096
2097fn inject_error_context(
2099 config: &mut StepConfig,
2100 failed_step: &str,
2101 error_msg: &str,
2102 duration_ms: u64,
2103) {
2104 match config {
2105 StepConfig::Shell(shell) => {
2106 shell
2107 .env
2108 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2109 shell
2110 .env
2111 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2112 shell.env.push((
2113 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2114 duration_ms.to_string(),
2115 ));
2116 }
2117 StepConfig::Agent(agent) => {
2118 agent.prompt = format!(
2119 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2120 failed_step, duration_ms, error_msg, agent.prompt
2121 );
2122 }
2123 StepConfig::Http(http) => {
2124 http.headers
2125 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2126 http.headers.push((
2127 "X-Ironflow-Error-Message".to_string(),
2128 error_msg.to_string(),
2129 ));
2130 }
2131 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2132 }
2133}
2134
2135fn allowed_failure_output(
2136 error_msg: &str,
2137 raw_response: Option<Value>,
2138 partial: Option<&StepPartialUsage>,
2139) -> StepOutput {
2140 StepOutput {
2141 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2142 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2143 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2144 input_tokens: partial.and_then(|p| p.input_tokens),
2145 output_tokens: partial.and_then(|p| p.output_tokens),
2146 model: None,
2147 debug_messages: None,
2148 }
2149}
2150
2151impl fmt::Debug for WorkflowContext {
2152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2153 f.debug_struct("WorkflowContext")
2154 .field("run_id", &self.run_id)
2155 .field("position", &self.position)
2156 .field("total_cost_usd", &self.total_cost_usd)
2157 .field("inherited_cost_usd", &self.inherited_cost_usd)
2158 .field("max_cost_usd", &self.max_cost_usd)
2159 .finish_non_exhaustive()
2160 }
2161}
2162
2163fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2166 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2167 debug_messages,
2168 ..
2169 })) = err
2170 && !debug_messages.is_empty()
2171 {
2172 return serde_json::to_value(debug_messages).ok();
2173 }
2174 None
2175}
2176
2177struct StepPartialUsage {
2183 cost_usd: Option<Decimal>,
2184 duration_ms: Option<u64>,
2185 input_tokens: Option<u64>,
2186 output_tokens: Option<u64>,
2187}
2188
2189fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2195 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2196 raw_response: Some(text),
2197 ..
2198 })) = err
2199 {
2200 return Some(Value::String(text.clone()));
2201 }
2202 None
2203}
2204
2205fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2206 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2207 partial_usage,
2208 ..
2209 })) = err
2210 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2211 {
2212 return Some(StepPartialUsage {
2213 cost_usd: partial_usage
2214 .cost_usd
2215 .and_then(|c| Decimal::try_from(c).ok()),
2216 duration_ms: partial_usage.duration_ms,
2217 input_tokens: partial_usage.input_tokens,
2218 output_tokens: partial_usage.output_tokens,
2219 });
2220 }
2221 None
2222}
2223
2224#[cfg(test)]
2225mod tests {
2226 use super::*;
2227 use ironflow_core::providers::claude::ClaudeCodeProvider;
2228 use ironflow_core::providers::record_replay::RecordReplayProvider;
2229 use ironflow_store::memory::InMemoryStore;
2230 use ironflow_store::models::{Run, RunActor, RunFilter};
2231 use ironflow_store::store::RunStore;
2232 use serde_json::json;
2233 use std::sync::Arc;
2234 use std::sync::atomic::{AtomicBool, Ordering};
2235 use uuid::Uuid;
2236
2237 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2239 let inner = ClaudeCodeProvider::new();
2240 Arc::new(RecordReplayProvider::replay(
2241 inner,
2242 "/tmp/ironflow-fixtures",
2243 ))
2244 }
2245
2246 fn create_test_context() -> WorkflowContext {
2248 let store = Arc::new(InMemoryStore::new());
2249 let provider = create_test_provider();
2250 let run_id = Uuid::now_v7();
2251 WorkflowContext::new(run_id, store, provider)
2252 }
2253
2254 #[test]
2255 fn context_new_initializes_correctly() {
2256 let ctx = create_test_context();
2257 assert_eq!(ctx.position, 0);
2258 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2259 assert_eq!(ctx.total_duration_ms, 0);
2260 assert!(ctx.last_step_ids.is_empty());
2261 assert!(ctx.replay_steps.is_empty());
2262 assert!(ctx.log_sender.is_none());
2263 }
2264
2265 #[test]
2266 fn context_run_id_returns_correct_id() {
2267 let run_id = Uuid::now_v7();
2268 let store = Arc::new(InMemoryStore::new());
2269 let provider = create_test_provider();
2270 let ctx = WorkflowContext::new(run_id, store, provider);
2271 assert_eq!(ctx.run_id(), run_id);
2272 }
2273
2274 #[test]
2275 fn context_total_cost_usd_initially_zero() {
2276 let ctx = create_test_context();
2277 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2278 }
2279
2280 #[test]
2281 fn context_total_duration_ms_initially_zero() {
2282 let ctx = create_test_context();
2283 assert_eq!(ctx.total_duration_ms(), 0);
2284 }
2285
2286 #[test]
2287 fn context_with_handler_resolver_creates_context_with_resolver() {
2288 let store = Arc::new(InMemoryStore::new());
2289 let provider = create_test_provider();
2290 let run_id = Uuid::now_v7();
2291
2292 let called = Arc::new(AtomicBool::new(false));
2293 let called_clone = called.clone();
2294
2295 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2296 called_clone.store(true, Ordering::SeqCst);
2297 None
2298 });
2299
2300 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2301
2302 assert_eq!(ctx.run_id(), run_id);
2303 assert!(ctx.handler_resolver.is_some());
2304 }
2305
2306 #[tokio::test]
2307 async fn context_set_log_sender_attaches_sender() {
2308 let mut ctx = create_test_context();
2309 let (sender, _receiver) = crate::log_sender::channel();
2310 ctx.set_log_sender(sender);
2311 assert!(ctx.log_sender.is_some());
2312 }
2313
2314 #[tokio::test]
2315 async fn context_skip_creates_skipped_step() {
2316 let store = Arc::new(InMemoryStore::new());
2317 let provider = create_test_provider();
2318
2319 store
2321 .create_run(NewRun {
2322 created_by: None,
2323 workflow_name: "test".to_string(),
2324 trigger: TriggerKind::Manual,
2325 payload: json!({}),
2326 max_retries: 0,
2327 handler_version: None,
2328 labels: Default::default(),
2329 scheduled_at: None,
2330 idempotency_key: None,
2331 max_cost_usd: None,
2332 })
2333 .await
2334 .expect("failed to create run")
2335 .into_run();
2336
2337 let runs = store
2339 .list_runs(RunFilter::default(), 1, 10)
2340 .await
2341 .expect("failed to list runs");
2342 let created_run_id = runs.items[0].id;
2343
2344 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2345 let initial_position = ctx.position;
2346
2347 ctx.skip("skip-step", "condition not met")
2348 .await
2349 .expect("skip failed");
2350
2351 assert_eq!(ctx.position, initial_position + 1);
2352 assert!(!ctx.last_step_ids.is_empty());
2353
2354 let steps = store
2356 .list_steps(created_run_id)
2357 .await
2358 .expect("failed to list steps");
2359 assert_eq!(steps.len(), 1);
2360 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2361 }
2362
2363 struct NoopSubWorkflow;
2366
2367 impl WorkflowHandler for NoopSubWorkflow {
2368 fn name(&self) -> &str {
2369 "noop-sub"
2370 }
2371
2372 fn execute<'a>(
2373 &'a self,
2374 _ctx: &'a mut WorkflowContext,
2375 ) -> crate::handler::HandlerFuture<'a> {
2376 Box::pin(async move { Ok(()) })
2377 }
2378 }
2379
2380 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2383 let store = Arc::new(InMemoryStore::new());
2384 let provider = create_test_provider();
2385
2386 let parent = store
2387 .create_run(NewRun {
2388 workflow_name: "parent".to_string(),
2389 trigger: TriggerKind::Api,
2390 payload: json!({}),
2391 max_retries: 0,
2392 handler_version: None,
2393 labels: Default::default(),
2394 scheduled_at: None,
2395 created_by,
2396 idempotency_key: None,
2397 max_cost_usd: None,
2398 })
2399 .await
2400 .expect("failed to create parent run")
2401 .into_run();
2402
2403 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2404 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2405 _ => None,
2406 });
2407
2408 let mut ctx =
2409 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2410 ctx.workflow(&NoopSubWorkflow, json!({}))
2411 .await
2412 .expect("sub-workflow failed");
2413
2414 let runs = store
2415 .list_runs(RunFilter::default(), 1, 10)
2416 .await
2417 .expect("failed to list runs");
2418 runs.items
2419 .into_iter()
2420 .find(|r| r.workflow_name == "noop-sub")
2421 .expect("child run was created")
2422 }
2423
2424 #[tokio::test]
2425 async fn child_run_inherits_the_parent_author() {
2426 let user_id = Uuid::now_v7();
2427 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2428
2429 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2430 }
2431
2432 #[tokio::test]
2433 async fn child_run_of_an_unattributed_parent_has_no_author() {
2434 let child = child_run_of_parent_authored_by(None).await;
2435
2436 assert!(child.created_by.is_none());
2437 }
2438
2439 #[tokio::test]
2440 async fn context_parallel_empty_steps_returns_empty_vec() {
2441 let mut ctx = create_test_context();
2442 let results = ctx
2443 .parallel(vec![], true)
2444 .await
2445 .expect("parallel should not fail on empty input");
2446 assert!(results.is_empty());
2447 }
2448
2449 #[tokio::test]
2450 async fn context_approval_first_execution_returns_error() {
2451 let store = Arc::new(InMemoryStore::new());
2452 let provider = create_test_provider();
2453
2454 store
2456 .create_run(NewRun {
2457 created_by: None,
2458 workflow_name: "test".to_string(),
2459 trigger: TriggerKind::Manual,
2460 payload: json!({}),
2461 max_retries: 0,
2462 handler_version: None,
2463 labels: Default::default(),
2464 scheduled_at: None,
2465 idempotency_key: None,
2466 max_cost_usd: None,
2467 })
2468 .await
2469 .expect("failed to create run")
2470 .into_run();
2471
2472 let runs = store
2474 .list_runs(RunFilter::default(), 1, 10)
2475 .await
2476 .expect("failed to list runs");
2477 let created_run_id = runs.items[0].id;
2478
2479 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2480
2481 let result = ctx
2482 .approval(
2483 "approve-step",
2484 crate::config::ApprovalConfig::new("Continue?"),
2485 )
2486 .await;
2487
2488 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2490
2491 assert_eq!(ctx.position, 1);
2493
2494 let steps = store
2496 .list_steps(created_run_id)
2497 .await
2498 .expect("failed to list steps");
2499 assert_eq!(steps.len(), 1);
2500 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2501 }
2502
2503 #[tokio::test]
2504 async fn context_approval_replay_returns_ok() {
2505 let store = Arc::new(InMemoryStore::new());
2506 let provider = create_test_provider();
2507
2508 store
2510 .create_run(NewRun {
2511 created_by: None,
2512 workflow_name: "test".to_string(),
2513 trigger: TriggerKind::Manual,
2514 payload: json!({}),
2515 max_retries: 0,
2516 handler_version: None,
2517 labels: Default::default(),
2518 scheduled_at: None,
2519 idempotency_key: None,
2520 max_cost_usd: None,
2521 })
2522 .await
2523 .expect("failed to create run")
2524 .into_run();
2525
2526 let runs = store
2528 .list_runs(RunFilter::default(), 1, 10)
2529 .await
2530 .expect("failed to list runs");
2531 let created_run_id = runs.items[0].id;
2532
2533 let step = store
2535 .create_step(NewStep {
2536 run_id: created_run_id,
2537 name: "approval".to_string(),
2538 kind: StepKind::Approval,
2539 position: 0,
2540 input: None,
2541 is_error_handler: false,
2542 })
2543 .await
2544 .expect("failed to create step");
2545
2546 store
2548 .update_step(
2549 step.id,
2550 StepUpdate {
2551 status: Some(StepStatus::Running),
2552 started_at: Some(Utc::now()),
2553 ..StepUpdate::default()
2554 },
2555 )
2556 .await
2557 .expect("failed to update step to Running");
2558
2559 store
2560 .update_step(
2561 step.id,
2562 StepUpdate {
2563 status: Some(StepStatus::AwaitingApproval),
2564 ..StepUpdate::default()
2565 },
2566 )
2567 .await
2568 .expect("failed to update step to AwaitingApproval");
2569
2570 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2572 ctx.load_replay_steps()
2573 .await
2574 .expect("failed to load replay steps");
2575
2576 let result = ctx
2578 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2579 .await;
2580
2581 assert!(result.is_ok());
2582
2583 let steps = store
2585 .list_steps(created_run_id)
2586 .await
2587 .expect("failed to list steps");
2588 assert_eq!(steps.len(), 1);
2589 assert_eq!(steps[0].status.state, StepStatus::Completed);
2590 }
2591
2592 #[tokio::test]
2593 async fn context_load_replay_steps_loads_completed_steps() {
2594 let store = Arc::new(InMemoryStore::new());
2595 let provider = create_test_provider();
2596
2597 store
2599 .create_run(NewRun {
2600 created_by: None,
2601 workflow_name: "test".to_string(),
2602 trigger: TriggerKind::Manual,
2603 payload: json!({}),
2604 max_retries: 0,
2605 handler_version: None,
2606 labels: Default::default(),
2607 scheduled_at: None,
2608 idempotency_key: None,
2609 max_cost_usd: None,
2610 })
2611 .await
2612 .expect("failed to create run")
2613 .into_run();
2614
2615 let runs = store
2617 .list_runs(RunFilter::default(), 1, 10)
2618 .await
2619 .expect("failed to list runs");
2620 let created_run_id = runs.items[0].id;
2621
2622 let completed_step = store
2624 .create_step(NewStep {
2625 run_id: created_run_id,
2626 name: "completed".to_string(),
2627 kind: StepKind::Shell,
2628 position: 0,
2629 input: None,
2630 is_error_handler: false,
2631 })
2632 .await
2633 .expect("failed to create step");
2634
2635 store
2637 .update_step(
2638 completed_step.id,
2639 StepUpdate {
2640 status: Some(StepStatus::Running),
2641 started_at: Some(Utc::now()),
2642 ..StepUpdate::default()
2643 },
2644 )
2645 .await
2646 .expect("failed to update step to Running");
2647
2648 store
2649 .update_step(
2650 completed_step.id,
2651 StepUpdate {
2652 status: Some(StepStatus::Completed),
2653 completed_at: Some(Utc::now()),
2654 ..StepUpdate::default()
2655 },
2656 )
2657 .await
2658 .expect("failed to update step to Completed");
2659
2660 let _pending_step = store
2661 .create_step(NewStep {
2662 run_id: created_run_id,
2663 name: "pending".to_string(),
2664 kind: StepKind::Shell,
2665 position: 1,
2666 input: None,
2667 is_error_handler: false,
2668 })
2669 .await
2670 .expect("failed to create step");
2671
2672 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2674 ctx.load_replay_steps()
2675 .await
2676 .expect("failed to load replay steps");
2677
2678 assert_eq!(ctx.replay_steps.len(), 1);
2680 assert!(ctx.replay_steps.contains_key(&0));
2681 assert!(!ctx.replay_steps.contains_key(&1));
2682 }
2683
2684 #[tokio::test]
2685 async fn context_payload_returns_run_payload() {
2686 let store = Arc::new(InMemoryStore::new());
2687 let provider = create_test_provider();
2688 let test_payload = json!({"key": "value", "number": 42});
2689
2690 store
2692 .create_run(NewRun {
2693 created_by: None,
2694 workflow_name: "test".to_string(),
2695 trigger: TriggerKind::Manual,
2696 payload: test_payload.clone(),
2697 max_retries: 0,
2698 handler_version: None,
2699 labels: Default::default(),
2700 scheduled_at: None,
2701 idempotency_key: None,
2702 max_cost_usd: None,
2703 })
2704 .await
2705 .expect("failed to create run")
2706 .into_run();
2707
2708 let runs = store
2710 .list_runs(RunFilter::default(), 1, 10)
2711 .await
2712 .expect("failed to list runs");
2713 let created_run_id = runs.items[0].id;
2714
2715 let ctx = WorkflowContext::new(created_run_id, store, provider);
2716 let payload = ctx.payload().await.expect("failed to get payload");
2717
2718 assert_eq!(payload, test_payload);
2719 }
2720
2721 #[tokio::test]
2722 async fn context_payload_returns_error_for_nonexistent_run() {
2723 let store = Arc::new(InMemoryStore::new());
2724 let provider = create_test_provider();
2725 let run_id = Uuid::now_v7();
2726
2727 let ctx = WorkflowContext::new(run_id, store, provider);
2728 let result = ctx.payload().await;
2729
2730 assert!(result.is_err());
2731 }
2732
2733 #[tokio::test]
2734 async fn context_store_returns_reference() {
2735 let ctx = create_test_context();
2736 let _store = ctx.store();
2737 }
2739
2740 #[test]
2741 fn context_debug_formatting() {
2742 let ctx = create_test_context();
2743 let debug_str = format!("{:?}", ctx);
2744 assert!(debug_str.contains("WorkflowContext"));
2745 assert!(debug_str.contains("run_id"));
2746 }
2747
2748 #[tokio::test]
2749 async fn context_last_step_ids_tracks_executed_steps() {
2750 let store = Arc::new(InMemoryStore::new());
2751 let provider = create_test_provider();
2752
2753 store
2755 .create_run(NewRun {
2756 created_by: None,
2757 workflow_name: "test".to_string(),
2758 trigger: TriggerKind::Manual,
2759 payload: json!({}),
2760 max_retries: 0,
2761 handler_version: None,
2762 labels: Default::default(),
2763 scheduled_at: None,
2764 idempotency_key: None,
2765 max_cost_usd: None,
2766 })
2767 .await
2768 .expect("failed to create run")
2769 .into_run();
2770
2771 let runs = store
2773 .list_runs(RunFilter::default(), 1, 10)
2774 .await
2775 .expect("failed to list runs");
2776 let created_run_id = runs.items[0].id;
2777
2778 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2779 assert!(ctx.last_step_ids.is_empty());
2780
2781 ctx.skip("step1", "reason").await.expect("skip failed");
2782
2783 assert_eq!(ctx.last_step_ids.len(), 1);
2784
2785 ctx.skip("step2", "reason").await.expect("skip failed");
2786
2787 assert_eq!(ctx.last_step_ids.len(), 1);
2789 }
2790}