1use std::collections::HashMap;
27use std::fmt;
28use std::sync::Arc;
29use std::time::Instant;
30
31use chrono::{DateTime, Utc};
32use futures_util::StreamExt;
33use rust_decimal::Decimal;
34use serde_json::{Value, json};
35use tokio::task::{Id, JoinSet};
36use tracing::{Span, error, info, warn};
37use uuid::Uuid;
38
39use ironflow_core::error::{AgentError, OperationError};
40use ironflow_core::provider::AgentProvider;
41use ironflow_core::trace_context::WorkflowTraceContext;
42use ironflow_store::models::{
43 ArtifactLookup, NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind,
44 StepStatus, StepUpdate, TriggerKind, step_trace_id,
45};
46use ironflow_store::store::Store;
47
48use ironflow_artifacts::name::guess_content_type;
49use ironflow_artifacts::stream_from_bytes;
50use ironflow_store::entities::Artifact;
51
52use crate::artifact::{
53 ArtifactSink, ArtifactUpload, StepLocation, collect_outputs, materialize_inputs,
54};
55use crate::budget::step_budget_usd;
56use crate::config::{
57 AgentStepConfig, ApprovalConfig, HttpConfig, ShellConfig, StepConfig, WorkflowStepConfig,
58};
59use crate::error::EngineError;
60use crate::executor::{ParallelStepResult, StepOutput, StepResult, execute_step_config};
61use crate::guard::{SharedGuardState, WorkflowGuardConfig, WorkflowRejection};
62use crate::handler::WorkflowHandler;
63use crate::log_sender::{LogSender, StepLogSender};
64#[cfg(not(feature = "secret-store"))]
65use crate::operation::NoopSecretResolver;
66use crate::operation::{Operation, OperationContext, SecretResolver};
67#[cfg(feature = "secret-store")]
68use ironflow_store::workflow_secrets::ScopedSecretStore;
69
70pub(crate) type HandlerResolver =
72 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
73
74pub struct WorkflowContext {
93 run_id: Uuid,
94 workflow_name: String,
95 store: Arc<dyn Store>,
96 provider: Arc<dyn AgentProvider>,
97 handler_resolver: Option<HandlerResolver>,
98 position: u32,
99 last_step_ids: Vec<Uuid>,
101 total_cost_usd: Decimal,
103 total_duration_ms: u64,
105 max_cost_usd: Option<Decimal>,
107 inherited_cost_usd: Decimal,
110 replay_steps: HashMap<u32, Step>,
113 granted_approvals: HashMap<u32, u32>,
117 attempt: u32,
119 carried_duration_ms: u64,
122 log_sender: Option<LogSender>,
124 artifact_sink: Option<Arc<dyn ArtifactSink>>,
128 has_allowed_failure: bool,
130 error_handlers: Vec<OnErrorHandler>,
132 guard_state: Option<SharedGuardState>,
134 guard_config: Option<WorkflowGuardConfig>,
136 step_results: Vec<StepResult>,
138 event_bus: Option<crate::notify::WorkflowEventBus>,
140 trace_context: WorkflowTraceContext,
142 operation_ctx: Option<OperationContext>,
144}
145
146struct OnErrorHandler {
148 name: String,
149 config: StepConfig,
150}
151
152impl WorkflowContext {
153 pub fn new(
158 run_id: Uuid,
159 workflow_name: String,
160 store: Arc<dyn Store>,
161 provider: Arc<dyn AgentProvider>,
162 ) -> Self {
163 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
164 Self {
165 run_id,
166 workflow_name,
167 store,
168 provider,
169 handler_resolver: None,
170 position: 0,
171 last_step_ids: Vec::new(),
172 total_cost_usd: Decimal::ZERO,
173 total_duration_ms: 0,
174 max_cost_usd: None,
175 inherited_cost_usd: Decimal::ZERO,
176 replay_steps: HashMap::new(),
177 granted_approvals: HashMap::new(),
178 attempt: 1,
179 carried_duration_ms: 0,
180 log_sender: None,
181 artifact_sink: None,
182 has_allowed_failure: false,
183 error_handlers: Vec::new(),
184 guard_state: None,
185 guard_config: None,
186 step_results: Vec::new(),
187 event_bus: None,
188 trace_context,
189 operation_ctx: None,
190 }
191 }
192
193 pub(crate) fn with_handler_resolver(
198 run_id: Uuid,
199 workflow_name: String,
200 store: Arc<dyn Store>,
201 provider: Arc<dyn AgentProvider>,
202 resolver: HandlerResolver,
203 ) -> Self {
204 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
205 Self {
206 run_id,
207 workflow_name,
208 store,
209 provider,
210 handler_resolver: Some(resolver),
211 position: 0,
212 last_step_ids: Vec::new(),
213 total_cost_usd: Decimal::ZERO,
214 total_duration_ms: 0,
215 max_cost_usd: None,
216 inherited_cost_usd: Decimal::ZERO,
217 replay_steps: HashMap::new(),
218 granted_approvals: HashMap::new(),
219 attempt: 1,
220 carried_duration_ms: 0,
221 log_sender: None,
222 artifact_sink: None,
223 has_allowed_failure: false,
224 error_handlers: Vec::new(),
225 guard_state: None,
226 guard_config: None,
227 step_results: Vec::new(),
228 event_bus: None,
229 trace_context,
230 operation_ctx: None,
231 }
232 }
233
234 pub fn set_log_sender(&mut self, sender: LogSender) {
236 self.log_sender = Some(sender);
237 }
238
239 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
259 self.artifact_sink = Some(sink);
260 }
261
262 pub fn trace_context(&self) -> &WorkflowTraceContext {
268 &self.trace_context
269 }
270
271 pub fn set_guard(&mut self, config: WorkflowGuardConfig, state: SharedGuardState) {
288 self.guard_config = Some(config);
289 self.guard_state = Some(state);
290 }
291
292 pub fn guard_config(&self) -> Option<&WorkflowGuardConfig> {
294 self.guard_config.as_ref()
295 }
296
297 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
303 self.event_bus = Some(bus);
304 }
305
306 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
308 self.artifact_sink.as_ref().ok_or_else(|| {
309 EngineError::ArtifactsUnavailable(
310 "no artifact storage is attached to this run".to_string(),
311 )
312 })
313 }
314
315 pub async fn put_artifact(
345 &self,
346 step_id: Uuid,
347 name: &str,
348 content_type: Option<&str>,
349 content: Vec<u8>,
350 ) -> Result<Artifact, EngineError> {
351 let sink = self.artifact_sink()?;
352 sink.put(
353 ArtifactUpload {
354 run_id: self.run_id,
355 step_id,
356 name: name.to_string(),
357 content_type: content_type
358 .map(str::to_string)
359 .unwrap_or_else(|| guess_content_type(name)),
360 },
361 stream_from_bytes(content),
362 )
363 .await
364 }
365
366 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
391 let sink = self.artifact_sink()?;
392
393 let artifact = self
394 .store
395 .find_artifact_for_input(ArtifactLookup {
396 run_id: self.run_id,
397 attempt: self.attempt,
398 before_position: self.position,
399 step_name: step.to_string(),
400 name: name.to_string(),
401 })
402 .await?
403 .ok_or_else(|| EngineError::ArtifactNotFound {
404 step: step.to_string(),
405 name: name.to_string(),
406 })?;
407
408 let mut content = sink.get(&artifact).await?;
409 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
410 while let Some(chunk) = content.next().await {
411 let chunk = chunk?;
412 buffer.extend_from_slice(chunk.as_ref());
413 }
414
415 Ok(buffer)
416 }
417
418 async fn prepare_step_inputs(
423 &self,
424 config: &StepConfig,
425 position: u32,
426 ) -> Result<(), EngineError> {
427 let StepConfig::Shell(shell) = config else {
428 return Ok(());
429 };
430 if shell.inputs.is_empty() {
431 return Ok(());
432 }
433
434 materialize_inputs(
435 self.artifact_sink()?,
436 &self.store,
437 shell,
438 StepLocation {
439 run_id: self.run_id,
440 attempt: self.attempt,
441 position,
442 },
443 )
444 .await
445 }
446
447 async fn store_step_outputs(
452 &self,
453 config: &StepConfig,
454 step_id: Uuid,
455 step_name: &str,
456 step_succeeded: bool,
457 ) -> Result<(), EngineError> {
458 let StepConfig::Shell(shell) = config else {
459 return Ok(());
460 };
461 if shell.outputs.is_empty() {
462 return Ok(());
463 }
464
465 let sink = match self.artifact_sink() {
466 Ok(sink) => sink,
467 Err(err) if step_succeeded => return Err(err),
468 Err(err) => {
469 warn!(
470 run_id = %self.run_id,
471 step = %step_name,
472 error = %err,
473 "cannot collect outputs of a failed step"
474 );
475 return Ok(());
476 }
477 };
478
479 let collected =
480 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
481
482 match collected {
483 Ok(()) => Ok(()),
484 Err(err) if step_succeeded => Err(err),
485 Err(err) => {
486 warn!(
487 run_id = %self.run_id,
488 step = %step_name,
489 error = %err,
490 "failed to collect outputs of a failed step"
491 );
492 Ok(())
493 }
494 }
495 }
496
497 pub(crate) fn carry_over_run_totals(
504 &mut self,
505 attempt: u32,
506 cost_usd: Decimal,
507 duration_ms: u64,
508 ) {
509 self.attempt = attempt;
510 self.total_cost_usd = cost_usd;
511 self.carried_duration_ms = duration_ms;
512 }
513
514 pub(crate) fn carried_duration_ms(&self) -> u64 {
516 self.carried_duration_ms
517 }
518
519 pub fn attempt(&self) -> u32 {
521 self.attempt
522 }
523
524 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
540 self.max_cost_usd = cap;
541 }
542
543 pub fn max_cost_usd(&self) -> Option<Decimal> {
545 self.max_cost_usd
546 }
547
548 pub fn charged_cost_usd(&self) -> Decimal {
553 self.inherited_cost_usd + self.total_cost_usd
554 }
555
556 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
567 let Some(limit) = self.max_cost_usd else {
568 return Ok(());
569 };
570
571 let spent = self.charged_cost_usd();
572 if spent + step_budget <= limit {
573 return Ok(());
574 }
575
576 error!(
577 run_id = %self.run_id,
578 limit_usd = %limit,
579 spent_usd = %spent,
580 step_budget_usd = %step_budget,
581 "run cost cap reached, refusing agent step"
582 );
583
584 Err(EngineError::RunBudgetExceeded {
585 run_id: self.run_id,
586 limit_usd: limit,
587 spent_usd: spent,
588 step_budget_usd: step_budget,
589 })
590 }
591
592 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
604 let steps = self.store.list_steps(self.run_id).await?;
605 for step in steps {
606 let dominated = matches!(
607 step.status.state,
608 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
609 );
610 if !dominated {
611 continue;
612 }
613
614 if step.attempt == self.attempt {
615 self.replay_steps.insert(step.position, step);
616 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
617 {
618 self.granted_approvals.insert(step.position, step.attempt);
619 }
620 }
621 Ok(())
622 }
623
624 pub fn run_id(&self) -> Uuid {
626 self.run_id
627 }
628
629 pub fn workflow_name(&self) -> &str {
631 &self.workflow_name
632 }
633
634 pub fn total_cost_usd(&self) -> Decimal {
636 self.total_cost_usd
637 }
638
639 pub fn has_allowed_failure(&self) -> bool {
641 self.has_allowed_failure
642 }
643
644 pub fn total_duration_ms(&self) -> u64 {
646 self.total_duration_ms
647 }
648
649 pub fn step_results(&self) -> &[StepResult] {
651 &self.step_results
652 }
653
654 #[cfg(feature = "secret-store")]
676 pub fn secrets(&self) -> ironflow_store::workflow_secrets::ScopedSecretStore {
677 let workflow_uuid = Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
678 ironflow_store::workflow_secrets::ScopedSecretStore::for_workflow(
679 workflow_uuid,
680 self.store.clone(),
681 )
682 }
683
684 fn ensure_operation_ctx(&mut self) -> &OperationContext {
685 self.operation_ctx.get_or_insert_with(|| {
686 #[cfg(feature = "secret-store")]
687 let secrets: Arc<dyn SecretResolver> = {
688 let workflow_uuid =
689 Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
690 Arc::new(ScopedSecretStore::for_workflow(
691 workflow_uuid,
692 self.store.clone(),
693 ))
694 };
695 #[cfg(not(feature = "secret-store"))]
696 let secrets: Arc<dyn SecretResolver> = Arc::new(NoopSecretResolver);
697
698 OperationContext::new(secrets)
699 })
700 }
701
702 async fn persist_progress(&self) {
708 if let Err(err) = self
709 .store
710 .update_run(
711 self.run_id,
712 RunUpdate {
713 cost_usd: Some(self.total_cost_usd),
714 duration_ms: Some(self.total_duration_ms),
715 ..RunUpdate::default()
716 },
717 )
718 .await
719 {
720 warn!(
721 run_id = %self.run_id,
722 error = %err,
723 "failed to persist run progress snapshot"
724 );
725 }
726 }
727
728 pub async fn parallel(
765 &mut self,
766 steps: Vec<(&str, StepConfig)>,
767 fail_fast: bool,
768 ) -> Result<Vec<ParallelStepResult>, EngineError> {
769 if steps.is_empty() {
770 return Ok(Vec::new());
771 }
772
773 self.check_guard_timeout()?;
775
776 let wave_budget: Decimal = steps
779 .iter()
780 .filter_map(|(_, config)| match config {
781 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
782 _ => None,
783 })
784 .map(step_budget_usd)
785 .sum();
786 self.check_run_budget(wave_budget)?;
787
788 let wave_position = self.position;
789 self.position += 1;
790
791 let now = Utc::now();
792 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
793 Vec::with_capacity(steps.len());
794
795 for (name, config) in &steps {
796 let kind = config.kind();
797 let trace_id = step_trace_id(self.run_id, name, wave_position);
798 let step = self
799 .store
800 .create_step(NewStep {
801 run_id: self.run_id,
802 trace_id,
803 name: name.to_string(),
804 kind,
805 position: wave_position,
806 input: Some(serde_json::to_value(config)?),
807 is_error_handler: false,
808 })
809 .await?;
810
811 self.start_step(step.id, now).await?;
812
813 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
816 self.fail_step(step.id, &err).await;
817 if !config.allow_failure() {
818 return Err(err);
819 }
820 self.has_allowed_failure = true;
821 info!(
822 run_id = %self.run_id,
823 step = %name,
824 error = %err,
825 "parallel step input preparation failed but allow_failure is set, skipping"
826 );
827 continue;
828 }
829
830 let mut config_with_trace = config.clone();
831 let step_trace = self.trace_context.child();
832 match config_with_trace {
833 StepConfig::Agent(ref mut agent_config) => {
834 agent_config.trace_context = Some(step_trace);
835 }
836 StepConfig::Http(ref mut http_config) => {
837 http_config.trace_context = Some(step_trace);
838 }
839 _ => {}
840 }
841 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
842 }
843
844 let mut join_set = JoinSet::new();
845 let mut task_index: HashMap<Id, usize> = HashMap::new();
846 let parallel_timeout = self.guard_remaining_timeout();
847 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
848 let provider = self.provider.clone();
849 let config = config.clone();
850 let step_log_sender = self
851 .log_sender
852 .as_ref()
853 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
854 let handle = join_set.spawn(async move {
855 let result = match parallel_timeout {
856 Some(dur) => {
857 match tokio::time::timeout(
858 dur,
859 execute_step_config(&config, &provider, step_log_sender),
860 )
861 .await
862 {
863 Ok(r) => r,
864 Err(_elapsed) => {
865 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
866 elapsed_secs: 0,
867 max: 0,
868 }))
869 }
870 }
871 }
872 None => execute_step_config(&config, &provider, step_log_sender).await,
873 };
874 (idx, result)
875 });
876 task_index.insert(handle.id(), idx);
877 }
878
879 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
881 vec![None; step_records.len()];
882 let mut first_error: Option<EngineError> = None;
883
884 while let Some(join_result) = join_set.join_next().await {
885 let (idx, step_result) = match join_result {
886 Ok(r) => r,
887 Err(e) => {
888 let error_msg = format!("join error: {e}");
889 if let Some(&idx) = task_index.get(&e.id()) {
890 let (step_id, _, step_name, _) = &step_records[idx];
891 let completed_at = Utc::now();
892 error!(
893 run_id = %self.run_id,
894 step = %step_name,
895 error = %error_msg,
896 "parallel step panicked or was cancelled"
897 );
898 if let Err(store_err) = self
899 .store
900 .update_step(
901 *step_id,
902 StepUpdate {
903 status: Some(StepStatus::Failed),
904 error: Some(error_msg.clone()),
905 completed_at: Some(completed_at),
906 ..StepUpdate::default()
907 },
908 )
909 .await
910 {
911 error!(
912 run_id = %self.run_id,
913 step_id = %step_id,
914 error = %store_err,
915 "failed to persist JoinError for step"
916 );
917 }
918 indexed_results[idx] = Some(Err(error_msg.clone()));
919 }
920 if first_error.is_none() {
921 first_error = Some(EngineError::StepConfig(error_msg));
922 }
923 if fail_fast {
924 join_set.abort_all();
925 }
926 continue;
927 }
928 };
929
930 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
931 let completed_at = Utc::now();
932
933 if let Err(err) = self
934 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
935 .await
936 {
937 self.fail_step(*step_id, &err).await;
938 indexed_results[idx] = Some(Err(err.to_string()));
939 if first_error.is_none() {
940 first_error = Some(err);
941 }
942 if fail_fast {
943 join_set.abort_all();
944 }
945 continue;
946 }
947
948 match step_result {
949 Ok(output) => {
950 self.total_cost_usd += output.cost_usd;
951 self.total_duration_ms += output.duration_ms;
952
953 if matches!(step_config, StepConfig::Agent(_)) {
955 let tokens = output
956 .input_tokens
957 .unwrap_or(0)
958 .saturating_add(output.output_tokens.unwrap_or(0));
959 if tokens > 0
960 && let Err(guard_err) = self.guard_record_tokens(tokens)
961 {
962 if first_error.is_none() {
963 first_error = Some(guard_err);
964 }
965 if fail_fast {
966 join_set.abort_all();
967 }
968 }
969 }
970
971 let debug_messages_json = output.debug_messages_json();
972
973 self.store
974 .update_step(
975 *step_id,
976 StepUpdate {
977 status: Some(StepStatus::Completed),
978 output: Some(output.output.clone()),
979 duration_ms: Some(output.duration_ms),
980 cost_usd: Some(output.cost_usd),
981 input_tokens: output.input_tokens,
982 output_tokens: output.output_tokens,
983 completed_at: Some(completed_at),
984 debug_messages: debug_messages_json,
985 ..StepUpdate::default()
986 },
987 )
988 .await?;
989
990 self.step_results.push(StepResult::from_success(
991 *step_trace,
992 step_name,
993 &output,
994 ));
995
996 if let Some(ref bus) = self.event_bus
997 && matches!(step_config, StepConfig::Agent(_))
998 {
999 let tokens = output
1000 .input_tokens
1001 .unwrap_or(0)
1002 .saturating_add(output.output_tokens.unwrap_or(0));
1003 bus.publish(
1004 self.run_id,
1005 crate::notify::WorkflowEvent::AgentStepTokensUsed {
1006 step_name: step_name.clone(),
1007 tokens,
1008 cost_usd: output.cost_usd,
1009 },
1010 );
1011 }
1012
1013 info!(
1014 run_id = %self.run_id,
1015 step = %step_name,
1016 trace_id = %step_trace,
1017 duration_ms = output.duration_ms,
1018 "parallel step completed"
1019 );
1020
1021 indexed_results[idx] = Some(Ok(output));
1022 }
1023 Err(err) => {
1024 let err_msg = err.to_string();
1025 let debug_messages_json = extract_debug_messages_from_error(&err);
1026 let partial = extract_partial_usage_from_error(&err);
1027 let raw_response_output = extract_raw_response_from_error(&err);
1028
1029 if let Some(ref usage) = partial {
1030 if let Some(cost) = usage.cost_usd {
1031 self.total_cost_usd += cost;
1032 }
1033 if let Some(dur) = usage.duration_ms {
1034 self.total_duration_ms += dur;
1035 }
1036 }
1037
1038 if let Err(store_err) = self
1039 .store
1040 .update_step(
1041 *step_id,
1042 StepUpdate {
1043 status: Some(StepStatus::Failed),
1044 error: Some(err_msg.clone()),
1045 output: raw_response_output.clone(),
1046 completed_at: Some(completed_at),
1047 debug_messages: debug_messages_json,
1048 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1049 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1050 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1051 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1052 ..StepUpdate::default()
1053 },
1054 )
1055 .await
1056 {
1057 tracing::error!(
1058 step_id = %step_id,
1059 error = %store_err,
1060 "failed to persist parallel step failure"
1061 );
1062 }
1063
1064 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1065 let err_cost = partial
1066 .as_ref()
1067 .and_then(|p| p.cost_usd)
1068 .unwrap_or(Decimal::ZERO);
1069 self.step_results.push(StepResult::from_failure(
1070 *step_trace,
1071 step_name,
1072 &err_msg,
1073 err_duration,
1074 err_cost,
1075 ));
1076
1077 if step_config.allow_failure() {
1078 self.has_allowed_failure = true;
1079 info!(
1080 run_id = %self.run_id,
1081 step = %step_name,
1082 error = %err_msg,
1083 "parallel step failed but allow_failure is set, continuing"
1084 );
1085 indexed_results[idx] = Some(Ok(allowed_failure_output(
1086 &err_msg,
1087 raw_response_output,
1088 partial.as_ref(),
1089 )));
1090 } else {
1091 indexed_results[idx] = Some(Err(err_msg.clone()));
1092
1093 if first_error.is_none() {
1094 first_error = Some(err);
1095 }
1096
1097 if fail_fast {
1098 join_set.abort_all();
1099 }
1100 }
1101 }
1102 }
1103 }
1104
1105 if let Some(err) = first_error {
1106 return Err(err);
1107 }
1108
1109 self.persist_progress().await;
1110
1111 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
1112
1113 let results: Vec<ParallelStepResult> = step_records
1115 .iter()
1116 .enumerate()
1117 .map(|(idx, (step_id, _trace_id, name, _))| {
1118 let output = match indexed_results[idx].take() {
1119 Some(Ok(o)) => o,
1120 _ => unreachable!("all steps succeeded if no error returned"),
1121 };
1122 ParallelStepResult {
1123 name: name.clone(),
1124 output,
1125 step_id: *step_id,
1126 }
1127 })
1128 .collect();
1129
1130 Ok(results)
1131 }
1132
1133 pub async fn shell(
1156 &mut self,
1157 name: &str,
1158 config: ShellConfig,
1159 ) -> Result<StepOutput, EngineError> {
1160 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1161 .await
1162 }
1163
1164 pub async fn http(
1184 &mut self,
1185 name: &str,
1186 config: HttpConfig,
1187 ) -> Result<StepOutput, EngineError> {
1188 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1189 .await
1190 }
1191
1192 pub async fn agent(
1212 &mut self,
1213 name: &str,
1214 config: impl Into<AgentStepConfig>,
1215 ) -> Result<StepOutput, EngineError> {
1216 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1217 .await
1218 }
1219
1220 pub async fn approval(
1251 &mut self,
1252 name: &str,
1253 config: ApprovalConfig,
1254 ) -> Result<(), EngineError> {
1255 let position = self.position;
1256 self.position += 1;
1257
1258 if let Some(existing) = self.replay_steps.get(&position)
1261 && existing.kind == StepKind::Approval
1262 {
1263 if existing.status.state == StepStatus::AwaitingApproval {
1264 self.store
1265 .update_step(
1266 existing.id,
1267 StepUpdate {
1268 status: Some(StepStatus::Completed),
1269 completed_at: Some(Utc::now()),
1270 ..StepUpdate::default()
1271 },
1272 )
1273 .await?;
1274 }
1275
1276 self.last_step_ids = vec![existing.id];
1277 info!(
1278 run_id = %self.run_id,
1279 step = %name,
1280 position,
1281 "approval step replayed (approved)"
1282 );
1283 return Ok(());
1284 }
1285
1286 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1290 let trace_id = step_trace_id(self.run_id, name, position);
1291 let step = self
1292 .store
1293 .create_step(NewStep {
1294 run_id: self.run_id,
1295 trace_id,
1296 name: name.to_string(),
1297 kind: StepKind::Approval,
1298 position,
1299 input: Some(serde_json::to_value(&config)?),
1300 is_error_handler: false,
1301 })
1302 .await?;
1303
1304 let now = Utc::now();
1305 self.start_step(step.id, now).await?;
1306 self.store
1307 .update_step(
1308 step.id,
1309 StepUpdate {
1310 status: Some(StepStatus::Completed),
1311 output: Some(json!({"approved_in_attempt": granted_in})),
1312 completed_at: Some(now),
1313 ..StepUpdate::default()
1314 },
1315 )
1316 .await?;
1317
1318 self.last_step_ids = vec![step.id];
1319 info!(
1320 run_id = %self.run_id,
1321 step = %name,
1322 position,
1323 granted_in_attempt = granted_in,
1324 attempt = self.attempt,
1325 "approval carried over from a previous attempt"
1326 );
1327 return Ok(());
1328 }
1329
1330 let trace_id = step_trace_id(self.run_id, name, position);
1332 let step = self
1333 .store
1334 .create_step(NewStep {
1335 run_id: self.run_id,
1336 trace_id,
1337 name: name.to_string(),
1338 kind: StepKind::Approval,
1339 position,
1340 input: Some(serde_json::to_value(&config)?),
1341 is_error_handler: false,
1342 })
1343 .await?;
1344
1345 self.start_step(step.id, Utc::now()).await?;
1346
1347 self.store
1350 .update_step(
1351 step.id,
1352 StepUpdate {
1353 status: Some(StepStatus::AwaitingApproval),
1354 ..StepUpdate::default()
1355 },
1356 )
1357 .await?;
1358
1359 self.last_step_ids = vec![step.id];
1360
1361 if let Some(ref bus) = self.event_bus {
1362 bus.publish(
1363 self.run_id,
1364 crate::notify::WorkflowEvent::ApprovalRequired {
1365 step_name: name.to_string(),
1366 step_index: position,
1367 approval_id: step.id,
1368 },
1369 );
1370 }
1371
1372 Err(EngineError::ApprovalRequired {
1373 run_id: self.run_id,
1374 step_id: step.id,
1375 message: config.message().to_string(),
1376 })
1377 }
1378
1379 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1408 let position = self.position;
1409 self.position += 1;
1410
1411 let trace_id = step_trace_id(self.run_id, name, position);
1412 let step = self
1413 .store
1414 .create_step(NewStep {
1415 run_id: self.run_id,
1416 trace_id,
1417 name: name.to_string(),
1418 kind: StepKind::Custom("skip".to_string()),
1419 position,
1420 input: None,
1421 is_error_handler: false,
1422 })
1423 .await?;
1424
1425 if !self.last_step_ids.is_empty() {
1426 let deps: Vec<NewStepDependency> = self
1427 .last_step_ids
1428 .iter()
1429 .map(|&depends_on| NewStepDependency {
1430 step_id: step.id,
1431 depends_on,
1432 })
1433 .collect();
1434 self.store.create_step_dependencies(deps).await?;
1435 }
1436
1437 let now = Utc::now();
1438 self.store
1439 .update_step(
1440 step.id,
1441 StepUpdate {
1442 status: Some(StepStatus::Skipped),
1443 output: Some(serde_json::json!({"reason": reason})),
1444 completed_at: Some(now),
1445 ..StepUpdate::default()
1446 },
1447 )
1448 .await?;
1449
1450 self.last_step_ids = vec![step.id];
1451
1452 info!(
1453 run_id = %self.run_id,
1454 step = %name,
1455 reason,
1456 "step skipped"
1457 );
1458
1459 Ok(())
1460 }
1461
1462 pub async fn operation(
1501 &mut self,
1502 name: &str,
1503 op: &dyn Operation,
1504 ) -> Result<StepOutput, EngineError> {
1505 let kind = StepKind::Custom(op.kind().to_string());
1506 let position = self.position;
1507 self.position += 1;
1508
1509 let trace_id = step_trace_id(self.run_id, name, position);
1510 let step = self
1511 .store
1512 .create_step(NewStep {
1513 run_id: self.run_id,
1514 trace_id,
1515 name: name.to_string(),
1516 kind,
1517 position,
1518 input: op.input(),
1519 is_error_handler: false,
1520 })
1521 .await?;
1522
1523 self.start_step(step.id, Utc::now()).await?;
1524
1525 let start = Instant::now();
1526
1527 let op_ctx = self.ensure_operation_ctx();
1528
1529 match op.execute(op_ctx).await {
1530 Ok(output_value) => {
1531 let duration_ms = start.elapsed().as_millis() as u64;
1532 self.total_duration_ms += duration_ms;
1533
1534 let completed_at = Utc::now();
1535 self.store
1536 .update_step(
1537 step.id,
1538 StepUpdate {
1539 status: Some(StepStatus::Completed),
1540 output: Some(output_value.clone()),
1541 duration_ms: Some(duration_ms),
1542 cost_usd: Some(Decimal::ZERO),
1543 completed_at: Some(completed_at),
1544 ..StepUpdate::default()
1545 },
1546 )
1547 .await?;
1548
1549 info!(
1550 run_id = %self.run_id,
1551 step = %name,
1552 kind = op.kind(),
1553 duration_ms,
1554 "operation step completed"
1555 );
1556
1557 self.last_step_ids = vec![step.id];
1558
1559 Ok(StepOutput {
1560 output: output_value,
1561 duration_ms,
1562 cost_usd: Decimal::ZERO,
1563 input_tokens: None,
1564 output_tokens: None,
1565 model: None,
1566 debug_messages: None,
1567 })
1568 }
1569 Err(err) => {
1570 let completed_at = Utc::now();
1571 let engine_err = EngineError::Operation(err);
1572 if let Err(store_err) = self
1573 .store
1574 .update_step(
1575 step.id,
1576 StepUpdate {
1577 status: Some(StepStatus::Failed),
1578 error: Some(engine_err.to_string()),
1579 completed_at: Some(completed_at),
1580 ..StepUpdate::default()
1581 },
1582 )
1583 .await
1584 {
1585 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1586 }
1587
1588 Err(engine_err)
1589 }
1590 }
1591 }
1592
1593 pub async fn workflow(
1620 &mut self,
1621 handler: &dyn WorkflowHandler,
1622 payload: Value,
1623 ) -> Result<StepOutput, EngineError> {
1624 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1626 let state = guard_state
1627 .lock()
1628 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1629 state.check(guard_config, handler.name())?;
1630 }
1631
1632 let config = WorkflowStepConfig::new(handler.name(), payload);
1633 let position = self.position;
1634 self.position += 1;
1635
1636 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1637 let step = self
1638 .store
1639 .create_step(NewStep {
1640 run_id: self.run_id,
1641 trace_id,
1642 name: config.workflow_name.clone(),
1643 kind: StepKind::Workflow,
1644 position,
1645 input: Some(serde_json::to_value(&config)?),
1646 is_error_handler: false,
1647 })
1648 .await?;
1649
1650 self.start_step(step.id, Utc::now()).await?;
1651
1652 if let Some(guard_state) = &self.guard_state {
1654 let mut state = guard_state
1655 .lock()
1656 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1657 state.record_invocation(handler.name());
1658 }
1659
1660 match self.execute_child_workflow(&config).await {
1661 Ok((output, child_had_allowed_failure)) => {
1662 self.total_cost_usd += output.cost_usd;
1663 self.total_duration_ms += output.duration_ms;
1664 if child_had_allowed_failure {
1665 self.has_allowed_failure = true;
1666 }
1667
1668 let completed_at = Utc::now();
1669 self.store
1670 .update_step(
1671 step.id,
1672 StepUpdate {
1673 status: Some(StepStatus::Completed),
1674 output: Some(output.output.clone()),
1675 duration_ms: Some(output.duration_ms),
1676 cost_usd: Some(output.cost_usd),
1677 completed_at: Some(completed_at),
1678 ..StepUpdate::default()
1679 },
1680 )
1681 .await?;
1682
1683 info!(
1684 run_id = %self.run_id,
1685 child_workflow = %config.workflow_name,
1686 duration_ms = output.duration_ms,
1687 "workflow step completed"
1688 );
1689
1690 self.last_step_ids = vec![step.id];
1691
1692 self.guard_record_return();
1693 Ok(output)
1694 }
1695 Err(err) => {
1696 let completed_at = Utc::now();
1697 if let Err(store_err) = self
1698 .store
1699 .update_step(
1700 step.id,
1701 StepUpdate {
1702 status: Some(StepStatus::Failed),
1703 error: Some(err.to_string()),
1704 completed_at: Some(completed_at),
1705 ..StepUpdate::default()
1706 },
1707 )
1708 .await
1709 {
1710 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1711 }
1712
1713 self.guard_record_return();
1714 Err(err)
1715 }
1716 }
1717 }
1718
1719 fn guard_record_return(&self) {
1724 if let Some(guard_state) = &self.guard_state {
1725 match guard_state.lock() {
1726 Ok(mut state) => state.record_return(),
1727 Err(_) => {
1728 error!(
1729 run_id = %self.run_id,
1730 "guard state mutex poisoned in record_return"
1731 );
1732 }
1733 }
1734 }
1735 }
1736
1737 async fn execute_with_guard_timeout(
1741 &self,
1742 config: &StepConfig,
1743 step_log_sender: Option<StepLogSender>,
1744 ) -> Result<StepOutput, EngineError> {
1745 let remaining = self.guard_remaining_timeout();
1746 match remaining {
1747 Some(dur) => {
1748 use tokio::time::timeout;
1749 match timeout(
1750 dur,
1751 execute_step_config(config, &self.provider, step_log_sender),
1752 )
1753 .await
1754 {
1755 Ok(result) => result,
1756 Err(_elapsed) => {
1757 let config_secs = self
1758 .guard_config
1759 .as_ref()
1760 .map_or(0, |c| c.workflow_timeout_secs);
1761 Err(WorkflowRejection::WorkflowTimeout {
1762 elapsed_secs: config_secs,
1763 max: config_secs,
1764 }
1765 .into())
1766 }
1767 }
1768 }
1769 None => execute_step_config(config, &self.provider, step_log_sender).await,
1770 }
1771 }
1772
1773 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1775 let config = self.guard_config.as_ref()?;
1776 let guard_state = self.guard_state.as_ref()?;
1777 let state = guard_state.lock().ok()?;
1778 let elapsed = state.elapsed_secs();
1779 let max = config.workflow_timeout_secs;
1780 if elapsed >= max {
1781 Some(std::time::Duration::ZERO)
1782 } else {
1783 Some(std::time::Duration::from_secs(max - elapsed))
1784 }
1785 }
1786
1787 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1789 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1790 let state = guard_state
1791 .lock()
1792 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1793 let elapsed = state.elapsed_secs();
1794 if elapsed >= config.workflow_timeout_secs {
1795 return Err(WorkflowRejection::WorkflowTimeout {
1796 elapsed_secs: elapsed,
1797 max: config.workflow_timeout_secs,
1798 }
1799 .into());
1800 }
1801 }
1802 Ok(())
1803 }
1804
1805 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1807 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1808 let mut state = guard_state
1809 .lock()
1810 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1811 state.record_tokens(config, tokens)?;
1812 }
1813 Ok(())
1814 }
1815
1816 async fn execute_child_workflow(
1819 &self,
1820 config: &WorkflowStepConfig,
1821 ) -> Result<(StepOutput, bool), EngineError> {
1822 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1823 EngineError::InvalidWorkflow(
1824 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1825 )
1826 })?;
1827
1828 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1829 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1830 })?;
1831
1832 let parent = self.store.get_run(self.run_id).await?;
1835 let (parent_labels, parent_author) =
1836 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1837
1838 let child_run = self
1839 .store
1840 .create_run(NewRun {
1841 workflow_name: config.workflow_name.clone(),
1842 trigger: TriggerKind::Workflow,
1843 payload: config.payload.clone(),
1844 max_retries: 0,
1845 handler_version: None,
1846 labels: parent_labels,
1847 scheduled_at: None,
1848 created_by: parent_author,
1849 idempotency_key: None,
1850 max_cost_usd: self.max_cost_usd,
1852 })
1853 .await?
1854 .into_run();
1855
1856 let child_run_id = child_run.id;
1857 info!(
1858 parent_run_id = %self.run_id,
1859 child_run_id = %child_run_id,
1860 workflow = %config.workflow_name,
1861 "child run created"
1862 );
1863
1864 self.store
1865 .update_run_status(child_run_id, RunStatus::Running)
1866 .await?;
1867
1868 let run_start = Instant::now();
1869 let mut child_ctx = WorkflowContext {
1870 run_id: child_run_id,
1871 workflow_name: config.workflow_name.clone(),
1872 store: self.store.clone(),
1873 provider: self.provider.clone(),
1874 handler_resolver: self.handler_resolver.clone(),
1875 position: 0,
1876 last_step_ids: Vec::new(),
1877 total_cost_usd: Decimal::ZERO,
1878 total_duration_ms: 0,
1879 max_cost_usd: self.max_cost_usd,
1880 inherited_cost_usd: self.charged_cost_usd(),
1883 replay_steps: HashMap::new(),
1884 granted_approvals: HashMap::new(),
1885 attempt: 1,
1887 carried_duration_ms: 0,
1888 log_sender: self.log_sender.clone(),
1889 artifact_sink: self.artifact_sink.clone(),
1892 has_allowed_failure: false,
1893 error_handlers: Vec::new(),
1894 guard_state: self.guard_state.clone(),
1895 guard_config: self.guard_config.clone(),
1896 step_results: Vec::new(),
1897 event_bus: self.event_bus.clone(),
1898 trace_context: self.trace_context.child(),
1899 operation_ctx: None,
1900 };
1901
1902 let result = handler.execute(&mut child_ctx).await;
1903 let total_duration = run_start.elapsed().as_millis() as u64;
1904 let completed_at = Utc::now();
1905
1906 match result {
1907 Ok(()) => {
1908 let child_status = if child_ctx.has_allowed_failure {
1909 RunStatus::Warning
1910 } else {
1911 RunStatus::Completed
1912 };
1913 self.store
1914 .update_run(
1915 child_run_id,
1916 RunUpdate {
1917 status: Some(child_status),
1918 cost_usd: Some(child_ctx.total_cost_usd),
1919 duration_ms: Some(total_duration),
1920 completed_at: Some(completed_at),
1921 ..RunUpdate::default()
1922 },
1923 )
1924 .await?;
1925
1926 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1927 Ok((
1928 StepOutput {
1929 output: serde_json::json!({
1930 "run_id": child_run_id,
1931 "workflow_name": config.workflow_name,
1932 "status": child_status,
1933 "cost_usd": child_ctx.total_cost_usd,
1934 "duration_ms": total_duration,
1935 }),
1936 duration_ms: total_duration,
1937 cost_usd: child_ctx.total_cost_usd,
1938 input_tokens: None,
1939 output_tokens: None,
1940 model: None,
1941 debug_messages: None,
1942 },
1943 child_had_allowed_failure,
1944 ))
1945 }
1946 Err(err) => {
1947 if let Err(store_err) = self
1948 .store
1949 .update_run(
1950 child_run_id,
1951 RunUpdate {
1952 status: Some(RunStatus::Failed),
1953 error: Some(err.to_string()),
1954 cost_usd: Some(child_ctx.total_cost_usd),
1955 duration_ms: Some(total_duration),
1956 completed_at: Some(completed_at),
1957 ..RunUpdate::default()
1958 },
1959 )
1960 .await
1961 {
1962 error!(
1963 child_run_id = %child_run_id,
1964 store_error = %store_err,
1965 "failed to persist child run failure"
1966 );
1967 }
1968
1969 Err(err)
1970 }
1971 }
1972 }
1973
1974 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1979 let step = self.replay_steps.get(&position)?;
1980 if step.status.state != StepStatus::Completed {
1981 return None;
1982 }
1983 let output = StepOutput {
1984 output: step.output.clone().unwrap_or(Value::Null),
1985 duration_ms: step.duration_ms,
1986 cost_usd: step.cost_usd,
1987 input_tokens: step.input_tokens,
1988 output_tokens: step.output_tokens,
1989 model: None,
1990 debug_messages: None,
1991 };
1992 self.total_cost_usd += output.cost_usd;
1993 self.total_duration_ms += output.duration_ms;
1994 self.last_step_ids = vec![step.id];
1995 info!(
1996 run_id = %self.run_id,
1997 step = %step.name,
1998 position,
1999 "step replayed from previous execution"
2000 );
2001 Some(output)
2002 }
2003
2004 #[tracing::instrument(
2006 name = "context.execute_step",
2007 skip_all,
2008 fields(
2009 run_id = %self.run_id,
2010 step.name = %name,
2011 step.kind,
2012 step.position = self.position,
2013 step.trace_id,
2014 )
2015 )]
2016 async fn execute_step(
2017 &mut self,
2018 name: &str,
2019 kind: StepKind,
2020 config: StepConfig,
2021 ) -> Result<StepOutput, EngineError> {
2022 let kind_str: &'static str = match kind {
2023 StepKind::Shell => "shell",
2024 StepKind::Http => "http",
2025 StepKind::Agent => "agent",
2026 StepKind::Workflow => "workflow",
2027 StepKind::Approval => "approval",
2028 StepKind::Custom(_) => "custom",
2029 };
2030 Span::current().record("step.kind", kind_str);
2031
2032 self.check_guard_timeout()?;
2034
2035 let position = self.position;
2036 self.position += 1;
2037
2038 if let Some(output) = self.try_replay_step(position) {
2040 return Ok(output);
2041 }
2042
2043 if let StepConfig::Agent(ref agent_config) = config {
2046 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2047 }
2048
2049 let trace_id = step_trace_id(self.run_id, name, position);
2051 Span::current().record("step.trace_id", trace_id.to_string().as_str());
2052 let step = self
2053 .store
2054 .create_step(NewStep {
2055 run_id: self.run_id,
2056 trace_id,
2057 name: name.to_string(),
2058 kind,
2059 position,
2060 input: Some(serde_json::to_value(&config)?),
2061 is_error_handler: false,
2062 })
2063 .await?;
2064
2065 self.start_step(step.id, Utc::now()).await?;
2066
2067 if let Some(ref bus) = self.event_bus {
2068 bus.publish(
2069 self.run_id,
2070 crate::notify::WorkflowEvent::StepStarted {
2071 step_name: name.to_string(),
2072 step_index: position,
2073 timestamp: Utc::now(),
2074 },
2075 );
2076 }
2077
2078 if let Err(err) = self.prepare_step_inputs(&config, position).await {
2081 self.fail_step(step.id, &err).await;
2082 if config.allow_failure() {
2083 self.has_allowed_failure = true;
2084 self.last_step_ids = vec![step.id];
2085 info!(
2086 run_id = %self.run_id,
2087 step = %name,
2088 error = %err,
2089 "step input preparation failed but allow_failure is set, continuing"
2090 );
2091 return Ok(StepOutput {
2092 output: json!({"error": err.to_string()}),
2093 duration_ms: 0,
2094 cost_usd: Decimal::ZERO,
2095 input_tokens: None,
2096 output_tokens: None,
2097 model: None,
2098 debug_messages: None,
2099 });
2100 }
2101 return Err(err);
2102 }
2103
2104 let mut config = config;
2105 let step_trace = self.trace_context.child();
2106 match config {
2107 StepConfig::Agent(ref mut agent_config) => {
2108 agent_config.trace_context = Some(step_trace);
2109 }
2110 StepConfig::Http(ref mut http_config) => {
2111 http_config.trace_context = Some(step_trace);
2112 }
2113 _ => {}
2114 }
2115
2116 let step_log_sender = self
2117 .log_sender
2118 .as_ref()
2119 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
2120
2121 let execution = self
2122 .execute_with_guard_timeout(&config, step_log_sender)
2123 .await;
2124
2125 let execution = self
2126 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
2127 .await;
2128
2129 if let Err(err) = self
2130 .store_step_outputs(&config, step.id, name, execution.is_ok())
2131 .await
2132 {
2133 self.fail_step(step.id, &err).await;
2134 return Err(err);
2135 }
2136
2137 match execution {
2138 Ok(output) => {
2139 self.total_cost_usd += output.cost_usd;
2140 self.total_duration_ms += output.duration_ms;
2141
2142 if matches!(config, StepConfig::Agent(_)) {
2144 let tokens = output
2145 .input_tokens
2146 .unwrap_or(0)
2147 .saturating_add(output.output_tokens.unwrap_or(0));
2148 if tokens > 0 {
2149 self.guard_record_tokens(tokens)?;
2150 }
2151 }
2152
2153 let debug_messages_json = output.debug_messages_json();
2154
2155 let completed_at = Utc::now();
2156 self.store
2157 .update_step(
2158 step.id,
2159 StepUpdate {
2160 status: Some(StepStatus::Completed),
2161 output: Some(output.output.clone()),
2162 duration_ms: Some(output.duration_ms),
2163 cost_usd: Some(output.cost_usd),
2164 input_tokens: output.input_tokens,
2165 output_tokens: output.output_tokens,
2166 completed_at: Some(completed_at),
2167 debug_messages: debug_messages_json,
2168 ..StepUpdate::default()
2169 },
2170 )
2171 .await?;
2172
2173 self.step_results
2174 .push(StepResult::from_success(trace_id, name, &output));
2175 self.persist_progress().await;
2176
2177 info!(
2178 run_id = %self.run_id,
2179 step = %name,
2180 trace_id = %trace_id,
2181 duration_ms = output.duration_ms,
2182 "step completed"
2183 );
2184
2185 if let Some(ref bus) = self.event_bus {
2186 bus.publish(
2187 self.run_id,
2188 crate::notify::WorkflowEvent::StepCompleted {
2189 step_name: name.to_string(),
2190 step_index: position,
2191 duration_ms: output.duration_ms,
2192 output_summary: None,
2193 },
2194 );
2195
2196 if matches!(config, StepConfig::Agent(_)) {
2197 let tokens = output
2198 .input_tokens
2199 .unwrap_or(0)
2200 .saturating_add(output.output_tokens.unwrap_or(0));
2201 bus.publish(
2202 self.run_id,
2203 crate::notify::WorkflowEvent::AgentStepTokensUsed {
2204 step_name: name.to_string(),
2205 tokens,
2206 cost_usd: output.cost_usd,
2207 },
2208 );
2209 }
2210 }
2211
2212 self.last_step_ids = vec![step.id];
2213
2214 Ok(output)
2215 }
2216 Err(err) => {
2217 let completed_at = Utc::now();
2218 let debug_messages_json = extract_debug_messages_from_error(&err);
2219 let partial = extract_partial_usage_from_error(&err);
2220 let raw_response_output = extract_raw_response_from_error(&err);
2221
2222 if let Some(ref usage) = partial {
2223 if let Some(cost) = usage.cost_usd {
2224 self.total_cost_usd += cost;
2225 }
2226 if let Some(dur) = usage.duration_ms {
2227 self.total_duration_ms += dur;
2228 }
2229 }
2230
2231 if let Err(store_err) = self
2232 .store
2233 .update_step(
2234 step.id,
2235 StepUpdate {
2236 status: Some(StepStatus::Failed),
2237 error: Some(err.to_string()),
2238 output: raw_response_output.clone(),
2239 completed_at: Some(completed_at),
2240 debug_messages: debug_messages_json,
2241 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2242 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2243 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2244 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2245 ..StepUpdate::default()
2246 },
2247 )
2248 .await
2249 {
2250 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2251 }
2252
2253 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2254 let err_cost = partial
2255 .as_ref()
2256 .and_then(|p| p.cost_usd)
2257 .unwrap_or(Decimal::ZERO);
2258 self.step_results.push(StepResult::from_failure(
2259 trace_id,
2260 name,
2261 &err.to_string(),
2262 err_duration,
2263 err_cost,
2264 ));
2265 self.persist_progress().await;
2266
2267 if let Some(ref bus) = self.event_bus {
2268 bus.publish(
2269 self.run_id,
2270 crate::notify::WorkflowEvent::StepFailed {
2271 step_name: name.to_string(),
2272 step_index: position,
2273 error: err.to_string(),
2274 duration_ms: err_duration,
2275 },
2276 );
2277 }
2278
2279 self.fire_error_handlers(name, &err.to_string(), err_duration)
2280 .await;
2281
2282 if config.allow_failure() {
2283 self.has_allowed_failure = true;
2284 self.last_step_ids = vec![step.id];
2285 info!(
2286 run_id = %self.run_id,
2287 step = %name,
2288 error = %err,
2289 "step failed but allow_failure is set, continuing"
2290 );
2291 Ok(allowed_failure_output(
2292 &err.to_string(),
2293 raw_response_output,
2294 partial.as_ref(),
2295 ))
2296 } else {
2297 Err(err)
2298 }
2299 }
2300 }
2301 }
2302
2303 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2309 async fn retry_step_if_configured(
2310 &self,
2311 name: &str,
2312 kind_str: &str,
2313 config: &StepConfig,
2314 step_id: Uuid,
2315 first_result: Result<StepOutput, EngineError>,
2316 ) -> Result<StepOutput, EngineError> {
2317 let policy = match config.retry() {
2318 Some(p) => p,
2319 None => return first_result,
2320 };
2321
2322 let mut last_result = match first_result {
2323 Ok(output) => return Ok(output),
2324 Err(err) if !is_step_retryable(&err) => return Err(err),
2325 Err(err) => Err(err),
2326 };
2327
2328 let step_log_sender = self
2329 .log_sender
2330 .as_ref()
2331 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2332
2333 for attempt in 0..policy.max_retries() {
2334 if let StepConfig::Agent(agent_config) = config {
2335 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2336 }
2337
2338 let delay = policy.delay_for_attempt(attempt);
2339 info!(
2340 run_id = %self.run_id,
2341 step = %name,
2342 attempt = attempt + 1,
2343 max_retries = policy.max_retries(),
2344 delay_ms = delay.as_millis() as u64,
2345 "retrying step after transient failure"
2346 );
2347 tokio::time::sleep(delay).await;
2348
2349 record_retry_metric(kind_str, "retry");
2350
2351 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2352 Ok(output) => return Ok(output),
2353 Err(err) if !is_step_retryable(&err) => return Err(err),
2354 err => last_result = err,
2355 }
2356 }
2357
2358 record_retry_metric(kind_str, "exhausted");
2359
2360 info!(
2361 run_id = %self.run_id,
2362 step = %name,
2363 max_retries = policy.max_retries(),
2364 "step retries exhausted"
2365 );
2366
2367 last_result
2368 }
2369
2370 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
2375 if !self.last_step_ids.is_empty() {
2376 let deps: Vec<NewStepDependency> = self
2377 .last_step_ids
2378 .iter()
2379 .map(|&depends_on| NewStepDependency {
2380 step_id,
2381 depends_on,
2382 })
2383 .collect();
2384 self.store.create_step_dependencies(deps).await?;
2385 }
2386
2387 self.store
2388 .update_step(
2389 step_id,
2390 StepUpdate {
2391 status: Some(StepStatus::Running),
2392 started_at: Some(now),
2393 ..StepUpdate::default()
2394 },
2395 )
2396 .await?;
2397
2398 Ok(())
2399 }
2400
2401 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2408 if let Err(store_err) = self
2409 .store
2410 .update_step(
2411 step_id,
2412 StepUpdate {
2413 status: Some(StepStatus::Failed),
2414 error: Some(err.to_string()),
2415 completed_at: Some(Utc::now()),
2416 ..StepUpdate::default()
2417 },
2418 )
2419 .await
2420 {
2421 error!(
2422 step_id = %step_id,
2423 error = %store_err,
2424 "failed to persist step failure"
2425 );
2426 }
2427 }
2428
2429 pub fn store(&self) -> &Arc<dyn Store> {
2431 &self.store
2432 }
2433
2434 pub async fn payload(&self) -> Result<Value, EngineError> {
2442 let run = self
2443 .store
2444 .get_run(self.run_id)
2445 .await?
2446 .ok_or(EngineError::Store(
2447 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2448 ))?;
2449 Ok(run.payload)
2450 }
2451
2452 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2480 let payload = self.payload().await?;
2481 serde_json::from_value(payload).map_err(EngineError::Serialization)
2482 }
2483
2484 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2507 self.error_handlers.push(OnErrorHandler {
2508 name: name.to_string(),
2509 config: config.into(),
2510 });
2511 }
2512
2513 pub fn clear_error_handlers(&mut self) {
2532 self.error_handlers.clear();
2533 }
2534
2535 async fn fire_error_handlers(
2541 &mut self,
2542 failed_step_name: &str,
2543 error_msg: &str,
2544 duration_ms: u64,
2545 ) {
2546 let handlers = std::mem::take(&mut self.error_handlers);
2547 if handlers.is_empty() {
2548 return;
2549 }
2550
2551 let error_context = json!({
2552 "failed_step": failed_step_name,
2553 "error": error_msg,
2554 "duration_ms": duration_ms,
2555 });
2556
2557 for handler in handlers {
2558 let mut config = handler.config.clone();
2559 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2560
2561 let position = self.position;
2562 self.position += 1;
2563
2564 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2565 let step = match self
2566 .store
2567 .create_step(NewStep {
2568 run_id: self.run_id,
2569 trace_id,
2570 name: handler.name.clone(),
2571 kind: config.kind(),
2572 position,
2573 input: Some(error_context.clone()),
2574 is_error_handler: true,
2575 })
2576 .await
2577 {
2578 Ok(step) => step,
2579 Err(err) => {
2580 warn!(
2581 run_id = %self.run_id,
2582 handler = %handler.name,
2583 error = %err,
2584 "failed to create error handler step"
2585 );
2586 continue;
2587 }
2588 };
2589
2590 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2591 warn!(
2592 run_id = %self.run_id,
2593 handler = %handler.name,
2594 error = %err,
2595 "failed to start error handler step"
2596 );
2597 continue;
2598 }
2599
2600 let step_log_sender = self
2601 .log_sender
2602 .as_ref()
2603 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2604
2605 let start = Instant::now();
2606 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2607 let handler_duration = start.elapsed().as_millis() as u64;
2608 let completed_at = Utc::now();
2609
2610 match result {
2611 Ok(output) => {
2612 if let Err(store_err) = self
2613 .store
2614 .update_step(
2615 step.id,
2616 StepUpdate {
2617 status: Some(StepStatus::Completed),
2618 output: Some(output.output),
2619 duration_ms: Some(handler_duration),
2620 cost_usd: Some(output.cost_usd),
2621 completed_at: Some(completed_at),
2622 ..StepUpdate::default()
2623 },
2624 )
2625 .await
2626 {
2627 warn!(
2628 run_id = %self.run_id,
2629 handler = %handler.name,
2630 error = %store_err,
2631 "failed to persist error handler completion"
2632 );
2633 }
2634
2635 info!(
2636 run_id = %self.run_id,
2637 handler = %handler.name,
2638 duration_ms = handler_duration,
2639 "error handler completed"
2640 );
2641 }
2642 Err(err) => {
2643 if let Err(store_err) = self
2644 .store
2645 .update_step(
2646 step.id,
2647 StepUpdate {
2648 status: Some(StepStatus::Failed),
2649 error: Some(err.to_string()),
2650 duration_ms: Some(handler_duration),
2651 completed_at: Some(completed_at),
2652 ..StepUpdate::default()
2653 },
2654 )
2655 .await
2656 {
2657 warn!(
2658 run_id = %self.run_id,
2659 handler = %handler.name,
2660 error = %store_err,
2661 "failed to persist error handler failure"
2662 );
2663 }
2664
2665 warn!(
2666 run_id = %self.run_id,
2667 handler = %handler.name,
2668 error = %err,
2669 "error handler failed (original error preserved)"
2670 );
2671 }
2672 }
2673 }
2674 }
2675}
2676
2677fn inject_error_context(
2679 config: &mut StepConfig,
2680 failed_step: &str,
2681 error_msg: &str,
2682 duration_ms: u64,
2683) {
2684 match config {
2685 StepConfig::Shell(shell) => {
2686 shell
2687 .env
2688 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2689 shell
2690 .env
2691 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2692 shell.env.push((
2693 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2694 duration_ms.to_string(),
2695 ));
2696 }
2697 StepConfig::Agent(agent) => {
2698 agent.prompt = format!(
2699 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2700 failed_step, duration_ms, error_msg, agent.prompt
2701 );
2702 }
2703 StepConfig::Http(http) => {
2704 http.headers
2705 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2706 http.headers.push((
2707 "X-Ironflow-Error-Message".to_string(),
2708 error_msg.to_string(),
2709 ));
2710 }
2711 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2712 }
2713}
2714
2715#[cfg(feature = "prometheus")]
2716fn record_retry_metric(kind: &str, outcome: &str) {
2717 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2718 use metrics::counter;
2719 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2720 .increment(1);
2721}
2722
2723#[cfg(not(feature = "prometheus"))]
2724fn record_retry_metric(_kind: &str, _outcome: &str) {}
2725
2726fn is_step_retryable(err: &EngineError) -> bool {
2730 use ironflow_core::error::{AgentError, OperationError};
2731
2732 match err {
2733 EngineError::Operation(op) => match op {
2734 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2735 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2736 OperationError::Deserialize { .. } => false,
2737 OperationError::Http {
2738 status: Some(code), ..
2739 } if (400..500).contains(code) && *code != 429 => false,
2740 _ => true,
2741 },
2742 _ => false,
2743 }
2744}
2745
2746fn allowed_failure_output(
2747 error_msg: &str,
2748 raw_response: Option<Value>,
2749 partial: Option<&StepPartialUsage>,
2750) -> StepOutput {
2751 StepOutput {
2752 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2753 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2754 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2755 input_tokens: partial.and_then(|p| p.input_tokens),
2756 output_tokens: partial.and_then(|p| p.output_tokens),
2757 model: None,
2758 debug_messages: None,
2759 }
2760}
2761
2762impl fmt::Debug for WorkflowContext {
2763 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2764 f.debug_struct("WorkflowContext")
2765 .field("run_id", &self.run_id)
2766 .field("position", &self.position)
2767 .field("total_cost_usd", &self.total_cost_usd)
2768 .field("inherited_cost_usd", &self.inherited_cost_usd)
2769 .field("max_cost_usd", &self.max_cost_usd)
2770 .finish_non_exhaustive()
2771 }
2772}
2773
2774fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2777 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2778 debug_messages,
2779 ..
2780 })) = err
2781 && !debug_messages.is_empty()
2782 {
2783 return serde_json::to_value(debug_messages).ok();
2784 }
2785 None
2786}
2787
2788struct StepPartialUsage {
2794 cost_usd: Option<Decimal>,
2795 duration_ms: Option<u64>,
2796 input_tokens: Option<u64>,
2797 output_tokens: Option<u64>,
2798}
2799
2800fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2806 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2807 raw_response: Some(text),
2808 ..
2809 })) = err
2810 {
2811 return Some(Value::String(text.clone()));
2812 }
2813 None
2814}
2815
2816fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2817 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2818 partial_usage,
2819 ..
2820 })) = err
2821 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2822 {
2823 return Some(StepPartialUsage {
2824 cost_usd: partial_usage
2825 .cost_usd
2826 .and_then(|c| Decimal::try_from(c).ok()),
2827 duration_ms: partial_usage.duration_ms,
2828 input_tokens: partial_usage.input_tokens,
2829 output_tokens: partial_usage.output_tokens,
2830 });
2831 }
2832 None
2833}
2834
2835#[cfg(test)]
2836mod tests {
2837 use super::*;
2838 use ironflow_core::providers::claude::ClaudeCodeProvider;
2839 use ironflow_core::providers::record_replay::RecordReplayProvider;
2840 use ironflow_store::memory::InMemoryStore;
2841 use ironflow_store::models::{Run, RunActor, RunFilter};
2842 use ironflow_store::store::RunStore;
2843 use serde_json::json;
2844 use std::sync::Arc;
2845 use std::sync::atomic::{AtomicBool, Ordering};
2846 use uuid::Uuid;
2847
2848 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2850 let inner = ClaudeCodeProvider::new();
2851 Arc::new(RecordReplayProvider::replay(
2852 inner,
2853 "/tmp/ironflow-fixtures",
2854 ))
2855 }
2856
2857 fn create_test_context() -> WorkflowContext {
2859 let store = Arc::new(InMemoryStore::new());
2860 let provider = create_test_provider();
2861 let run_id = Uuid::now_v7();
2862 WorkflowContext::new(run_id, "test".to_string(), store, provider)
2863 }
2864
2865 #[test]
2866 fn context_new_initializes_correctly() {
2867 let ctx = create_test_context();
2868 assert_eq!(ctx.position, 0);
2869 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2870 assert_eq!(ctx.total_duration_ms, 0);
2871 assert!(ctx.last_step_ids.is_empty());
2872 assert!(ctx.replay_steps.is_empty());
2873 assert!(ctx.log_sender.is_none());
2874 }
2875
2876 #[test]
2877 fn context_run_id_returns_correct_id() {
2878 let run_id = Uuid::now_v7();
2879 let store = Arc::new(InMemoryStore::new());
2880 let provider = create_test_provider();
2881 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
2882 assert_eq!(ctx.run_id(), run_id);
2883 }
2884
2885 #[test]
2886 fn context_total_cost_usd_initially_zero() {
2887 let ctx = create_test_context();
2888 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2889 }
2890
2891 #[test]
2892 fn context_total_duration_ms_initially_zero() {
2893 let ctx = create_test_context();
2894 assert_eq!(ctx.total_duration_ms(), 0);
2895 }
2896
2897 #[test]
2898 fn context_with_handler_resolver_creates_context_with_resolver() {
2899 let store = Arc::new(InMemoryStore::new());
2900 let provider = create_test_provider();
2901 let run_id = Uuid::now_v7();
2902
2903 let called = Arc::new(AtomicBool::new(false));
2904 let called_clone = called.clone();
2905
2906 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2907 called_clone.store(true, Ordering::SeqCst);
2908 None
2909 });
2910
2911 let ctx = WorkflowContext::with_handler_resolver(
2912 run_id,
2913 "test".to_string(),
2914 store,
2915 provider,
2916 resolver,
2917 );
2918
2919 assert_eq!(ctx.run_id(), run_id);
2920 assert!(ctx.handler_resolver.is_some());
2921 }
2922
2923 #[tokio::test]
2924 async fn context_set_log_sender_attaches_sender() {
2925 let mut ctx = create_test_context();
2926 let (sender, _receiver) = crate::log_sender::channel();
2927 ctx.set_log_sender(sender);
2928 assert!(ctx.log_sender.is_some());
2929 }
2930
2931 #[tokio::test]
2932 async fn context_skip_creates_skipped_step() {
2933 let store = Arc::new(InMemoryStore::new());
2934 let provider = create_test_provider();
2935
2936 store
2938 .create_run(NewRun {
2939 created_by: None,
2940 workflow_name: "test".to_string(),
2941 trigger: TriggerKind::Manual,
2942 payload: json!({}),
2943 max_retries: 0,
2944 handler_version: None,
2945 labels: Default::default(),
2946 scheduled_at: None,
2947 idempotency_key: None,
2948 max_cost_usd: None,
2949 })
2950 .await
2951 .expect("failed to create run")
2952 .into_run();
2953
2954 let runs = store
2956 .list_runs(RunFilter::default(), 1, 10)
2957 .await
2958 .expect("failed to list runs");
2959 let created_run_id = runs.items[0].id;
2960
2961 let mut ctx =
2962 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
2963 let initial_position = ctx.position;
2964
2965 ctx.skip("skip-step", "condition not met")
2966 .await
2967 .expect("skip failed");
2968
2969 assert_eq!(ctx.position, initial_position + 1);
2970 assert!(!ctx.last_step_ids.is_empty());
2971
2972 let steps = store
2974 .list_steps(created_run_id)
2975 .await
2976 .expect("failed to list steps");
2977 assert_eq!(steps.len(), 1);
2978 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2979 }
2980
2981 struct NoopSubWorkflow;
2984
2985 impl WorkflowHandler for NoopSubWorkflow {
2986 fn name(&self) -> &str {
2987 "noop-sub"
2988 }
2989
2990 fn execute<'a>(
2991 &'a self,
2992 _ctx: &'a mut WorkflowContext,
2993 ) -> crate::handler::HandlerFuture<'a> {
2994 Box::pin(async move { Ok(()) })
2995 }
2996 }
2997
2998 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
3001 let store = Arc::new(InMemoryStore::new());
3002 let provider = create_test_provider();
3003
3004 let parent = store
3005 .create_run(NewRun {
3006 workflow_name: "parent".to_string(),
3007 trigger: TriggerKind::Api,
3008 payload: json!({}),
3009 max_retries: 0,
3010 handler_version: None,
3011 labels: Default::default(),
3012 scheduled_at: None,
3013 created_by,
3014 idempotency_key: None,
3015 max_cost_usd: None,
3016 })
3017 .await
3018 .expect("failed to create parent run")
3019 .into_run();
3020
3021 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
3022 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
3023 _ => None,
3024 });
3025
3026 let mut ctx = WorkflowContext::with_handler_resolver(
3027 parent.id,
3028 "parent".to_string(),
3029 store.clone(),
3030 provider,
3031 resolver,
3032 );
3033 ctx.workflow(&NoopSubWorkflow, json!({}))
3034 .await
3035 .expect("sub-workflow failed");
3036
3037 let runs = store
3038 .list_runs(RunFilter::default(), 1, 10)
3039 .await
3040 .expect("failed to list runs");
3041 runs.items
3042 .into_iter()
3043 .find(|r| r.workflow_name == "noop-sub")
3044 .expect("child run was created")
3045 }
3046
3047 #[tokio::test]
3048 async fn child_run_inherits_the_parent_author() {
3049 let user_id = Uuid::now_v7();
3050 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
3051
3052 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
3053 }
3054
3055 #[tokio::test]
3056 async fn child_run_of_an_unattributed_parent_has_no_author() {
3057 let child = child_run_of_parent_authored_by(None).await;
3058
3059 assert!(child.created_by.is_none());
3060 }
3061
3062 #[tokio::test]
3063 async fn context_parallel_empty_steps_returns_empty_vec() {
3064 let mut ctx = create_test_context();
3065 let results = ctx
3066 .parallel(vec![], true)
3067 .await
3068 .expect("parallel should not fail on empty input");
3069 assert!(results.is_empty());
3070 }
3071
3072 #[tokio::test]
3073 async fn context_approval_first_execution_returns_error() {
3074 let store = Arc::new(InMemoryStore::new());
3075 let provider = create_test_provider();
3076
3077 store
3079 .create_run(NewRun {
3080 created_by: None,
3081 workflow_name: "test".to_string(),
3082 trigger: TriggerKind::Manual,
3083 payload: json!({}),
3084 max_retries: 0,
3085 handler_version: None,
3086 labels: Default::default(),
3087 scheduled_at: None,
3088 idempotency_key: None,
3089 max_cost_usd: None,
3090 })
3091 .await
3092 .expect("failed to create run")
3093 .into_run();
3094
3095 let runs = store
3097 .list_runs(RunFilter::default(), 1, 10)
3098 .await
3099 .expect("failed to list runs");
3100 let created_run_id = runs.items[0].id;
3101
3102 let mut ctx =
3103 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3104
3105 let result = ctx
3106 .approval(
3107 "approve-step",
3108 crate::config::ApprovalConfig::new("Continue?"),
3109 )
3110 .await;
3111
3112 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
3114
3115 assert_eq!(ctx.position, 1);
3117
3118 let steps = store
3120 .list_steps(created_run_id)
3121 .await
3122 .expect("failed to list steps");
3123 assert_eq!(steps.len(), 1);
3124 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3125 }
3126
3127 #[tokio::test]
3128 async fn context_approval_replay_returns_ok() {
3129 let store = Arc::new(InMemoryStore::new());
3130 let provider = create_test_provider();
3131
3132 store
3134 .create_run(NewRun {
3135 created_by: None,
3136 workflow_name: "test".to_string(),
3137 trigger: TriggerKind::Manual,
3138 payload: json!({}),
3139 max_retries: 0,
3140 handler_version: None,
3141 labels: Default::default(),
3142 scheduled_at: None,
3143 idempotency_key: None,
3144 max_cost_usd: None,
3145 })
3146 .await
3147 .expect("failed to create run")
3148 .into_run();
3149
3150 let runs = store
3152 .list_runs(RunFilter::default(), 1, 10)
3153 .await
3154 .expect("failed to list runs");
3155 let created_run_id = runs.items[0].id;
3156
3157 let step = store
3159 .create_step(NewStep {
3160 run_id: created_run_id,
3161 trace_id: step_trace_id(created_run_id, "approval", 0),
3162 name: "approval".to_string(),
3163 kind: StepKind::Approval,
3164 position: 0,
3165 input: None,
3166 is_error_handler: false,
3167 })
3168 .await
3169 .expect("failed to create step");
3170
3171 store
3173 .update_step(
3174 step.id,
3175 StepUpdate {
3176 status: Some(StepStatus::Running),
3177 started_at: Some(Utc::now()),
3178 ..StepUpdate::default()
3179 },
3180 )
3181 .await
3182 .expect("failed to update step to Running");
3183
3184 store
3185 .update_step(
3186 step.id,
3187 StepUpdate {
3188 status: Some(StepStatus::AwaitingApproval),
3189 ..StepUpdate::default()
3190 },
3191 )
3192 .await
3193 .expect("failed to update step to AwaitingApproval");
3194
3195 let mut ctx =
3197 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3198 ctx.load_replay_steps()
3199 .await
3200 .expect("failed to load replay steps");
3201
3202 let result = ctx
3204 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3205 .await;
3206
3207 assert!(result.is_ok());
3208
3209 let steps = store
3211 .list_steps(created_run_id)
3212 .await
3213 .expect("failed to list steps");
3214 assert_eq!(steps.len(), 1);
3215 assert_eq!(steps[0].status.state, StepStatus::Completed);
3216 }
3217
3218 #[tokio::test]
3219 async fn context_load_replay_steps_loads_completed_steps() {
3220 let store = Arc::new(InMemoryStore::new());
3221 let provider = create_test_provider();
3222
3223 store
3225 .create_run(NewRun {
3226 created_by: None,
3227 workflow_name: "test".to_string(),
3228 trigger: TriggerKind::Manual,
3229 payload: json!({}),
3230 max_retries: 0,
3231 handler_version: None,
3232 labels: Default::default(),
3233 scheduled_at: None,
3234 idempotency_key: None,
3235 max_cost_usd: None,
3236 })
3237 .await
3238 .expect("failed to create run")
3239 .into_run();
3240
3241 let runs = store
3243 .list_runs(RunFilter::default(), 1, 10)
3244 .await
3245 .expect("failed to list runs");
3246 let created_run_id = runs.items[0].id;
3247
3248 let completed_step = store
3250 .create_step(NewStep {
3251 run_id: created_run_id,
3252 trace_id: step_trace_id(created_run_id, "completed", 0),
3253 name: "completed".to_string(),
3254 kind: StepKind::Shell,
3255 position: 0,
3256 input: None,
3257 is_error_handler: false,
3258 })
3259 .await
3260 .expect("failed to create step");
3261
3262 store
3264 .update_step(
3265 completed_step.id,
3266 StepUpdate {
3267 status: Some(StepStatus::Running),
3268 started_at: Some(Utc::now()),
3269 ..StepUpdate::default()
3270 },
3271 )
3272 .await
3273 .expect("failed to update step to Running");
3274
3275 store
3276 .update_step(
3277 completed_step.id,
3278 StepUpdate {
3279 status: Some(StepStatus::Completed),
3280 completed_at: Some(Utc::now()),
3281 ..StepUpdate::default()
3282 },
3283 )
3284 .await
3285 .expect("failed to update step to Completed");
3286
3287 let _pending_step = store
3288 .create_step(NewStep {
3289 run_id: created_run_id,
3290 trace_id: step_trace_id(created_run_id, "pending", 1),
3291 name: "pending".to_string(),
3292 kind: StepKind::Shell,
3293 position: 1,
3294 input: None,
3295 is_error_handler: false,
3296 })
3297 .await
3298 .expect("failed to create step");
3299
3300 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3302 ctx.load_replay_steps()
3303 .await
3304 .expect("failed to load replay steps");
3305
3306 assert_eq!(ctx.replay_steps.len(), 1);
3308 assert!(ctx.replay_steps.contains_key(&0));
3309 assert!(!ctx.replay_steps.contains_key(&1));
3310 }
3311
3312 #[tokio::test]
3313 async fn context_payload_returns_run_payload() {
3314 let store = Arc::new(InMemoryStore::new());
3315 let provider = create_test_provider();
3316 let test_payload = json!({"key": "value", "number": 42});
3317
3318 store
3320 .create_run(NewRun {
3321 created_by: None,
3322 workflow_name: "test".to_string(),
3323 trigger: TriggerKind::Manual,
3324 payload: test_payload.clone(),
3325 max_retries: 0,
3326 handler_version: None,
3327 labels: Default::default(),
3328 scheduled_at: None,
3329 idempotency_key: None,
3330 max_cost_usd: None,
3331 })
3332 .await
3333 .expect("failed to create run")
3334 .into_run();
3335
3336 let runs = store
3338 .list_runs(RunFilter::default(), 1, 10)
3339 .await
3340 .expect("failed to list runs");
3341 let created_run_id = runs.items[0].id;
3342
3343 let ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3344 let payload = ctx.payload().await.expect("failed to get payload");
3345
3346 assert_eq!(payload, test_payload);
3347 }
3348
3349 #[tokio::test]
3350 async fn context_payload_returns_error_for_nonexistent_run() {
3351 let store = Arc::new(InMemoryStore::new());
3352 let provider = create_test_provider();
3353 let run_id = Uuid::now_v7();
3354
3355 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
3356 let result = ctx.payload().await;
3357
3358 assert!(result.is_err());
3359 }
3360
3361 #[tokio::test]
3362 async fn context_store_returns_reference() {
3363 let ctx = create_test_context();
3364 let _store = ctx.store();
3365 }
3367
3368 #[test]
3369 fn context_debug_formatting() {
3370 let ctx = create_test_context();
3371 let debug_str = format!("{:?}", ctx);
3372 assert!(debug_str.contains("WorkflowContext"));
3373 assert!(debug_str.contains("run_id"));
3374 }
3375
3376 #[tokio::test]
3377 async fn context_last_step_ids_tracks_executed_steps() {
3378 let store = Arc::new(InMemoryStore::new());
3379 let provider = create_test_provider();
3380
3381 store
3383 .create_run(NewRun {
3384 created_by: None,
3385 workflow_name: "test".to_string(),
3386 trigger: TriggerKind::Manual,
3387 payload: json!({}),
3388 max_retries: 0,
3389 handler_version: None,
3390 labels: Default::default(),
3391 scheduled_at: None,
3392 idempotency_key: None,
3393 max_cost_usd: None,
3394 })
3395 .await
3396 .expect("failed to create run")
3397 .into_run();
3398
3399 let runs = store
3401 .list_runs(RunFilter::default(), 1, 10)
3402 .await
3403 .expect("failed to list runs");
3404 let created_run_id = runs.items[0].id;
3405
3406 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3407 assert!(ctx.last_step_ids.is_empty());
3408
3409 ctx.skip("step1", "reason").await.expect("skip failed");
3410
3411 assert_eq!(ctx.last_step_ids.len(), 1);
3412
3413 ctx.skip("step2", "reason").await.expect("skip failed");
3414
3415 assert_eq!(ctx.last_step_ids.len(), 1);
3417 }
3418}