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::decision::{DecisionOutput, DecisionProvider};
40use ironflow_core::error::{AgentError, OperationError};
41
42mod decision_impl;
43use ironflow_core::provider::AgentProvider;
44use ironflow_core::trace_context::WorkflowTraceContext;
45use ironflow_store::models::{
46 ArtifactLookup, NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind,
47 StepStatus, StepUpdate, TriggerKind, step_trace_id,
48};
49use ironflow_store::store::Store;
50
51use ironflow_artifacts::name::guess_content_type;
52use ironflow_artifacts::stream_from_bytes;
53use ironflow_store::entities::Artifact;
54
55use crate::artifact::{
56 ArtifactSink, ArtifactUpload, StepLocation, collect_outputs, materialize_inputs,
57};
58use crate::budget::step_budget_usd;
59use crate::config::{
60 AgentStepConfig, ApprovalConfig, DecisionConfig, HttpConfig, ShellConfig, StepConfig,
61 WorkflowStepConfig,
62};
63use crate::error::EngineError;
64use crate::executor::{ParallelStepResult, StepOutput, StepResult, execute_step_config};
65use crate::guard::{SharedGuardState, WorkflowGuardConfig, WorkflowRejection};
66use crate::handler::WorkflowHandler;
67use crate::log_sender::{LogSender, StepLogSender};
68#[cfg(not(feature = "secret-store"))]
69use crate::operation::NoopSecretResolver;
70use crate::operation::{Operation, OperationContext, SecretResolver};
71#[cfg(feature = "secret-store")]
72use ironflow_store::workflow_secrets::ScopedSecretStore;
73
74pub(crate) type HandlerResolver =
76 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
77
78pub struct WorkflowContext {
97 run_id: Uuid,
98 workflow_name: String,
99 store: Arc<dyn Store>,
100 provider: Arc<dyn AgentProvider>,
101 decision_provider: Option<Arc<dyn DecisionProvider>>,
105 handler_resolver: Option<HandlerResolver>,
106 position: u32,
107 last_step_ids: Vec<Uuid>,
109 total_cost_usd: Decimal,
111 total_duration_ms: u64,
113 max_cost_usd: Option<Decimal>,
115 inherited_cost_usd: Decimal,
118 replay_steps: HashMap<u32, Step>,
121 granted_approvals: HashMap<u32, u32>,
125 attempt: u32,
127 carried_duration_ms: u64,
130 log_sender: Option<LogSender>,
132 artifact_sink: Option<Arc<dyn ArtifactSink>>,
136 has_allowed_failure: bool,
138 error_handlers: Vec<OnErrorHandler>,
140 guard_state: Option<SharedGuardState>,
142 guard_config: Option<WorkflowGuardConfig>,
144 step_results: Vec<StepResult>,
146 event_bus: Option<crate::notify::WorkflowEventBus>,
148 trace_context: WorkflowTraceContext,
150 operation_ctx: Option<OperationContext>,
152}
153
154struct OnErrorHandler {
156 name: String,
157 config: StepConfig,
158}
159
160impl WorkflowContext {
161 pub fn new(
166 run_id: Uuid,
167 workflow_name: String,
168 store: Arc<dyn Store>,
169 provider: Arc<dyn AgentProvider>,
170 ) -> Self {
171 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
172 Self {
173 run_id,
174 workflow_name,
175 store,
176 provider,
177 decision_provider: None,
178 handler_resolver: None,
179 position: 0,
180 last_step_ids: Vec::new(),
181 total_cost_usd: Decimal::ZERO,
182 total_duration_ms: 0,
183 max_cost_usd: None,
184 inherited_cost_usd: Decimal::ZERO,
185 replay_steps: HashMap::new(),
186 granted_approvals: HashMap::new(),
187 attempt: 1,
188 carried_duration_ms: 0,
189 log_sender: None,
190 artifact_sink: None,
191 has_allowed_failure: false,
192 error_handlers: Vec::new(),
193 guard_state: None,
194 guard_config: None,
195 step_results: Vec::new(),
196 event_bus: None,
197 trace_context,
198 operation_ctx: None,
199 }
200 }
201
202 pub(crate) fn with_handler_resolver(
207 run_id: Uuid,
208 workflow_name: String,
209 store: Arc<dyn Store>,
210 provider: Arc<dyn AgentProvider>,
211 resolver: HandlerResolver,
212 ) -> Self {
213 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
214 Self {
215 run_id,
216 workflow_name,
217 store,
218 provider,
219 decision_provider: None,
220 handler_resolver: Some(resolver),
221 position: 0,
222 last_step_ids: Vec::new(),
223 total_cost_usd: Decimal::ZERO,
224 total_duration_ms: 0,
225 max_cost_usd: None,
226 inherited_cost_usd: Decimal::ZERO,
227 replay_steps: HashMap::new(),
228 granted_approvals: HashMap::new(),
229 attempt: 1,
230 carried_duration_ms: 0,
231 log_sender: None,
232 artifact_sink: None,
233 has_allowed_failure: false,
234 error_handlers: Vec::new(),
235 guard_state: None,
236 guard_config: None,
237 step_results: Vec::new(),
238 event_bus: None,
239 trace_context,
240 operation_ctx: None,
241 }
242 }
243
244 pub fn set_log_sender(&mut self, sender: LogSender) {
246 self.log_sender = Some(sender);
247 }
248
249 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
269 self.artifact_sink = Some(sink);
270 }
271
272 pub fn trace_context(&self) -> &WorkflowTraceContext {
278 &self.trace_context
279 }
280
281 pub fn set_guard(&mut self, config: WorkflowGuardConfig, state: SharedGuardState) {
298 self.guard_config = Some(config);
299 self.guard_state = Some(state);
300 }
301
302 pub fn guard_config(&self) -> Option<&WorkflowGuardConfig> {
304 self.guard_config.as_ref()
305 }
306
307 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
313 self.event_bus = Some(bus);
314 }
315
316 pub fn set_decision_provider(&mut self, provider: Arc<dyn DecisionProvider>) {
321 self.decision_provider = Some(provider);
322 }
323
324 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
326 self.artifact_sink.as_ref().ok_or_else(|| {
327 EngineError::ArtifactsUnavailable(
328 "no artifact storage is attached to this run".to_string(),
329 )
330 })
331 }
332
333 pub async fn put_artifact(
363 &self,
364 step_id: Uuid,
365 name: &str,
366 content_type: Option<&str>,
367 content: Vec<u8>,
368 ) -> Result<Artifact, EngineError> {
369 let sink = self.artifact_sink()?;
370 sink.put(
371 ArtifactUpload {
372 run_id: self.run_id,
373 step_id,
374 name: name.to_string(),
375 content_type: content_type
376 .map(str::to_string)
377 .unwrap_or_else(|| guess_content_type(name)),
378 },
379 stream_from_bytes(content),
380 )
381 .await
382 }
383
384 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
409 let sink = self.artifact_sink()?;
410
411 let artifact = self
412 .store
413 .find_artifact_for_input(ArtifactLookup {
414 run_id: self.run_id,
415 attempt: self.attempt,
416 before_position: self.position,
417 step_name: step.to_string(),
418 name: name.to_string(),
419 })
420 .await?
421 .ok_or_else(|| EngineError::ArtifactNotFound {
422 step: step.to_string(),
423 name: name.to_string(),
424 })?;
425
426 let mut content = sink.get(&artifact).await?;
427 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
428 while let Some(chunk) = content.next().await {
429 let chunk = chunk?;
430 buffer.extend_from_slice(chunk.as_ref());
431 }
432
433 Ok(buffer)
434 }
435
436 async fn prepare_step_inputs(
441 &self,
442 config: &StepConfig,
443 position: u32,
444 ) -> Result<(), EngineError> {
445 let StepConfig::Shell(shell) = config else {
446 return Ok(());
447 };
448 if shell.inputs.is_empty() {
449 return Ok(());
450 }
451
452 materialize_inputs(
453 self.artifact_sink()?,
454 &self.store,
455 shell,
456 StepLocation {
457 run_id: self.run_id,
458 attempt: self.attempt,
459 position,
460 },
461 )
462 .await
463 }
464
465 async fn store_step_outputs(
470 &self,
471 config: &StepConfig,
472 step_id: Uuid,
473 step_name: &str,
474 step_succeeded: bool,
475 ) -> Result<(), EngineError> {
476 let StepConfig::Shell(shell) = config else {
477 return Ok(());
478 };
479 if shell.outputs.is_empty() {
480 return Ok(());
481 }
482
483 let sink = match self.artifact_sink() {
484 Ok(sink) => sink,
485 Err(err) if step_succeeded => return Err(err),
486 Err(err) => {
487 warn!(
488 run_id = %self.run_id,
489 step = %step_name,
490 error = %err,
491 "cannot collect outputs of a failed step"
492 );
493 return Ok(());
494 }
495 };
496
497 let collected =
498 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
499
500 match collected {
501 Ok(()) => Ok(()),
502 Err(err) if step_succeeded => Err(err),
503 Err(err) => {
504 warn!(
505 run_id = %self.run_id,
506 step = %step_name,
507 error = %err,
508 "failed to collect outputs of a failed step"
509 );
510 Ok(())
511 }
512 }
513 }
514
515 pub(crate) fn carry_over_run_totals(
522 &mut self,
523 attempt: u32,
524 cost_usd: Decimal,
525 duration_ms: u64,
526 ) {
527 self.attempt = attempt;
528 self.total_cost_usd = cost_usd;
529 self.carried_duration_ms = duration_ms;
530 }
531
532 pub(crate) fn carried_duration_ms(&self) -> u64 {
534 self.carried_duration_ms
535 }
536
537 pub fn attempt(&self) -> u32 {
539 self.attempt
540 }
541
542 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
558 self.max_cost_usd = cap;
559 }
560
561 pub fn max_cost_usd(&self) -> Option<Decimal> {
563 self.max_cost_usd
564 }
565
566 pub fn charged_cost_usd(&self) -> Decimal {
571 self.inherited_cost_usd + self.total_cost_usd
572 }
573
574 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
585 let Some(limit) = self.max_cost_usd else {
586 return Ok(());
587 };
588
589 let spent = self.charged_cost_usd();
590 if spent + step_budget <= limit {
591 return Ok(());
592 }
593
594 error!(
595 run_id = %self.run_id,
596 limit_usd = %limit,
597 spent_usd = %spent,
598 step_budget_usd = %step_budget,
599 "run cost cap reached, refusing agent step"
600 );
601
602 Err(EngineError::RunBudgetExceeded {
603 run_id: self.run_id,
604 limit_usd: limit,
605 spent_usd: spent,
606 step_budget_usd: step_budget,
607 })
608 }
609
610 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
622 let steps = self.store.list_steps(self.run_id).await?;
623 for step in steps {
624 let dominated = matches!(
625 step.status.state,
626 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
627 );
628 if !dominated {
629 continue;
630 }
631
632 if step.attempt == self.attempt {
633 self.replay_steps.insert(step.position, step);
634 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
635 {
636 self.granted_approvals.insert(step.position, step.attempt);
637 }
638 }
639 Ok(())
640 }
641
642 pub fn run_id(&self) -> Uuid {
644 self.run_id
645 }
646
647 pub fn workflow_name(&self) -> &str {
649 &self.workflow_name
650 }
651
652 pub fn total_cost_usd(&self) -> Decimal {
654 self.total_cost_usd
655 }
656
657 pub fn has_allowed_failure(&self) -> bool {
659 self.has_allowed_failure
660 }
661
662 pub fn total_duration_ms(&self) -> u64 {
664 self.total_duration_ms
665 }
666
667 pub fn step_results(&self) -> &[StepResult] {
669 &self.step_results
670 }
671
672 #[cfg(feature = "secret-store")]
694 pub fn secrets(&self) -> ironflow_store::workflow_secrets::ScopedSecretStore {
695 let workflow_uuid = Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
696 ironflow_store::workflow_secrets::ScopedSecretStore::for_workflow(
697 workflow_uuid,
698 self.store.clone(),
699 )
700 }
701
702 fn ensure_operation_ctx(&mut self) -> &OperationContext {
703 self.operation_ctx.get_or_insert_with(|| {
704 #[cfg(feature = "secret-store")]
705 let secrets: Arc<dyn SecretResolver> = {
706 let workflow_uuid =
707 Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
708 Arc::new(ScopedSecretStore::for_workflow(
709 workflow_uuid,
710 self.store.clone(),
711 ))
712 };
713 #[cfg(not(feature = "secret-store"))]
714 let secrets: Arc<dyn SecretResolver> = Arc::new(NoopSecretResolver);
715
716 OperationContext::new(secrets)
717 })
718 }
719
720 async fn persist_progress(&self) {
726 if let Err(err) = self
727 .store
728 .update_run(
729 self.run_id,
730 RunUpdate {
731 cost_usd: Some(self.total_cost_usd),
732 duration_ms: Some(self.total_duration_ms),
733 ..RunUpdate::default()
734 },
735 )
736 .await
737 {
738 warn!(
739 run_id = %self.run_id,
740 error = %err,
741 "failed to persist run progress snapshot"
742 );
743 }
744 }
745
746 pub async fn parallel(
783 &mut self,
784 steps: Vec<(&str, StepConfig)>,
785 fail_fast: bool,
786 ) -> Result<Vec<ParallelStepResult>, EngineError> {
787 if steps.is_empty() {
788 return Ok(Vec::new());
789 }
790
791 self.check_guard_timeout()?;
793
794 let wave_budget: Decimal = steps
797 .iter()
798 .filter_map(|(_, config)| match config {
799 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
800 _ => None,
801 })
802 .map(step_budget_usd)
803 .sum();
804 self.check_run_budget(wave_budget)?;
805
806 let wave_position = self.position;
807 self.position += 1;
808
809 let now = Utc::now();
810 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
811 Vec::with_capacity(steps.len());
812
813 for (name, config) in &steps {
814 let kind = config.kind();
815 let trace_id = step_trace_id(self.run_id, name, wave_position);
816 let step = self
817 .store
818 .create_step(NewStep {
819 run_id: self.run_id,
820 trace_id,
821 name: name.to_string(),
822 kind,
823 position: wave_position,
824 input: Some(serde_json::to_value(config)?),
825 is_error_handler: false,
826 })
827 .await?;
828
829 self.start_step(step.id, now).await?;
830
831 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
834 self.fail_step(step.id, &err).await;
835 if !config.allow_failure() {
836 return Err(err);
837 }
838 self.has_allowed_failure = true;
839 info!(
840 run_id = %self.run_id,
841 step = %name,
842 error = %err,
843 "parallel step input preparation failed but allow_failure is set, skipping"
844 );
845 continue;
846 }
847
848 let mut config_with_trace = config.clone();
849 let step_trace = self.trace_context.child();
850 match config_with_trace {
851 StepConfig::Agent(ref mut agent_config) => {
852 agent_config.trace_context = Some(step_trace);
853 }
854 StepConfig::Http(ref mut http_config) => {
855 http_config.trace_context = Some(step_trace);
856 }
857 _ => {}
858 }
859 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
860 }
861
862 let mut join_set = JoinSet::new();
863 let mut task_index: HashMap<Id, usize> = HashMap::new();
864 let parallel_timeout = self.guard_remaining_timeout();
865 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
866 let provider = self.provider.clone();
867 let config = config.clone();
868 let step_log_sender = self
869 .log_sender
870 .as_ref()
871 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
872 let handle = join_set.spawn(async move {
873 let result = match parallel_timeout {
874 Some(dur) => {
875 match tokio::time::timeout(
876 dur,
877 execute_step_config(&config, &provider, step_log_sender),
878 )
879 .await
880 {
881 Ok(r) => r,
882 Err(_elapsed) => {
883 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
884 elapsed_secs: 0,
885 max: 0,
886 }))
887 }
888 }
889 }
890 None => execute_step_config(&config, &provider, step_log_sender).await,
891 };
892 (idx, result)
893 });
894 task_index.insert(handle.id(), idx);
895 }
896
897 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
899 vec![None; step_records.len()];
900 let mut first_error: Option<EngineError> = None;
901
902 while let Some(join_result) = join_set.join_next().await {
903 let (idx, step_result) = match join_result {
904 Ok(r) => r,
905 Err(e) => {
906 let error_msg = format!("join error: {e}");
907 if let Some(&idx) = task_index.get(&e.id()) {
908 let (step_id, _, step_name, _) = &step_records[idx];
909 let completed_at = Utc::now();
910 error!(
911 run_id = %self.run_id,
912 step = %step_name,
913 error = %error_msg,
914 "parallel step panicked or was cancelled"
915 );
916 if let Err(store_err) = self
917 .store
918 .update_step(
919 *step_id,
920 StepUpdate {
921 status: Some(StepStatus::Failed),
922 error: Some(error_msg.clone()),
923 completed_at: Some(completed_at),
924 ..StepUpdate::default()
925 },
926 )
927 .await
928 {
929 error!(
930 run_id = %self.run_id,
931 step_id = %step_id,
932 error = %store_err,
933 "failed to persist JoinError for step"
934 );
935 }
936 indexed_results[idx] = Some(Err(error_msg.clone()));
937 }
938 if first_error.is_none() {
939 first_error = Some(EngineError::StepConfig(error_msg));
940 }
941 if fail_fast {
942 join_set.abort_all();
943 }
944 continue;
945 }
946 };
947
948 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
949 let completed_at = Utc::now();
950
951 if let Err(err) = self
952 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
953 .await
954 {
955 self.fail_step(*step_id, &err).await;
956 indexed_results[idx] = Some(Err(err.to_string()));
957 if first_error.is_none() {
958 first_error = Some(err);
959 }
960 if fail_fast {
961 join_set.abort_all();
962 }
963 continue;
964 }
965
966 match step_result {
967 Ok(output) => {
968 self.total_cost_usd += output.cost_usd;
969 self.total_duration_ms += output.duration_ms;
970
971 if matches!(step_config, StepConfig::Agent(_)) {
973 let tokens = output
974 .input_tokens
975 .unwrap_or(0)
976 .saturating_add(output.output_tokens.unwrap_or(0));
977 if tokens > 0
978 && let Err(guard_err) = self.guard_record_tokens(tokens)
979 {
980 if first_error.is_none() {
981 first_error = Some(guard_err);
982 }
983 if fail_fast {
984 join_set.abort_all();
985 }
986 }
987 }
988
989 let debug_messages_json = output.debug_messages_json();
990
991 self.store
992 .update_step(
993 *step_id,
994 StepUpdate {
995 status: Some(StepStatus::Completed),
996 output: Some(output.output.clone()),
997 duration_ms: Some(output.duration_ms),
998 cost_usd: Some(output.cost_usd),
999 input_tokens: output.input_tokens,
1000 output_tokens: output.output_tokens,
1001 completed_at: Some(completed_at),
1002 debug_messages: debug_messages_json,
1003 ..StepUpdate::default()
1004 },
1005 )
1006 .await?;
1007
1008 self.step_results.push(StepResult::from_success(
1009 *step_trace,
1010 step_name,
1011 &output,
1012 ));
1013
1014 if let Some(ref bus) = self.event_bus
1015 && matches!(step_config, StepConfig::Agent(_))
1016 {
1017 let tokens = output
1018 .input_tokens
1019 .unwrap_or(0)
1020 .saturating_add(output.output_tokens.unwrap_or(0));
1021 bus.publish(
1022 self.run_id,
1023 crate::notify::WorkflowEvent::AgentStepTokensUsed {
1024 step_name: step_name.clone(),
1025 tokens,
1026 cost_usd: output.cost_usd,
1027 },
1028 );
1029 }
1030
1031 info!(
1032 run_id = %self.run_id,
1033 step = %step_name,
1034 trace_id = %step_trace,
1035 duration_ms = output.duration_ms,
1036 "parallel step completed"
1037 );
1038
1039 indexed_results[idx] = Some(Ok(output));
1040 }
1041 Err(err) => {
1042 let err_msg = err.to_string();
1043 let debug_messages_json = extract_debug_messages_from_error(&err);
1044 let partial = extract_partial_usage_from_error(&err);
1045 let raw_response_output = extract_raw_response_from_error(&err);
1046
1047 if let Some(ref usage) = partial {
1048 if let Some(cost) = usage.cost_usd {
1049 self.total_cost_usd += cost;
1050 }
1051 if let Some(dur) = usage.duration_ms {
1052 self.total_duration_ms += dur;
1053 }
1054 }
1055
1056 if let Err(store_err) = self
1057 .store
1058 .update_step(
1059 *step_id,
1060 StepUpdate {
1061 status: Some(StepStatus::Failed),
1062 error: Some(err_msg.clone()),
1063 output: raw_response_output.clone(),
1064 completed_at: Some(completed_at),
1065 debug_messages: debug_messages_json,
1066 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1067 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1068 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1069 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1070 ..StepUpdate::default()
1071 },
1072 )
1073 .await
1074 {
1075 tracing::error!(
1076 step_id = %step_id,
1077 error = %store_err,
1078 "failed to persist parallel step failure"
1079 );
1080 }
1081
1082 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1083 let err_cost = partial
1084 .as_ref()
1085 .and_then(|p| p.cost_usd)
1086 .unwrap_or(Decimal::ZERO);
1087 self.step_results.push(StepResult::from_failure(
1088 *step_trace,
1089 step_name,
1090 &err_msg,
1091 err_duration,
1092 err_cost,
1093 ));
1094
1095 if step_config.allow_failure() {
1096 self.has_allowed_failure = true;
1097 info!(
1098 run_id = %self.run_id,
1099 step = %step_name,
1100 error = %err_msg,
1101 "parallel step failed but allow_failure is set, continuing"
1102 );
1103 indexed_results[idx] = Some(Ok(allowed_failure_output(
1104 &err_msg,
1105 raw_response_output,
1106 partial.as_ref(),
1107 )));
1108 } else {
1109 indexed_results[idx] = Some(Err(err_msg.clone()));
1110
1111 if first_error.is_none() {
1112 first_error = Some(err);
1113 }
1114
1115 if fail_fast {
1116 join_set.abort_all();
1117 }
1118 }
1119 }
1120 }
1121 }
1122
1123 if let Some(err) = first_error {
1124 return Err(err);
1125 }
1126
1127 self.persist_progress().await;
1128
1129 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
1130
1131 let results: Vec<ParallelStepResult> = step_records
1133 .iter()
1134 .enumerate()
1135 .map(|(idx, (step_id, _trace_id, name, _))| {
1136 let output = match indexed_results[idx].take() {
1137 Some(Ok(o)) => o,
1138 _ => unreachable!("all steps succeeded if no error returned"),
1139 };
1140 ParallelStepResult {
1141 name: name.clone(),
1142 output,
1143 step_id: *step_id,
1144 }
1145 })
1146 .collect();
1147
1148 Ok(results)
1149 }
1150
1151 pub async fn shell(
1174 &mut self,
1175 name: &str,
1176 config: ShellConfig,
1177 ) -> Result<StepOutput, EngineError> {
1178 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1179 .await
1180 }
1181
1182 pub async fn http(
1202 &mut self,
1203 name: &str,
1204 config: HttpConfig,
1205 ) -> Result<StepOutput, EngineError> {
1206 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1207 .await
1208 }
1209
1210 pub async fn agent(
1230 &mut self,
1231 name: &str,
1232 config: impl Into<AgentStepConfig>,
1233 ) -> Result<StepOutput, EngineError> {
1234 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1235 .await
1236 }
1237
1238 pub async fn approval(
1269 &mut self,
1270 name: &str,
1271 config: ApprovalConfig,
1272 ) -> Result<(), EngineError> {
1273 let position = self.position;
1274 self.position += 1;
1275
1276 if let Some(existing) = self.replay_steps.get(&position)
1279 && existing.kind == StepKind::Approval
1280 {
1281 if existing.status.state == StepStatus::AwaitingApproval {
1282 self.store
1283 .update_step(
1284 existing.id,
1285 StepUpdate {
1286 status: Some(StepStatus::Completed),
1287 completed_at: Some(Utc::now()),
1288 ..StepUpdate::default()
1289 },
1290 )
1291 .await?;
1292 }
1293
1294 self.last_step_ids = vec![existing.id];
1295 info!(
1296 run_id = %self.run_id,
1297 step = %name,
1298 position,
1299 "approval step replayed (approved)"
1300 );
1301 return Ok(());
1302 }
1303
1304 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1308 let trace_id = step_trace_id(self.run_id, name, position);
1309 let step = self
1310 .store
1311 .create_step(NewStep {
1312 run_id: self.run_id,
1313 trace_id,
1314 name: name.to_string(),
1315 kind: StepKind::Approval,
1316 position,
1317 input: Some(serde_json::to_value(&config)?),
1318 is_error_handler: false,
1319 })
1320 .await?;
1321
1322 let now = Utc::now();
1323 self.start_step(step.id, now).await?;
1324 self.store
1325 .update_step(
1326 step.id,
1327 StepUpdate {
1328 status: Some(StepStatus::Completed),
1329 output: Some(json!({"approved_in_attempt": granted_in})),
1330 completed_at: Some(now),
1331 ..StepUpdate::default()
1332 },
1333 )
1334 .await?;
1335
1336 self.last_step_ids = vec![step.id];
1337 info!(
1338 run_id = %self.run_id,
1339 step = %name,
1340 position,
1341 granted_in_attempt = granted_in,
1342 attempt = self.attempt,
1343 "approval carried over from a previous attempt"
1344 );
1345 return Ok(());
1346 }
1347
1348 let trace_id = step_trace_id(self.run_id, name, position);
1350 let step = self
1351 .store
1352 .create_step(NewStep {
1353 run_id: self.run_id,
1354 trace_id,
1355 name: name.to_string(),
1356 kind: StepKind::Approval,
1357 position,
1358 input: Some(serde_json::to_value(&config)?),
1359 is_error_handler: false,
1360 })
1361 .await?;
1362
1363 self.start_step(step.id, Utc::now()).await?;
1364
1365 self.store
1368 .update_step(
1369 step.id,
1370 StepUpdate {
1371 status: Some(StepStatus::AwaitingApproval),
1372 ..StepUpdate::default()
1373 },
1374 )
1375 .await?;
1376
1377 self.last_step_ids = vec![step.id];
1378
1379 if let Some(ref bus) = self.event_bus {
1380 bus.publish(
1381 self.run_id,
1382 crate::notify::WorkflowEvent::ApprovalRequired {
1383 step_name: name.to_string(),
1384 step_index: position,
1385 approval_id: step.id,
1386 },
1387 );
1388 }
1389
1390 Err(EngineError::ApprovalRequired {
1391 run_id: self.run_id,
1392 step_id: step.id,
1393 message: config.message().to_string(),
1394 })
1395 }
1396
1397 pub async fn decision(
1410 &mut self,
1411 name: &str,
1412 config: DecisionConfig,
1413 ) -> Result<DecisionOutput, EngineError> {
1414 if let Some(output) = self.decision_replay(name, &config).await? {
1415 return Ok(output);
1416 }
1417 self.decision_execute(name, config).await
1418 }
1419
1420 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1449 let position = self.position;
1450 self.position += 1;
1451
1452 let trace_id = step_trace_id(self.run_id, name, position);
1453 let step = self
1454 .store
1455 .create_step(NewStep {
1456 run_id: self.run_id,
1457 trace_id,
1458 name: name.to_string(),
1459 kind: StepKind::Custom("skip".to_string()),
1460 position,
1461 input: None,
1462 is_error_handler: false,
1463 })
1464 .await?;
1465
1466 if !self.last_step_ids.is_empty() {
1467 let deps: Vec<NewStepDependency> = self
1468 .last_step_ids
1469 .iter()
1470 .map(|&depends_on| NewStepDependency {
1471 step_id: step.id,
1472 depends_on,
1473 })
1474 .collect();
1475 self.store.create_step_dependencies(deps).await?;
1476 }
1477
1478 let now = Utc::now();
1479 self.store
1480 .update_step(
1481 step.id,
1482 StepUpdate {
1483 status: Some(StepStatus::Skipped),
1484 output: Some(serde_json::json!({"reason": reason})),
1485 completed_at: Some(now),
1486 ..StepUpdate::default()
1487 },
1488 )
1489 .await?;
1490
1491 self.last_step_ids = vec![step.id];
1492
1493 info!(
1494 run_id = %self.run_id,
1495 step = %name,
1496 reason,
1497 "step skipped"
1498 );
1499
1500 Ok(())
1501 }
1502
1503 pub async fn operation(
1542 &mut self,
1543 name: &str,
1544 op: &dyn Operation,
1545 ) -> Result<StepOutput, EngineError> {
1546 let kind = StepKind::Custom(op.kind().to_string());
1547 let position = self.position;
1548 self.position += 1;
1549
1550 let trace_id = step_trace_id(self.run_id, name, position);
1551 let step = self
1552 .store
1553 .create_step(NewStep {
1554 run_id: self.run_id,
1555 trace_id,
1556 name: name.to_string(),
1557 kind,
1558 position,
1559 input: op.input(),
1560 is_error_handler: false,
1561 })
1562 .await?;
1563
1564 self.start_step(step.id, Utc::now()).await?;
1565
1566 let start = Instant::now();
1567
1568 let op_ctx = self.ensure_operation_ctx();
1569
1570 match op.execute(op_ctx).await {
1571 Ok(output_value) => {
1572 let duration_ms = start.elapsed().as_millis() as u64;
1573 self.total_duration_ms += duration_ms;
1574
1575 let completed_at = Utc::now();
1576 self.store
1577 .update_step(
1578 step.id,
1579 StepUpdate {
1580 status: Some(StepStatus::Completed),
1581 output: Some(output_value.clone()),
1582 duration_ms: Some(duration_ms),
1583 cost_usd: Some(Decimal::ZERO),
1584 completed_at: Some(completed_at),
1585 ..StepUpdate::default()
1586 },
1587 )
1588 .await?;
1589
1590 info!(
1591 run_id = %self.run_id,
1592 step = %name,
1593 kind = op.kind(),
1594 duration_ms,
1595 "operation step completed"
1596 );
1597
1598 self.last_step_ids = vec![step.id];
1599
1600 Ok(StepOutput {
1601 output: output_value,
1602 duration_ms,
1603 cost_usd: Decimal::ZERO,
1604 input_tokens: None,
1605 output_tokens: None,
1606 model: None,
1607 debug_messages: None,
1608 })
1609 }
1610 Err(err) => {
1611 let completed_at = Utc::now();
1612 let engine_err = EngineError::Operation(err);
1613 if let Err(store_err) = self
1614 .store
1615 .update_step(
1616 step.id,
1617 StepUpdate {
1618 status: Some(StepStatus::Failed),
1619 error: Some(engine_err.to_string()),
1620 completed_at: Some(completed_at),
1621 ..StepUpdate::default()
1622 },
1623 )
1624 .await
1625 {
1626 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1627 }
1628
1629 Err(engine_err)
1630 }
1631 }
1632 }
1633
1634 pub async fn workflow(
1661 &mut self,
1662 handler: &dyn WorkflowHandler,
1663 payload: Value,
1664 ) -> Result<StepOutput, EngineError> {
1665 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1667 let state = guard_state
1668 .lock()
1669 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1670 state.check(guard_config, handler.name())?;
1671 }
1672
1673 let config = WorkflowStepConfig::new(handler.name(), payload);
1674 let position = self.position;
1675 self.position += 1;
1676
1677 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1678 let step = self
1679 .store
1680 .create_step(NewStep {
1681 run_id: self.run_id,
1682 trace_id,
1683 name: config.workflow_name.clone(),
1684 kind: StepKind::Workflow,
1685 position,
1686 input: Some(serde_json::to_value(&config)?),
1687 is_error_handler: false,
1688 })
1689 .await?;
1690
1691 self.start_step(step.id, Utc::now()).await?;
1692
1693 if let Some(guard_state) = &self.guard_state {
1695 let mut state = guard_state
1696 .lock()
1697 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1698 state.record_invocation(handler.name());
1699 }
1700
1701 match self.execute_child_workflow(&config).await {
1702 Ok((output, child_had_allowed_failure)) => {
1703 self.total_cost_usd += output.cost_usd;
1704 self.total_duration_ms += output.duration_ms;
1705 if child_had_allowed_failure {
1706 self.has_allowed_failure = true;
1707 }
1708
1709 let completed_at = Utc::now();
1710 self.store
1711 .update_step(
1712 step.id,
1713 StepUpdate {
1714 status: Some(StepStatus::Completed),
1715 output: Some(output.output.clone()),
1716 duration_ms: Some(output.duration_ms),
1717 cost_usd: Some(output.cost_usd),
1718 completed_at: Some(completed_at),
1719 ..StepUpdate::default()
1720 },
1721 )
1722 .await?;
1723
1724 info!(
1725 run_id = %self.run_id,
1726 child_workflow = %config.workflow_name,
1727 duration_ms = output.duration_ms,
1728 "workflow step completed"
1729 );
1730
1731 self.last_step_ids = vec![step.id];
1732
1733 self.guard_record_return();
1734 Ok(output)
1735 }
1736 Err(err) => {
1737 let completed_at = Utc::now();
1738 if let Err(store_err) = self
1739 .store
1740 .update_step(
1741 step.id,
1742 StepUpdate {
1743 status: Some(StepStatus::Failed),
1744 error: Some(err.to_string()),
1745 completed_at: Some(completed_at),
1746 ..StepUpdate::default()
1747 },
1748 )
1749 .await
1750 {
1751 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1752 }
1753
1754 self.guard_record_return();
1755 Err(err)
1756 }
1757 }
1758 }
1759
1760 fn guard_record_return(&self) {
1765 if let Some(guard_state) = &self.guard_state {
1766 match guard_state.lock() {
1767 Ok(mut state) => state.record_return(),
1768 Err(_) => {
1769 error!(
1770 run_id = %self.run_id,
1771 "guard state mutex poisoned in record_return"
1772 );
1773 }
1774 }
1775 }
1776 }
1777
1778 async fn execute_with_guard_timeout(
1782 &self,
1783 config: &StepConfig,
1784 step_log_sender: Option<StepLogSender>,
1785 ) -> Result<StepOutput, EngineError> {
1786 let remaining = self.guard_remaining_timeout();
1787 match remaining {
1788 Some(dur) => {
1789 use tokio::time::timeout;
1790 match timeout(
1791 dur,
1792 execute_step_config(config, &self.provider, step_log_sender),
1793 )
1794 .await
1795 {
1796 Ok(result) => result,
1797 Err(_elapsed) => {
1798 let config_secs = self
1799 .guard_config
1800 .as_ref()
1801 .map_or(0, |c| c.workflow_timeout_secs);
1802 Err(WorkflowRejection::WorkflowTimeout {
1803 elapsed_secs: config_secs,
1804 max: config_secs,
1805 }
1806 .into())
1807 }
1808 }
1809 }
1810 None => execute_step_config(config, &self.provider, step_log_sender).await,
1811 }
1812 }
1813
1814 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1816 let config = self.guard_config.as_ref()?;
1817 let guard_state = self.guard_state.as_ref()?;
1818 let state = guard_state.lock().ok()?;
1819 let elapsed = state.elapsed_secs();
1820 let max = config.workflow_timeout_secs;
1821 if elapsed >= max {
1822 Some(std::time::Duration::ZERO)
1823 } else {
1824 Some(std::time::Duration::from_secs(max - elapsed))
1825 }
1826 }
1827
1828 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1830 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1831 let state = guard_state
1832 .lock()
1833 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1834 let elapsed = state.elapsed_secs();
1835 if elapsed >= config.workflow_timeout_secs {
1836 return Err(WorkflowRejection::WorkflowTimeout {
1837 elapsed_secs: elapsed,
1838 max: config.workflow_timeout_secs,
1839 }
1840 .into());
1841 }
1842 }
1843 Ok(())
1844 }
1845
1846 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1848 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1849 let mut state = guard_state
1850 .lock()
1851 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1852 state.record_tokens(config, tokens)?;
1853 }
1854 Ok(())
1855 }
1856
1857 async fn execute_child_workflow(
1860 &self,
1861 config: &WorkflowStepConfig,
1862 ) -> Result<(StepOutput, bool), EngineError> {
1863 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1864 EngineError::InvalidWorkflow(
1865 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1866 )
1867 })?;
1868
1869 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1870 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1871 })?;
1872
1873 let parent = self.store.get_run(self.run_id).await?;
1876 let (parent_labels, parent_author) =
1877 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1878
1879 let child_run = self
1880 .store
1881 .create_run(NewRun {
1882 workflow_name: config.workflow_name.clone(),
1883 trigger: TriggerKind::Workflow,
1884 payload: config.payload.clone(),
1885 max_retries: 0,
1886 handler_version: None,
1887 labels: parent_labels,
1888 scheduled_at: None,
1889 created_by: parent_author,
1890 idempotency_key: None,
1891 max_cost_usd: self.max_cost_usd,
1893 })
1894 .await?
1895 .into_run();
1896
1897 let child_run_id = child_run.id;
1898 info!(
1899 parent_run_id = %self.run_id,
1900 child_run_id = %child_run_id,
1901 workflow = %config.workflow_name,
1902 "child run created"
1903 );
1904
1905 self.store
1906 .update_run_status(child_run_id, RunStatus::Running)
1907 .await?;
1908
1909 let run_start = Instant::now();
1910 let mut child_ctx = WorkflowContext {
1911 run_id: child_run_id,
1912 workflow_name: config.workflow_name.clone(),
1913 store: self.store.clone(),
1914 provider: self.provider.clone(),
1915 decision_provider: self.decision_provider.clone(),
1916 handler_resolver: self.handler_resolver.clone(),
1917 position: 0,
1918 last_step_ids: Vec::new(),
1919 total_cost_usd: Decimal::ZERO,
1920 total_duration_ms: 0,
1921 max_cost_usd: self.max_cost_usd,
1922 inherited_cost_usd: self.charged_cost_usd(),
1925 replay_steps: HashMap::new(),
1926 granted_approvals: HashMap::new(),
1927 attempt: 1,
1929 carried_duration_ms: 0,
1930 log_sender: self.log_sender.clone(),
1931 artifact_sink: self.artifact_sink.clone(),
1934 has_allowed_failure: false,
1935 error_handlers: Vec::new(),
1936 guard_state: self.guard_state.clone(),
1937 guard_config: self.guard_config.clone(),
1938 step_results: Vec::new(),
1939 event_bus: self.event_bus.clone(),
1940 trace_context: self.trace_context.child(),
1941 operation_ctx: None,
1942 };
1943
1944 let result = handler.execute(&mut child_ctx).await;
1945 let total_duration = run_start.elapsed().as_millis() as u64;
1946 let completed_at = Utc::now();
1947
1948 match result {
1949 Ok(()) => {
1950 let child_status = if child_ctx.has_allowed_failure {
1951 RunStatus::Warning
1952 } else {
1953 RunStatus::Completed
1954 };
1955 self.store
1956 .update_run(
1957 child_run_id,
1958 RunUpdate {
1959 status: Some(child_status),
1960 cost_usd: Some(child_ctx.total_cost_usd),
1961 duration_ms: Some(total_duration),
1962 completed_at: Some(completed_at),
1963 ..RunUpdate::default()
1964 },
1965 )
1966 .await?;
1967
1968 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1969 Ok((
1970 StepOutput {
1971 output: serde_json::json!({
1972 "run_id": child_run_id,
1973 "workflow_name": config.workflow_name,
1974 "status": child_status,
1975 "cost_usd": child_ctx.total_cost_usd,
1976 "duration_ms": total_duration,
1977 }),
1978 duration_ms: total_duration,
1979 cost_usd: child_ctx.total_cost_usd,
1980 input_tokens: None,
1981 output_tokens: None,
1982 model: None,
1983 debug_messages: None,
1984 },
1985 child_had_allowed_failure,
1986 ))
1987 }
1988 Err(err) => {
1989 if let Err(store_err) = self
1990 .store
1991 .update_run(
1992 child_run_id,
1993 RunUpdate {
1994 status: Some(RunStatus::Failed),
1995 error: Some(err.to_string()),
1996 cost_usd: Some(child_ctx.total_cost_usd),
1997 duration_ms: Some(total_duration),
1998 completed_at: Some(completed_at),
1999 ..RunUpdate::default()
2000 },
2001 )
2002 .await
2003 {
2004 error!(
2005 child_run_id = %child_run_id,
2006 store_error = %store_err,
2007 "failed to persist child run failure"
2008 );
2009 }
2010
2011 Err(err)
2012 }
2013 }
2014 }
2015
2016 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
2021 let step = self.replay_steps.get(&position)?;
2022 if step.status.state != StepStatus::Completed {
2023 return None;
2024 }
2025 let output = StepOutput {
2026 output: step.output.clone().unwrap_or(Value::Null),
2027 duration_ms: step.duration_ms,
2028 cost_usd: step.cost_usd,
2029 input_tokens: step.input_tokens,
2030 output_tokens: step.output_tokens,
2031 model: None,
2032 debug_messages: None,
2033 };
2034 self.total_cost_usd += output.cost_usd;
2035 self.total_duration_ms += output.duration_ms;
2036 self.last_step_ids = vec![step.id];
2037 info!(
2038 run_id = %self.run_id,
2039 step = %step.name,
2040 position,
2041 "step replayed from previous execution"
2042 );
2043 Some(output)
2044 }
2045
2046 #[tracing::instrument(
2048 name = "context.execute_step",
2049 skip_all,
2050 fields(
2051 run_id = %self.run_id,
2052 step.name = %name,
2053 step.kind,
2054 step.position = self.position,
2055 step.trace_id,
2056 )
2057 )]
2058 pub(crate) async fn execute_step(
2059 &mut self,
2060 name: &str,
2061 kind: StepKind,
2062 config: StepConfig,
2063 ) -> Result<StepOutput, EngineError> {
2064 let kind_str: &'static str = match kind {
2065 StepKind::Shell => "shell",
2066 StepKind::Http => "http",
2067 StepKind::Agent => "agent",
2068 StepKind::Workflow => "workflow",
2069 StepKind::Approval => "approval",
2070 StepKind::Decision => "decision",
2071 StepKind::Custom(_) => "custom",
2072 };
2073 Span::current().record("step.kind", kind_str);
2074
2075 self.check_guard_timeout()?;
2077
2078 let position = self.position;
2079 self.position += 1;
2080
2081 if let Some(output) = self.try_replay_step(position) {
2083 return Ok(output);
2084 }
2085
2086 if let StepConfig::Agent(ref agent_config) = config {
2089 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2090 }
2091
2092 let trace_id = step_trace_id(self.run_id, name, position);
2094 Span::current().record("step.trace_id", trace_id.to_string().as_str());
2095 let step = self
2096 .store
2097 .create_step(NewStep {
2098 run_id: self.run_id,
2099 trace_id,
2100 name: name.to_string(),
2101 kind,
2102 position,
2103 input: Some(serde_json::to_value(&config)?),
2104 is_error_handler: false,
2105 })
2106 .await?;
2107
2108 self.start_step(step.id, Utc::now()).await?;
2109
2110 if let Some(ref bus) = self.event_bus {
2111 bus.publish(
2112 self.run_id,
2113 crate::notify::WorkflowEvent::StepStarted {
2114 step_name: name.to_string(),
2115 step_index: position,
2116 timestamp: Utc::now(),
2117 },
2118 );
2119 }
2120
2121 if let Err(err) = self.prepare_step_inputs(&config, position).await {
2124 self.fail_step(step.id, &err).await;
2125 if config.allow_failure() {
2126 self.has_allowed_failure = true;
2127 self.last_step_ids = vec![step.id];
2128 info!(
2129 run_id = %self.run_id,
2130 step = %name,
2131 error = %err,
2132 "step input preparation failed but allow_failure is set, continuing"
2133 );
2134 return Ok(StepOutput {
2135 output: json!({"error": err.to_string()}),
2136 duration_ms: 0,
2137 cost_usd: Decimal::ZERO,
2138 input_tokens: None,
2139 output_tokens: None,
2140 model: None,
2141 debug_messages: None,
2142 });
2143 }
2144 return Err(err);
2145 }
2146
2147 let mut config = config;
2148 let step_trace = self.trace_context.child();
2149 match config {
2150 StepConfig::Agent(ref mut agent_config) => {
2151 agent_config.trace_context = Some(step_trace);
2152 }
2153 StepConfig::Http(ref mut http_config) => {
2154 http_config.trace_context = Some(step_trace);
2155 }
2156 _ => {}
2157 }
2158
2159 let step_log_sender = self
2160 .log_sender
2161 .as_ref()
2162 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
2163
2164 let execution = self
2165 .execute_with_guard_timeout(&config, step_log_sender)
2166 .await;
2167
2168 let execution = self
2169 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
2170 .await;
2171
2172 if let Err(err) = self
2173 .store_step_outputs(&config, step.id, name, execution.is_ok())
2174 .await
2175 {
2176 self.fail_step(step.id, &err).await;
2177 return Err(err);
2178 }
2179
2180 match execution {
2181 Ok(output) => {
2182 self.total_cost_usd += output.cost_usd;
2183 self.total_duration_ms += output.duration_ms;
2184
2185 if matches!(config, StepConfig::Agent(_)) {
2187 let tokens = output
2188 .input_tokens
2189 .unwrap_or(0)
2190 .saturating_add(output.output_tokens.unwrap_or(0));
2191 if tokens > 0 {
2192 self.guard_record_tokens(tokens)?;
2193 }
2194 }
2195
2196 let debug_messages_json = output.debug_messages_json();
2197
2198 let completed_at = Utc::now();
2199 self.store
2200 .update_step(
2201 step.id,
2202 StepUpdate {
2203 status: Some(StepStatus::Completed),
2204 output: Some(output.output.clone()),
2205 duration_ms: Some(output.duration_ms),
2206 cost_usd: Some(output.cost_usd),
2207 input_tokens: output.input_tokens,
2208 output_tokens: output.output_tokens,
2209 completed_at: Some(completed_at),
2210 debug_messages: debug_messages_json,
2211 ..StepUpdate::default()
2212 },
2213 )
2214 .await?;
2215
2216 self.step_results
2217 .push(StepResult::from_success(trace_id, name, &output));
2218 self.persist_progress().await;
2219
2220 info!(
2221 run_id = %self.run_id,
2222 step = %name,
2223 trace_id = %trace_id,
2224 duration_ms = output.duration_ms,
2225 "step completed"
2226 );
2227
2228 if let Some(ref bus) = self.event_bus {
2229 bus.publish(
2230 self.run_id,
2231 crate::notify::WorkflowEvent::StepCompleted {
2232 step_name: name.to_string(),
2233 step_index: position,
2234 duration_ms: output.duration_ms,
2235 output_summary: None,
2236 },
2237 );
2238
2239 if matches!(config, StepConfig::Agent(_)) {
2240 let tokens = output
2241 .input_tokens
2242 .unwrap_or(0)
2243 .saturating_add(output.output_tokens.unwrap_or(0));
2244 bus.publish(
2245 self.run_id,
2246 crate::notify::WorkflowEvent::AgentStepTokensUsed {
2247 step_name: name.to_string(),
2248 tokens,
2249 cost_usd: output.cost_usd,
2250 },
2251 );
2252 }
2253 }
2254
2255 self.last_step_ids = vec![step.id];
2256
2257 Ok(output)
2258 }
2259 Err(err) => {
2260 let completed_at = Utc::now();
2261 let debug_messages_json = extract_debug_messages_from_error(&err);
2262 let partial = extract_partial_usage_from_error(&err);
2263 let raw_response_output = extract_raw_response_from_error(&err);
2264
2265 if let Some(ref usage) = partial {
2266 if let Some(cost) = usage.cost_usd {
2267 self.total_cost_usd += cost;
2268 }
2269 if let Some(dur) = usage.duration_ms {
2270 self.total_duration_ms += dur;
2271 }
2272 }
2273
2274 if let Err(store_err) = self
2275 .store
2276 .update_step(
2277 step.id,
2278 StepUpdate {
2279 status: Some(StepStatus::Failed),
2280 error: Some(err.to_string()),
2281 output: raw_response_output.clone(),
2282 completed_at: Some(completed_at),
2283 debug_messages: debug_messages_json,
2284 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2285 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2286 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2287 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2288 ..StepUpdate::default()
2289 },
2290 )
2291 .await
2292 {
2293 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2294 }
2295
2296 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2297 let err_cost = partial
2298 .as_ref()
2299 .and_then(|p| p.cost_usd)
2300 .unwrap_or(Decimal::ZERO);
2301 self.step_results.push(StepResult::from_failure(
2302 trace_id,
2303 name,
2304 &err.to_string(),
2305 err_duration,
2306 err_cost,
2307 ));
2308 self.persist_progress().await;
2309
2310 if let Some(ref bus) = self.event_bus {
2311 bus.publish(
2312 self.run_id,
2313 crate::notify::WorkflowEvent::StepFailed {
2314 step_name: name.to_string(),
2315 step_index: position,
2316 error: err.to_string(),
2317 duration_ms: err_duration,
2318 },
2319 );
2320 }
2321
2322 self.fire_error_handlers(name, &err.to_string(), err_duration)
2323 .await;
2324
2325 if config.allow_failure() {
2326 self.has_allowed_failure = true;
2327 self.last_step_ids = vec![step.id];
2328 info!(
2329 run_id = %self.run_id,
2330 step = %name,
2331 error = %err,
2332 "step failed but allow_failure is set, continuing"
2333 );
2334 Ok(allowed_failure_output(
2335 &err.to_string(),
2336 raw_response_output,
2337 partial.as_ref(),
2338 ))
2339 } else {
2340 Err(err)
2341 }
2342 }
2343 }
2344 }
2345
2346 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2352 async fn retry_step_if_configured(
2353 &self,
2354 name: &str,
2355 kind_str: &str,
2356 config: &StepConfig,
2357 step_id: Uuid,
2358 first_result: Result<StepOutput, EngineError>,
2359 ) -> Result<StepOutput, EngineError> {
2360 let policy = match config.retry() {
2361 Some(p) => p,
2362 None => return first_result,
2363 };
2364
2365 let mut last_result = match first_result {
2366 Ok(output) => return Ok(output),
2367 Err(err) if !is_step_retryable(&err) => return Err(err),
2368 Err(err) => Err(err),
2369 };
2370
2371 let step_log_sender = self
2372 .log_sender
2373 .as_ref()
2374 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2375
2376 for attempt in 0..policy.max_retries() {
2377 if let StepConfig::Agent(agent_config) = config {
2378 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2379 }
2380
2381 let delay = policy.delay_for_attempt(attempt);
2382 info!(
2383 run_id = %self.run_id,
2384 step = %name,
2385 attempt = attempt + 1,
2386 max_retries = policy.max_retries(),
2387 delay_ms = delay.as_millis() as u64,
2388 "retrying step after transient failure"
2389 );
2390 tokio::time::sleep(delay).await;
2391
2392 record_retry_metric(kind_str, "retry");
2393
2394 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2395 Ok(output) => return Ok(output),
2396 Err(err) if !is_step_retryable(&err) => return Err(err),
2397 err => last_result = err,
2398 }
2399 }
2400
2401 record_retry_metric(kind_str, "exhausted");
2402
2403 info!(
2404 run_id = %self.run_id,
2405 step = %name,
2406 max_retries = policy.max_retries(),
2407 "step retries exhausted"
2408 );
2409
2410 last_result
2411 }
2412
2413 pub(crate) async fn start_step(
2418 &self,
2419 step_id: Uuid,
2420 now: DateTime<Utc>,
2421 ) -> Result<(), EngineError> {
2422 if !self.last_step_ids.is_empty() {
2423 let deps: Vec<NewStepDependency> = self
2424 .last_step_ids
2425 .iter()
2426 .map(|&depends_on| NewStepDependency {
2427 step_id,
2428 depends_on,
2429 })
2430 .collect();
2431 self.store.create_step_dependencies(deps).await?;
2432 }
2433
2434 self.store
2435 .update_step(
2436 step_id,
2437 StepUpdate {
2438 status: Some(StepStatus::Running),
2439 started_at: Some(now),
2440 ..StepUpdate::default()
2441 },
2442 )
2443 .await?;
2444
2445 Ok(())
2446 }
2447
2448 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2455 if let Err(store_err) = self
2456 .store
2457 .update_step(
2458 step_id,
2459 StepUpdate {
2460 status: Some(StepStatus::Failed),
2461 error: Some(err.to_string()),
2462 completed_at: Some(Utc::now()),
2463 ..StepUpdate::default()
2464 },
2465 )
2466 .await
2467 {
2468 error!(
2469 step_id = %step_id,
2470 error = %store_err,
2471 "failed to persist step failure"
2472 );
2473 }
2474 }
2475
2476 pub fn store(&self) -> &Arc<dyn Store> {
2478 &self.store
2479 }
2480
2481 pub(crate) fn next_position(&mut self) -> u32 {
2483 let pos = self.position;
2484 self.position += 1;
2485 pos
2486 }
2487
2488 pub(crate) fn replay_steps(&self) -> &HashMap<u32, Step> {
2490 &self.replay_steps
2491 }
2492
2493 pub(crate) fn set_last_step_ids(&mut self, ids: Vec<Uuid>) {
2495 self.last_step_ids = ids;
2496 }
2497
2498 pub async fn payload(&self) -> Result<Value, EngineError> {
2506 let run = self
2507 .store
2508 .get_run(self.run_id)
2509 .await?
2510 .ok_or(EngineError::Store(
2511 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2512 ))?;
2513 Ok(run.payload)
2514 }
2515
2516 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2544 let payload = self.payload().await?;
2545 serde_json::from_value(payload).map_err(EngineError::Serialization)
2546 }
2547
2548 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2571 self.error_handlers.push(OnErrorHandler {
2572 name: name.to_string(),
2573 config: config.into(),
2574 });
2575 }
2576
2577 pub fn clear_error_handlers(&mut self) {
2596 self.error_handlers.clear();
2597 }
2598
2599 async fn fire_error_handlers(
2605 &mut self,
2606 failed_step_name: &str,
2607 error_msg: &str,
2608 duration_ms: u64,
2609 ) {
2610 let handlers = std::mem::take(&mut self.error_handlers);
2611 if handlers.is_empty() {
2612 return;
2613 }
2614
2615 let error_context = json!({
2616 "failed_step": failed_step_name,
2617 "error": error_msg,
2618 "duration_ms": duration_ms,
2619 });
2620
2621 for handler in handlers {
2622 let mut config = handler.config.clone();
2623 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2624
2625 let position = self.position;
2626 self.position += 1;
2627
2628 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2629 let step = match self
2630 .store
2631 .create_step(NewStep {
2632 run_id: self.run_id,
2633 trace_id,
2634 name: handler.name.clone(),
2635 kind: config.kind(),
2636 position,
2637 input: Some(error_context.clone()),
2638 is_error_handler: true,
2639 })
2640 .await
2641 {
2642 Ok(step) => step,
2643 Err(err) => {
2644 warn!(
2645 run_id = %self.run_id,
2646 handler = %handler.name,
2647 error = %err,
2648 "failed to create error handler step"
2649 );
2650 continue;
2651 }
2652 };
2653
2654 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2655 warn!(
2656 run_id = %self.run_id,
2657 handler = %handler.name,
2658 error = %err,
2659 "failed to start error handler step"
2660 );
2661 continue;
2662 }
2663
2664 let step_log_sender = self
2665 .log_sender
2666 .as_ref()
2667 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2668
2669 let start = Instant::now();
2670 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2671 let handler_duration = start.elapsed().as_millis() as u64;
2672 let completed_at = Utc::now();
2673
2674 match result {
2675 Ok(output) => {
2676 if let Err(store_err) = self
2677 .store
2678 .update_step(
2679 step.id,
2680 StepUpdate {
2681 status: Some(StepStatus::Completed),
2682 output: Some(output.output),
2683 duration_ms: Some(handler_duration),
2684 cost_usd: Some(output.cost_usd),
2685 completed_at: Some(completed_at),
2686 ..StepUpdate::default()
2687 },
2688 )
2689 .await
2690 {
2691 warn!(
2692 run_id = %self.run_id,
2693 handler = %handler.name,
2694 error = %store_err,
2695 "failed to persist error handler completion"
2696 );
2697 }
2698
2699 info!(
2700 run_id = %self.run_id,
2701 handler = %handler.name,
2702 duration_ms = handler_duration,
2703 "error handler completed"
2704 );
2705 }
2706 Err(err) => {
2707 if let Err(store_err) = self
2708 .store
2709 .update_step(
2710 step.id,
2711 StepUpdate {
2712 status: Some(StepStatus::Failed),
2713 error: Some(err.to_string()),
2714 duration_ms: Some(handler_duration),
2715 completed_at: Some(completed_at),
2716 ..StepUpdate::default()
2717 },
2718 )
2719 .await
2720 {
2721 warn!(
2722 run_id = %self.run_id,
2723 handler = %handler.name,
2724 error = %store_err,
2725 "failed to persist error handler failure"
2726 );
2727 }
2728
2729 warn!(
2730 run_id = %self.run_id,
2731 handler = %handler.name,
2732 error = %err,
2733 "error handler failed (original error preserved)"
2734 );
2735 }
2736 }
2737 }
2738 }
2739}
2740
2741fn inject_error_context(
2743 config: &mut StepConfig,
2744 failed_step: &str,
2745 error_msg: &str,
2746 duration_ms: u64,
2747) {
2748 match config {
2749 StepConfig::Shell(shell) => {
2750 shell
2751 .env
2752 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2753 shell
2754 .env
2755 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2756 shell.env.push((
2757 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2758 duration_ms.to_string(),
2759 ));
2760 }
2761 StepConfig::Agent(agent) => {
2762 agent.prompt = format!(
2763 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2764 failed_step, duration_ms, error_msg, agent.prompt
2765 );
2766 }
2767 StepConfig::Http(http) => {
2768 http.headers
2769 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2770 http.headers.push((
2771 "X-Ironflow-Error-Message".to_string(),
2772 error_msg.to_string(),
2773 ));
2774 }
2775 StepConfig::Workflow(_)
2776 | StepConfig::Approval(_)
2777 | StepConfig::Decision(_)
2778 | StepConfig::Delay(_) => {}
2779 }
2780}
2781
2782#[cfg(feature = "prometheus")]
2783fn record_retry_metric(kind: &str, outcome: &str) {
2784 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2785 use metrics::counter;
2786 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2787 .increment(1);
2788}
2789
2790#[cfg(not(feature = "prometheus"))]
2791fn record_retry_metric(_kind: &str, _outcome: &str) {}
2792
2793fn is_step_retryable(err: &EngineError) -> bool {
2797 use ironflow_core::error::{AgentError, OperationError};
2798
2799 match err {
2800 EngineError::Operation(op) => match op {
2801 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2802 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2803 OperationError::Deserialize { .. } => false,
2804 OperationError::Http {
2805 status: Some(code), ..
2806 } if (400..500).contains(code) && *code != 429 => false,
2807 _ => true,
2808 },
2809 _ => false,
2810 }
2811}
2812
2813fn allowed_failure_output(
2814 error_msg: &str,
2815 raw_response: Option<Value>,
2816 partial: Option<&StepPartialUsage>,
2817) -> StepOutput {
2818 StepOutput {
2819 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2820 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2821 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2822 input_tokens: partial.and_then(|p| p.input_tokens),
2823 output_tokens: partial.and_then(|p| p.output_tokens),
2824 model: None,
2825 debug_messages: None,
2826 }
2827}
2828
2829impl fmt::Debug for WorkflowContext {
2830 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2831 f.debug_struct("WorkflowContext")
2832 .field("run_id", &self.run_id)
2833 .field("position", &self.position)
2834 .field("total_cost_usd", &self.total_cost_usd)
2835 .field("inherited_cost_usd", &self.inherited_cost_usd)
2836 .field("max_cost_usd", &self.max_cost_usd)
2837 .finish_non_exhaustive()
2838 }
2839}
2840
2841fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2844 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2845 debug_messages,
2846 ..
2847 })) = err
2848 && !debug_messages.is_empty()
2849 {
2850 return serde_json::to_value(debug_messages).ok();
2851 }
2852 None
2853}
2854
2855struct StepPartialUsage {
2861 cost_usd: Option<Decimal>,
2862 duration_ms: Option<u64>,
2863 input_tokens: Option<u64>,
2864 output_tokens: Option<u64>,
2865}
2866
2867fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2873 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2874 raw_response: Some(text),
2875 ..
2876 })) = err
2877 {
2878 return Some(Value::String(text.clone()));
2879 }
2880 None
2881}
2882
2883fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2884 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2885 partial_usage,
2886 ..
2887 })) = err
2888 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2889 {
2890 return Some(StepPartialUsage {
2891 cost_usd: partial_usage
2892 .cost_usd
2893 .and_then(|c| Decimal::try_from(c).ok()),
2894 duration_ms: partial_usage.duration_ms,
2895 input_tokens: partial_usage.input_tokens,
2896 output_tokens: partial_usage.output_tokens,
2897 });
2898 }
2899 None
2900}
2901
2902#[cfg(test)]
2903mod tests {
2904 use super::*;
2905 use ironflow_core::providers::claude::ClaudeCodeProvider;
2906 use ironflow_core::providers::record_replay::RecordReplayProvider;
2907 use ironflow_store::memory::InMemoryStore;
2908 use ironflow_store::models::{Run, RunActor, RunFilter};
2909 use ironflow_store::store::RunStore;
2910 use serde_json::json;
2911 use std::sync::Arc;
2912 use std::sync::atomic::{AtomicBool, Ordering};
2913 use uuid::Uuid;
2914
2915 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2917 let inner = ClaudeCodeProvider::new();
2918 Arc::new(RecordReplayProvider::replay(
2919 inner,
2920 "/tmp/ironflow-fixtures",
2921 ))
2922 }
2923
2924 fn create_test_context() -> WorkflowContext {
2926 let store = Arc::new(InMemoryStore::new());
2927 let provider = create_test_provider();
2928 let run_id = Uuid::now_v7();
2929 WorkflowContext::new(run_id, "test".to_string(), store, provider)
2930 }
2931
2932 #[test]
2933 fn context_new_initializes_correctly() {
2934 let ctx = create_test_context();
2935 assert_eq!(ctx.position, 0);
2936 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2937 assert_eq!(ctx.total_duration_ms, 0);
2938 assert!(ctx.last_step_ids.is_empty());
2939 assert!(ctx.replay_steps.is_empty());
2940 assert!(ctx.log_sender.is_none());
2941 }
2942
2943 #[test]
2944 fn context_run_id_returns_correct_id() {
2945 let run_id = Uuid::now_v7();
2946 let store = Arc::new(InMemoryStore::new());
2947 let provider = create_test_provider();
2948 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
2949 assert_eq!(ctx.run_id(), run_id);
2950 }
2951
2952 #[test]
2953 fn context_total_cost_usd_initially_zero() {
2954 let ctx = create_test_context();
2955 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2956 }
2957
2958 #[test]
2959 fn context_total_duration_ms_initially_zero() {
2960 let ctx = create_test_context();
2961 assert_eq!(ctx.total_duration_ms(), 0);
2962 }
2963
2964 #[test]
2965 fn context_with_handler_resolver_creates_context_with_resolver() {
2966 let store = Arc::new(InMemoryStore::new());
2967 let provider = create_test_provider();
2968 let run_id = Uuid::now_v7();
2969
2970 let called = Arc::new(AtomicBool::new(false));
2971 let called_clone = called.clone();
2972
2973 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2974 called_clone.store(true, Ordering::SeqCst);
2975 None
2976 });
2977
2978 let ctx = WorkflowContext::with_handler_resolver(
2979 run_id,
2980 "test".to_string(),
2981 store,
2982 provider,
2983 resolver,
2984 );
2985
2986 assert_eq!(ctx.run_id(), run_id);
2987 assert!(ctx.handler_resolver.is_some());
2988 }
2989
2990 #[tokio::test]
2991 async fn context_set_log_sender_attaches_sender() {
2992 let mut ctx = create_test_context();
2993 let (sender, _receiver) = crate::log_sender::channel();
2994 ctx.set_log_sender(sender);
2995 assert!(ctx.log_sender.is_some());
2996 }
2997
2998 #[tokio::test]
2999 async fn context_skip_creates_skipped_step() {
3000 let store = Arc::new(InMemoryStore::new());
3001 let provider = create_test_provider();
3002
3003 store
3005 .create_run(NewRun {
3006 created_by: None,
3007 workflow_name: "test".to_string(),
3008 trigger: TriggerKind::Manual,
3009 payload: json!({}),
3010 max_retries: 0,
3011 handler_version: None,
3012 labels: Default::default(),
3013 scheduled_at: None,
3014 idempotency_key: None,
3015 max_cost_usd: None,
3016 })
3017 .await
3018 .expect("failed to create run")
3019 .into_run();
3020
3021 let runs = store
3023 .list_runs(RunFilter::default(), 1, 10)
3024 .await
3025 .expect("failed to list runs");
3026 let created_run_id = runs.items[0].id;
3027
3028 let mut ctx =
3029 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3030 let initial_position = ctx.position;
3031
3032 ctx.skip("skip-step", "condition not met")
3033 .await
3034 .expect("skip failed");
3035
3036 assert_eq!(ctx.position, initial_position + 1);
3037 assert!(!ctx.last_step_ids.is_empty());
3038
3039 let steps = store
3041 .list_steps(created_run_id)
3042 .await
3043 .expect("failed to list steps");
3044 assert_eq!(steps.len(), 1);
3045 assert_eq!(steps[0].status.state, StepStatus::Skipped);
3046 }
3047
3048 struct NoopSubWorkflow;
3051
3052 impl WorkflowHandler for NoopSubWorkflow {
3053 fn name(&self) -> &str {
3054 "noop-sub"
3055 }
3056
3057 fn execute<'a>(
3058 &'a self,
3059 _ctx: &'a mut WorkflowContext,
3060 ) -> crate::handler::HandlerFuture<'a> {
3061 Box::pin(async move { Ok(()) })
3062 }
3063 }
3064
3065 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
3068 let store = Arc::new(InMemoryStore::new());
3069 let provider = create_test_provider();
3070
3071 let parent = store
3072 .create_run(NewRun {
3073 workflow_name: "parent".to_string(),
3074 trigger: TriggerKind::Api,
3075 payload: json!({}),
3076 max_retries: 0,
3077 handler_version: None,
3078 labels: Default::default(),
3079 scheduled_at: None,
3080 created_by,
3081 idempotency_key: None,
3082 max_cost_usd: None,
3083 })
3084 .await
3085 .expect("failed to create parent run")
3086 .into_run();
3087
3088 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
3089 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
3090 _ => None,
3091 });
3092
3093 let mut ctx = WorkflowContext::with_handler_resolver(
3094 parent.id,
3095 "parent".to_string(),
3096 store.clone(),
3097 provider,
3098 resolver,
3099 );
3100 ctx.workflow(&NoopSubWorkflow, json!({}))
3101 .await
3102 .expect("sub-workflow failed");
3103
3104 let runs = store
3105 .list_runs(RunFilter::default(), 1, 10)
3106 .await
3107 .expect("failed to list runs");
3108 runs.items
3109 .into_iter()
3110 .find(|r| r.workflow_name == "noop-sub")
3111 .expect("child run was created")
3112 }
3113
3114 #[tokio::test]
3115 async fn child_run_inherits_the_parent_author() {
3116 let user_id = Uuid::now_v7();
3117 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
3118
3119 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
3120 }
3121
3122 #[tokio::test]
3123 async fn child_run_of_an_unattributed_parent_has_no_author() {
3124 let child = child_run_of_parent_authored_by(None).await;
3125
3126 assert!(child.created_by.is_none());
3127 }
3128
3129 #[tokio::test]
3130 async fn context_parallel_empty_steps_returns_empty_vec() {
3131 let mut ctx = create_test_context();
3132 let results = ctx
3133 .parallel(vec![], true)
3134 .await
3135 .expect("parallel should not fail on empty input");
3136 assert!(results.is_empty());
3137 }
3138
3139 #[tokio::test]
3140 async fn context_approval_first_execution_returns_error() {
3141 let store = Arc::new(InMemoryStore::new());
3142 let provider = create_test_provider();
3143
3144 store
3146 .create_run(NewRun {
3147 created_by: None,
3148 workflow_name: "test".to_string(),
3149 trigger: TriggerKind::Manual,
3150 payload: json!({}),
3151 max_retries: 0,
3152 handler_version: None,
3153 labels: Default::default(),
3154 scheduled_at: None,
3155 idempotency_key: None,
3156 max_cost_usd: None,
3157 })
3158 .await
3159 .expect("failed to create run")
3160 .into_run();
3161
3162 let runs = store
3164 .list_runs(RunFilter::default(), 1, 10)
3165 .await
3166 .expect("failed to list runs");
3167 let created_run_id = runs.items[0].id;
3168
3169 let mut ctx =
3170 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3171
3172 let result = ctx
3173 .approval(
3174 "approve-step",
3175 crate::config::ApprovalConfig::new("Continue?"),
3176 )
3177 .await;
3178
3179 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
3181
3182 assert_eq!(ctx.position, 1);
3184
3185 let steps = store
3187 .list_steps(created_run_id)
3188 .await
3189 .expect("failed to list steps");
3190 assert_eq!(steps.len(), 1);
3191 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3192 }
3193
3194 #[tokio::test]
3195 async fn context_approval_replay_returns_ok() {
3196 let store = Arc::new(InMemoryStore::new());
3197 let provider = create_test_provider();
3198
3199 store
3201 .create_run(NewRun {
3202 created_by: None,
3203 workflow_name: "test".to_string(),
3204 trigger: TriggerKind::Manual,
3205 payload: json!({}),
3206 max_retries: 0,
3207 handler_version: None,
3208 labels: Default::default(),
3209 scheduled_at: None,
3210 idempotency_key: None,
3211 max_cost_usd: None,
3212 })
3213 .await
3214 .expect("failed to create run")
3215 .into_run();
3216
3217 let runs = store
3219 .list_runs(RunFilter::default(), 1, 10)
3220 .await
3221 .expect("failed to list runs");
3222 let created_run_id = runs.items[0].id;
3223
3224 let step = store
3226 .create_step(NewStep {
3227 run_id: created_run_id,
3228 trace_id: step_trace_id(created_run_id, "approval", 0),
3229 name: "approval".to_string(),
3230 kind: StepKind::Approval,
3231 position: 0,
3232 input: None,
3233 is_error_handler: false,
3234 })
3235 .await
3236 .expect("failed to create step");
3237
3238 store
3240 .update_step(
3241 step.id,
3242 StepUpdate {
3243 status: Some(StepStatus::Running),
3244 started_at: Some(Utc::now()),
3245 ..StepUpdate::default()
3246 },
3247 )
3248 .await
3249 .expect("failed to update step to Running");
3250
3251 store
3252 .update_step(
3253 step.id,
3254 StepUpdate {
3255 status: Some(StepStatus::AwaitingApproval),
3256 ..StepUpdate::default()
3257 },
3258 )
3259 .await
3260 .expect("failed to update step to AwaitingApproval");
3261
3262 let mut ctx =
3264 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3265 ctx.load_replay_steps()
3266 .await
3267 .expect("failed to load replay steps");
3268
3269 let result = ctx
3271 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3272 .await;
3273
3274 assert!(result.is_ok());
3275
3276 let steps = store
3278 .list_steps(created_run_id)
3279 .await
3280 .expect("failed to list steps");
3281 assert_eq!(steps.len(), 1);
3282 assert_eq!(steps[0].status.state, StepStatus::Completed);
3283 }
3284
3285 #[tokio::test]
3286 async fn context_load_replay_steps_loads_completed_steps() {
3287 let store = Arc::new(InMemoryStore::new());
3288 let provider = create_test_provider();
3289
3290 store
3292 .create_run(NewRun {
3293 created_by: None,
3294 workflow_name: "test".to_string(),
3295 trigger: TriggerKind::Manual,
3296 payload: json!({}),
3297 max_retries: 0,
3298 handler_version: None,
3299 labels: Default::default(),
3300 scheduled_at: None,
3301 idempotency_key: None,
3302 max_cost_usd: None,
3303 })
3304 .await
3305 .expect("failed to create run")
3306 .into_run();
3307
3308 let runs = store
3310 .list_runs(RunFilter::default(), 1, 10)
3311 .await
3312 .expect("failed to list runs");
3313 let created_run_id = runs.items[0].id;
3314
3315 let completed_step = store
3317 .create_step(NewStep {
3318 run_id: created_run_id,
3319 trace_id: step_trace_id(created_run_id, "completed", 0),
3320 name: "completed".to_string(),
3321 kind: StepKind::Shell,
3322 position: 0,
3323 input: None,
3324 is_error_handler: false,
3325 })
3326 .await
3327 .expect("failed to create step");
3328
3329 store
3331 .update_step(
3332 completed_step.id,
3333 StepUpdate {
3334 status: Some(StepStatus::Running),
3335 started_at: Some(Utc::now()),
3336 ..StepUpdate::default()
3337 },
3338 )
3339 .await
3340 .expect("failed to update step to Running");
3341
3342 store
3343 .update_step(
3344 completed_step.id,
3345 StepUpdate {
3346 status: Some(StepStatus::Completed),
3347 completed_at: Some(Utc::now()),
3348 ..StepUpdate::default()
3349 },
3350 )
3351 .await
3352 .expect("failed to update step to Completed");
3353
3354 let _pending_step = store
3355 .create_step(NewStep {
3356 run_id: created_run_id,
3357 trace_id: step_trace_id(created_run_id, "pending", 1),
3358 name: "pending".to_string(),
3359 kind: StepKind::Shell,
3360 position: 1,
3361 input: None,
3362 is_error_handler: false,
3363 })
3364 .await
3365 .expect("failed to create step");
3366
3367 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3369 ctx.load_replay_steps()
3370 .await
3371 .expect("failed to load replay steps");
3372
3373 assert_eq!(ctx.replay_steps.len(), 1);
3375 assert!(ctx.replay_steps.contains_key(&0));
3376 assert!(!ctx.replay_steps.contains_key(&1));
3377 }
3378
3379 #[tokio::test]
3380 async fn context_payload_returns_run_payload() {
3381 let store = Arc::new(InMemoryStore::new());
3382 let provider = create_test_provider();
3383 let test_payload = json!({"key": "value", "number": 42});
3384
3385 store
3387 .create_run(NewRun {
3388 created_by: None,
3389 workflow_name: "test".to_string(),
3390 trigger: TriggerKind::Manual,
3391 payload: test_payload.clone(),
3392 max_retries: 0,
3393 handler_version: None,
3394 labels: Default::default(),
3395 scheduled_at: None,
3396 idempotency_key: None,
3397 max_cost_usd: None,
3398 })
3399 .await
3400 .expect("failed to create run")
3401 .into_run();
3402
3403 let runs = store
3405 .list_runs(RunFilter::default(), 1, 10)
3406 .await
3407 .expect("failed to list runs");
3408 let created_run_id = runs.items[0].id;
3409
3410 let ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3411 let payload = ctx.payload().await.expect("failed to get payload");
3412
3413 assert_eq!(payload, test_payload);
3414 }
3415
3416 #[tokio::test]
3417 async fn context_payload_returns_error_for_nonexistent_run() {
3418 let store = Arc::new(InMemoryStore::new());
3419 let provider = create_test_provider();
3420 let run_id = Uuid::now_v7();
3421
3422 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
3423 let result = ctx.payload().await;
3424
3425 assert!(result.is_err());
3426 }
3427
3428 #[tokio::test]
3429 async fn context_store_returns_reference() {
3430 let ctx = create_test_context();
3431 let _store = ctx.store();
3432 }
3434
3435 #[test]
3436 fn context_debug_formatting() {
3437 let ctx = create_test_context();
3438 let debug_str = format!("{:?}", ctx);
3439 assert!(debug_str.contains("WorkflowContext"));
3440 assert!(debug_str.contains("run_id"));
3441 }
3442
3443 #[tokio::test]
3444 async fn context_last_step_ids_tracks_executed_steps() {
3445 let store = Arc::new(InMemoryStore::new());
3446 let provider = create_test_provider();
3447
3448 store
3450 .create_run(NewRun {
3451 created_by: None,
3452 workflow_name: "test".to_string(),
3453 trigger: TriggerKind::Manual,
3454 payload: json!({}),
3455 max_retries: 0,
3456 handler_version: None,
3457 labels: Default::default(),
3458 scheduled_at: None,
3459 idempotency_key: None,
3460 max_cost_usd: None,
3461 })
3462 .await
3463 .expect("failed to create run")
3464 .into_run();
3465
3466 let runs = store
3468 .list_runs(RunFilter::default(), 1, 10)
3469 .await
3470 .expect("failed to list runs");
3471 let created_run_id = runs.items[0].id;
3472
3473 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3474 assert!(ctx.last_step_ids.is_empty());
3475
3476 ctx.skip("step1", "reason").await.expect("skip failed");
3477
3478 assert_eq!(ctx.last_step_ids.len(), 1);
3479
3480 ctx.skip("step2", "reason").await.expect("skip failed");
3481
3482 assert_eq!(ctx.last_step_ids.len(), 1);
3484 }
3485}