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 pub(crate) 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 pub(crate) async fn start_step(
2375 &self,
2376 step_id: Uuid,
2377 now: DateTime<Utc>,
2378 ) -> Result<(), EngineError> {
2379 if !self.last_step_ids.is_empty() {
2380 let deps: Vec<NewStepDependency> = self
2381 .last_step_ids
2382 .iter()
2383 .map(|&depends_on| NewStepDependency {
2384 step_id,
2385 depends_on,
2386 })
2387 .collect();
2388 self.store.create_step_dependencies(deps).await?;
2389 }
2390
2391 self.store
2392 .update_step(
2393 step_id,
2394 StepUpdate {
2395 status: Some(StepStatus::Running),
2396 started_at: Some(now),
2397 ..StepUpdate::default()
2398 },
2399 )
2400 .await?;
2401
2402 Ok(())
2403 }
2404
2405 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2412 if let Err(store_err) = self
2413 .store
2414 .update_step(
2415 step_id,
2416 StepUpdate {
2417 status: Some(StepStatus::Failed),
2418 error: Some(err.to_string()),
2419 completed_at: Some(Utc::now()),
2420 ..StepUpdate::default()
2421 },
2422 )
2423 .await
2424 {
2425 error!(
2426 step_id = %step_id,
2427 error = %store_err,
2428 "failed to persist step failure"
2429 );
2430 }
2431 }
2432
2433 pub fn store(&self) -> &Arc<dyn Store> {
2435 &self.store
2436 }
2437
2438 pub(crate) fn next_position(&mut self) -> u32 {
2440 let pos = self.position;
2441 self.position += 1;
2442 pos
2443 }
2444
2445 pub(crate) fn replay_steps(&self) -> &HashMap<u32, Step> {
2447 &self.replay_steps
2448 }
2449
2450 pub(crate) fn set_last_step_ids(&mut self, ids: Vec<Uuid>) {
2452 self.last_step_ids = ids;
2453 }
2454
2455 pub async fn payload(&self) -> Result<Value, EngineError> {
2463 let run = self
2464 .store
2465 .get_run(self.run_id)
2466 .await?
2467 .ok_or(EngineError::Store(
2468 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2469 ))?;
2470 Ok(run.payload)
2471 }
2472
2473 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2501 let payload = self.payload().await?;
2502 serde_json::from_value(payload).map_err(EngineError::Serialization)
2503 }
2504
2505 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2528 self.error_handlers.push(OnErrorHandler {
2529 name: name.to_string(),
2530 config: config.into(),
2531 });
2532 }
2533
2534 pub fn clear_error_handlers(&mut self) {
2553 self.error_handlers.clear();
2554 }
2555
2556 async fn fire_error_handlers(
2562 &mut self,
2563 failed_step_name: &str,
2564 error_msg: &str,
2565 duration_ms: u64,
2566 ) {
2567 let handlers = std::mem::take(&mut self.error_handlers);
2568 if handlers.is_empty() {
2569 return;
2570 }
2571
2572 let error_context = json!({
2573 "failed_step": failed_step_name,
2574 "error": error_msg,
2575 "duration_ms": duration_ms,
2576 });
2577
2578 for handler in handlers {
2579 let mut config = handler.config.clone();
2580 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2581
2582 let position = self.position;
2583 self.position += 1;
2584
2585 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2586 let step = match self
2587 .store
2588 .create_step(NewStep {
2589 run_id: self.run_id,
2590 trace_id,
2591 name: handler.name.clone(),
2592 kind: config.kind(),
2593 position,
2594 input: Some(error_context.clone()),
2595 is_error_handler: true,
2596 })
2597 .await
2598 {
2599 Ok(step) => step,
2600 Err(err) => {
2601 warn!(
2602 run_id = %self.run_id,
2603 handler = %handler.name,
2604 error = %err,
2605 "failed to create error handler step"
2606 );
2607 continue;
2608 }
2609 };
2610
2611 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2612 warn!(
2613 run_id = %self.run_id,
2614 handler = %handler.name,
2615 error = %err,
2616 "failed to start error handler step"
2617 );
2618 continue;
2619 }
2620
2621 let step_log_sender = self
2622 .log_sender
2623 .as_ref()
2624 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2625
2626 let start = Instant::now();
2627 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2628 let handler_duration = start.elapsed().as_millis() as u64;
2629 let completed_at = Utc::now();
2630
2631 match result {
2632 Ok(output) => {
2633 if let Err(store_err) = self
2634 .store
2635 .update_step(
2636 step.id,
2637 StepUpdate {
2638 status: Some(StepStatus::Completed),
2639 output: Some(output.output),
2640 duration_ms: Some(handler_duration),
2641 cost_usd: Some(output.cost_usd),
2642 completed_at: Some(completed_at),
2643 ..StepUpdate::default()
2644 },
2645 )
2646 .await
2647 {
2648 warn!(
2649 run_id = %self.run_id,
2650 handler = %handler.name,
2651 error = %store_err,
2652 "failed to persist error handler completion"
2653 );
2654 }
2655
2656 info!(
2657 run_id = %self.run_id,
2658 handler = %handler.name,
2659 duration_ms = handler_duration,
2660 "error handler completed"
2661 );
2662 }
2663 Err(err) => {
2664 if let Err(store_err) = self
2665 .store
2666 .update_step(
2667 step.id,
2668 StepUpdate {
2669 status: Some(StepStatus::Failed),
2670 error: Some(err.to_string()),
2671 duration_ms: Some(handler_duration),
2672 completed_at: Some(completed_at),
2673 ..StepUpdate::default()
2674 },
2675 )
2676 .await
2677 {
2678 warn!(
2679 run_id = %self.run_id,
2680 handler = %handler.name,
2681 error = %store_err,
2682 "failed to persist error handler failure"
2683 );
2684 }
2685
2686 warn!(
2687 run_id = %self.run_id,
2688 handler = %handler.name,
2689 error = %err,
2690 "error handler failed (original error preserved)"
2691 );
2692 }
2693 }
2694 }
2695 }
2696}
2697
2698fn inject_error_context(
2700 config: &mut StepConfig,
2701 failed_step: &str,
2702 error_msg: &str,
2703 duration_ms: u64,
2704) {
2705 match config {
2706 StepConfig::Shell(shell) => {
2707 shell
2708 .env
2709 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2710 shell
2711 .env
2712 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2713 shell.env.push((
2714 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2715 duration_ms.to_string(),
2716 ));
2717 }
2718 StepConfig::Agent(agent) => {
2719 agent.prompt = format!(
2720 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2721 failed_step, duration_ms, error_msg, agent.prompt
2722 );
2723 }
2724 StepConfig::Http(http) => {
2725 http.headers
2726 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2727 http.headers.push((
2728 "X-Ironflow-Error-Message".to_string(),
2729 error_msg.to_string(),
2730 ));
2731 }
2732 StepConfig::Workflow(_) | StepConfig::Approval(_) | StepConfig::Delay(_) => {}
2733 }
2734}
2735
2736#[cfg(feature = "prometheus")]
2737fn record_retry_metric(kind: &str, outcome: &str) {
2738 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2739 use metrics::counter;
2740 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2741 .increment(1);
2742}
2743
2744#[cfg(not(feature = "prometheus"))]
2745fn record_retry_metric(_kind: &str, _outcome: &str) {}
2746
2747fn is_step_retryable(err: &EngineError) -> bool {
2751 use ironflow_core::error::{AgentError, OperationError};
2752
2753 match err {
2754 EngineError::Operation(op) => match op {
2755 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2756 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2757 OperationError::Deserialize { .. } => false,
2758 OperationError::Http {
2759 status: Some(code), ..
2760 } if (400..500).contains(code) && *code != 429 => false,
2761 _ => true,
2762 },
2763 _ => false,
2764 }
2765}
2766
2767fn allowed_failure_output(
2768 error_msg: &str,
2769 raw_response: Option<Value>,
2770 partial: Option<&StepPartialUsage>,
2771) -> StepOutput {
2772 StepOutput {
2773 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2774 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2775 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2776 input_tokens: partial.and_then(|p| p.input_tokens),
2777 output_tokens: partial.and_then(|p| p.output_tokens),
2778 model: None,
2779 debug_messages: None,
2780 }
2781}
2782
2783impl fmt::Debug for WorkflowContext {
2784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2785 f.debug_struct("WorkflowContext")
2786 .field("run_id", &self.run_id)
2787 .field("position", &self.position)
2788 .field("total_cost_usd", &self.total_cost_usd)
2789 .field("inherited_cost_usd", &self.inherited_cost_usd)
2790 .field("max_cost_usd", &self.max_cost_usd)
2791 .finish_non_exhaustive()
2792 }
2793}
2794
2795fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2798 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2799 debug_messages,
2800 ..
2801 })) = err
2802 && !debug_messages.is_empty()
2803 {
2804 return serde_json::to_value(debug_messages).ok();
2805 }
2806 None
2807}
2808
2809struct StepPartialUsage {
2815 cost_usd: Option<Decimal>,
2816 duration_ms: Option<u64>,
2817 input_tokens: Option<u64>,
2818 output_tokens: Option<u64>,
2819}
2820
2821fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2827 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2828 raw_response: Some(text),
2829 ..
2830 })) = err
2831 {
2832 return Some(Value::String(text.clone()));
2833 }
2834 None
2835}
2836
2837fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2838 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2839 partial_usage,
2840 ..
2841 })) = err
2842 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2843 {
2844 return Some(StepPartialUsage {
2845 cost_usd: partial_usage
2846 .cost_usd
2847 .and_then(|c| Decimal::try_from(c).ok()),
2848 duration_ms: partial_usage.duration_ms,
2849 input_tokens: partial_usage.input_tokens,
2850 output_tokens: partial_usage.output_tokens,
2851 });
2852 }
2853 None
2854}
2855
2856#[cfg(test)]
2857mod tests {
2858 use super::*;
2859 use ironflow_core::providers::claude::ClaudeCodeProvider;
2860 use ironflow_core::providers::record_replay::RecordReplayProvider;
2861 use ironflow_store::memory::InMemoryStore;
2862 use ironflow_store::models::{Run, RunActor, RunFilter};
2863 use ironflow_store::store::RunStore;
2864 use serde_json::json;
2865 use std::sync::Arc;
2866 use std::sync::atomic::{AtomicBool, Ordering};
2867 use uuid::Uuid;
2868
2869 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2871 let inner = ClaudeCodeProvider::new();
2872 Arc::new(RecordReplayProvider::replay(
2873 inner,
2874 "/tmp/ironflow-fixtures",
2875 ))
2876 }
2877
2878 fn create_test_context() -> WorkflowContext {
2880 let store = Arc::new(InMemoryStore::new());
2881 let provider = create_test_provider();
2882 let run_id = Uuid::now_v7();
2883 WorkflowContext::new(run_id, "test".to_string(), store, provider)
2884 }
2885
2886 #[test]
2887 fn context_new_initializes_correctly() {
2888 let ctx = create_test_context();
2889 assert_eq!(ctx.position, 0);
2890 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2891 assert_eq!(ctx.total_duration_ms, 0);
2892 assert!(ctx.last_step_ids.is_empty());
2893 assert!(ctx.replay_steps.is_empty());
2894 assert!(ctx.log_sender.is_none());
2895 }
2896
2897 #[test]
2898 fn context_run_id_returns_correct_id() {
2899 let run_id = Uuid::now_v7();
2900 let store = Arc::new(InMemoryStore::new());
2901 let provider = create_test_provider();
2902 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
2903 assert_eq!(ctx.run_id(), run_id);
2904 }
2905
2906 #[test]
2907 fn context_total_cost_usd_initially_zero() {
2908 let ctx = create_test_context();
2909 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2910 }
2911
2912 #[test]
2913 fn context_total_duration_ms_initially_zero() {
2914 let ctx = create_test_context();
2915 assert_eq!(ctx.total_duration_ms(), 0);
2916 }
2917
2918 #[test]
2919 fn context_with_handler_resolver_creates_context_with_resolver() {
2920 let store = Arc::new(InMemoryStore::new());
2921 let provider = create_test_provider();
2922 let run_id = Uuid::now_v7();
2923
2924 let called = Arc::new(AtomicBool::new(false));
2925 let called_clone = called.clone();
2926
2927 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2928 called_clone.store(true, Ordering::SeqCst);
2929 None
2930 });
2931
2932 let ctx = WorkflowContext::with_handler_resolver(
2933 run_id,
2934 "test".to_string(),
2935 store,
2936 provider,
2937 resolver,
2938 );
2939
2940 assert_eq!(ctx.run_id(), run_id);
2941 assert!(ctx.handler_resolver.is_some());
2942 }
2943
2944 #[tokio::test]
2945 async fn context_set_log_sender_attaches_sender() {
2946 let mut ctx = create_test_context();
2947 let (sender, _receiver) = crate::log_sender::channel();
2948 ctx.set_log_sender(sender);
2949 assert!(ctx.log_sender.is_some());
2950 }
2951
2952 #[tokio::test]
2953 async fn context_skip_creates_skipped_step() {
2954 let store = Arc::new(InMemoryStore::new());
2955 let provider = create_test_provider();
2956
2957 store
2959 .create_run(NewRun {
2960 created_by: None,
2961 workflow_name: "test".to_string(),
2962 trigger: TriggerKind::Manual,
2963 payload: json!({}),
2964 max_retries: 0,
2965 handler_version: None,
2966 labels: Default::default(),
2967 scheduled_at: None,
2968 idempotency_key: None,
2969 max_cost_usd: None,
2970 })
2971 .await
2972 .expect("failed to create run")
2973 .into_run();
2974
2975 let runs = store
2977 .list_runs(RunFilter::default(), 1, 10)
2978 .await
2979 .expect("failed to list runs");
2980 let created_run_id = runs.items[0].id;
2981
2982 let mut ctx =
2983 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
2984 let initial_position = ctx.position;
2985
2986 ctx.skip("skip-step", "condition not met")
2987 .await
2988 .expect("skip failed");
2989
2990 assert_eq!(ctx.position, initial_position + 1);
2991 assert!(!ctx.last_step_ids.is_empty());
2992
2993 let steps = store
2995 .list_steps(created_run_id)
2996 .await
2997 .expect("failed to list steps");
2998 assert_eq!(steps.len(), 1);
2999 assert_eq!(steps[0].status.state, StepStatus::Skipped);
3000 }
3001
3002 struct NoopSubWorkflow;
3005
3006 impl WorkflowHandler for NoopSubWorkflow {
3007 fn name(&self) -> &str {
3008 "noop-sub"
3009 }
3010
3011 fn execute<'a>(
3012 &'a self,
3013 _ctx: &'a mut WorkflowContext,
3014 ) -> crate::handler::HandlerFuture<'a> {
3015 Box::pin(async move { Ok(()) })
3016 }
3017 }
3018
3019 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
3022 let store = Arc::new(InMemoryStore::new());
3023 let provider = create_test_provider();
3024
3025 let parent = store
3026 .create_run(NewRun {
3027 workflow_name: "parent".to_string(),
3028 trigger: TriggerKind::Api,
3029 payload: json!({}),
3030 max_retries: 0,
3031 handler_version: None,
3032 labels: Default::default(),
3033 scheduled_at: None,
3034 created_by,
3035 idempotency_key: None,
3036 max_cost_usd: None,
3037 })
3038 .await
3039 .expect("failed to create parent run")
3040 .into_run();
3041
3042 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
3043 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
3044 _ => None,
3045 });
3046
3047 let mut ctx = WorkflowContext::with_handler_resolver(
3048 parent.id,
3049 "parent".to_string(),
3050 store.clone(),
3051 provider,
3052 resolver,
3053 );
3054 ctx.workflow(&NoopSubWorkflow, json!({}))
3055 .await
3056 .expect("sub-workflow failed");
3057
3058 let runs = store
3059 .list_runs(RunFilter::default(), 1, 10)
3060 .await
3061 .expect("failed to list runs");
3062 runs.items
3063 .into_iter()
3064 .find(|r| r.workflow_name == "noop-sub")
3065 .expect("child run was created")
3066 }
3067
3068 #[tokio::test]
3069 async fn child_run_inherits_the_parent_author() {
3070 let user_id = Uuid::now_v7();
3071 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
3072
3073 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
3074 }
3075
3076 #[tokio::test]
3077 async fn child_run_of_an_unattributed_parent_has_no_author() {
3078 let child = child_run_of_parent_authored_by(None).await;
3079
3080 assert!(child.created_by.is_none());
3081 }
3082
3083 #[tokio::test]
3084 async fn context_parallel_empty_steps_returns_empty_vec() {
3085 let mut ctx = create_test_context();
3086 let results = ctx
3087 .parallel(vec![], true)
3088 .await
3089 .expect("parallel should not fail on empty input");
3090 assert!(results.is_empty());
3091 }
3092
3093 #[tokio::test]
3094 async fn context_approval_first_execution_returns_error() {
3095 let store = Arc::new(InMemoryStore::new());
3096 let provider = create_test_provider();
3097
3098 store
3100 .create_run(NewRun {
3101 created_by: None,
3102 workflow_name: "test".to_string(),
3103 trigger: TriggerKind::Manual,
3104 payload: json!({}),
3105 max_retries: 0,
3106 handler_version: None,
3107 labels: Default::default(),
3108 scheduled_at: None,
3109 idempotency_key: None,
3110 max_cost_usd: None,
3111 })
3112 .await
3113 .expect("failed to create run")
3114 .into_run();
3115
3116 let runs = store
3118 .list_runs(RunFilter::default(), 1, 10)
3119 .await
3120 .expect("failed to list runs");
3121 let created_run_id = runs.items[0].id;
3122
3123 let mut ctx =
3124 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3125
3126 let result = ctx
3127 .approval(
3128 "approve-step",
3129 crate::config::ApprovalConfig::new("Continue?"),
3130 )
3131 .await;
3132
3133 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
3135
3136 assert_eq!(ctx.position, 1);
3138
3139 let steps = store
3141 .list_steps(created_run_id)
3142 .await
3143 .expect("failed to list steps");
3144 assert_eq!(steps.len(), 1);
3145 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3146 }
3147
3148 #[tokio::test]
3149 async fn context_approval_replay_returns_ok() {
3150 let store = Arc::new(InMemoryStore::new());
3151 let provider = create_test_provider();
3152
3153 store
3155 .create_run(NewRun {
3156 created_by: None,
3157 workflow_name: "test".to_string(),
3158 trigger: TriggerKind::Manual,
3159 payload: json!({}),
3160 max_retries: 0,
3161 handler_version: None,
3162 labels: Default::default(),
3163 scheduled_at: None,
3164 idempotency_key: None,
3165 max_cost_usd: None,
3166 })
3167 .await
3168 .expect("failed to create run")
3169 .into_run();
3170
3171 let runs = store
3173 .list_runs(RunFilter::default(), 1, 10)
3174 .await
3175 .expect("failed to list runs");
3176 let created_run_id = runs.items[0].id;
3177
3178 let step = store
3180 .create_step(NewStep {
3181 run_id: created_run_id,
3182 trace_id: step_trace_id(created_run_id, "approval", 0),
3183 name: "approval".to_string(),
3184 kind: StepKind::Approval,
3185 position: 0,
3186 input: None,
3187 is_error_handler: false,
3188 })
3189 .await
3190 .expect("failed to create step");
3191
3192 store
3194 .update_step(
3195 step.id,
3196 StepUpdate {
3197 status: Some(StepStatus::Running),
3198 started_at: Some(Utc::now()),
3199 ..StepUpdate::default()
3200 },
3201 )
3202 .await
3203 .expect("failed to update step to Running");
3204
3205 store
3206 .update_step(
3207 step.id,
3208 StepUpdate {
3209 status: Some(StepStatus::AwaitingApproval),
3210 ..StepUpdate::default()
3211 },
3212 )
3213 .await
3214 .expect("failed to update step to AwaitingApproval");
3215
3216 let mut ctx =
3218 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3219 ctx.load_replay_steps()
3220 .await
3221 .expect("failed to load replay steps");
3222
3223 let result = ctx
3225 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3226 .await;
3227
3228 assert!(result.is_ok());
3229
3230 let steps = store
3232 .list_steps(created_run_id)
3233 .await
3234 .expect("failed to list steps");
3235 assert_eq!(steps.len(), 1);
3236 assert_eq!(steps[0].status.state, StepStatus::Completed);
3237 }
3238
3239 #[tokio::test]
3240 async fn context_load_replay_steps_loads_completed_steps() {
3241 let store = Arc::new(InMemoryStore::new());
3242 let provider = create_test_provider();
3243
3244 store
3246 .create_run(NewRun {
3247 created_by: None,
3248 workflow_name: "test".to_string(),
3249 trigger: TriggerKind::Manual,
3250 payload: json!({}),
3251 max_retries: 0,
3252 handler_version: None,
3253 labels: Default::default(),
3254 scheduled_at: None,
3255 idempotency_key: None,
3256 max_cost_usd: None,
3257 })
3258 .await
3259 .expect("failed to create run")
3260 .into_run();
3261
3262 let runs = store
3264 .list_runs(RunFilter::default(), 1, 10)
3265 .await
3266 .expect("failed to list runs");
3267 let created_run_id = runs.items[0].id;
3268
3269 let completed_step = store
3271 .create_step(NewStep {
3272 run_id: created_run_id,
3273 trace_id: step_trace_id(created_run_id, "completed", 0),
3274 name: "completed".to_string(),
3275 kind: StepKind::Shell,
3276 position: 0,
3277 input: None,
3278 is_error_handler: false,
3279 })
3280 .await
3281 .expect("failed to create step");
3282
3283 store
3285 .update_step(
3286 completed_step.id,
3287 StepUpdate {
3288 status: Some(StepStatus::Running),
3289 started_at: Some(Utc::now()),
3290 ..StepUpdate::default()
3291 },
3292 )
3293 .await
3294 .expect("failed to update step to Running");
3295
3296 store
3297 .update_step(
3298 completed_step.id,
3299 StepUpdate {
3300 status: Some(StepStatus::Completed),
3301 completed_at: Some(Utc::now()),
3302 ..StepUpdate::default()
3303 },
3304 )
3305 .await
3306 .expect("failed to update step to Completed");
3307
3308 let _pending_step = store
3309 .create_step(NewStep {
3310 run_id: created_run_id,
3311 trace_id: step_trace_id(created_run_id, "pending", 1),
3312 name: "pending".to_string(),
3313 kind: StepKind::Shell,
3314 position: 1,
3315 input: None,
3316 is_error_handler: false,
3317 })
3318 .await
3319 .expect("failed to create step");
3320
3321 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3323 ctx.load_replay_steps()
3324 .await
3325 .expect("failed to load replay steps");
3326
3327 assert_eq!(ctx.replay_steps.len(), 1);
3329 assert!(ctx.replay_steps.contains_key(&0));
3330 assert!(!ctx.replay_steps.contains_key(&1));
3331 }
3332
3333 #[tokio::test]
3334 async fn context_payload_returns_run_payload() {
3335 let store = Arc::new(InMemoryStore::new());
3336 let provider = create_test_provider();
3337 let test_payload = json!({"key": "value", "number": 42});
3338
3339 store
3341 .create_run(NewRun {
3342 created_by: None,
3343 workflow_name: "test".to_string(),
3344 trigger: TriggerKind::Manual,
3345 payload: test_payload.clone(),
3346 max_retries: 0,
3347 handler_version: None,
3348 labels: Default::default(),
3349 scheduled_at: None,
3350 idempotency_key: None,
3351 max_cost_usd: None,
3352 })
3353 .await
3354 .expect("failed to create run")
3355 .into_run();
3356
3357 let runs = store
3359 .list_runs(RunFilter::default(), 1, 10)
3360 .await
3361 .expect("failed to list runs");
3362 let created_run_id = runs.items[0].id;
3363
3364 let ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3365 let payload = ctx.payload().await.expect("failed to get payload");
3366
3367 assert_eq!(payload, test_payload);
3368 }
3369
3370 #[tokio::test]
3371 async fn context_payload_returns_error_for_nonexistent_run() {
3372 let store = Arc::new(InMemoryStore::new());
3373 let provider = create_test_provider();
3374 let run_id = Uuid::now_v7();
3375
3376 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
3377 let result = ctx.payload().await;
3378
3379 assert!(result.is_err());
3380 }
3381
3382 #[tokio::test]
3383 async fn context_store_returns_reference() {
3384 let ctx = create_test_context();
3385 let _store = ctx.store();
3386 }
3388
3389 #[test]
3390 fn context_debug_formatting() {
3391 let ctx = create_test_context();
3392 let debug_str = format!("{:?}", ctx);
3393 assert!(debug_str.contains("WorkflowContext"));
3394 assert!(debug_str.contains("run_id"));
3395 }
3396
3397 #[tokio::test]
3398 async fn context_last_step_ids_tracks_executed_steps() {
3399 let store = Arc::new(InMemoryStore::new());
3400 let provider = create_test_provider();
3401
3402 store
3404 .create_run(NewRun {
3405 created_by: None,
3406 workflow_name: "test".to_string(),
3407 trigger: TriggerKind::Manual,
3408 payload: json!({}),
3409 max_retries: 0,
3410 handler_version: None,
3411 labels: Default::default(),
3412 scheduled_at: None,
3413 idempotency_key: None,
3414 max_cost_usd: None,
3415 })
3416 .await
3417 .expect("failed to create run")
3418 .into_run();
3419
3420 let runs = store
3422 .list_runs(RunFilter::default(), 1, 10)
3423 .await
3424 .expect("failed to list runs");
3425 let created_run_id = runs.items[0].id;
3426
3427 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3428 assert!(ctx.last_step_ids.is_empty());
3429
3430 ctx.skip("step1", "reason").await.expect("skip failed");
3431
3432 assert_eq!(ctx.last_step_ids.len(), 1);
3433
3434 ctx.skip("step2", "reason").await.expect("skip failed");
3435
3436 assert_eq!(ctx.last_step_ids.len(), 1);
3438 }
3439}