1use std::collections::HashMap;
27use std::fmt;
28use std::sync::Arc;
29use std::time::Instant;
30
31use chrono::{DateTime, TimeDelta, 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};
68use crate::notify::{
69 WorkflowAgentStepTokensUsedEvent, WorkflowApprovalRequiredEvent, WorkflowEvent,
70 WorkflowEventBus, WorkflowStepCompletedEvent, WorkflowStepFailedEvent,
71 WorkflowStepStartedEvent,
72};
73#[cfg(not(feature = "secret-store"))]
74use crate::operation::NoopSecretResolver;
75use crate::operation::{Operation, OperationContext, SecretResolver};
76#[cfg(feature = "secret-store")]
77use ironflow_store::workflow_secrets::ScopedSecretStore;
78
79pub(crate) type HandlerResolver =
81 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
82
83pub struct WorkflowContext {
102 run_id: Uuid,
103 workflow_name: String,
104 store: Arc<dyn Store>,
105 provider: Arc<dyn AgentProvider>,
106 decision_provider: Option<Arc<dyn DecisionProvider>>,
110 handler_resolver: Option<HandlerResolver>,
111 position: u32,
112 last_step_ids: Vec<Uuid>,
114 total_cost_usd: Decimal,
116 total_duration_ms: u64,
118 max_cost_usd: Option<Decimal>,
120 inherited_cost_usd: Decimal,
123 replay_steps: HashMap<u32, Step>,
126 granted_approvals: HashMap<u32, u32>,
130 attempt: u32,
132 carried_duration_ms: u64,
135 log_sender: Option<LogSender>,
137 artifact_sink: Option<Arc<dyn ArtifactSink>>,
141 has_allowed_failure: bool,
143 error_handlers: Vec<OnErrorHandler>,
145 guard_state: Option<SharedGuardState>,
147 guard_config: Option<WorkflowGuardConfig>,
149 step_results: Vec<StepResult>,
151 event_bus: Option<WorkflowEventBus>,
153 trace_context: WorkflowTraceContext,
155 operation_ctx: Option<OperationContext>,
157}
158
159struct OnErrorHandler {
161 name: String,
162 config: StepConfig,
163}
164
165impl WorkflowContext {
166 pub fn new(
171 run_id: Uuid,
172 workflow_name: String,
173 store: Arc<dyn Store>,
174 provider: Arc<dyn AgentProvider>,
175 ) -> Self {
176 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
177 Self {
178 run_id,
179 workflow_name,
180 store,
181 provider,
182 decision_provider: None,
183 handler_resolver: None,
184 position: 0,
185 last_step_ids: Vec::new(),
186 total_cost_usd: Decimal::ZERO,
187 total_duration_ms: 0,
188 max_cost_usd: None,
189 inherited_cost_usd: Decimal::ZERO,
190 replay_steps: HashMap::new(),
191 granted_approvals: HashMap::new(),
192 attempt: 1,
193 carried_duration_ms: 0,
194 log_sender: None,
195 artifact_sink: None,
196 has_allowed_failure: false,
197 error_handlers: Vec::new(),
198 guard_state: None,
199 guard_config: None,
200 step_results: Vec::new(),
201 event_bus: None,
202 trace_context,
203 operation_ctx: None,
204 }
205 }
206
207 pub(crate) fn with_handler_resolver(
212 run_id: Uuid,
213 workflow_name: String,
214 store: Arc<dyn Store>,
215 provider: Arc<dyn AgentProvider>,
216 resolver: HandlerResolver,
217 ) -> Self {
218 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
219 Self {
220 run_id,
221 workflow_name,
222 store,
223 provider,
224 decision_provider: None,
225 handler_resolver: Some(resolver),
226 position: 0,
227 last_step_ids: Vec::new(),
228 total_cost_usd: Decimal::ZERO,
229 total_duration_ms: 0,
230 max_cost_usd: None,
231 inherited_cost_usd: Decimal::ZERO,
232 replay_steps: HashMap::new(),
233 granted_approvals: HashMap::new(),
234 attempt: 1,
235 carried_duration_ms: 0,
236 log_sender: None,
237 artifact_sink: None,
238 has_allowed_failure: false,
239 error_handlers: Vec::new(),
240 guard_state: None,
241 guard_config: None,
242 step_results: Vec::new(),
243 event_bus: None,
244 trace_context,
245 operation_ctx: None,
246 }
247 }
248
249 pub fn set_log_sender(&mut self, sender: LogSender) {
251 self.log_sender = Some(sender);
252 }
253
254 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
274 self.artifact_sink = Some(sink);
275 }
276
277 pub fn trace_context(&self) -> &WorkflowTraceContext {
283 &self.trace_context
284 }
285
286 pub fn set_guard(&mut self, config: WorkflowGuardConfig, state: SharedGuardState) {
303 self.guard_config = Some(config);
304 self.guard_state = Some(state);
305 }
306
307 pub fn guard_config(&self) -> Option<&WorkflowGuardConfig> {
309 self.guard_config.as_ref()
310 }
311
312 pub fn set_event_bus(&mut self, bus: WorkflowEventBus) {
317 self.event_bus = Some(bus);
318 }
319
320 pub fn set_decision_provider(&mut self, provider: Arc<dyn DecisionProvider>) {
325 self.decision_provider = Some(provider);
326 }
327
328 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
330 self.artifact_sink.as_ref().ok_or_else(|| {
331 EngineError::ArtifactsUnavailable(
332 "no artifact storage is attached to this run".to_string(),
333 )
334 })
335 }
336
337 pub async fn put_artifact(
367 &self,
368 step_id: Uuid,
369 name: &str,
370 content_type: Option<&str>,
371 content: Vec<u8>,
372 ) -> Result<Artifact, EngineError> {
373 let sink = self.artifact_sink()?;
374 sink.put(
375 ArtifactUpload {
376 run_id: self.run_id,
377 step_id,
378 name: name.to_string(),
379 content_type: content_type
380 .map(str::to_string)
381 .unwrap_or_else(|| guess_content_type(name)),
382 },
383 stream_from_bytes(content),
384 )
385 .await
386 }
387
388 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
413 let sink = self.artifact_sink()?;
414
415 let artifact = self
416 .store
417 .find_artifact_for_input(ArtifactLookup {
418 run_id: self.run_id,
419 attempt: self.attempt,
420 before_position: self.position,
421 step_name: step.to_string(),
422 name: name.to_string(),
423 })
424 .await?
425 .ok_or_else(|| EngineError::ArtifactNotFound {
426 step: step.to_string(),
427 name: name.to_string(),
428 })?;
429
430 let mut content = sink.get(&artifact).await?;
431 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
432 while let Some(chunk) = content.next().await {
433 let chunk = chunk?;
434 buffer.extend_from_slice(chunk.as_ref());
435 }
436
437 Ok(buffer)
438 }
439
440 async fn prepare_step_inputs(
445 &self,
446 config: &StepConfig,
447 position: u32,
448 ) -> Result<(), EngineError> {
449 let StepConfig::Shell(shell) = config else {
450 return Ok(());
451 };
452 if shell.inputs.is_empty() {
453 return Ok(());
454 }
455
456 materialize_inputs(
457 self.artifact_sink()?,
458 &self.store,
459 shell,
460 StepLocation {
461 run_id: self.run_id,
462 attempt: self.attempt,
463 position,
464 },
465 )
466 .await
467 }
468
469 async fn store_step_outputs(
474 &self,
475 config: &StepConfig,
476 step_id: Uuid,
477 step_name: &str,
478 step_succeeded: bool,
479 ) -> Result<(), EngineError> {
480 let StepConfig::Shell(shell) = config else {
481 return Ok(());
482 };
483 if shell.outputs.is_empty() {
484 return Ok(());
485 }
486
487 let sink = match self.artifact_sink() {
488 Ok(sink) => sink,
489 Err(err) if step_succeeded => return Err(err),
490 Err(err) => {
491 warn!(
492 run_id = %self.run_id,
493 step = %step_name,
494 error = %err,
495 "cannot collect outputs of a failed step"
496 );
497 return Ok(());
498 }
499 };
500
501 let collected =
502 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
503
504 match collected {
505 Ok(()) => Ok(()),
506 Err(err) if step_succeeded => Err(err),
507 Err(err) => {
508 warn!(
509 run_id = %self.run_id,
510 step = %step_name,
511 error = %err,
512 "failed to collect outputs of a failed step"
513 );
514 Ok(())
515 }
516 }
517 }
518
519 pub(crate) fn carry_over_run_totals(
526 &mut self,
527 attempt: u32,
528 cost_usd: Decimal,
529 duration_ms: u64,
530 ) {
531 self.attempt = attempt;
532 self.total_cost_usd = cost_usd;
533 self.carried_duration_ms = duration_ms;
534 }
535
536 pub(crate) fn carried_duration_ms(&self) -> u64 {
538 self.carried_duration_ms
539 }
540
541 pub fn attempt(&self) -> u32 {
543 self.attempt
544 }
545
546 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
562 self.max_cost_usd = cap;
563 }
564
565 pub fn max_cost_usd(&self) -> Option<Decimal> {
567 self.max_cost_usd
568 }
569
570 pub fn charged_cost_usd(&self) -> Decimal {
575 self.inherited_cost_usd + self.total_cost_usd
576 }
577
578 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
589 let Some(limit) = self.max_cost_usd else {
590 return Ok(());
591 };
592
593 let spent = self.charged_cost_usd();
594 if spent + step_budget <= limit {
595 return Ok(());
596 }
597
598 error!(
599 run_id = %self.run_id,
600 limit_usd = %limit,
601 spent_usd = %spent,
602 step_budget_usd = %step_budget,
603 "run cost cap reached, refusing agent step"
604 );
605
606 Err(EngineError::RunBudgetExceeded {
607 run_id: self.run_id,
608 limit_usd: limit,
609 spent_usd: spent,
610 step_budget_usd: step_budget,
611 })
612 }
613
614 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
626 let steps = self.store.list_steps(self.run_id).await?;
627 for step in steps {
628 let dominated = matches!(
629 step.status.state,
630 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
631 );
632 if !dominated {
633 continue;
634 }
635
636 if step.attempt == self.attempt {
637 self.replay_steps.insert(step.position, step);
638 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
639 {
640 self.granted_approvals.insert(step.position, step.attempt);
641 }
642 }
643 Ok(())
644 }
645
646 pub fn run_id(&self) -> Uuid {
648 self.run_id
649 }
650
651 pub fn workflow_name(&self) -> &str {
653 &self.workflow_name
654 }
655
656 pub fn total_cost_usd(&self) -> Decimal {
658 self.total_cost_usd
659 }
660
661 pub fn has_allowed_failure(&self) -> bool {
663 self.has_allowed_failure
664 }
665
666 pub fn total_duration_ms(&self) -> u64 {
668 self.total_duration_ms
669 }
670
671 pub fn step_results(&self) -> &[StepResult] {
673 &self.step_results
674 }
675
676 #[cfg(feature = "secret-store")]
698 pub fn secrets(&self) -> ironflow_store::workflow_secrets::ScopedSecretStore {
699 let workflow_uuid = Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
700 ironflow_store::workflow_secrets::ScopedSecretStore::for_workflow(
701 workflow_uuid,
702 self.store.clone(),
703 )
704 }
705
706 fn ensure_operation_ctx(&mut self) -> &OperationContext {
707 self.operation_ctx.get_or_insert_with(|| {
708 #[cfg(feature = "secret-store")]
709 let secrets: Arc<dyn SecretResolver> = {
710 let workflow_uuid =
711 Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
712 Arc::new(ScopedSecretStore::for_workflow(
713 workflow_uuid,
714 self.store.clone(),
715 ))
716 };
717 #[cfg(not(feature = "secret-store"))]
718 let secrets: Arc<dyn SecretResolver> = Arc::new(NoopSecretResolver);
719
720 OperationContext::new(secrets)
721 })
722 }
723
724 async fn persist_progress(&self) {
730 if let Err(err) = self
731 .store
732 .update_run(
733 self.run_id,
734 RunUpdate {
735 cost_usd: Some(self.total_cost_usd),
736 duration_ms: Some(self.total_duration_ms),
737 ..RunUpdate::default()
738 },
739 )
740 .await
741 {
742 warn!(
743 run_id = %self.run_id,
744 error = %err,
745 "failed to persist run progress snapshot"
746 );
747 }
748 }
749
750 pub async fn parallel(
787 &mut self,
788 steps: Vec<(&str, StepConfig)>,
789 fail_fast: bool,
790 ) -> Result<Vec<ParallelStepResult>, EngineError> {
791 if steps.is_empty() {
792 return Ok(Vec::new());
793 }
794
795 self.check_guard_timeout()?;
797
798 let wave_budget: Decimal = steps
801 .iter()
802 .filter_map(|(_, config)| match config {
803 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
804 _ => None,
805 })
806 .map(step_budget_usd)
807 .sum();
808 self.check_run_budget(wave_budget)?;
809
810 let wave_position = self.position;
811 self.position += 1;
812
813 let now = Utc::now();
814 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
815 Vec::with_capacity(steps.len());
816
817 for (name, config) in &steps {
818 let kind = config.kind();
819 let trace_id = step_trace_id(self.run_id, name, wave_position);
820 let step = self
821 .store
822 .create_step(NewStep {
823 run_id: self.run_id,
824 trace_id,
825 name: name.to_string(),
826 kind,
827 position: wave_position,
828 input: Some(serde_json::to_value(config)?),
829 is_error_handler: false,
830 })
831 .await?;
832
833 self.start_step(step.id, now).await?;
834
835 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
838 self.fail_step(step.id, &err).await;
839 if !config.allow_failure() {
840 return Err(err);
841 }
842 self.has_allowed_failure = true;
843 info!(
844 run_id = %self.run_id,
845 step = %name,
846 error = %err,
847 "parallel step input preparation failed but allow_failure is set, skipping"
848 );
849 continue;
850 }
851
852 let mut config_with_trace = config.clone();
853 let step_trace = self.trace_context.child();
854 match config_with_trace {
855 StepConfig::Agent(ref mut agent_config) => {
856 agent_config.trace_context = Some(step_trace);
857 }
858 StepConfig::Http(ref mut http_config) => {
859 http_config.trace_context = Some(step_trace);
860 }
861 _ => {}
862 }
863 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
864 }
865
866 let mut join_set = JoinSet::new();
867 let mut task_index: HashMap<Id, usize> = HashMap::new();
868 let parallel_timeout = self.guard_remaining_timeout();
869 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
870 let provider = self.provider.clone();
871 let config = config.clone();
872 let step_log_sender = self
873 .log_sender
874 .as_ref()
875 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
876 let handle = join_set.spawn(async move {
877 let result = match parallel_timeout {
878 Some(dur) => {
879 match tokio::time::timeout(
880 dur,
881 execute_step_config(&config, &provider, step_log_sender),
882 )
883 .await
884 {
885 Ok(r) => r,
886 Err(_elapsed) => {
887 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
888 elapsed_secs: 0,
889 max: 0,
890 }))
891 }
892 }
893 }
894 None => execute_step_config(&config, &provider, step_log_sender).await,
895 };
896 (idx, result)
897 });
898 task_index.insert(handle.id(), idx);
899 }
900
901 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
903 vec![None; step_records.len()];
904 let mut first_error: Option<EngineError> = None;
905
906 while let Some(join_result) = join_set.join_next().await {
907 let (idx, step_result) = match join_result {
908 Ok(r) => r,
909 Err(e) => {
910 let error_msg = format!("join error: {e}");
911 if let Some(&idx) = task_index.get(&e.id()) {
912 let (step_id, _, step_name, _) = &step_records[idx];
913 let completed_at = Utc::now();
914 error!(
915 run_id = %self.run_id,
916 step = %step_name,
917 error = %error_msg,
918 "parallel step panicked or was cancelled"
919 );
920 if let Err(store_err) = self
921 .store
922 .update_step(
923 *step_id,
924 StepUpdate {
925 status: Some(StepStatus::Failed),
926 error: Some(error_msg.clone()),
927 completed_at: Some(completed_at),
928 ..StepUpdate::default()
929 },
930 )
931 .await
932 {
933 error!(
934 run_id = %self.run_id,
935 step_id = %step_id,
936 error = %store_err,
937 "failed to persist JoinError for step"
938 );
939 }
940 indexed_results[idx] = Some(Err(error_msg.clone()));
941 }
942 if first_error.is_none() {
943 first_error = Some(EngineError::StepConfig(error_msg));
944 }
945 if fail_fast {
946 join_set.abort_all();
947 }
948 continue;
949 }
950 };
951
952 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
953 let completed_at = Utc::now();
954
955 if let Err(err) = self
956 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
957 .await
958 {
959 self.fail_step(*step_id, &err).await;
960 indexed_results[idx] = Some(Err(err.to_string()));
961 if first_error.is_none() {
962 first_error = Some(err);
963 }
964 if fail_fast {
965 join_set.abort_all();
966 }
967 continue;
968 }
969
970 match step_result {
971 Ok(output) => {
972 self.total_cost_usd += output.cost_usd;
973 self.total_duration_ms += output.duration_ms;
974
975 if matches!(step_config, StepConfig::Agent(_)) {
977 let tokens = output
978 .input_tokens
979 .unwrap_or(0)
980 .saturating_add(output.output_tokens.unwrap_or(0));
981 if tokens > 0
982 && let Err(guard_err) = self.guard_record_tokens(tokens)
983 {
984 if first_error.is_none() {
985 first_error = Some(guard_err);
986 }
987 if fail_fast {
988 join_set.abort_all();
989 }
990 }
991 }
992
993 let debug_messages_json = output.debug_messages_json();
994
995 self.store
996 .update_step(
997 *step_id,
998 StepUpdate {
999 status: Some(StepStatus::Completed),
1000 output: Some(output.output.clone()),
1001 duration_ms: Some(output.duration_ms),
1002 cost_usd: Some(output.cost_usd),
1003 input_tokens: output.input_tokens,
1004 output_tokens: output.output_tokens,
1005 completed_at: Some(completed_at),
1006 debug_messages: debug_messages_json,
1007 ..StepUpdate::default()
1008 },
1009 )
1010 .await?;
1011
1012 self.step_results.push(StepResult::from_success(
1013 *step_trace,
1014 step_name,
1015 &output,
1016 ));
1017
1018 if let Some(ref bus) = self.event_bus
1019 && matches!(step_config, StepConfig::Agent(_))
1020 {
1021 let tokens = output
1022 .input_tokens
1023 .unwrap_or(0)
1024 .saturating_add(output.output_tokens.unwrap_or(0));
1025 bus.publish(
1026 self.run_id,
1027 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
1028 step_name: step_name.clone(),
1029 tokens,
1030 cost_usd: output.cost_usd,
1031 }),
1032 );
1033 }
1034
1035 info!(
1036 run_id = %self.run_id,
1037 step = %step_name,
1038 trace_id = %step_trace,
1039 duration_ms = output.duration_ms,
1040 "parallel step completed"
1041 );
1042
1043 indexed_results[idx] = Some(Ok(output));
1044 }
1045 Err(err) => {
1046 let err_msg = err.to_string();
1047 let debug_messages_json = extract_debug_messages_from_error(&err);
1048 let partial = extract_partial_usage_from_error(&err);
1049 let raw_response_output = extract_raw_response_from_error(&err);
1050
1051 if let Some(ref usage) = partial {
1052 if let Some(cost) = usage.cost_usd {
1053 self.total_cost_usd += cost;
1054 }
1055 if let Some(dur) = usage.duration_ms {
1056 self.total_duration_ms += dur;
1057 }
1058 }
1059
1060 if let Err(store_err) = self
1061 .store
1062 .update_step(
1063 *step_id,
1064 StepUpdate {
1065 status: Some(StepStatus::Failed),
1066 error: Some(err_msg.clone()),
1067 output: raw_response_output.clone(),
1068 completed_at: Some(completed_at),
1069 debug_messages: debug_messages_json,
1070 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1071 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1072 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1073 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1074 ..StepUpdate::default()
1075 },
1076 )
1077 .await
1078 {
1079 tracing::error!(
1080 step_id = %step_id,
1081 error = %store_err,
1082 "failed to persist parallel step failure"
1083 );
1084 }
1085
1086 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1087 let err_cost = partial
1088 .as_ref()
1089 .and_then(|p| p.cost_usd)
1090 .unwrap_or(Decimal::ZERO);
1091 self.step_results.push(StepResult::from_failure(
1092 *step_trace,
1093 step_name,
1094 &err_msg,
1095 err_duration,
1096 err_cost,
1097 ));
1098
1099 if step_config.allow_failure() {
1100 self.has_allowed_failure = true;
1101 info!(
1102 run_id = %self.run_id,
1103 step = %step_name,
1104 error = %err_msg,
1105 "parallel step failed but allow_failure is set, continuing"
1106 );
1107 indexed_results[idx] = Some(Ok(allowed_failure_output(
1108 &err_msg,
1109 raw_response_output,
1110 partial.as_ref(),
1111 )));
1112 } else {
1113 indexed_results[idx] = Some(Err(err_msg.clone()));
1114
1115 if first_error.is_none() {
1116 first_error = Some(err);
1117 }
1118
1119 if fail_fast {
1120 join_set.abort_all();
1121 }
1122 }
1123 }
1124 }
1125 }
1126
1127 if let Some(err) = first_error {
1128 return Err(err);
1129 }
1130
1131 self.persist_progress().await;
1132
1133 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
1134
1135 let results: Vec<ParallelStepResult> = step_records
1137 .iter()
1138 .enumerate()
1139 .map(|(idx, (step_id, _trace_id, name, _))| {
1140 let output = match indexed_results[idx].take() {
1141 Some(Ok(o)) => o,
1142 _ => unreachable!("all steps succeeded if no error returned"),
1143 };
1144 ParallelStepResult {
1145 name: name.clone(),
1146 output,
1147 step_id: *step_id,
1148 }
1149 })
1150 .collect();
1151
1152 Ok(results)
1153 }
1154
1155 pub async fn shell(
1178 &mut self,
1179 name: &str,
1180 config: ShellConfig,
1181 ) -> Result<StepOutput, EngineError> {
1182 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1183 .await
1184 }
1185
1186 pub async fn http(
1206 &mut self,
1207 name: &str,
1208 config: HttpConfig,
1209 ) -> Result<StepOutput, EngineError> {
1210 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1211 .await
1212 }
1213
1214 pub async fn agent(
1234 &mut self,
1235 name: &str,
1236 config: impl Into<AgentStepConfig>,
1237 ) -> Result<StepOutput, EngineError> {
1238 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1239 .await
1240 }
1241
1242 pub async fn approval(
1280 &mut self,
1281 name: &str,
1282 config: ApprovalConfig,
1283 ) -> Result<(), EngineError> {
1284 let position = self.position;
1285 self.position += 1;
1286
1287 if let Some(existing) = self.replay_steps.get(&position)
1290 && existing.kind == StepKind::Approval
1291 {
1292 if existing.status.state == StepStatus::AwaitingApproval {
1293 self.store
1294 .update_step(
1295 existing.id,
1296 StepUpdate {
1297 status: Some(StepStatus::Completed),
1298 completed_at: Some(Utc::now()),
1299 clear_approval_deadline: true,
1301 ..StepUpdate::default()
1302 },
1303 )
1304 .await?;
1305 }
1306
1307 self.last_step_ids = vec![existing.id];
1308 info!(
1309 run_id = %self.run_id,
1310 step = %name,
1311 position,
1312 "approval step replayed (approved)"
1313 );
1314 return Ok(());
1315 }
1316
1317 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1321 let trace_id = step_trace_id(self.run_id, name, position);
1322 let step = self
1323 .store
1324 .create_step(NewStep {
1325 run_id: self.run_id,
1326 trace_id,
1327 name: name.to_string(),
1328 kind: StepKind::Approval,
1329 position,
1330 input: Some(serde_json::to_value(&config)?),
1331 is_error_handler: false,
1332 })
1333 .await?;
1334
1335 let now = Utc::now();
1336 self.start_step(step.id, now).await?;
1337 self.store
1338 .update_step(
1339 step.id,
1340 StepUpdate {
1341 status: Some(StepStatus::Completed),
1342 output: Some(json!({"approved_in_attempt": granted_in})),
1343 completed_at: Some(now),
1344 ..StepUpdate::default()
1345 },
1346 )
1347 .await?;
1348
1349 self.last_step_ids = vec![step.id];
1350 info!(
1351 run_id = %self.run_id,
1352 step = %name,
1353 position,
1354 granted_in_attempt = granted_in,
1355 attempt = self.attempt,
1356 "approval carried over from a previous attempt"
1357 );
1358 return Ok(());
1359 }
1360
1361 let trace_id = step_trace_id(self.run_id, name, position);
1363 let step = self
1364 .store
1365 .create_step(NewStep {
1366 run_id: self.run_id,
1367 trace_id,
1368 name: name.to_string(),
1369 kind: StepKind::Approval,
1370 position,
1371 input: Some(serde_json::to_value(&config)?),
1372 is_error_handler: false,
1373 })
1374 .await?;
1375
1376 self.start_step(step.id, Utc::now()).await?;
1377
1378 let deadline_at = config
1382 .effective_deadline_secs()
1383 .map(|secs| Utc::now() + TimeDelta::seconds(secs as i64));
1384
1385 self.store
1386 .update_step(
1387 step.id,
1388 StepUpdate {
1389 status: Some(StepStatus::AwaitingApproval),
1390 approval_deadline_at: deadline_at,
1391 approval_stage: Some(0),
1392 approval_assignee: config.assignee().cloned(),
1393 ..StepUpdate::default()
1394 },
1395 )
1396 .await?;
1397
1398 self.last_step_ids = vec![step.id];
1399
1400 if let Some(ref bus) = self.event_bus {
1401 bus.publish(
1402 self.run_id,
1403 WorkflowEvent::ApprovalRequired(WorkflowApprovalRequiredEvent {
1404 step_name: name.to_string(),
1405 step_index: position,
1406 approval_id: step.id,
1407 }),
1408 );
1409 }
1410
1411 Err(EngineError::ApprovalRequired {
1412 run_id: self.run_id,
1413 step_id: step.id,
1414 message: config.message().to_string(),
1415 })
1416 }
1417
1418 pub async fn decision(
1431 &mut self,
1432 name: &str,
1433 config: DecisionConfig,
1434 ) -> Result<DecisionOutput, EngineError> {
1435 if let Some(output) = self.decision_replay(name, &config).await? {
1436 return Ok(output);
1437 }
1438 self.decision_execute(name, config).await
1439 }
1440
1441 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1470 let position = self.position;
1471 self.position += 1;
1472
1473 let trace_id = step_trace_id(self.run_id, name, position);
1474 let step = self
1475 .store
1476 .create_step(NewStep {
1477 run_id: self.run_id,
1478 trace_id,
1479 name: name.to_string(),
1480 kind: StepKind::Custom("skip".to_string()),
1481 position,
1482 input: None,
1483 is_error_handler: false,
1484 })
1485 .await?;
1486
1487 if !self.last_step_ids.is_empty() {
1488 let deps: Vec<NewStepDependency> = self
1489 .last_step_ids
1490 .iter()
1491 .map(|&depends_on| NewStepDependency {
1492 step_id: step.id,
1493 depends_on,
1494 })
1495 .collect();
1496 self.store.create_step_dependencies(deps).await?;
1497 }
1498
1499 let now = Utc::now();
1500 self.store
1501 .update_step(
1502 step.id,
1503 StepUpdate {
1504 status: Some(StepStatus::Skipped),
1505 output: Some(serde_json::json!({"reason": reason})),
1506 completed_at: Some(now),
1507 ..StepUpdate::default()
1508 },
1509 )
1510 .await?;
1511
1512 self.last_step_ids = vec![step.id];
1513
1514 info!(
1515 run_id = %self.run_id,
1516 step = %name,
1517 reason,
1518 "step skipped"
1519 );
1520
1521 Ok(())
1522 }
1523
1524 pub async fn operation(
1563 &mut self,
1564 name: &str,
1565 op: &dyn Operation,
1566 ) -> Result<StepOutput, EngineError> {
1567 let kind = StepKind::Custom(op.kind().to_string());
1568 let position = self.position;
1569 self.position += 1;
1570
1571 let trace_id = step_trace_id(self.run_id, name, position);
1572 let step = self
1573 .store
1574 .create_step(NewStep {
1575 run_id: self.run_id,
1576 trace_id,
1577 name: name.to_string(),
1578 kind,
1579 position,
1580 input: op.input(),
1581 is_error_handler: false,
1582 })
1583 .await?;
1584
1585 self.start_step(step.id, Utc::now()).await?;
1586
1587 let start = Instant::now();
1588
1589 let op_ctx = self.ensure_operation_ctx();
1590
1591 match op.execute(op_ctx).await {
1592 Ok(output_value) => {
1593 let duration_ms = start.elapsed().as_millis() as u64;
1594 self.total_duration_ms += duration_ms;
1595
1596 let completed_at = Utc::now();
1597 self.store
1598 .update_step(
1599 step.id,
1600 StepUpdate {
1601 status: Some(StepStatus::Completed),
1602 output: Some(output_value.clone()),
1603 duration_ms: Some(duration_ms),
1604 cost_usd: Some(Decimal::ZERO),
1605 completed_at: Some(completed_at),
1606 ..StepUpdate::default()
1607 },
1608 )
1609 .await?;
1610
1611 info!(
1612 run_id = %self.run_id,
1613 step = %name,
1614 kind = op.kind(),
1615 duration_ms,
1616 "operation step completed"
1617 );
1618
1619 self.last_step_ids = vec![step.id];
1620
1621 Ok(StepOutput {
1622 output: output_value,
1623 duration_ms,
1624 cost_usd: Decimal::ZERO,
1625 input_tokens: None,
1626 output_tokens: None,
1627 model: None,
1628 debug_messages: None,
1629 })
1630 }
1631 Err(err) => {
1632 let completed_at = Utc::now();
1633 let engine_err = EngineError::Operation(err);
1634 if let Err(store_err) = self
1635 .store
1636 .update_step(
1637 step.id,
1638 StepUpdate {
1639 status: Some(StepStatus::Failed),
1640 error: Some(engine_err.to_string()),
1641 completed_at: Some(completed_at),
1642 ..StepUpdate::default()
1643 },
1644 )
1645 .await
1646 {
1647 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1648 }
1649
1650 Err(engine_err)
1651 }
1652 }
1653 }
1654
1655 pub async fn workflow(
1682 &mut self,
1683 handler: &dyn WorkflowHandler,
1684 payload: Value,
1685 ) -> Result<StepOutput, EngineError> {
1686 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1688 let state = guard_state
1689 .lock()
1690 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1691 state.check(guard_config, handler.name())?;
1692 }
1693
1694 let config = WorkflowStepConfig::new(handler.name(), payload);
1695 let position = self.position;
1696 self.position += 1;
1697
1698 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1699 let step = self
1700 .store
1701 .create_step(NewStep {
1702 run_id: self.run_id,
1703 trace_id,
1704 name: config.workflow_name.clone(),
1705 kind: StepKind::Workflow,
1706 position,
1707 input: Some(serde_json::to_value(&config)?),
1708 is_error_handler: false,
1709 })
1710 .await?;
1711
1712 self.start_step(step.id, Utc::now()).await?;
1713
1714 if let Some(guard_state) = &self.guard_state {
1716 let mut state = guard_state
1717 .lock()
1718 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1719 state.record_invocation(handler.name());
1720 }
1721
1722 match self.execute_child_workflow(&config).await {
1723 Ok((output, child_had_allowed_failure)) => {
1724 self.total_cost_usd += output.cost_usd;
1725 self.total_duration_ms += output.duration_ms;
1726 if child_had_allowed_failure {
1727 self.has_allowed_failure = true;
1728 }
1729
1730 let completed_at = Utc::now();
1731 self.store
1732 .update_step(
1733 step.id,
1734 StepUpdate {
1735 status: Some(StepStatus::Completed),
1736 output: Some(output.output.clone()),
1737 duration_ms: Some(output.duration_ms),
1738 cost_usd: Some(output.cost_usd),
1739 completed_at: Some(completed_at),
1740 ..StepUpdate::default()
1741 },
1742 )
1743 .await?;
1744
1745 info!(
1746 run_id = %self.run_id,
1747 child_workflow = %config.workflow_name,
1748 duration_ms = output.duration_ms,
1749 "workflow step completed"
1750 );
1751
1752 self.last_step_ids = vec![step.id];
1753
1754 self.guard_record_return();
1755 Ok(output)
1756 }
1757 Err(err) => {
1758 let completed_at = Utc::now();
1759 if let Err(store_err) = self
1760 .store
1761 .update_step(
1762 step.id,
1763 StepUpdate {
1764 status: Some(StepStatus::Failed),
1765 error: Some(err.to_string()),
1766 completed_at: Some(completed_at),
1767 ..StepUpdate::default()
1768 },
1769 )
1770 .await
1771 {
1772 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1773 }
1774
1775 self.guard_record_return();
1776 Err(err)
1777 }
1778 }
1779 }
1780
1781 fn guard_record_return(&self) {
1786 if let Some(guard_state) = &self.guard_state {
1787 match guard_state.lock() {
1788 Ok(mut state) => state.record_return(),
1789 Err(_) => {
1790 error!(
1791 run_id = %self.run_id,
1792 "guard state mutex poisoned in record_return"
1793 );
1794 }
1795 }
1796 }
1797 }
1798
1799 async fn execute_with_guard_timeout(
1803 &self,
1804 config: &StepConfig,
1805 step_log_sender: Option<StepLogSender>,
1806 ) -> Result<StepOutput, EngineError> {
1807 let remaining = self.guard_remaining_timeout();
1808 match remaining {
1809 Some(dur) => {
1810 use tokio::time::timeout;
1811 match timeout(
1812 dur,
1813 execute_step_config(config, &self.provider, step_log_sender),
1814 )
1815 .await
1816 {
1817 Ok(result) => result,
1818 Err(_elapsed) => {
1819 let config_secs = self
1820 .guard_config
1821 .as_ref()
1822 .map_or(0, |c| c.workflow_timeout_secs);
1823 Err(WorkflowRejection::WorkflowTimeout {
1824 elapsed_secs: config_secs,
1825 max: config_secs,
1826 }
1827 .into())
1828 }
1829 }
1830 }
1831 None => execute_step_config(config, &self.provider, step_log_sender).await,
1832 }
1833 }
1834
1835 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1837 let config = self.guard_config.as_ref()?;
1838 let guard_state = self.guard_state.as_ref()?;
1839 let state = guard_state.lock().ok()?;
1840 let elapsed = state.elapsed_secs();
1841 let max = config.workflow_timeout_secs;
1842 if elapsed >= max {
1843 Some(std::time::Duration::ZERO)
1844 } else {
1845 Some(std::time::Duration::from_secs(max - elapsed))
1846 }
1847 }
1848
1849 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1851 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1852 let state = guard_state
1853 .lock()
1854 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1855 let elapsed = state.elapsed_secs();
1856 if elapsed >= config.workflow_timeout_secs {
1857 return Err(WorkflowRejection::WorkflowTimeout {
1858 elapsed_secs: elapsed,
1859 max: config.workflow_timeout_secs,
1860 }
1861 .into());
1862 }
1863 }
1864 Ok(())
1865 }
1866
1867 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1869 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1870 let mut state = guard_state
1871 .lock()
1872 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1873 state.record_tokens(config, tokens)?;
1874 }
1875 Ok(())
1876 }
1877
1878 async fn execute_child_workflow(
1881 &self,
1882 config: &WorkflowStepConfig,
1883 ) -> Result<(StepOutput, bool), EngineError> {
1884 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1885 EngineError::InvalidWorkflow(
1886 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1887 )
1888 })?;
1889
1890 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1891 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1892 })?;
1893
1894 let parent = self.store.get_run(self.run_id).await?;
1897 let (parent_labels, parent_author) =
1898 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1899
1900 let child_run = self
1901 .store
1902 .create_run(NewRun {
1903 workflow_name: config.workflow_name.clone(),
1904 trigger: TriggerKind::Workflow,
1905 payload: config.payload.clone(),
1906 max_retries: 0,
1907 handler_version: None,
1908 labels: parent_labels,
1909 scheduled_at: None,
1910 created_by: parent_author,
1911 idempotency_key: None,
1912 max_cost_usd: self.max_cost_usd,
1914 })
1915 .await?
1916 .into_run();
1917
1918 let child_run_id = child_run.id;
1919 info!(
1920 parent_run_id = %self.run_id,
1921 child_run_id = %child_run_id,
1922 workflow = %config.workflow_name,
1923 "child run created"
1924 );
1925
1926 self.store
1927 .update_run_status(child_run_id, RunStatus::Running)
1928 .await?;
1929
1930 let run_start = Instant::now();
1931 let mut child_ctx = WorkflowContext {
1932 run_id: child_run_id,
1933 workflow_name: config.workflow_name.clone(),
1934 store: self.store.clone(),
1935 provider: self.provider.clone(),
1936 decision_provider: self.decision_provider.clone(),
1937 handler_resolver: self.handler_resolver.clone(),
1938 position: 0,
1939 last_step_ids: Vec::new(),
1940 total_cost_usd: Decimal::ZERO,
1941 total_duration_ms: 0,
1942 max_cost_usd: self.max_cost_usd,
1943 inherited_cost_usd: self.charged_cost_usd(),
1946 replay_steps: HashMap::new(),
1947 granted_approvals: HashMap::new(),
1948 attempt: 1,
1950 carried_duration_ms: 0,
1951 log_sender: self.log_sender.clone(),
1952 artifact_sink: self.artifact_sink.clone(),
1955 has_allowed_failure: false,
1956 error_handlers: Vec::new(),
1957 guard_state: self.guard_state.clone(),
1958 guard_config: self.guard_config.clone(),
1959 step_results: Vec::new(),
1960 event_bus: self.event_bus.clone(),
1961 trace_context: self.trace_context.child(),
1962 operation_ctx: None,
1963 };
1964
1965 let result = handler.execute(&mut child_ctx).await;
1966 let total_duration = run_start.elapsed().as_millis() as u64;
1967 let completed_at = Utc::now();
1968
1969 match result {
1970 Ok(()) => {
1971 let child_status = if child_ctx.has_allowed_failure {
1972 RunStatus::Warning
1973 } else {
1974 RunStatus::Completed
1975 };
1976 self.store
1977 .update_run(
1978 child_run_id,
1979 RunUpdate {
1980 status: Some(child_status),
1981 cost_usd: Some(child_ctx.total_cost_usd),
1982 duration_ms: Some(total_duration),
1983 completed_at: Some(completed_at),
1984 ..RunUpdate::default()
1985 },
1986 )
1987 .await?;
1988
1989 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1990 Ok((
1991 StepOutput {
1992 output: serde_json::json!({
1993 "run_id": child_run_id,
1994 "workflow_name": config.workflow_name,
1995 "status": child_status,
1996 "cost_usd": child_ctx.total_cost_usd,
1997 "duration_ms": total_duration,
1998 }),
1999 duration_ms: total_duration,
2000 cost_usd: child_ctx.total_cost_usd,
2001 input_tokens: None,
2002 output_tokens: None,
2003 model: None,
2004 debug_messages: None,
2005 },
2006 child_had_allowed_failure,
2007 ))
2008 }
2009 Err(err) => {
2010 if let Err(store_err) = self
2011 .store
2012 .update_run(
2013 child_run_id,
2014 RunUpdate {
2015 status: Some(RunStatus::Failed),
2016 error: Some(err.to_string()),
2017 cost_usd: Some(child_ctx.total_cost_usd),
2018 duration_ms: Some(total_duration),
2019 completed_at: Some(completed_at),
2020 ..RunUpdate::default()
2021 },
2022 )
2023 .await
2024 {
2025 error!(
2026 child_run_id = %child_run_id,
2027 store_error = %store_err,
2028 "failed to persist child run failure"
2029 );
2030 }
2031
2032 Err(err)
2033 }
2034 }
2035 }
2036
2037 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
2042 let step = self.replay_steps.get(&position)?;
2043 if step.status.state != StepStatus::Completed {
2044 return None;
2045 }
2046 let output = StepOutput {
2047 output: step.output.clone().unwrap_or(Value::Null),
2048 duration_ms: step.duration_ms,
2049 cost_usd: step.cost_usd,
2050 input_tokens: step.input_tokens,
2051 output_tokens: step.output_tokens,
2052 model: None,
2053 debug_messages: None,
2054 };
2055 self.total_cost_usd += output.cost_usd;
2056 self.total_duration_ms += output.duration_ms;
2057 self.last_step_ids = vec![step.id];
2058 info!(
2059 run_id = %self.run_id,
2060 step = %step.name,
2061 position,
2062 "step replayed from previous execution"
2063 );
2064 Some(output)
2065 }
2066
2067 #[tracing::instrument(
2069 name = "context.execute_step",
2070 skip_all,
2071 fields(
2072 run_id = %self.run_id,
2073 step.name = %name,
2074 step.kind,
2075 step.position = self.position,
2076 step.trace_id,
2077 )
2078 )]
2079 pub(crate) async fn execute_step(
2080 &mut self,
2081 name: &str,
2082 kind: StepKind,
2083 config: StepConfig,
2084 ) -> Result<StepOutput, EngineError> {
2085 let kind_str: &'static str = match kind {
2086 StepKind::Shell => "shell",
2087 StepKind::Http => "http",
2088 StepKind::Agent => "agent",
2089 StepKind::Workflow => "workflow",
2090 StepKind::Approval => "approval",
2091 StepKind::Decision => "decision",
2092 StepKind::Custom(_) => "custom",
2093 };
2094 Span::current().record("step.kind", kind_str);
2095
2096 self.check_guard_timeout()?;
2098
2099 let position = self.position;
2100 self.position += 1;
2101
2102 if let Some(output) = self.try_replay_step(position) {
2104 return Ok(output);
2105 }
2106
2107 if let StepConfig::Agent(ref agent_config) = config {
2110 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2111 }
2112
2113 let trace_id = step_trace_id(self.run_id, name, position);
2115 Span::current().record("step.trace_id", trace_id.to_string().as_str());
2116 let step = self
2117 .store
2118 .create_step(NewStep {
2119 run_id: self.run_id,
2120 trace_id,
2121 name: name.to_string(),
2122 kind,
2123 position,
2124 input: Some(serde_json::to_value(&config)?),
2125 is_error_handler: false,
2126 })
2127 .await?;
2128
2129 self.start_step(step.id, Utc::now()).await?;
2130
2131 if let Some(ref bus) = self.event_bus {
2132 bus.publish(
2133 self.run_id,
2134 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
2135 step_name: name.to_string(),
2136 step_index: position,
2137 timestamp: Utc::now(),
2138 }),
2139 );
2140 }
2141
2142 if let Err(err) = self.prepare_step_inputs(&config, position).await {
2145 self.fail_step(step.id, &err).await;
2146 if config.allow_failure() {
2147 self.has_allowed_failure = true;
2148 self.last_step_ids = vec![step.id];
2149 info!(
2150 run_id = %self.run_id,
2151 step = %name,
2152 error = %err,
2153 "step input preparation failed but allow_failure is set, continuing"
2154 );
2155 return Ok(StepOutput {
2156 output: json!({"error": err.to_string()}),
2157 duration_ms: 0,
2158 cost_usd: Decimal::ZERO,
2159 input_tokens: None,
2160 output_tokens: None,
2161 model: None,
2162 debug_messages: None,
2163 });
2164 }
2165 return Err(err);
2166 }
2167
2168 let mut config = config;
2169 let step_trace = self.trace_context.child();
2170 match config {
2171 StepConfig::Agent(ref mut agent_config) => {
2172 agent_config.trace_context = Some(step_trace);
2173 }
2174 StepConfig::Http(ref mut http_config) => {
2175 http_config.trace_context = Some(step_trace);
2176 }
2177 _ => {}
2178 }
2179
2180 let step_log_sender = self
2181 .log_sender
2182 .as_ref()
2183 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
2184
2185 let execution = self
2186 .execute_with_guard_timeout(&config, step_log_sender)
2187 .await;
2188
2189 let execution = self
2190 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
2191 .await;
2192
2193 if let Err(err) = self
2194 .store_step_outputs(&config, step.id, name, execution.is_ok())
2195 .await
2196 {
2197 self.fail_step(step.id, &err).await;
2198 return Err(err);
2199 }
2200
2201 match execution {
2202 Ok(output) => {
2203 self.total_cost_usd += output.cost_usd;
2204 self.total_duration_ms += output.duration_ms;
2205
2206 if matches!(config, StepConfig::Agent(_)) {
2208 let tokens = output
2209 .input_tokens
2210 .unwrap_or(0)
2211 .saturating_add(output.output_tokens.unwrap_or(0));
2212 if tokens > 0 {
2213 self.guard_record_tokens(tokens)?;
2214 }
2215 }
2216
2217 let debug_messages_json = output.debug_messages_json();
2218
2219 let completed_at = Utc::now();
2220 self.store
2221 .update_step(
2222 step.id,
2223 StepUpdate {
2224 status: Some(StepStatus::Completed),
2225 output: Some(output.output.clone()),
2226 duration_ms: Some(output.duration_ms),
2227 cost_usd: Some(output.cost_usd),
2228 input_tokens: output.input_tokens,
2229 output_tokens: output.output_tokens,
2230 completed_at: Some(completed_at),
2231 debug_messages: debug_messages_json,
2232 ..StepUpdate::default()
2233 },
2234 )
2235 .await?;
2236
2237 self.step_results
2238 .push(StepResult::from_success(trace_id, name, &output));
2239 self.persist_progress().await;
2240
2241 info!(
2242 run_id = %self.run_id,
2243 step = %name,
2244 trace_id = %trace_id,
2245 duration_ms = output.duration_ms,
2246 "step completed"
2247 );
2248
2249 if let Some(ref bus) = self.event_bus {
2250 bus.publish(
2251 self.run_id,
2252 WorkflowEvent::StepCompleted(WorkflowStepCompletedEvent {
2253 step_name: name.to_string(),
2254 step_index: position,
2255 duration_ms: output.duration_ms,
2256 output_summary: None,
2257 }),
2258 );
2259
2260 if matches!(config, StepConfig::Agent(_)) {
2261 let tokens = output
2262 .input_tokens
2263 .unwrap_or(0)
2264 .saturating_add(output.output_tokens.unwrap_or(0));
2265 bus.publish(
2266 self.run_id,
2267 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
2268 step_name: name.to_string(),
2269 tokens,
2270 cost_usd: output.cost_usd,
2271 }),
2272 );
2273 }
2274 }
2275
2276 self.last_step_ids = vec![step.id];
2277
2278 Ok(output)
2279 }
2280 Err(err) => {
2281 let completed_at = Utc::now();
2282 let debug_messages_json = extract_debug_messages_from_error(&err);
2283 let partial = extract_partial_usage_from_error(&err);
2284 let raw_response_output = extract_raw_response_from_error(&err);
2285
2286 if let Some(ref usage) = partial {
2287 if let Some(cost) = usage.cost_usd {
2288 self.total_cost_usd += cost;
2289 }
2290 if let Some(dur) = usage.duration_ms {
2291 self.total_duration_ms += dur;
2292 }
2293 }
2294
2295 if let Err(store_err) = self
2296 .store
2297 .update_step(
2298 step.id,
2299 StepUpdate {
2300 status: Some(StepStatus::Failed),
2301 error: Some(err.to_string()),
2302 output: raw_response_output.clone(),
2303 completed_at: Some(completed_at),
2304 debug_messages: debug_messages_json,
2305 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2306 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2307 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2308 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2309 ..StepUpdate::default()
2310 },
2311 )
2312 .await
2313 {
2314 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2315 }
2316
2317 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2318 let err_cost = partial
2319 .as_ref()
2320 .and_then(|p| p.cost_usd)
2321 .unwrap_or(Decimal::ZERO);
2322 self.step_results.push(StepResult::from_failure(
2323 trace_id,
2324 name,
2325 &err.to_string(),
2326 err_duration,
2327 err_cost,
2328 ));
2329 self.persist_progress().await;
2330
2331 if let Some(ref bus) = self.event_bus {
2332 bus.publish(
2333 self.run_id,
2334 WorkflowEvent::StepFailed(WorkflowStepFailedEvent {
2335 step_name: name.to_string(),
2336 step_index: position,
2337 error: err.to_string(),
2338 duration_ms: err_duration,
2339 }),
2340 );
2341 }
2342
2343 self.fire_error_handlers(name, &err.to_string(), err_duration)
2344 .await;
2345
2346 if config.allow_failure() {
2347 self.has_allowed_failure = true;
2348 self.last_step_ids = vec![step.id];
2349 info!(
2350 run_id = %self.run_id,
2351 step = %name,
2352 error = %err,
2353 "step failed but allow_failure is set, continuing"
2354 );
2355 Ok(allowed_failure_output(
2356 &err.to_string(),
2357 raw_response_output,
2358 partial.as_ref(),
2359 ))
2360 } else {
2361 Err(err)
2362 }
2363 }
2364 }
2365 }
2366
2367 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2373 async fn retry_step_if_configured(
2374 &self,
2375 name: &str,
2376 kind_str: &str,
2377 config: &StepConfig,
2378 step_id: Uuid,
2379 first_result: Result<StepOutput, EngineError>,
2380 ) -> Result<StepOutput, EngineError> {
2381 let policy = match config.retry() {
2382 Some(p) => p,
2383 None => return first_result,
2384 };
2385
2386 let mut last_result = match first_result {
2387 Ok(output) => return Ok(output),
2388 Err(err) if !is_step_retryable(&err) => return Err(err),
2389 Err(err) => Err(err),
2390 };
2391
2392 let step_log_sender = self
2393 .log_sender
2394 .as_ref()
2395 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2396
2397 for attempt in 0..policy.max_retries() {
2398 if let StepConfig::Agent(agent_config) = config {
2399 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2400 }
2401
2402 let delay = policy.delay_for_attempt(attempt);
2403 info!(
2404 run_id = %self.run_id,
2405 step = %name,
2406 attempt = attempt + 1,
2407 max_retries = policy.max_retries(),
2408 delay_ms = delay.as_millis() as u64,
2409 "retrying step after transient failure"
2410 );
2411 tokio::time::sleep(delay).await;
2412
2413 record_retry_metric(kind_str, "retry");
2414
2415 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2416 Ok(output) => return Ok(output),
2417 Err(err) if !is_step_retryable(&err) => return Err(err),
2418 err => last_result = err,
2419 }
2420 }
2421
2422 record_retry_metric(kind_str, "exhausted");
2423
2424 info!(
2425 run_id = %self.run_id,
2426 step = %name,
2427 max_retries = policy.max_retries(),
2428 "step retries exhausted"
2429 );
2430
2431 last_result
2432 }
2433
2434 pub(crate) async fn start_step(
2439 &self,
2440 step_id: Uuid,
2441 now: DateTime<Utc>,
2442 ) -> Result<(), EngineError> {
2443 if !self.last_step_ids.is_empty() {
2444 let deps: Vec<NewStepDependency> = self
2445 .last_step_ids
2446 .iter()
2447 .map(|&depends_on| NewStepDependency {
2448 step_id,
2449 depends_on,
2450 })
2451 .collect();
2452 self.store.create_step_dependencies(deps).await?;
2453 }
2454
2455 self.store
2456 .update_step(
2457 step_id,
2458 StepUpdate {
2459 status: Some(StepStatus::Running),
2460 started_at: Some(now),
2461 ..StepUpdate::default()
2462 },
2463 )
2464 .await?;
2465
2466 Ok(())
2467 }
2468
2469 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2476 if let Err(store_err) = self
2477 .store
2478 .update_step(
2479 step_id,
2480 StepUpdate {
2481 status: Some(StepStatus::Failed),
2482 error: Some(err.to_string()),
2483 completed_at: Some(Utc::now()),
2484 ..StepUpdate::default()
2485 },
2486 )
2487 .await
2488 {
2489 error!(
2490 step_id = %step_id,
2491 error = %store_err,
2492 "failed to persist step failure"
2493 );
2494 }
2495 }
2496
2497 pub fn store(&self) -> &Arc<dyn Store> {
2499 &self.store
2500 }
2501
2502 pub(crate) fn next_position(&mut self) -> u32 {
2504 let pos = self.position;
2505 self.position += 1;
2506 pos
2507 }
2508
2509 pub(crate) fn replay_steps(&self) -> &HashMap<u32, Step> {
2511 &self.replay_steps
2512 }
2513
2514 pub(crate) fn set_last_step_ids(&mut self, ids: Vec<Uuid>) {
2516 self.last_step_ids = ids;
2517 }
2518
2519 pub async fn payload(&self) -> Result<Value, EngineError> {
2527 let run = self
2528 .store
2529 .get_run(self.run_id)
2530 .await?
2531 .ok_or(EngineError::Store(
2532 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2533 ))?;
2534 Ok(run.payload)
2535 }
2536
2537 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2565 let payload = self.payload().await?;
2566 serde_json::from_value(payload).map_err(EngineError::Serialization)
2567 }
2568
2569 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2592 self.error_handlers.push(OnErrorHandler {
2593 name: name.to_string(),
2594 config: config.into(),
2595 });
2596 }
2597
2598 pub fn clear_error_handlers(&mut self) {
2617 self.error_handlers.clear();
2618 }
2619
2620 async fn fire_error_handlers(
2626 &mut self,
2627 failed_step_name: &str,
2628 error_msg: &str,
2629 duration_ms: u64,
2630 ) {
2631 let handlers = std::mem::take(&mut self.error_handlers);
2632 if handlers.is_empty() {
2633 return;
2634 }
2635
2636 let error_context = json!({
2637 "failed_step": failed_step_name,
2638 "error": error_msg,
2639 "duration_ms": duration_ms,
2640 });
2641
2642 for handler in handlers {
2643 let mut config = handler.config.clone();
2644 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2645
2646 let position = self.position;
2647 self.position += 1;
2648
2649 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2650 let step = match self
2651 .store
2652 .create_step(NewStep {
2653 run_id: self.run_id,
2654 trace_id,
2655 name: handler.name.clone(),
2656 kind: config.kind(),
2657 position,
2658 input: Some(error_context.clone()),
2659 is_error_handler: true,
2660 })
2661 .await
2662 {
2663 Ok(step) => step,
2664 Err(err) => {
2665 warn!(
2666 run_id = %self.run_id,
2667 handler = %handler.name,
2668 error = %err,
2669 "failed to create error handler step"
2670 );
2671 continue;
2672 }
2673 };
2674
2675 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2676 warn!(
2677 run_id = %self.run_id,
2678 handler = %handler.name,
2679 error = %err,
2680 "failed to start error handler step"
2681 );
2682 continue;
2683 }
2684
2685 let step_log_sender = self
2686 .log_sender
2687 .as_ref()
2688 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2689
2690 let start = Instant::now();
2691 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2692 let handler_duration = start.elapsed().as_millis() as u64;
2693 let completed_at = Utc::now();
2694
2695 match result {
2696 Ok(output) => {
2697 if let Err(store_err) = self
2698 .store
2699 .update_step(
2700 step.id,
2701 StepUpdate {
2702 status: Some(StepStatus::Completed),
2703 output: Some(output.output),
2704 duration_ms: Some(handler_duration),
2705 cost_usd: Some(output.cost_usd),
2706 completed_at: Some(completed_at),
2707 ..StepUpdate::default()
2708 },
2709 )
2710 .await
2711 {
2712 warn!(
2713 run_id = %self.run_id,
2714 handler = %handler.name,
2715 error = %store_err,
2716 "failed to persist error handler completion"
2717 );
2718 }
2719
2720 info!(
2721 run_id = %self.run_id,
2722 handler = %handler.name,
2723 duration_ms = handler_duration,
2724 "error handler completed"
2725 );
2726 }
2727 Err(err) => {
2728 if let Err(store_err) = self
2729 .store
2730 .update_step(
2731 step.id,
2732 StepUpdate {
2733 status: Some(StepStatus::Failed),
2734 error: Some(err.to_string()),
2735 duration_ms: Some(handler_duration),
2736 completed_at: Some(completed_at),
2737 ..StepUpdate::default()
2738 },
2739 )
2740 .await
2741 {
2742 warn!(
2743 run_id = %self.run_id,
2744 handler = %handler.name,
2745 error = %store_err,
2746 "failed to persist error handler failure"
2747 );
2748 }
2749
2750 warn!(
2751 run_id = %self.run_id,
2752 handler = %handler.name,
2753 error = %err,
2754 "error handler failed (original error preserved)"
2755 );
2756 }
2757 }
2758 }
2759 }
2760}
2761
2762fn inject_error_context(
2764 config: &mut StepConfig,
2765 failed_step: &str,
2766 error_msg: &str,
2767 duration_ms: u64,
2768) {
2769 match config {
2770 StepConfig::Shell(shell) => {
2771 shell
2772 .env
2773 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2774 shell
2775 .env
2776 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2777 shell.env.push((
2778 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2779 duration_ms.to_string(),
2780 ));
2781 }
2782 StepConfig::Agent(agent) => {
2783 agent.prompt = format!(
2784 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2785 failed_step, duration_ms, error_msg, agent.prompt
2786 );
2787 }
2788 StepConfig::Http(http) => {
2789 http.headers
2790 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2791 http.headers.push((
2792 "X-Ironflow-Error-Message".to_string(),
2793 error_msg.to_string(),
2794 ));
2795 }
2796 StepConfig::Workflow(_)
2797 | StepConfig::Approval(_)
2798 | StepConfig::Decision(_)
2799 | StepConfig::Delay(_) => {}
2800 }
2801}
2802
2803#[cfg(feature = "prometheus")]
2804fn record_retry_metric(kind: &str, outcome: &str) {
2805 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2806 use metrics::counter;
2807 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2808 .increment(1);
2809}
2810
2811#[cfg(not(feature = "prometheus"))]
2812fn record_retry_metric(_kind: &str, _outcome: &str) {}
2813
2814fn is_step_retryable(err: &EngineError) -> bool {
2818 use ironflow_core::error::{AgentError, OperationError};
2819
2820 match err {
2821 EngineError::Operation(op) => match op {
2822 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2823 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2824 OperationError::Deserialize { .. } => false,
2825 OperationError::Http {
2826 status: Some(code), ..
2827 } if (400..500).contains(code) && *code != 429 => false,
2828 _ => true,
2829 },
2830 _ => false,
2831 }
2832}
2833
2834fn allowed_failure_output(
2835 error_msg: &str,
2836 raw_response: Option<Value>,
2837 partial: Option<&StepPartialUsage>,
2838) -> StepOutput {
2839 StepOutput {
2840 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2841 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2842 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2843 input_tokens: partial.and_then(|p| p.input_tokens),
2844 output_tokens: partial.and_then(|p| p.output_tokens),
2845 model: None,
2846 debug_messages: None,
2847 }
2848}
2849
2850impl fmt::Debug for WorkflowContext {
2851 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2852 f.debug_struct("WorkflowContext")
2853 .field("run_id", &self.run_id)
2854 .field("position", &self.position)
2855 .field("total_cost_usd", &self.total_cost_usd)
2856 .field("inherited_cost_usd", &self.inherited_cost_usd)
2857 .field("max_cost_usd", &self.max_cost_usd)
2858 .finish_non_exhaustive()
2859 }
2860}
2861
2862fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2865 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2866 debug_messages,
2867 ..
2868 })) = err
2869 && !debug_messages.is_empty()
2870 {
2871 return serde_json::to_value(debug_messages).ok();
2872 }
2873 None
2874}
2875
2876struct StepPartialUsage {
2882 cost_usd: Option<Decimal>,
2883 duration_ms: Option<u64>,
2884 input_tokens: Option<u64>,
2885 output_tokens: Option<u64>,
2886}
2887
2888fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2894 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2895 raw_response: Some(text),
2896 ..
2897 })) = err
2898 {
2899 return Some(Value::String(text.clone()));
2900 }
2901 None
2902}
2903
2904fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2905 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2906 partial_usage,
2907 ..
2908 })) = err
2909 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2910 {
2911 return Some(StepPartialUsage {
2912 cost_usd: partial_usage
2913 .cost_usd
2914 .and_then(|c| Decimal::try_from(c).ok()),
2915 duration_ms: partial_usage.duration_ms,
2916 input_tokens: partial_usage.input_tokens,
2917 output_tokens: partial_usage.output_tokens,
2918 });
2919 }
2920 None
2921}
2922
2923#[cfg(test)]
2924mod tests {
2925 use super::*;
2926 use ironflow_core::providers::claude::ClaudeCodeProvider;
2927 use ironflow_core::providers::record_replay::RecordReplayProvider;
2928 use ironflow_store::memory::InMemoryStore;
2929 use ironflow_store::models::{Assignee, Run, RunActor, RunFilter};
2930 use ironflow_store::store::RunStore;
2931 use serde_json::json;
2932 use std::sync::Arc;
2933 use std::sync::atomic::{AtomicBool, Ordering};
2934 use uuid::Uuid;
2935
2936 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2938 let inner = ClaudeCodeProvider::new();
2939 Arc::new(RecordReplayProvider::replay(
2940 inner,
2941 "/tmp/ironflow-fixtures",
2942 ))
2943 }
2944
2945 fn create_test_context() -> WorkflowContext {
2947 let store = Arc::new(InMemoryStore::new());
2948 let provider = create_test_provider();
2949 let run_id = Uuid::now_v7();
2950 WorkflowContext::new(run_id, "test".to_string(), store, provider)
2951 }
2952
2953 #[test]
2954 fn context_new_initializes_correctly() {
2955 let ctx = create_test_context();
2956 assert_eq!(ctx.position, 0);
2957 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2958 assert_eq!(ctx.total_duration_ms, 0);
2959 assert!(ctx.last_step_ids.is_empty());
2960 assert!(ctx.replay_steps.is_empty());
2961 assert!(ctx.log_sender.is_none());
2962 }
2963
2964 #[test]
2965 fn context_run_id_returns_correct_id() {
2966 let run_id = Uuid::now_v7();
2967 let store = Arc::new(InMemoryStore::new());
2968 let provider = create_test_provider();
2969 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
2970 assert_eq!(ctx.run_id(), run_id);
2971 }
2972
2973 #[test]
2974 fn context_total_cost_usd_initially_zero() {
2975 let ctx = create_test_context();
2976 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2977 }
2978
2979 #[test]
2980 fn context_total_duration_ms_initially_zero() {
2981 let ctx = create_test_context();
2982 assert_eq!(ctx.total_duration_ms(), 0);
2983 }
2984
2985 #[test]
2986 fn context_with_handler_resolver_creates_context_with_resolver() {
2987 let store = Arc::new(InMemoryStore::new());
2988 let provider = create_test_provider();
2989 let run_id = Uuid::now_v7();
2990
2991 let called = Arc::new(AtomicBool::new(false));
2992 let called_clone = called.clone();
2993
2994 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2995 called_clone.store(true, Ordering::SeqCst);
2996 None
2997 });
2998
2999 let ctx = WorkflowContext::with_handler_resolver(
3000 run_id,
3001 "test".to_string(),
3002 store,
3003 provider,
3004 resolver,
3005 );
3006
3007 assert_eq!(ctx.run_id(), run_id);
3008 assert!(ctx.handler_resolver.is_some());
3009 }
3010
3011 #[tokio::test]
3012 async fn context_set_log_sender_attaches_sender() {
3013 let mut ctx = create_test_context();
3014 let (sender, _receiver) = crate::log_sender::channel();
3015 ctx.set_log_sender(sender);
3016 assert!(ctx.log_sender.is_some());
3017 }
3018
3019 #[tokio::test]
3020 async fn context_skip_creates_skipped_step() {
3021 let store = Arc::new(InMemoryStore::new());
3022 let provider = create_test_provider();
3023
3024 store
3026 .create_run(NewRun {
3027 created_by: None,
3028 workflow_name: "test".to_string(),
3029 trigger: TriggerKind::Manual,
3030 payload: json!({}),
3031 max_retries: 0,
3032 handler_version: None,
3033 labels: Default::default(),
3034 scheduled_at: None,
3035 idempotency_key: None,
3036 max_cost_usd: None,
3037 })
3038 .await
3039 .expect("failed to create run")
3040 .into_run();
3041
3042 let runs = store
3044 .list_runs(RunFilter::default(), 1, 10)
3045 .await
3046 .expect("failed to list runs");
3047 let created_run_id = runs.items[0].id;
3048
3049 let mut ctx =
3050 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3051 let initial_position = ctx.position;
3052
3053 ctx.skip("skip-step", "condition not met")
3054 .await
3055 .expect("skip failed");
3056
3057 assert_eq!(ctx.position, initial_position + 1);
3058 assert!(!ctx.last_step_ids.is_empty());
3059
3060 let steps = store
3062 .list_steps(created_run_id)
3063 .await
3064 .expect("failed to list steps");
3065 assert_eq!(steps.len(), 1);
3066 assert_eq!(steps[0].status.state, StepStatus::Skipped);
3067 }
3068
3069 struct NoopSubWorkflow;
3072
3073 impl WorkflowHandler for NoopSubWorkflow {
3074 fn name(&self) -> &str {
3075 "noop-sub"
3076 }
3077
3078 fn execute<'a>(
3079 &'a self,
3080 _ctx: &'a mut WorkflowContext,
3081 ) -> crate::handler::HandlerFuture<'a> {
3082 Box::pin(async move { Ok(()) })
3083 }
3084 }
3085
3086 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
3089 let store = Arc::new(InMemoryStore::new());
3090 let provider = create_test_provider();
3091
3092 let parent = store
3093 .create_run(NewRun {
3094 workflow_name: "parent".to_string(),
3095 trigger: TriggerKind::Api,
3096 payload: json!({}),
3097 max_retries: 0,
3098 handler_version: None,
3099 labels: Default::default(),
3100 scheduled_at: None,
3101 created_by,
3102 idempotency_key: None,
3103 max_cost_usd: None,
3104 })
3105 .await
3106 .expect("failed to create parent run")
3107 .into_run();
3108
3109 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
3110 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
3111 _ => None,
3112 });
3113
3114 let mut ctx = WorkflowContext::with_handler_resolver(
3115 parent.id,
3116 "parent".to_string(),
3117 store.clone(),
3118 provider,
3119 resolver,
3120 );
3121 ctx.workflow(&NoopSubWorkflow, json!({}))
3122 .await
3123 .expect("sub-workflow failed");
3124
3125 let runs = store
3126 .list_runs(RunFilter::default(), 1, 10)
3127 .await
3128 .expect("failed to list runs");
3129 runs.items
3130 .into_iter()
3131 .find(|r| r.workflow_name == "noop-sub")
3132 .expect("child run was created")
3133 }
3134
3135 #[tokio::test]
3136 async fn child_run_inherits_the_parent_author() {
3137 let user_id = Uuid::now_v7();
3138 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
3139
3140 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
3141 }
3142
3143 #[tokio::test]
3144 async fn child_run_of_an_unattributed_parent_has_no_author() {
3145 let child = child_run_of_parent_authored_by(None).await;
3146
3147 assert!(child.created_by.is_none());
3148 }
3149
3150 #[tokio::test]
3151 async fn context_parallel_empty_steps_returns_empty_vec() {
3152 let mut ctx = create_test_context();
3153 let results = ctx
3154 .parallel(vec![], true)
3155 .await
3156 .expect("parallel should not fail on empty input");
3157 assert!(results.is_empty());
3158 }
3159
3160 #[tokio::test]
3161 async fn context_approval_first_execution_returns_error() {
3162 let store = Arc::new(InMemoryStore::new());
3163 let provider = create_test_provider();
3164
3165 store
3167 .create_run(NewRun {
3168 created_by: None,
3169 workflow_name: "test".to_string(),
3170 trigger: TriggerKind::Manual,
3171 payload: json!({}),
3172 max_retries: 0,
3173 handler_version: None,
3174 labels: Default::default(),
3175 scheduled_at: None,
3176 idempotency_key: None,
3177 max_cost_usd: None,
3178 })
3179 .await
3180 .expect("failed to create run")
3181 .into_run();
3182
3183 let runs = store
3185 .list_runs(RunFilter::default(), 1, 10)
3186 .await
3187 .expect("failed to list runs");
3188 let created_run_id = runs.items[0].id;
3189
3190 let mut ctx =
3191 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3192
3193 let result = ctx
3194 .approval(
3195 "approve-step",
3196 crate::config::ApprovalConfig::new("Continue?"),
3197 )
3198 .await;
3199
3200 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
3202
3203 assert_eq!(ctx.position, 1);
3205
3206 let steps = store
3208 .list_steps(created_run_id)
3209 .await
3210 .expect("failed to list steps");
3211 assert_eq!(steps.len(), 1);
3212 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3213 }
3214
3215 #[tokio::test]
3216 async fn context_approval_replay_returns_ok() {
3217 let store = Arc::new(InMemoryStore::new());
3218 let provider = create_test_provider();
3219
3220 store
3222 .create_run(NewRun {
3223 created_by: None,
3224 workflow_name: "test".to_string(),
3225 trigger: TriggerKind::Manual,
3226 payload: json!({}),
3227 max_retries: 0,
3228 handler_version: None,
3229 labels: Default::default(),
3230 scheduled_at: None,
3231 idempotency_key: None,
3232 max_cost_usd: None,
3233 })
3234 .await
3235 .expect("failed to create run")
3236 .into_run();
3237
3238 let runs = store
3240 .list_runs(RunFilter::default(), 1, 10)
3241 .await
3242 .expect("failed to list runs");
3243 let created_run_id = runs.items[0].id;
3244
3245 let step = store
3247 .create_step(NewStep {
3248 run_id: created_run_id,
3249 trace_id: step_trace_id(created_run_id, "approval", 0),
3250 name: "approval".to_string(),
3251 kind: StepKind::Approval,
3252 position: 0,
3253 input: None,
3254 is_error_handler: false,
3255 })
3256 .await
3257 .expect("failed to create step");
3258
3259 store
3261 .update_step(
3262 step.id,
3263 StepUpdate {
3264 status: Some(StepStatus::Running),
3265 started_at: Some(Utc::now()),
3266 ..StepUpdate::default()
3267 },
3268 )
3269 .await
3270 .expect("failed to update step to Running");
3271
3272 store
3273 .update_step(
3274 step.id,
3275 StepUpdate {
3276 status: Some(StepStatus::AwaitingApproval),
3277 ..StepUpdate::default()
3278 },
3279 )
3280 .await
3281 .expect("failed to update step to AwaitingApproval");
3282
3283 let mut ctx =
3285 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3286 ctx.load_replay_steps()
3287 .await
3288 .expect("failed to load replay steps");
3289
3290 let result = ctx
3292 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3293 .await;
3294
3295 assert!(result.is_ok());
3296
3297 let steps = store
3299 .list_steps(created_run_id)
3300 .await
3301 .expect("failed to list steps");
3302 assert_eq!(steps.len(), 1);
3303 assert_eq!(steps[0].status.state, StepStatus::Completed);
3304 }
3305
3306 #[tokio::test]
3307 async fn context_load_replay_steps_loads_completed_steps() {
3308 let store = Arc::new(InMemoryStore::new());
3309 let provider = create_test_provider();
3310
3311 store
3313 .create_run(NewRun {
3314 created_by: None,
3315 workflow_name: "test".to_string(),
3316 trigger: TriggerKind::Manual,
3317 payload: json!({}),
3318 max_retries: 0,
3319 handler_version: None,
3320 labels: Default::default(),
3321 scheduled_at: None,
3322 idempotency_key: None,
3323 max_cost_usd: None,
3324 })
3325 .await
3326 .expect("failed to create run")
3327 .into_run();
3328
3329 let runs = store
3331 .list_runs(RunFilter::default(), 1, 10)
3332 .await
3333 .expect("failed to list runs");
3334 let created_run_id = runs.items[0].id;
3335
3336 let completed_step = store
3338 .create_step(NewStep {
3339 run_id: created_run_id,
3340 trace_id: step_trace_id(created_run_id, "completed", 0),
3341 name: "completed".to_string(),
3342 kind: StepKind::Shell,
3343 position: 0,
3344 input: None,
3345 is_error_handler: false,
3346 })
3347 .await
3348 .expect("failed to create step");
3349
3350 store
3352 .update_step(
3353 completed_step.id,
3354 StepUpdate {
3355 status: Some(StepStatus::Running),
3356 started_at: Some(Utc::now()),
3357 ..StepUpdate::default()
3358 },
3359 )
3360 .await
3361 .expect("failed to update step to Running");
3362
3363 store
3364 .update_step(
3365 completed_step.id,
3366 StepUpdate {
3367 status: Some(StepStatus::Completed),
3368 completed_at: Some(Utc::now()),
3369 ..StepUpdate::default()
3370 },
3371 )
3372 .await
3373 .expect("failed to update step to Completed");
3374
3375 let _pending_step = store
3376 .create_step(NewStep {
3377 run_id: created_run_id,
3378 trace_id: step_trace_id(created_run_id, "pending", 1),
3379 name: "pending".to_string(),
3380 kind: StepKind::Shell,
3381 position: 1,
3382 input: None,
3383 is_error_handler: false,
3384 })
3385 .await
3386 .expect("failed to create step");
3387
3388 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3390 ctx.load_replay_steps()
3391 .await
3392 .expect("failed to load replay steps");
3393
3394 assert_eq!(ctx.replay_steps.len(), 1);
3396 assert!(ctx.replay_steps.contains_key(&0));
3397 assert!(!ctx.replay_steps.contains_key(&1));
3398 }
3399
3400 #[tokio::test]
3401 async fn context_payload_returns_run_payload() {
3402 let store = Arc::new(InMemoryStore::new());
3403 let provider = create_test_provider();
3404 let test_payload = json!({"key": "value", "number": 42});
3405
3406 store
3408 .create_run(NewRun {
3409 created_by: None,
3410 workflow_name: "test".to_string(),
3411 trigger: TriggerKind::Manual,
3412 payload: test_payload.clone(),
3413 max_retries: 0,
3414 handler_version: None,
3415 labels: Default::default(),
3416 scheduled_at: None,
3417 idempotency_key: None,
3418 max_cost_usd: None,
3419 })
3420 .await
3421 .expect("failed to create run")
3422 .into_run();
3423
3424 let runs = store
3426 .list_runs(RunFilter::default(), 1, 10)
3427 .await
3428 .expect("failed to list runs");
3429 let created_run_id = runs.items[0].id;
3430
3431 let ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3432 let payload = ctx.payload().await.expect("failed to get payload");
3433
3434 assert_eq!(payload, test_payload);
3435 }
3436
3437 #[tokio::test]
3438 async fn context_payload_returns_error_for_nonexistent_run() {
3439 let store = Arc::new(InMemoryStore::new());
3440 let provider = create_test_provider();
3441 let run_id = Uuid::now_v7();
3442
3443 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
3444 let result = ctx.payload().await;
3445
3446 assert!(result.is_err());
3447 }
3448
3449 #[tokio::test]
3450 async fn context_store_returns_reference() {
3451 let ctx = create_test_context();
3452 let _store = ctx.store();
3453 }
3455
3456 #[test]
3457 fn context_debug_formatting() {
3458 let ctx = create_test_context();
3459 let debug_str = format!("{:?}", ctx);
3460 assert!(debug_str.contains("WorkflowContext"));
3461 assert!(debug_str.contains("run_id"));
3462 }
3463
3464 #[tokio::test]
3465 async fn context_last_step_ids_tracks_executed_steps() {
3466 let store = Arc::new(InMemoryStore::new());
3467 let provider = create_test_provider();
3468
3469 store
3471 .create_run(NewRun {
3472 created_by: None,
3473 workflow_name: "test".to_string(),
3474 trigger: TriggerKind::Manual,
3475 payload: json!({}),
3476 max_retries: 0,
3477 handler_version: None,
3478 labels: Default::default(),
3479 scheduled_at: None,
3480 idempotency_key: None,
3481 max_cost_usd: None,
3482 })
3483 .await
3484 .expect("failed to create run")
3485 .into_run();
3486
3487 let runs = store
3489 .list_runs(RunFilter::default(), 1, 10)
3490 .await
3491 .expect("failed to list runs");
3492 let created_run_id = runs.items[0].id;
3493
3494 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3495 assert!(ctx.last_step_ids.is_empty());
3496
3497 ctx.skip("step1", "reason").await.expect("skip failed");
3498
3499 assert_eq!(ctx.last_step_ids.len(), 1);
3500
3501 ctx.skip("step2", "reason").await.expect("skip failed");
3502
3503 assert_eq!(ctx.last_step_ids.len(), 1);
3505 }
3506
3507 async fn context_with_run() -> (Arc<InMemoryStore>, WorkflowContext) {
3511 let store = Arc::new(InMemoryStore::new());
3512 let run = store
3513 .create_run(NewRun {
3514 created_by: None,
3515 workflow_name: "test".to_string(),
3516 trigger: TriggerKind::Manual,
3517 payload: json!({}),
3518 max_retries: 0,
3519 handler_version: None,
3520 labels: Default::default(),
3521 scheduled_at: None,
3522 idempotency_key: None,
3523 max_cost_usd: None,
3524 })
3525 .await
3526 .expect("failed to create run")
3527 .into_run();
3528
3529 let ctx = WorkflowContext::new(
3530 run.id,
3531 "test".to_string(),
3532 store.clone(),
3533 create_test_provider(),
3534 );
3535 (store, ctx)
3536 }
3537
3538 #[tokio::test]
3539 async fn approval_without_deadline_leaves_timer_unset() {
3540 let (store, mut ctx) = context_with_run().await;
3541
3542 let err = ctx
3543 .approval("gate", ApprovalConfig::new("Approve?"))
3544 .await
3545 .expect_err("approval suspends the run");
3546 assert!(matches!(err, EngineError::ApprovalRequired { .. }));
3547
3548 let steps = store.list_steps(ctx.run_id()).await.expect("list steps");
3549 assert_eq!(steps.len(), 1);
3550 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3551 assert!(steps[0].approval_deadline_at.is_none());
3552 assert_eq!(steps[0].approval_stage, 0);
3553 assert!(steps[0].approval_assignee.is_none());
3554 }
3555
3556 #[tokio::test]
3557 async fn approval_with_deadline_arms_timer() {
3558 let (store, mut ctx) = context_with_run().await;
3559
3560 let before = Utc::now();
3561 let config = ApprovalConfig::new("Approve?")
3562 .with_deadline_secs(3600)
3563 .assigned_to(Assignee::group("release-managers"));
3564 ctx.approval("gate", config)
3565 .await
3566 .expect_err("approval suspends the run");
3567
3568 let steps = store.list_steps(ctx.run_id()).await.expect("list steps");
3569 let deadline = steps[0].approval_deadline_at.expect("timer is armed");
3570 assert!(deadline >= before + TimeDelta::seconds(3600));
3571 assert!(deadline <= Utc::now() + TimeDelta::seconds(3600));
3572 assert_eq!(steps[0].approval_stage, 0);
3573 assert_eq!(
3574 steps[0].approval_assignee,
3575 Some(Assignee::group("release-managers"))
3576 );
3577 }
3578
3579 #[tokio::test]
3580 async fn approval_honours_the_legacy_timeout_seconds() {
3581 let (store, mut ctx) = context_with_run().await;
3582
3583 ctx.approval(
3584 "gate",
3585 ApprovalConfig::new("Approve?").with_timeout_seconds(60),
3586 )
3587 .await
3588 .expect_err("approval suspends the run");
3589
3590 let steps = store.list_steps(ctx.run_id()).await.expect("list steps");
3591 assert!(steps[0].approval_deadline_at.is_some());
3592 }
3593
3594 #[tokio::test]
3595 async fn approval_replay_clears_deadline() {
3596 let (store, mut ctx) = context_with_run().await;
3597
3598 ctx.approval(
3599 "gate",
3600 ApprovalConfig::new("Approve?").with_deadline_secs(3600),
3601 )
3602 .await
3603 .expect_err("approval suspends the run");
3604
3605 let mut resumed = WorkflowContext::new(
3607 ctx.run_id(),
3608 "test".to_string(),
3609 store.clone(),
3610 create_test_provider(),
3611 );
3612 resumed
3613 .load_replay_steps()
3614 .await
3615 .expect("load replay steps");
3616 resumed
3617 .approval(
3618 "gate",
3619 ApprovalConfig::new("Approve?").with_deadline_secs(3600),
3620 )
3621 .await
3622 .expect("replayed gate continues");
3623
3624 let steps = store.list_steps(ctx.run_id()).await.expect("list steps");
3625 assert_eq!(steps[0].status.state, StepStatus::Completed);
3626 assert!(steps[0].approval_deadline_at.is_none());
3627 }
3628}