1use std::collections::HashMap;
27use std::fmt;
28use std::sync::Arc;
29use std::time::Instant;
30
31use chrono::{DateTime, Utc};
32use futures_util::StreamExt;
33use rust_decimal::Decimal;
34use serde_json::{Value, json};
35use tokio::task::{Id, JoinSet};
36use tracing::{error, info, warn};
37use uuid::Uuid;
38
39use ironflow_core::error::{AgentError, OperationError};
40use ironflow_core::provider::AgentProvider;
41use ironflow_store::models::{
42 ArtifactLookup, NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind,
43 StepStatus, StepUpdate, TriggerKind,
44};
45use ironflow_store::store::Store;
46
47use ironflow_artifacts::name::guess_content_type;
48use ironflow_artifacts::stream_from_bytes;
49use ironflow_store::entities::Artifact;
50
51use crate::artifact::{
52 ArtifactSink, ArtifactUpload, StepLocation, collect_outputs, materialize_inputs,
53};
54use crate::budget::step_budget_usd;
55use crate::config::{
56 AgentStepConfig, ApprovalConfig, HttpConfig, ShellConfig, StepConfig, WorkflowStepConfig,
57};
58use crate::error::EngineError;
59use crate::executor::{ParallelStepResult, StepOutput, execute_step_config};
60use crate::handler::WorkflowHandler;
61use crate::log_sender::{LogSender, StepLogSender};
62use crate::operation::Operation;
63
64pub(crate) type HandlerResolver =
66 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
67
68pub struct WorkflowContext {
87 run_id: Uuid,
88 store: Arc<dyn Store>,
89 provider: Arc<dyn AgentProvider>,
90 handler_resolver: Option<HandlerResolver>,
91 position: u32,
92 last_step_ids: Vec<Uuid>,
94 total_cost_usd: Decimal,
96 total_duration_ms: u64,
98 max_cost_usd: Option<Decimal>,
100 inherited_cost_usd: Decimal,
103 replay_steps: HashMap<u32, Step>,
106 granted_approvals: HashMap<u32, u32>,
110 attempt: u32,
112 carried_duration_ms: u64,
115 log_sender: Option<LogSender>,
117 artifact_sink: Option<Arc<dyn ArtifactSink>>,
121 error_handlers: Vec<OnErrorHandler>,
123}
124
125struct OnErrorHandler {
127 name: String,
128 config: StepConfig,
129}
130
131impl WorkflowContext {
132 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
137 Self {
138 run_id,
139 store,
140 provider,
141 handler_resolver: None,
142 position: 0,
143 last_step_ids: Vec::new(),
144 total_cost_usd: Decimal::ZERO,
145 total_duration_ms: 0,
146 max_cost_usd: None,
147 inherited_cost_usd: Decimal::ZERO,
148 replay_steps: HashMap::new(),
149 granted_approvals: HashMap::new(),
150 attempt: 1,
151 carried_duration_ms: 0,
152 log_sender: None,
153 artifact_sink: None,
154 error_handlers: Vec::new(),
155 }
156 }
157
158 pub(crate) fn with_handler_resolver(
163 run_id: Uuid,
164 store: Arc<dyn Store>,
165 provider: Arc<dyn AgentProvider>,
166 resolver: HandlerResolver,
167 ) -> Self {
168 Self {
169 run_id,
170 store,
171 provider,
172 handler_resolver: Some(resolver),
173 position: 0,
174 last_step_ids: Vec::new(),
175 total_cost_usd: Decimal::ZERO,
176 total_duration_ms: 0,
177 max_cost_usd: None,
178 inherited_cost_usd: Decimal::ZERO,
179 replay_steps: HashMap::new(),
180 granted_approvals: HashMap::new(),
181 attempt: 1,
182 carried_duration_ms: 0,
183 log_sender: None,
184 artifact_sink: None,
185 error_handlers: Vec::new(),
186 }
187 }
188
189 pub fn set_log_sender(&mut self, sender: LogSender) {
191 self.log_sender = Some(sender);
192 }
193
194 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
214 self.artifact_sink = Some(sink);
215 }
216
217 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
219 self.artifact_sink.as_ref().ok_or_else(|| {
220 EngineError::ArtifactsUnavailable(
221 "no artifact storage is attached to this run".to_string(),
222 )
223 })
224 }
225
226 pub async fn put_artifact(
256 &self,
257 step_id: Uuid,
258 name: &str,
259 content_type: Option<&str>,
260 content: Vec<u8>,
261 ) -> Result<Artifact, EngineError> {
262 let sink = self.artifact_sink()?;
263 sink.put(
264 ArtifactUpload {
265 run_id: self.run_id,
266 step_id,
267 name: name.to_string(),
268 content_type: content_type
269 .map(str::to_string)
270 .unwrap_or_else(|| guess_content_type(name)),
271 },
272 stream_from_bytes(content),
273 )
274 .await
275 }
276
277 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
302 let sink = self.artifact_sink()?;
303
304 let artifact = self
305 .store
306 .find_artifact_for_input(ArtifactLookup {
307 run_id: self.run_id,
308 attempt: self.attempt,
309 before_position: self.position,
310 step_name: step.to_string(),
311 name: name.to_string(),
312 })
313 .await?
314 .ok_or_else(|| EngineError::ArtifactNotFound {
315 step: step.to_string(),
316 name: name.to_string(),
317 })?;
318
319 let mut content = sink.get(&artifact).await?;
320 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
321 while let Some(chunk) = content.next().await {
322 let chunk = chunk?;
323 buffer.extend_from_slice(chunk.as_ref());
324 }
325
326 Ok(buffer)
327 }
328
329 async fn prepare_step_inputs(
334 &self,
335 config: &StepConfig,
336 position: u32,
337 ) -> Result<(), EngineError> {
338 let StepConfig::Shell(shell) = config else {
339 return Ok(());
340 };
341 if shell.inputs.is_empty() {
342 return Ok(());
343 }
344
345 materialize_inputs(
346 self.artifact_sink()?,
347 &self.store,
348 shell,
349 StepLocation {
350 run_id: self.run_id,
351 attempt: self.attempt,
352 position,
353 },
354 )
355 .await
356 }
357
358 async fn store_step_outputs(
363 &self,
364 config: &StepConfig,
365 step_id: Uuid,
366 step_name: &str,
367 step_succeeded: bool,
368 ) -> Result<(), EngineError> {
369 let StepConfig::Shell(shell) = config else {
370 return Ok(());
371 };
372 if shell.outputs.is_empty() {
373 return Ok(());
374 }
375
376 let sink = match self.artifact_sink() {
377 Ok(sink) => sink,
378 Err(err) if step_succeeded => return Err(err),
379 Err(err) => {
380 warn!(
381 run_id = %self.run_id,
382 step = %step_name,
383 error = %err,
384 "cannot collect outputs of a failed step"
385 );
386 return Ok(());
387 }
388 };
389
390 let collected =
391 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
392
393 match collected {
394 Ok(()) => Ok(()),
395 Err(err) if step_succeeded => Err(err),
396 Err(err) => {
397 warn!(
398 run_id = %self.run_id,
399 step = %step_name,
400 error = %err,
401 "failed to collect outputs of a failed step"
402 );
403 Ok(())
404 }
405 }
406 }
407
408 pub(crate) fn carry_over_run_totals(
415 &mut self,
416 attempt: u32,
417 cost_usd: Decimal,
418 duration_ms: u64,
419 ) {
420 self.attempt = attempt;
421 self.total_cost_usd = cost_usd;
422 self.carried_duration_ms = duration_ms;
423 }
424
425 pub(crate) fn carried_duration_ms(&self) -> u64 {
427 self.carried_duration_ms
428 }
429
430 pub fn attempt(&self) -> u32 {
432 self.attempt
433 }
434
435 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
451 self.max_cost_usd = cap;
452 }
453
454 pub fn max_cost_usd(&self) -> Option<Decimal> {
456 self.max_cost_usd
457 }
458
459 pub fn charged_cost_usd(&self) -> Decimal {
464 self.inherited_cost_usd + self.total_cost_usd
465 }
466
467 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
478 let Some(limit) = self.max_cost_usd else {
479 return Ok(());
480 };
481
482 let spent = self.charged_cost_usd();
483 if spent + step_budget <= limit {
484 return Ok(());
485 }
486
487 error!(
488 run_id = %self.run_id,
489 limit_usd = %limit,
490 spent_usd = %spent,
491 step_budget_usd = %step_budget,
492 "run cost cap reached, refusing agent step"
493 );
494
495 Err(EngineError::RunBudgetExceeded {
496 run_id: self.run_id,
497 limit_usd: limit,
498 spent_usd: spent,
499 step_budget_usd: step_budget,
500 })
501 }
502
503 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
515 let steps = self.store.list_steps(self.run_id).await?;
516 for step in steps {
517 let dominated = matches!(
518 step.status.state,
519 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
520 );
521 if !dominated {
522 continue;
523 }
524
525 if step.attempt == self.attempt {
526 self.replay_steps.insert(step.position, step);
527 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
528 {
529 self.granted_approvals.insert(step.position, step.attempt);
530 }
531 }
532 Ok(())
533 }
534
535 pub fn run_id(&self) -> Uuid {
537 self.run_id
538 }
539
540 pub fn total_cost_usd(&self) -> Decimal {
542 self.total_cost_usd
543 }
544
545 pub fn total_duration_ms(&self) -> u64 {
547 self.total_duration_ms
548 }
549
550 pub async fn parallel(
587 &mut self,
588 steps: Vec<(&str, StepConfig)>,
589 fail_fast: bool,
590 ) -> Result<Vec<ParallelStepResult>, EngineError> {
591 if steps.is_empty() {
592 return Ok(Vec::new());
593 }
594
595 let wave_budget: Decimal = steps
598 .iter()
599 .filter_map(|(_, config)| match config {
600 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
601 _ => None,
602 })
603 .map(step_budget_usd)
604 .sum();
605 self.check_run_budget(wave_budget)?;
606
607 let wave_position = self.position;
608 self.position += 1;
609
610 let now = Utc::now();
611 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
612
613 for (name, config) in &steps {
614 let kind = config.kind();
615 let step = self
616 .store
617 .create_step(NewStep {
618 run_id: self.run_id,
619 name: name.to_string(),
620 kind,
621 position: wave_position,
622 input: Some(serde_json::to_value(config)?),
623 is_error_handler: false,
624 })
625 .await?;
626
627 self.start_step(step.id, now).await?;
628
629 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
632 self.fail_step(step.id, &err).await;
633 return Err(err);
634 }
635
636 step_records.push((step.id, name.to_string(), config.clone()));
637 }
638
639 let mut join_set = JoinSet::new();
640 let mut task_index: HashMap<Id, usize> = HashMap::new();
641 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
642 let provider = self.provider.clone();
643 let config = config.clone();
644 let step_log_sender = self
645 .log_sender
646 .as_ref()
647 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
648 let handle = join_set.spawn(async move {
649 (
650 idx,
651 execute_step_config(&config, &provider, step_log_sender).await,
652 )
653 });
654 task_index.insert(handle.id(), idx);
655 }
656
657 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
659 vec![None; step_records.len()];
660 let mut first_error: Option<EngineError> = None;
661
662 while let Some(join_result) = join_set.join_next().await {
663 let (idx, step_result) = match join_result {
664 Ok(r) => r,
665 Err(e) => {
666 let error_msg = format!("join error: {e}");
667 if let Some(&idx) = task_index.get(&e.id()) {
668 let (step_id, step_name, _) = &step_records[idx];
669 let completed_at = Utc::now();
670 error!(
671 run_id = %self.run_id,
672 step = %step_name,
673 error = %error_msg,
674 "parallel step panicked or was cancelled"
675 );
676 if let Err(store_err) = self
677 .store
678 .update_step(
679 *step_id,
680 StepUpdate {
681 status: Some(StepStatus::Failed),
682 error: Some(error_msg.clone()),
683 completed_at: Some(completed_at),
684 ..StepUpdate::default()
685 },
686 )
687 .await
688 {
689 error!(
690 run_id = %self.run_id,
691 step_id = %step_id,
692 error = %store_err,
693 "failed to persist JoinError for step"
694 );
695 }
696 indexed_results[idx] = Some(Err(error_msg.clone()));
697 }
698 if first_error.is_none() {
699 first_error = Some(EngineError::StepConfig(error_msg));
700 }
701 if fail_fast {
702 join_set.abort_all();
703 }
704 continue;
705 }
706 };
707
708 let (step_id, step_name, step_config) = &step_records[idx];
709 let completed_at = Utc::now();
710
711 if let Err(err) = self
712 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
713 .await
714 {
715 self.fail_step(*step_id, &err).await;
716 indexed_results[idx] = Some(Err(err.to_string()));
717 if first_error.is_none() {
718 first_error = Some(err);
719 }
720 if fail_fast {
721 join_set.abort_all();
722 }
723 continue;
724 }
725
726 match step_result {
727 Ok(output) => {
728 self.total_cost_usd += output.cost_usd;
729 self.total_duration_ms += output.duration_ms;
730
731 let debug_messages_json = output.debug_messages_json();
732
733 self.store
734 .update_step(
735 *step_id,
736 StepUpdate {
737 status: Some(StepStatus::Completed),
738 output: Some(output.output.clone()),
739 duration_ms: Some(output.duration_ms),
740 cost_usd: Some(output.cost_usd),
741 input_tokens: output.input_tokens,
742 output_tokens: output.output_tokens,
743 completed_at: Some(completed_at),
744 debug_messages: debug_messages_json,
745 ..StepUpdate::default()
746 },
747 )
748 .await?;
749
750 info!(
751 run_id = %self.run_id,
752 step = %step_name,
753 duration_ms = output.duration_ms,
754 "parallel step completed"
755 );
756
757 indexed_results[idx] = Some(Ok(output));
758 }
759 Err(err) => {
760 let err_msg = err.to_string();
761 let debug_messages_json = extract_debug_messages_from_error(&err);
762 let partial = extract_partial_usage_from_error(&err);
763 let raw_response_output = extract_raw_response_from_error(&err);
764
765 if let Some(ref usage) = partial {
766 if let Some(cost) = usage.cost_usd {
767 self.total_cost_usd += cost;
768 }
769 if let Some(dur) = usage.duration_ms {
770 self.total_duration_ms += dur;
771 }
772 }
773
774 if let Err(store_err) = self
775 .store
776 .update_step(
777 *step_id,
778 StepUpdate {
779 status: Some(StepStatus::Failed),
780 error: Some(err_msg.clone()),
781 output: raw_response_output,
782 completed_at: Some(completed_at),
783 debug_messages: debug_messages_json,
784 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
785 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
786 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
787 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
788 ..StepUpdate::default()
789 },
790 )
791 .await
792 {
793 tracing::error!(
794 step_id = %step_id,
795 error = %store_err,
796 "failed to persist parallel step failure"
797 );
798 }
799
800 indexed_results[idx] = Some(Err(err_msg.clone()));
801
802 if first_error.is_none() {
803 first_error = Some(err);
804 }
805
806 if fail_fast {
807 join_set.abort_all();
808 }
809 }
810 }
811 }
812
813 if let Some(err) = first_error {
814 return Err(err);
815 }
816
817 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
818
819 let results: Vec<ParallelStepResult> = step_records
821 .iter()
822 .enumerate()
823 .map(|(idx, (step_id, name, _))| {
824 let output = match indexed_results[idx].take() {
825 Some(Ok(o)) => o,
826 _ => unreachable!("all steps succeeded if no error returned"),
827 };
828 ParallelStepResult {
829 name: name.clone(),
830 output,
831 step_id: *step_id,
832 }
833 })
834 .collect();
835
836 Ok(results)
837 }
838
839 pub async fn shell(
862 &mut self,
863 name: &str,
864 config: ShellConfig,
865 ) -> Result<StepOutput, EngineError> {
866 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
867 .await
868 }
869
870 pub async fn http(
890 &mut self,
891 name: &str,
892 config: HttpConfig,
893 ) -> Result<StepOutput, EngineError> {
894 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
895 .await
896 }
897
898 pub async fn agent(
918 &mut self,
919 name: &str,
920 config: impl Into<AgentStepConfig>,
921 ) -> Result<StepOutput, EngineError> {
922 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
923 .await
924 }
925
926 pub async fn approval(
957 &mut self,
958 name: &str,
959 config: ApprovalConfig,
960 ) -> Result<(), EngineError> {
961 let position = self.position;
962 self.position += 1;
963
964 if let Some(existing) = self.replay_steps.get(&position)
967 && existing.kind == StepKind::Approval
968 {
969 if existing.status.state == StepStatus::AwaitingApproval {
970 self.store
971 .update_step(
972 existing.id,
973 StepUpdate {
974 status: Some(StepStatus::Completed),
975 completed_at: Some(Utc::now()),
976 ..StepUpdate::default()
977 },
978 )
979 .await?;
980 }
981
982 self.last_step_ids = vec![existing.id];
983 info!(
984 run_id = %self.run_id,
985 step = %name,
986 position,
987 "approval step replayed (approved)"
988 );
989 return Ok(());
990 }
991
992 if let Some(&granted_in) = self.granted_approvals.get(&position) {
996 let step = self
997 .store
998 .create_step(NewStep {
999 run_id: self.run_id,
1000 name: name.to_string(),
1001 kind: StepKind::Approval,
1002 position,
1003 input: Some(serde_json::to_value(&config)?),
1004 is_error_handler: false,
1005 })
1006 .await?;
1007
1008 let now = Utc::now();
1009 self.start_step(step.id, now).await?;
1010 self.store
1011 .update_step(
1012 step.id,
1013 StepUpdate {
1014 status: Some(StepStatus::Completed),
1015 output: Some(json!({"approved_in_attempt": granted_in})),
1016 completed_at: Some(now),
1017 ..StepUpdate::default()
1018 },
1019 )
1020 .await?;
1021
1022 self.last_step_ids = vec![step.id];
1023 info!(
1024 run_id = %self.run_id,
1025 step = %name,
1026 position,
1027 granted_in_attempt = granted_in,
1028 attempt = self.attempt,
1029 "approval carried over from a previous attempt"
1030 );
1031 return Ok(());
1032 }
1033
1034 let step = self
1036 .store
1037 .create_step(NewStep {
1038 run_id: self.run_id,
1039 name: name.to_string(),
1040 kind: StepKind::Approval,
1041 position,
1042 input: Some(serde_json::to_value(&config)?),
1043 is_error_handler: false,
1044 })
1045 .await?;
1046
1047 self.start_step(step.id, Utc::now()).await?;
1048
1049 self.store
1052 .update_step(
1053 step.id,
1054 StepUpdate {
1055 status: Some(StepStatus::AwaitingApproval),
1056 ..StepUpdate::default()
1057 },
1058 )
1059 .await?;
1060
1061 self.last_step_ids = vec![step.id];
1062
1063 Err(EngineError::ApprovalRequired {
1064 run_id: self.run_id,
1065 step_id: step.id,
1066 message: config.message().to_string(),
1067 })
1068 }
1069
1070 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1099 let position = self.position;
1100 self.position += 1;
1101
1102 let step = self
1103 .store
1104 .create_step(NewStep {
1105 run_id: self.run_id,
1106 name: name.to_string(),
1107 kind: StepKind::Custom("skip".to_string()),
1108 position,
1109 input: None,
1110 is_error_handler: false,
1111 })
1112 .await?;
1113
1114 if !self.last_step_ids.is_empty() {
1115 let deps: Vec<NewStepDependency> = self
1116 .last_step_ids
1117 .iter()
1118 .map(|&depends_on| NewStepDependency {
1119 step_id: step.id,
1120 depends_on,
1121 })
1122 .collect();
1123 self.store.create_step_dependencies(deps).await?;
1124 }
1125
1126 let now = Utc::now();
1127 self.store
1128 .update_step(
1129 step.id,
1130 StepUpdate {
1131 status: Some(StepStatus::Skipped),
1132 output: Some(serde_json::json!({"reason": reason})),
1133 completed_at: Some(now),
1134 ..StepUpdate::default()
1135 },
1136 )
1137 .await?;
1138
1139 self.last_step_ids = vec![step.id];
1140
1141 info!(
1142 run_id = %self.run_id,
1143 step = %name,
1144 reason,
1145 "step skipped"
1146 );
1147
1148 Ok(())
1149 }
1150
1151 pub async fn operation(
1189 &mut self,
1190 name: &str,
1191 op: &dyn Operation,
1192 ) -> Result<StepOutput, EngineError> {
1193 let kind = StepKind::Custom(op.kind().to_string());
1194 let position = self.position;
1195 self.position += 1;
1196
1197 let step = self
1198 .store
1199 .create_step(NewStep {
1200 run_id: self.run_id,
1201 name: name.to_string(),
1202 kind,
1203 position,
1204 input: op.input(),
1205 is_error_handler: false,
1206 })
1207 .await?;
1208
1209 self.start_step(step.id, Utc::now()).await?;
1210
1211 let start = Instant::now();
1212
1213 match op.execute().await {
1214 Ok(output_value) => {
1215 let duration_ms = start.elapsed().as_millis() as u64;
1216 self.total_duration_ms += duration_ms;
1217
1218 let completed_at = Utc::now();
1219 self.store
1220 .update_step(
1221 step.id,
1222 StepUpdate {
1223 status: Some(StepStatus::Completed),
1224 output: Some(output_value.clone()),
1225 duration_ms: Some(duration_ms),
1226 cost_usd: Some(Decimal::ZERO),
1227 completed_at: Some(completed_at),
1228 ..StepUpdate::default()
1229 },
1230 )
1231 .await?;
1232
1233 info!(
1234 run_id = %self.run_id,
1235 step = %name,
1236 kind = op.kind(),
1237 duration_ms,
1238 "operation step completed"
1239 );
1240
1241 self.last_step_ids = vec![step.id];
1242
1243 Ok(StepOutput {
1244 output: output_value,
1245 duration_ms,
1246 cost_usd: Decimal::ZERO,
1247 input_tokens: None,
1248 output_tokens: None,
1249 model: None,
1250 debug_messages: None,
1251 })
1252 }
1253 Err(err) => {
1254 let completed_at = Utc::now();
1255 if let Err(store_err) = self
1256 .store
1257 .update_step(
1258 step.id,
1259 StepUpdate {
1260 status: Some(StepStatus::Failed),
1261 error: Some(err.to_string()),
1262 completed_at: Some(completed_at),
1263 ..StepUpdate::default()
1264 },
1265 )
1266 .await
1267 {
1268 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1269 }
1270
1271 Err(err)
1272 }
1273 }
1274 }
1275
1276 pub async fn workflow(
1303 &mut self,
1304 handler: &dyn WorkflowHandler,
1305 payload: Value,
1306 ) -> Result<StepOutput, EngineError> {
1307 let config = WorkflowStepConfig::new(handler.name(), payload);
1308 let position = self.position;
1309 self.position += 1;
1310
1311 let step = self
1312 .store
1313 .create_step(NewStep {
1314 run_id: self.run_id,
1315 name: config.workflow_name.clone(),
1316 kind: StepKind::Workflow,
1317 position,
1318 input: Some(serde_json::to_value(&config)?),
1319 is_error_handler: false,
1320 })
1321 .await?;
1322
1323 self.start_step(step.id, Utc::now()).await?;
1324
1325 match self.execute_child_workflow(&config).await {
1326 Ok(output) => {
1327 self.total_cost_usd += output.cost_usd;
1328 self.total_duration_ms += output.duration_ms;
1329
1330 let completed_at = Utc::now();
1331 self.store
1332 .update_step(
1333 step.id,
1334 StepUpdate {
1335 status: Some(StepStatus::Completed),
1336 output: Some(output.output.clone()),
1337 duration_ms: Some(output.duration_ms),
1338 cost_usd: Some(output.cost_usd),
1339 completed_at: Some(completed_at),
1340 ..StepUpdate::default()
1341 },
1342 )
1343 .await?;
1344
1345 info!(
1346 run_id = %self.run_id,
1347 child_workflow = %config.workflow_name,
1348 duration_ms = output.duration_ms,
1349 "workflow step completed"
1350 );
1351
1352 self.last_step_ids = vec![step.id];
1353
1354 Ok(output)
1355 }
1356 Err(err) => {
1357 let completed_at = Utc::now();
1358 if let Err(store_err) = self
1359 .store
1360 .update_step(
1361 step.id,
1362 StepUpdate {
1363 status: Some(StepStatus::Failed),
1364 error: Some(err.to_string()),
1365 completed_at: Some(completed_at),
1366 ..StepUpdate::default()
1367 },
1368 )
1369 .await
1370 {
1371 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1372 }
1373
1374 Err(err)
1375 }
1376 }
1377 }
1378
1379 async fn execute_child_workflow(
1381 &self,
1382 config: &WorkflowStepConfig,
1383 ) -> Result<StepOutput, EngineError> {
1384 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1385 EngineError::InvalidWorkflow(
1386 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1387 )
1388 })?;
1389
1390 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1391 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1392 })?;
1393
1394 let parent = self.store.get_run(self.run_id).await?;
1397 let (parent_labels, parent_author) =
1398 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1399
1400 let child_run = self
1401 .store
1402 .create_run(NewRun {
1403 workflow_name: config.workflow_name.clone(),
1404 trigger: TriggerKind::Workflow,
1405 payload: config.payload.clone(),
1406 max_retries: 0,
1407 handler_version: None,
1408 labels: parent_labels,
1409 scheduled_at: None,
1410 created_by: parent_author,
1411 idempotency_key: None,
1412 max_cost_usd: self.max_cost_usd,
1414 })
1415 .await?
1416 .into_run();
1417
1418 let child_run_id = child_run.id;
1419 info!(
1420 parent_run_id = %self.run_id,
1421 child_run_id = %child_run_id,
1422 workflow = %config.workflow_name,
1423 "child run created"
1424 );
1425
1426 self.store
1427 .update_run_status(child_run_id, RunStatus::Running)
1428 .await?;
1429
1430 let run_start = Instant::now();
1431 let mut child_ctx = WorkflowContext {
1432 run_id: child_run_id,
1433 store: self.store.clone(),
1434 provider: self.provider.clone(),
1435 handler_resolver: self.handler_resolver.clone(),
1436 position: 0,
1437 last_step_ids: Vec::new(),
1438 total_cost_usd: Decimal::ZERO,
1439 total_duration_ms: 0,
1440 max_cost_usd: self.max_cost_usd,
1441 inherited_cost_usd: self.charged_cost_usd(),
1444 replay_steps: HashMap::new(),
1445 granted_approvals: HashMap::new(),
1446 attempt: 1,
1448 carried_duration_ms: 0,
1449 log_sender: self.log_sender.clone(),
1450 artifact_sink: self.artifact_sink.clone(),
1453 error_handlers: Vec::new(),
1454 };
1455
1456 let result = handler.execute(&mut child_ctx).await;
1457 let total_duration = run_start.elapsed().as_millis() as u64;
1458 let completed_at = Utc::now();
1459
1460 match result {
1461 Ok(()) => {
1462 self.store
1463 .update_run(
1464 child_run_id,
1465 RunUpdate {
1466 status: Some(RunStatus::Completed),
1467 cost_usd: Some(child_ctx.total_cost_usd),
1468 duration_ms: Some(total_duration),
1469 completed_at: Some(completed_at),
1470 ..RunUpdate::default()
1471 },
1472 )
1473 .await?;
1474
1475 Ok(StepOutput {
1476 output: serde_json::json!({
1477 "run_id": child_run_id,
1478 "workflow_name": config.workflow_name,
1479 "status": RunStatus::Completed,
1480 "cost_usd": child_ctx.total_cost_usd,
1481 "duration_ms": total_duration,
1482 }),
1483 duration_ms: total_duration,
1484 cost_usd: child_ctx.total_cost_usd,
1485 input_tokens: None,
1486 output_tokens: None,
1487 model: None,
1488 debug_messages: None,
1489 })
1490 }
1491 Err(err) => {
1492 if let Err(store_err) = self
1493 .store
1494 .update_run(
1495 child_run_id,
1496 RunUpdate {
1497 status: Some(RunStatus::Failed),
1498 error: Some(err.to_string()),
1499 cost_usd: Some(child_ctx.total_cost_usd),
1500 duration_ms: Some(total_duration),
1501 completed_at: Some(completed_at),
1502 ..RunUpdate::default()
1503 },
1504 )
1505 .await
1506 {
1507 error!(
1508 child_run_id = %child_run_id,
1509 store_error = %store_err,
1510 "failed to persist child run failure"
1511 );
1512 }
1513
1514 Err(err)
1515 }
1516 }
1517 }
1518
1519 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1524 let step = self.replay_steps.get(&position)?;
1525 if step.status.state != StepStatus::Completed {
1526 return None;
1527 }
1528 let output = StepOutput {
1529 output: step.output.clone().unwrap_or(Value::Null),
1530 duration_ms: step.duration_ms,
1531 cost_usd: step.cost_usd,
1532 input_tokens: step.input_tokens,
1533 output_tokens: step.output_tokens,
1534 model: None,
1535 debug_messages: None,
1536 };
1537 self.total_cost_usd += output.cost_usd;
1538 self.total_duration_ms += output.duration_ms;
1539 self.last_step_ids = vec![step.id];
1540 info!(
1541 run_id = %self.run_id,
1542 step = %step.name,
1543 position,
1544 "step replayed from previous execution"
1545 );
1546 Some(output)
1547 }
1548
1549 async fn execute_step(
1551 &mut self,
1552 name: &str,
1553 kind: StepKind,
1554 config: StepConfig,
1555 ) -> Result<StepOutput, EngineError> {
1556 let position = self.position;
1557 self.position += 1;
1558
1559 if let Some(output) = self.try_replay_step(position) {
1561 return Ok(output);
1562 }
1563
1564 if let StepConfig::Agent(ref agent_config) = config {
1567 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1568 }
1569
1570 let step = self
1572 .store
1573 .create_step(NewStep {
1574 run_id: self.run_id,
1575 name: name.to_string(),
1576 kind,
1577 position,
1578 input: Some(serde_json::to_value(&config)?),
1579 is_error_handler: false,
1580 })
1581 .await?;
1582
1583 self.start_step(step.id, Utc::now()).await?;
1584
1585 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1588 self.fail_step(step.id, &err).await;
1589 return Err(err);
1590 }
1591
1592 let step_log_sender = self
1593 .log_sender
1594 .as_ref()
1595 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1596
1597 let execution = execute_step_config(&config, &self.provider, step_log_sender).await;
1598
1599 if let Err(err) = self
1600 .store_step_outputs(&config, step.id, name, execution.is_ok())
1601 .await
1602 {
1603 self.fail_step(step.id, &err).await;
1604 return Err(err);
1605 }
1606
1607 match execution {
1608 Ok(output) => {
1609 self.total_cost_usd += output.cost_usd;
1610 self.total_duration_ms += output.duration_ms;
1611
1612 let debug_messages_json = output.debug_messages_json();
1613
1614 let completed_at = Utc::now();
1615 self.store
1616 .update_step(
1617 step.id,
1618 StepUpdate {
1619 status: Some(StepStatus::Completed),
1620 output: Some(output.output.clone()),
1621 duration_ms: Some(output.duration_ms),
1622 cost_usd: Some(output.cost_usd),
1623 input_tokens: output.input_tokens,
1624 output_tokens: output.output_tokens,
1625 completed_at: Some(completed_at),
1626 debug_messages: debug_messages_json,
1627 ..StepUpdate::default()
1628 },
1629 )
1630 .await?;
1631
1632 info!(
1633 run_id = %self.run_id,
1634 step = %name,
1635 duration_ms = output.duration_ms,
1636 "step completed"
1637 );
1638
1639 self.last_step_ids = vec![step.id];
1640
1641 Ok(output)
1642 }
1643 Err(err) => {
1644 let completed_at = Utc::now();
1645 let debug_messages_json = extract_debug_messages_from_error(&err);
1646 let partial = extract_partial_usage_from_error(&err);
1647 let raw_response_output = extract_raw_response_from_error(&err);
1648
1649 if let Some(ref usage) = partial {
1650 if let Some(cost) = usage.cost_usd {
1651 self.total_cost_usd += cost;
1652 }
1653 if let Some(dur) = usage.duration_ms {
1654 self.total_duration_ms += dur;
1655 }
1656 }
1657
1658 if let Err(store_err) = self
1659 .store
1660 .update_step(
1661 step.id,
1662 StepUpdate {
1663 status: Some(StepStatus::Failed),
1664 error: Some(err.to_string()),
1665 output: raw_response_output,
1666 completed_at: Some(completed_at),
1667 debug_messages: debug_messages_json,
1668 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1669 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1670 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1671 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1672 ..StepUpdate::default()
1673 },
1674 )
1675 .await
1676 {
1677 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1678 }
1679
1680 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1681 self.fire_error_handlers(name, &err.to_string(), err_duration)
1682 .await;
1683
1684 Err(err)
1685 }
1686 }
1687 }
1688
1689 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1694 if !self.last_step_ids.is_empty() {
1695 let deps: Vec<NewStepDependency> = self
1696 .last_step_ids
1697 .iter()
1698 .map(|&depends_on| NewStepDependency {
1699 step_id,
1700 depends_on,
1701 })
1702 .collect();
1703 self.store.create_step_dependencies(deps).await?;
1704 }
1705
1706 self.store
1707 .update_step(
1708 step_id,
1709 StepUpdate {
1710 status: Some(StepStatus::Running),
1711 started_at: Some(now),
1712 ..StepUpdate::default()
1713 },
1714 )
1715 .await?;
1716
1717 Ok(())
1718 }
1719
1720 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
1727 if let Err(store_err) = self
1728 .store
1729 .update_step(
1730 step_id,
1731 StepUpdate {
1732 status: Some(StepStatus::Failed),
1733 error: Some(err.to_string()),
1734 completed_at: Some(Utc::now()),
1735 ..StepUpdate::default()
1736 },
1737 )
1738 .await
1739 {
1740 error!(
1741 step_id = %step_id,
1742 error = %store_err,
1743 "failed to persist step failure"
1744 );
1745 }
1746 }
1747
1748 pub fn store(&self) -> &Arc<dyn Store> {
1750 &self.store
1751 }
1752
1753 pub async fn payload(&self) -> Result<Value, EngineError> {
1761 let run = self
1762 .store
1763 .get_run(self.run_id)
1764 .await?
1765 .ok_or(EngineError::Store(
1766 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1767 ))?;
1768 Ok(run.payload)
1769 }
1770
1771 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
1794 self.error_handlers.push(OnErrorHandler {
1795 name: name.to_string(),
1796 config: config.into(),
1797 });
1798 }
1799
1800 pub fn clear_error_handlers(&mut self) {
1819 self.error_handlers.clear();
1820 }
1821
1822 async fn fire_error_handlers(
1828 &mut self,
1829 failed_step_name: &str,
1830 error_msg: &str,
1831 duration_ms: u64,
1832 ) {
1833 let handlers = std::mem::take(&mut self.error_handlers);
1834 if handlers.is_empty() {
1835 return;
1836 }
1837
1838 let error_context = json!({
1839 "failed_step": failed_step_name,
1840 "error": error_msg,
1841 "duration_ms": duration_ms,
1842 });
1843
1844 for handler in handlers {
1845 let mut config = handler.config.clone();
1846 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
1847
1848 let position = self.position;
1849 self.position += 1;
1850
1851 let step = match self
1852 .store
1853 .create_step(NewStep {
1854 run_id: self.run_id,
1855 name: handler.name.clone(),
1856 kind: config.kind(),
1857 position,
1858 input: Some(error_context.clone()),
1859 is_error_handler: true,
1860 })
1861 .await
1862 {
1863 Ok(step) => step,
1864 Err(err) => {
1865 warn!(
1866 run_id = %self.run_id,
1867 handler = %handler.name,
1868 error = %err,
1869 "failed to create error handler step"
1870 );
1871 continue;
1872 }
1873 };
1874
1875 if let Err(err) = self.start_step(step.id, Utc::now()).await {
1876 warn!(
1877 run_id = %self.run_id,
1878 handler = %handler.name,
1879 error = %err,
1880 "failed to start error handler step"
1881 );
1882 continue;
1883 }
1884
1885 let step_log_sender = self
1886 .log_sender
1887 .as_ref()
1888 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
1889
1890 let start = Instant::now();
1891 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
1892 let handler_duration = start.elapsed().as_millis() as u64;
1893 let completed_at = Utc::now();
1894
1895 match result {
1896 Ok(output) => {
1897 if let Err(store_err) = self
1898 .store
1899 .update_step(
1900 step.id,
1901 StepUpdate {
1902 status: Some(StepStatus::Completed),
1903 output: Some(output.output),
1904 duration_ms: Some(handler_duration),
1905 cost_usd: Some(output.cost_usd),
1906 completed_at: Some(completed_at),
1907 ..StepUpdate::default()
1908 },
1909 )
1910 .await
1911 {
1912 warn!(
1913 run_id = %self.run_id,
1914 handler = %handler.name,
1915 error = %store_err,
1916 "failed to persist error handler completion"
1917 );
1918 }
1919
1920 info!(
1921 run_id = %self.run_id,
1922 handler = %handler.name,
1923 duration_ms = handler_duration,
1924 "error handler completed"
1925 );
1926 }
1927 Err(err) => {
1928 if let Err(store_err) = self
1929 .store
1930 .update_step(
1931 step.id,
1932 StepUpdate {
1933 status: Some(StepStatus::Failed),
1934 error: Some(err.to_string()),
1935 duration_ms: Some(handler_duration),
1936 completed_at: Some(completed_at),
1937 ..StepUpdate::default()
1938 },
1939 )
1940 .await
1941 {
1942 warn!(
1943 run_id = %self.run_id,
1944 handler = %handler.name,
1945 error = %store_err,
1946 "failed to persist error handler failure"
1947 );
1948 }
1949
1950 warn!(
1951 run_id = %self.run_id,
1952 handler = %handler.name,
1953 error = %err,
1954 "error handler failed (original error preserved)"
1955 );
1956 }
1957 }
1958 }
1959 }
1960}
1961
1962fn inject_error_context(
1964 config: &mut StepConfig,
1965 failed_step: &str,
1966 error_msg: &str,
1967 duration_ms: u64,
1968) {
1969 match config {
1970 StepConfig::Shell(shell) => {
1971 shell
1972 .env
1973 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
1974 shell
1975 .env
1976 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
1977 shell.env.push((
1978 "IRONFLOW_ERROR_DURATION_MS".to_string(),
1979 duration_ms.to_string(),
1980 ));
1981 }
1982 StepConfig::Agent(agent) => {
1983 agent.prompt = format!(
1984 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
1985 failed_step, duration_ms, error_msg, agent.prompt
1986 );
1987 }
1988 StepConfig::Http(http) => {
1989 http.headers
1990 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
1991 http.headers.push((
1992 "X-Ironflow-Error-Message".to_string(),
1993 error_msg.to_string(),
1994 ));
1995 }
1996 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
1997 }
1998}
1999
2000impl fmt::Debug for WorkflowContext {
2001 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2002 f.debug_struct("WorkflowContext")
2003 .field("run_id", &self.run_id)
2004 .field("position", &self.position)
2005 .field("total_cost_usd", &self.total_cost_usd)
2006 .field("inherited_cost_usd", &self.inherited_cost_usd)
2007 .field("max_cost_usd", &self.max_cost_usd)
2008 .finish_non_exhaustive()
2009 }
2010}
2011
2012fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2015 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2016 debug_messages,
2017 ..
2018 })) = err
2019 && !debug_messages.is_empty()
2020 {
2021 return serde_json::to_value(debug_messages).ok();
2022 }
2023 None
2024}
2025
2026struct StepPartialUsage {
2032 cost_usd: Option<Decimal>,
2033 duration_ms: Option<u64>,
2034 input_tokens: Option<u64>,
2035 output_tokens: Option<u64>,
2036}
2037
2038fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2044 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2045 raw_response: Some(text),
2046 ..
2047 })) = err
2048 {
2049 return Some(Value::String(text.clone()));
2050 }
2051 None
2052}
2053
2054fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2055 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2056 partial_usage,
2057 ..
2058 })) = err
2059 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2060 {
2061 return Some(StepPartialUsage {
2062 cost_usd: partial_usage
2063 .cost_usd
2064 .and_then(|c| Decimal::try_from(c).ok()),
2065 duration_ms: partial_usage.duration_ms,
2066 input_tokens: partial_usage.input_tokens,
2067 output_tokens: partial_usage.output_tokens,
2068 });
2069 }
2070 None
2071}
2072
2073#[cfg(test)]
2074mod tests {
2075 use super::*;
2076 use ironflow_core::providers::claude::ClaudeCodeProvider;
2077 use ironflow_core::providers::record_replay::RecordReplayProvider;
2078 use ironflow_store::memory::InMemoryStore;
2079 use ironflow_store::models::{Run, RunActor, RunFilter};
2080 use ironflow_store::store::RunStore;
2081 use serde_json::json;
2082 use std::sync::Arc;
2083 use std::sync::atomic::{AtomicBool, Ordering};
2084 use uuid::Uuid;
2085
2086 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2088 let inner = ClaudeCodeProvider::new();
2089 Arc::new(RecordReplayProvider::replay(
2090 inner,
2091 "/tmp/ironflow-fixtures",
2092 ))
2093 }
2094
2095 fn create_test_context() -> WorkflowContext {
2097 let store = Arc::new(InMemoryStore::new());
2098 let provider = create_test_provider();
2099 let run_id = Uuid::now_v7();
2100 WorkflowContext::new(run_id, store, provider)
2101 }
2102
2103 #[test]
2104 fn context_new_initializes_correctly() {
2105 let ctx = create_test_context();
2106 assert_eq!(ctx.position, 0);
2107 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2108 assert_eq!(ctx.total_duration_ms, 0);
2109 assert!(ctx.last_step_ids.is_empty());
2110 assert!(ctx.replay_steps.is_empty());
2111 assert!(ctx.log_sender.is_none());
2112 }
2113
2114 #[test]
2115 fn context_run_id_returns_correct_id() {
2116 let run_id = Uuid::now_v7();
2117 let store = Arc::new(InMemoryStore::new());
2118 let provider = create_test_provider();
2119 let ctx = WorkflowContext::new(run_id, store, provider);
2120 assert_eq!(ctx.run_id(), run_id);
2121 }
2122
2123 #[test]
2124 fn context_total_cost_usd_initially_zero() {
2125 let ctx = create_test_context();
2126 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2127 }
2128
2129 #[test]
2130 fn context_total_duration_ms_initially_zero() {
2131 let ctx = create_test_context();
2132 assert_eq!(ctx.total_duration_ms(), 0);
2133 }
2134
2135 #[test]
2136 fn context_with_handler_resolver_creates_context_with_resolver() {
2137 let store = Arc::new(InMemoryStore::new());
2138 let provider = create_test_provider();
2139 let run_id = Uuid::now_v7();
2140
2141 let called = Arc::new(AtomicBool::new(false));
2142 let called_clone = called.clone();
2143
2144 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2145 called_clone.store(true, Ordering::SeqCst);
2146 None
2147 });
2148
2149 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2150
2151 assert_eq!(ctx.run_id(), run_id);
2152 assert!(ctx.handler_resolver.is_some());
2153 }
2154
2155 #[tokio::test]
2156 async fn context_set_log_sender_attaches_sender() {
2157 let mut ctx = create_test_context();
2158 let (sender, _receiver) = crate::log_sender::channel();
2159 ctx.set_log_sender(sender);
2160 assert!(ctx.log_sender.is_some());
2161 }
2162
2163 #[tokio::test]
2164 async fn context_skip_creates_skipped_step() {
2165 let store = Arc::new(InMemoryStore::new());
2166 let provider = create_test_provider();
2167
2168 store
2170 .create_run(NewRun {
2171 created_by: None,
2172 workflow_name: "test".to_string(),
2173 trigger: TriggerKind::Manual,
2174 payload: json!({}),
2175 max_retries: 0,
2176 handler_version: None,
2177 labels: Default::default(),
2178 scheduled_at: None,
2179 idempotency_key: None,
2180 max_cost_usd: None,
2181 })
2182 .await
2183 .expect("failed to create run")
2184 .into_run();
2185
2186 let runs = store
2188 .list_runs(RunFilter::default(), 1, 10)
2189 .await
2190 .expect("failed to list runs");
2191 let created_run_id = runs.items[0].id;
2192
2193 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2194 let initial_position = ctx.position;
2195
2196 ctx.skip("skip-step", "condition not met")
2197 .await
2198 .expect("skip failed");
2199
2200 assert_eq!(ctx.position, initial_position + 1);
2201 assert!(!ctx.last_step_ids.is_empty());
2202
2203 let steps = store
2205 .list_steps(created_run_id)
2206 .await
2207 .expect("failed to list steps");
2208 assert_eq!(steps.len(), 1);
2209 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2210 }
2211
2212 struct NoopSubWorkflow;
2215
2216 impl WorkflowHandler for NoopSubWorkflow {
2217 fn name(&self) -> &str {
2218 "noop-sub"
2219 }
2220
2221 fn execute<'a>(
2222 &'a self,
2223 _ctx: &'a mut WorkflowContext,
2224 ) -> crate::handler::HandlerFuture<'a> {
2225 Box::pin(async move { Ok(()) })
2226 }
2227 }
2228
2229 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2232 let store = Arc::new(InMemoryStore::new());
2233 let provider = create_test_provider();
2234
2235 let parent = store
2236 .create_run(NewRun {
2237 workflow_name: "parent".to_string(),
2238 trigger: TriggerKind::Api,
2239 payload: json!({}),
2240 max_retries: 0,
2241 handler_version: None,
2242 labels: Default::default(),
2243 scheduled_at: None,
2244 created_by,
2245 idempotency_key: None,
2246 max_cost_usd: None,
2247 })
2248 .await
2249 .expect("failed to create parent run")
2250 .into_run();
2251
2252 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2253 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2254 _ => None,
2255 });
2256
2257 let mut ctx =
2258 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2259 ctx.workflow(&NoopSubWorkflow, json!({}))
2260 .await
2261 .expect("sub-workflow failed");
2262
2263 let runs = store
2264 .list_runs(RunFilter::default(), 1, 10)
2265 .await
2266 .expect("failed to list runs");
2267 runs.items
2268 .into_iter()
2269 .find(|r| r.workflow_name == "noop-sub")
2270 .expect("child run was created")
2271 }
2272
2273 #[tokio::test]
2274 async fn child_run_inherits_the_parent_author() {
2275 let user_id = Uuid::now_v7();
2276 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2277
2278 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2279 }
2280
2281 #[tokio::test]
2282 async fn child_run_of_an_unattributed_parent_has_no_author() {
2283 let child = child_run_of_parent_authored_by(None).await;
2284
2285 assert!(child.created_by.is_none());
2286 }
2287
2288 #[tokio::test]
2289 async fn context_parallel_empty_steps_returns_empty_vec() {
2290 let mut ctx = create_test_context();
2291 let results = ctx
2292 .parallel(vec![], true)
2293 .await
2294 .expect("parallel should not fail on empty input");
2295 assert!(results.is_empty());
2296 }
2297
2298 #[tokio::test]
2299 async fn context_approval_first_execution_returns_error() {
2300 let store = Arc::new(InMemoryStore::new());
2301 let provider = create_test_provider();
2302
2303 store
2305 .create_run(NewRun {
2306 created_by: None,
2307 workflow_name: "test".to_string(),
2308 trigger: TriggerKind::Manual,
2309 payload: json!({}),
2310 max_retries: 0,
2311 handler_version: None,
2312 labels: Default::default(),
2313 scheduled_at: None,
2314 idempotency_key: None,
2315 max_cost_usd: None,
2316 })
2317 .await
2318 .expect("failed to create run")
2319 .into_run();
2320
2321 let runs = store
2323 .list_runs(RunFilter::default(), 1, 10)
2324 .await
2325 .expect("failed to list runs");
2326 let created_run_id = runs.items[0].id;
2327
2328 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2329
2330 let result = ctx
2331 .approval(
2332 "approve-step",
2333 crate::config::ApprovalConfig::new("Continue?"),
2334 )
2335 .await;
2336
2337 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2339
2340 assert_eq!(ctx.position, 1);
2342
2343 let steps = store
2345 .list_steps(created_run_id)
2346 .await
2347 .expect("failed to list steps");
2348 assert_eq!(steps.len(), 1);
2349 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2350 }
2351
2352 #[tokio::test]
2353 async fn context_approval_replay_returns_ok() {
2354 let store = Arc::new(InMemoryStore::new());
2355 let provider = create_test_provider();
2356
2357 store
2359 .create_run(NewRun {
2360 created_by: None,
2361 workflow_name: "test".to_string(),
2362 trigger: TriggerKind::Manual,
2363 payload: json!({}),
2364 max_retries: 0,
2365 handler_version: None,
2366 labels: Default::default(),
2367 scheduled_at: None,
2368 idempotency_key: None,
2369 max_cost_usd: None,
2370 })
2371 .await
2372 .expect("failed to create run")
2373 .into_run();
2374
2375 let runs = store
2377 .list_runs(RunFilter::default(), 1, 10)
2378 .await
2379 .expect("failed to list runs");
2380 let created_run_id = runs.items[0].id;
2381
2382 let step = store
2384 .create_step(NewStep {
2385 run_id: created_run_id,
2386 name: "approval".to_string(),
2387 kind: StepKind::Approval,
2388 position: 0,
2389 input: None,
2390 is_error_handler: false,
2391 })
2392 .await
2393 .expect("failed to create step");
2394
2395 store
2397 .update_step(
2398 step.id,
2399 StepUpdate {
2400 status: Some(StepStatus::Running),
2401 started_at: Some(Utc::now()),
2402 ..StepUpdate::default()
2403 },
2404 )
2405 .await
2406 .expect("failed to update step to Running");
2407
2408 store
2409 .update_step(
2410 step.id,
2411 StepUpdate {
2412 status: Some(StepStatus::AwaitingApproval),
2413 ..StepUpdate::default()
2414 },
2415 )
2416 .await
2417 .expect("failed to update step to AwaitingApproval");
2418
2419 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2421 ctx.load_replay_steps()
2422 .await
2423 .expect("failed to load replay steps");
2424
2425 let result = ctx
2427 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2428 .await;
2429
2430 assert!(result.is_ok());
2431
2432 let steps = store
2434 .list_steps(created_run_id)
2435 .await
2436 .expect("failed to list steps");
2437 assert_eq!(steps.len(), 1);
2438 assert_eq!(steps[0].status.state, StepStatus::Completed);
2439 }
2440
2441 #[tokio::test]
2442 async fn context_load_replay_steps_loads_completed_steps() {
2443 let store = Arc::new(InMemoryStore::new());
2444 let provider = create_test_provider();
2445
2446 store
2448 .create_run(NewRun {
2449 created_by: None,
2450 workflow_name: "test".to_string(),
2451 trigger: TriggerKind::Manual,
2452 payload: json!({}),
2453 max_retries: 0,
2454 handler_version: None,
2455 labels: Default::default(),
2456 scheduled_at: None,
2457 idempotency_key: None,
2458 max_cost_usd: None,
2459 })
2460 .await
2461 .expect("failed to create run")
2462 .into_run();
2463
2464 let runs = store
2466 .list_runs(RunFilter::default(), 1, 10)
2467 .await
2468 .expect("failed to list runs");
2469 let created_run_id = runs.items[0].id;
2470
2471 let completed_step = store
2473 .create_step(NewStep {
2474 run_id: created_run_id,
2475 name: "completed".to_string(),
2476 kind: StepKind::Shell,
2477 position: 0,
2478 input: None,
2479 is_error_handler: false,
2480 })
2481 .await
2482 .expect("failed to create step");
2483
2484 store
2486 .update_step(
2487 completed_step.id,
2488 StepUpdate {
2489 status: Some(StepStatus::Running),
2490 started_at: Some(Utc::now()),
2491 ..StepUpdate::default()
2492 },
2493 )
2494 .await
2495 .expect("failed to update step to Running");
2496
2497 store
2498 .update_step(
2499 completed_step.id,
2500 StepUpdate {
2501 status: Some(StepStatus::Completed),
2502 completed_at: Some(Utc::now()),
2503 ..StepUpdate::default()
2504 },
2505 )
2506 .await
2507 .expect("failed to update step to Completed");
2508
2509 let _pending_step = store
2510 .create_step(NewStep {
2511 run_id: created_run_id,
2512 name: "pending".to_string(),
2513 kind: StepKind::Shell,
2514 position: 1,
2515 input: None,
2516 is_error_handler: false,
2517 })
2518 .await
2519 .expect("failed to create step");
2520
2521 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2523 ctx.load_replay_steps()
2524 .await
2525 .expect("failed to load replay steps");
2526
2527 assert_eq!(ctx.replay_steps.len(), 1);
2529 assert!(ctx.replay_steps.contains_key(&0));
2530 assert!(!ctx.replay_steps.contains_key(&1));
2531 }
2532
2533 #[tokio::test]
2534 async fn context_payload_returns_run_payload() {
2535 let store = Arc::new(InMemoryStore::new());
2536 let provider = create_test_provider();
2537 let test_payload = json!({"key": "value", "number": 42});
2538
2539 store
2541 .create_run(NewRun {
2542 created_by: None,
2543 workflow_name: "test".to_string(),
2544 trigger: TriggerKind::Manual,
2545 payload: test_payload.clone(),
2546 max_retries: 0,
2547 handler_version: None,
2548 labels: Default::default(),
2549 scheduled_at: None,
2550 idempotency_key: None,
2551 max_cost_usd: None,
2552 })
2553 .await
2554 .expect("failed to create run")
2555 .into_run();
2556
2557 let runs = store
2559 .list_runs(RunFilter::default(), 1, 10)
2560 .await
2561 .expect("failed to list runs");
2562 let created_run_id = runs.items[0].id;
2563
2564 let ctx = WorkflowContext::new(created_run_id, store, provider);
2565 let payload = ctx.payload().await.expect("failed to get payload");
2566
2567 assert_eq!(payload, test_payload);
2568 }
2569
2570 #[tokio::test]
2571 async fn context_payload_returns_error_for_nonexistent_run() {
2572 let store = Arc::new(InMemoryStore::new());
2573 let provider = create_test_provider();
2574 let run_id = Uuid::now_v7();
2575
2576 let ctx = WorkflowContext::new(run_id, store, provider);
2577 let result = ctx.payload().await;
2578
2579 assert!(result.is_err());
2580 }
2581
2582 #[tokio::test]
2583 async fn context_store_returns_reference() {
2584 let ctx = create_test_context();
2585 let _store = ctx.store();
2586 }
2588
2589 #[test]
2590 fn context_debug_formatting() {
2591 let ctx = create_test_context();
2592 let debug_str = format!("{:?}", ctx);
2593 assert!(debug_str.contains("WorkflowContext"));
2594 assert!(debug_str.contains("run_id"));
2595 }
2596
2597 #[tokio::test]
2598 async fn context_last_step_ids_tracks_executed_steps() {
2599 let store = Arc::new(InMemoryStore::new());
2600 let provider = create_test_provider();
2601
2602 store
2604 .create_run(NewRun {
2605 created_by: None,
2606 workflow_name: "test".to_string(),
2607 trigger: TriggerKind::Manual,
2608 payload: json!({}),
2609 max_retries: 0,
2610 handler_version: None,
2611 labels: Default::default(),
2612 scheduled_at: None,
2613 idempotency_key: None,
2614 max_cost_usd: None,
2615 })
2616 .await
2617 .expect("failed to create run")
2618 .into_run();
2619
2620 let runs = store
2622 .list_runs(RunFilter::default(), 1, 10)
2623 .await
2624 .expect("failed to list runs");
2625 let created_run_id = runs.items[0].id;
2626
2627 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2628 assert!(ctx.last_step_ids.is_empty());
2629
2630 ctx.skip("step1", "reason").await.expect("skip failed");
2631
2632 assert_eq!(ctx.last_step_ids.len(), 1);
2633
2634 ctx.skip("step2", "reason").await.expect("skip failed");
2635
2636 assert_eq!(ctx.last_step_ids.len(), 1);
2638 }
2639}