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};
64use crate::operation::Operation;
65
66pub(crate) type HandlerResolver =
68 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
69
70pub struct WorkflowContext {
89 run_id: Uuid,
90 workflow_name: String,
91 store: Arc<dyn Store>,
92 provider: Arc<dyn AgentProvider>,
93 handler_resolver: Option<HandlerResolver>,
94 position: u32,
95 last_step_ids: Vec<Uuid>,
97 total_cost_usd: Decimal,
99 total_duration_ms: u64,
101 max_cost_usd: Option<Decimal>,
103 inherited_cost_usd: Decimal,
106 replay_steps: HashMap<u32, Step>,
109 granted_approvals: HashMap<u32, u32>,
113 attempt: u32,
115 carried_duration_ms: u64,
118 log_sender: Option<LogSender>,
120 artifact_sink: Option<Arc<dyn ArtifactSink>>,
124 has_allowed_failure: bool,
126 error_handlers: Vec<OnErrorHandler>,
128 guard_state: Option<SharedGuardState>,
130 guard_config: Option<WorkflowGuardConfig>,
132 step_results: Vec<StepResult>,
134 event_bus: Option<crate::notify::WorkflowEventBus>,
136 trace_context: WorkflowTraceContext,
138}
139
140struct OnErrorHandler {
142 name: String,
143 config: StepConfig,
144}
145
146impl WorkflowContext {
147 pub fn new(
152 run_id: Uuid,
153 workflow_name: String,
154 store: Arc<dyn Store>,
155 provider: Arc<dyn AgentProvider>,
156 ) -> Self {
157 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
158 Self {
159 run_id,
160 workflow_name,
161 store,
162 provider,
163 handler_resolver: None,
164 position: 0,
165 last_step_ids: Vec::new(),
166 total_cost_usd: Decimal::ZERO,
167 total_duration_ms: 0,
168 max_cost_usd: None,
169 inherited_cost_usd: Decimal::ZERO,
170 replay_steps: HashMap::new(),
171 granted_approvals: HashMap::new(),
172 attempt: 1,
173 carried_duration_ms: 0,
174 log_sender: None,
175 artifact_sink: None,
176 has_allowed_failure: false,
177 error_handlers: Vec::new(),
178 guard_state: None,
179 guard_config: None,
180 step_results: Vec::new(),
181 event_bus: None,
182 trace_context,
183 }
184 }
185
186 pub(crate) fn with_handler_resolver(
191 run_id: Uuid,
192 workflow_name: String,
193 store: Arc<dyn Store>,
194 provider: Arc<dyn AgentProvider>,
195 resolver: HandlerResolver,
196 ) -> Self {
197 let trace_context = WorkflowTraceContext::from_workflow_run_id(&run_id.to_string());
198 Self {
199 run_id,
200 workflow_name,
201 store,
202 provider,
203 handler_resolver: Some(resolver),
204 position: 0,
205 last_step_ids: Vec::new(),
206 total_cost_usd: Decimal::ZERO,
207 total_duration_ms: 0,
208 max_cost_usd: None,
209 inherited_cost_usd: Decimal::ZERO,
210 replay_steps: HashMap::new(),
211 granted_approvals: HashMap::new(),
212 attempt: 1,
213 carried_duration_ms: 0,
214 log_sender: None,
215 artifact_sink: None,
216 has_allowed_failure: false,
217 error_handlers: Vec::new(),
218 guard_state: None,
219 guard_config: None,
220 step_results: Vec::new(),
221 event_bus: None,
222 trace_context,
223 }
224 }
225
226 pub fn set_log_sender(&mut self, sender: LogSender) {
228 self.log_sender = Some(sender);
229 }
230
231 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
251 self.artifact_sink = Some(sink);
252 }
253
254 pub fn trace_context(&self) -> &WorkflowTraceContext {
260 &self.trace_context
261 }
262
263 pub fn set_guard(&mut self, config: WorkflowGuardConfig, state: SharedGuardState) {
280 self.guard_config = Some(config);
281 self.guard_state = Some(state);
282 }
283
284 pub fn guard_config(&self) -> Option<&WorkflowGuardConfig> {
286 self.guard_config.as_ref()
287 }
288
289 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
295 self.event_bus = Some(bus);
296 }
297
298 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
300 self.artifact_sink.as_ref().ok_or_else(|| {
301 EngineError::ArtifactsUnavailable(
302 "no artifact storage is attached to this run".to_string(),
303 )
304 })
305 }
306
307 pub async fn put_artifact(
337 &self,
338 step_id: Uuid,
339 name: &str,
340 content_type: Option<&str>,
341 content: Vec<u8>,
342 ) -> Result<Artifact, EngineError> {
343 let sink = self.artifact_sink()?;
344 sink.put(
345 ArtifactUpload {
346 run_id: self.run_id,
347 step_id,
348 name: name.to_string(),
349 content_type: content_type
350 .map(str::to_string)
351 .unwrap_or_else(|| guess_content_type(name)),
352 },
353 stream_from_bytes(content),
354 )
355 .await
356 }
357
358 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
383 let sink = self.artifact_sink()?;
384
385 let artifact = self
386 .store
387 .find_artifact_for_input(ArtifactLookup {
388 run_id: self.run_id,
389 attempt: self.attempt,
390 before_position: self.position,
391 step_name: step.to_string(),
392 name: name.to_string(),
393 })
394 .await?
395 .ok_or_else(|| EngineError::ArtifactNotFound {
396 step: step.to_string(),
397 name: name.to_string(),
398 })?;
399
400 let mut content = sink.get(&artifact).await?;
401 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
402 while let Some(chunk) = content.next().await {
403 let chunk = chunk?;
404 buffer.extend_from_slice(chunk.as_ref());
405 }
406
407 Ok(buffer)
408 }
409
410 async fn prepare_step_inputs(
415 &self,
416 config: &StepConfig,
417 position: u32,
418 ) -> Result<(), EngineError> {
419 let StepConfig::Shell(shell) = config else {
420 return Ok(());
421 };
422 if shell.inputs.is_empty() {
423 return Ok(());
424 }
425
426 materialize_inputs(
427 self.artifact_sink()?,
428 &self.store,
429 shell,
430 StepLocation {
431 run_id: self.run_id,
432 attempt: self.attempt,
433 position,
434 },
435 )
436 .await
437 }
438
439 async fn store_step_outputs(
444 &self,
445 config: &StepConfig,
446 step_id: Uuid,
447 step_name: &str,
448 step_succeeded: bool,
449 ) -> Result<(), EngineError> {
450 let StepConfig::Shell(shell) = config else {
451 return Ok(());
452 };
453 if shell.outputs.is_empty() {
454 return Ok(());
455 }
456
457 let sink = match self.artifact_sink() {
458 Ok(sink) => sink,
459 Err(err) if step_succeeded => return Err(err),
460 Err(err) => {
461 warn!(
462 run_id = %self.run_id,
463 step = %step_name,
464 error = %err,
465 "cannot collect outputs of a failed step"
466 );
467 return Ok(());
468 }
469 };
470
471 let collected =
472 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
473
474 match collected {
475 Ok(()) => Ok(()),
476 Err(err) if step_succeeded => Err(err),
477 Err(err) => {
478 warn!(
479 run_id = %self.run_id,
480 step = %step_name,
481 error = %err,
482 "failed to collect outputs of a failed step"
483 );
484 Ok(())
485 }
486 }
487 }
488
489 pub(crate) fn carry_over_run_totals(
496 &mut self,
497 attempt: u32,
498 cost_usd: Decimal,
499 duration_ms: u64,
500 ) {
501 self.attempt = attempt;
502 self.total_cost_usd = cost_usd;
503 self.carried_duration_ms = duration_ms;
504 }
505
506 pub(crate) fn carried_duration_ms(&self) -> u64 {
508 self.carried_duration_ms
509 }
510
511 pub fn attempt(&self) -> u32 {
513 self.attempt
514 }
515
516 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
532 self.max_cost_usd = cap;
533 }
534
535 pub fn max_cost_usd(&self) -> Option<Decimal> {
537 self.max_cost_usd
538 }
539
540 pub fn charged_cost_usd(&self) -> Decimal {
545 self.inherited_cost_usd + self.total_cost_usd
546 }
547
548 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
559 let Some(limit) = self.max_cost_usd else {
560 return Ok(());
561 };
562
563 let spent = self.charged_cost_usd();
564 if spent + step_budget <= limit {
565 return Ok(());
566 }
567
568 error!(
569 run_id = %self.run_id,
570 limit_usd = %limit,
571 spent_usd = %spent,
572 step_budget_usd = %step_budget,
573 "run cost cap reached, refusing agent step"
574 );
575
576 Err(EngineError::RunBudgetExceeded {
577 run_id: self.run_id,
578 limit_usd: limit,
579 spent_usd: spent,
580 step_budget_usd: step_budget,
581 })
582 }
583
584 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
596 let steps = self.store.list_steps(self.run_id).await?;
597 for step in steps {
598 let dominated = matches!(
599 step.status.state,
600 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
601 );
602 if !dominated {
603 continue;
604 }
605
606 if step.attempt == self.attempt {
607 self.replay_steps.insert(step.position, step);
608 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
609 {
610 self.granted_approvals.insert(step.position, step.attempt);
611 }
612 }
613 Ok(())
614 }
615
616 pub fn run_id(&self) -> Uuid {
618 self.run_id
619 }
620
621 pub fn workflow_name(&self) -> &str {
623 &self.workflow_name
624 }
625
626 pub fn total_cost_usd(&self) -> Decimal {
628 self.total_cost_usd
629 }
630
631 pub fn has_allowed_failure(&self) -> bool {
633 self.has_allowed_failure
634 }
635
636 pub fn total_duration_ms(&self) -> u64 {
638 self.total_duration_ms
639 }
640
641 pub fn step_results(&self) -> &[StepResult] {
643 &self.step_results
644 }
645
646 #[cfg(feature = "secret-store")]
668 pub fn secrets(&self) -> ironflow_store::workflow_secrets::ScopedSecretStore {
669 let workflow_uuid = Uuid::new_v5(&Uuid::NAMESPACE_OID, self.workflow_name.as_bytes());
670 ironflow_store::workflow_secrets::ScopedSecretStore::for_workflow(
671 workflow_uuid,
672 self.store.clone(),
673 )
674 }
675
676 async fn persist_progress(&self) {
682 if let Err(err) = self
683 .store
684 .update_run(
685 self.run_id,
686 RunUpdate {
687 cost_usd: Some(self.total_cost_usd),
688 duration_ms: Some(self.total_duration_ms),
689 ..RunUpdate::default()
690 },
691 )
692 .await
693 {
694 warn!(
695 run_id = %self.run_id,
696 error = %err,
697 "failed to persist run progress snapshot"
698 );
699 }
700 }
701
702 pub async fn parallel(
739 &mut self,
740 steps: Vec<(&str, StepConfig)>,
741 fail_fast: bool,
742 ) -> Result<Vec<ParallelStepResult>, EngineError> {
743 if steps.is_empty() {
744 return Ok(Vec::new());
745 }
746
747 self.check_guard_timeout()?;
749
750 let wave_budget: Decimal = steps
753 .iter()
754 .filter_map(|(_, config)| match config {
755 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
756 _ => None,
757 })
758 .map(step_budget_usd)
759 .sum();
760 self.check_run_budget(wave_budget)?;
761
762 let wave_position = self.position;
763 self.position += 1;
764
765 let now = Utc::now();
766 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
767 Vec::with_capacity(steps.len());
768
769 for (name, config) in &steps {
770 let kind = config.kind();
771 let trace_id = step_trace_id(self.run_id, name, wave_position);
772 let step = self
773 .store
774 .create_step(NewStep {
775 run_id: self.run_id,
776 trace_id,
777 name: name.to_string(),
778 kind,
779 position: wave_position,
780 input: Some(serde_json::to_value(config)?),
781 is_error_handler: false,
782 })
783 .await?;
784
785 self.start_step(step.id, now).await?;
786
787 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
790 self.fail_step(step.id, &err).await;
791 if !config.allow_failure() {
792 return Err(err);
793 }
794 self.has_allowed_failure = true;
795 info!(
796 run_id = %self.run_id,
797 step = %name,
798 error = %err,
799 "parallel step input preparation failed but allow_failure is set, skipping"
800 );
801 continue;
802 }
803
804 let mut config_with_trace = config.clone();
805 let step_trace = self.trace_context.child();
806 match config_with_trace {
807 StepConfig::Agent(ref mut agent_config) => {
808 agent_config.trace_context = Some(step_trace);
809 }
810 StepConfig::Http(ref mut http_config) => {
811 http_config.trace_context = Some(step_trace);
812 }
813 _ => {}
814 }
815 step_records.push((step.id, trace_id, name.to_string(), config_with_trace));
816 }
817
818 let mut join_set = JoinSet::new();
819 let mut task_index: HashMap<Id, usize> = HashMap::new();
820 let parallel_timeout = self.guard_remaining_timeout();
821 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
822 let provider = self.provider.clone();
823 let config = config.clone();
824 let step_log_sender = self
825 .log_sender
826 .as_ref()
827 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
828 let handle = join_set.spawn(async move {
829 let result = match parallel_timeout {
830 Some(dur) => {
831 match tokio::time::timeout(
832 dur,
833 execute_step_config(&config, &provider, step_log_sender),
834 )
835 .await
836 {
837 Ok(r) => r,
838 Err(_elapsed) => {
839 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
840 elapsed_secs: 0,
841 max: 0,
842 }))
843 }
844 }
845 }
846 None => execute_step_config(&config, &provider, step_log_sender).await,
847 };
848 (idx, result)
849 });
850 task_index.insert(handle.id(), idx);
851 }
852
853 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
855 vec![None; step_records.len()];
856 let mut first_error: Option<EngineError> = None;
857
858 while let Some(join_result) = join_set.join_next().await {
859 let (idx, step_result) = match join_result {
860 Ok(r) => r,
861 Err(e) => {
862 let error_msg = format!("join error: {e}");
863 if let Some(&idx) = task_index.get(&e.id()) {
864 let (step_id, _, step_name, _) = &step_records[idx];
865 let completed_at = Utc::now();
866 error!(
867 run_id = %self.run_id,
868 step = %step_name,
869 error = %error_msg,
870 "parallel step panicked or was cancelled"
871 );
872 if let Err(store_err) = self
873 .store
874 .update_step(
875 *step_id,
876 StepUpdate {
877 status: Some(StepStatus::Failed),
878 error: Some(error_msg.clone()),
879 completed_at: Some(completed_at),
880 ..StepUpdate::default()
881 },
882 )
883 .await
884 {
885 error!(
886 run_id = %self.run_id,
887 step_id = %step_id,
888 error = %store_err,
889 "failed to persist JoinError for step"
890 );
891 }
892 indexed_results[idx] = Some(Err(error_msg.clone()));
893 }
894 if first_error.is_none() {
895 first_error = Some(EngineError::StepConfig(error_msg));
896 }
897 if fail_fast {
898 join_set.abort_all();
899 }
900 continue;
901 }
902 };
903
904 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
905 let completed_at = Utc::now();
906
907 if let Err(err) = self
908 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
909 .await
910 {
911 self.fail_step(*step_id, &err).await;
912 indexed_results[idx] = Some(Err(err.to_string()));
913 if first_error.is_none() {
914 first_error = Some(err);
915 }
916 if fail_fast {
917 join_set.abort_all();
918 }
919 continue;
920 }
921
922 match step_result {
923 Ok(output) => {
924 self.total_cost_usd += output.cost_usd;
925 self.total_duration_ms += output.duration_ms;
926
927 if matches!(step_config, StepConfig::Agent(_)) {
929 let tokens = output
930 .input_tokens
931 .unwrap_or(0)
932 .saturating_add(output.output_tokens.unwrap_or(0));
933 if tokens > 0
934 && let Err(guard_err) = self.guard_record_tokens(tokens)
935 {
936 if first_error.is_none() {
937 first_error = Some(guard_err);
938 }
939 if fail_fast {
940 join_set.abort_all();
941 }
942 }
943 }
944
945 let debug_messages_json = output.debug_messages_json();
946
947 self.store
948 .update_step(
949 *step_id,
950 StepUpdate {
951 status: Some(StepStatus::Completed),
952 output: Some(output.output.clone()),
953 duration_ms: Some(output.duration_ms),
954 cost_usd: Some(output.cost_usd),
955 input_tokens: output.input_tokens,
956 output_tokens: output.output_tokens,
957 completed_at: Some(completed_at),
958 debug_messages: debug_messages_json,
959 ..StepUpdate::default()
960 },
961 )
962 .await?;
963
964 self.step_results.push(StepResult::from_success(
965 *step_trace,
966 step_name,
967 &output,
968 ));
969
970 if let Some(ref bus) = self.event_bus
971 && matches!(step_config, StepConfig::Agent(_))
972 {
973 let tokens = output
974 .input_tokens
975 .unwrap_or(0)
976 .saturating_add(output.output_tokens.unwrap_or(0));
977 bus.publish(
978 self.run_id,
979 crate::notify::WorkflowEvent::AgentStepTokensUsed {
980 step_name: step_name.clone(),
981 tokens,
982 cost_usd: output.cost_usd,
983 },
984 );
985 }
986
987 info!(
988 run_id = %self.run_id,
989 step = %step_name,
990 trace_id = %step_trace,
991 duration_ms = output.duration_ms,
992 "parallel step completed"
993 );
994
995 indexed_results[idx] = Some(Ok(output));
996 }
997 Err(err) => {
998 let err_msg = err.to_string();
999 let debug_messages_json = extract_debug_messages_from_error(&err);
1000 let partial = extract_partial_usage_from_error(&err);
1001 let raw_response_output = extract_raw_response_from_error(&err);
1002
1003 if let Some(ref usage) = partial {
1004 if let Some(cost) = usage.cost_usd {
1005 self.total_cost_usd += cost;
1006 }
1007 if let Some(dur) = usage.duration_ms {
1008 self.total_duration_ms += dur;
1009 }
1010 }
1011
1012 if let Err(store_err) = self
1013 .store
1014 .update_step(
1015 *step_id,
1016 StepUpdate {
1017 status: Some(StepStatus::Failed),
1018 error: Some(err_msg.clone()),
1019 output: raw_response_output.clone(),
1020 completed_at: Some(completed_at),
1021 debug_messages: debug_messages_json,
1022 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1023 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1024 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1025 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1026 ..StepUpdate::default()
1027 },
1028 )
1029 .await
1030 {
1031 tracing::error!(
1032 step_id = %step_id,
1033 error = %store_err,
1034 "failed to persist parallel step failure"
1035 );
1036 }
1037
1038 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
1039 let err_cost = partial
1040 .as_ref()
1041 .and_then(|p| p.cost_usd)
1042 .unwrap_or(Decimal::ZERO);
1043 self.step_results.push(StepResult::from_failure(
1044 *step_trace,
1045 step_name,
1046 &err_msg,
1047 err_duration,
1048 err_cost,
1049 ));
1050
1051 if step_config.allow_failure() {
1052 self.has_allowed_failure = true;
1053 info!(
1054 run_id = %self.run_id,
1055 step = %step_name,
1056 error = %err_msg,
1057 "parallel step failed but allow_failure is set, continuing"
1058 );
1059 indexed_results[idx] = Some(Ok(allowed_failure_output(
1060 &err_msg,
1061 raw_response_output,
1062 partial.as_ref(),
1063 )));
1064 } else {
1065 indexed_results[idx] = Some(Err(err_msg.clone()));
1066
1067 if first_error.is_none() {
1068 first_error = Some(err);
1069 }
1070
1071 if fail_fast {
1072 join_set.abort_all();
1073 }
1074 }
1075 }
1076 }
1077 }
1078
1079 if let Some(err) = first_error {
1080 return Err(err);
1081 }
1082
1083 self.persist_progress().await;
1084
1085 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
1086
1087 let results: Vec<ParallelStepResult> = step_records
1089 .iter()
1090 .enumerate()
1091 .map(|(idx, (step_id, _trace_id, name, _))| {
1092 let output = match indexed_results[idx].take() {
1093 Some(Ok(o)) => o,
1094 _ => unreachable!("all steps succeeded if no error returned"),
1095 };
1096 ParallelStepResult {
1097 name: name.clone(),
1098 output,
1099 step_id: *step_id,
1100 }
1101 })
1102 .collect();
1103
1104 Ok(results)
1105 }
1106
1107 pub async fn shell(
1130 &mut self,
1131 name: &str,
1132 config: ShellConfig,
1133 ) -> Result<StepOutput, EngineError> {
1134 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1135 .await
1136 }
1137
1138 pub async fn http(
1158 &mut self,
1159 name: &str,
1160 config: HttpConfig,
1161 ) -> Result<StepOutput, EngineError> {
1162 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1163 .await
1164 }
1165
1166 pub async fn agent(
1186 &mut self,
1187 name: &str,
1188 config: impl Into<AgentStepConfig>,
1189 ) -> Result<StepOutput, EngineError> {
1190 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1191 .await
1192 }
1193
1194 pub async fn approval(
1225 &mut self,
1226 name: &str,
1227 config: ApprovalConfig,
1228 ) -> Result<(), EngineError> {
1229 let position = self.position;
1230 self.position += 1;
1231
1232 if let Some(existing) = self.replay_steps.get(&position)
1235 && existing.kind == StepKind::Approval
1236 {
1237 if existing.status.state == StepStatus::AwaitingApproval {
1238 self.store
1239 .update_step(
1240 existing.id,
1241 StepUpdate {
1242 status: Some(StepStatus::Completed),
1243 completed_at: Some(Utc::now()),
1244 ..StepUpdate::default()
1245 },
1246 )
1247 .await?;
1248 }
1249
1250 self.last_step_ids = vec![existing.id];
1251 info!(
1252 run_id = %self.run_id,
1253 step = %name,
1254 position,
1255 "approval step replayed (approved)"
1256 );
1257 return Ok(());
1258 }
1259
1260 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1264 let trace_id = step_trace_id(self.run_id, name, position);
1265 let step = self
1266 .store
1267 .create_step(NewStep {
1268 run_id: self.run_id,
1269 trace_id,
1270 name: name.to_string(),
1271 kind: StepKind::Approval,
1272 position,
1273 input: Some(serde_json::to_value(&config)?),
1274 is_error_handler: false,
1275 })
1276 .await?;
1277
1278 let now = Utc::now();
1279 self.start_step(step.id, now).await?;
1280 self.store
1281 .update_step(
1282 step.id,
1283 StepUpdate {
1284 status: Some(StepStatus::Completed),
1285 output: Some(json!({"approved_in_attempt": granted_in})),
1286 completed_at: Some(now),
1287 ..StepUpdate::default()
1288 },
1289 )
1290 .await?;
1291
1292 self.last_step_ids = vec![step.id];
1293 info!(
1294 run_id = %self.run_id,
1295 step = %name,
1296 position,
1297 granted_in_attempt = granted_in,
1298 attempt = self.attempt,
1299 "approval carried over from a previous attempt"
1300 );
1301 return Ok(());
1302 }
1303
1304 let trace_id = step_trace_id(self.run_id, name, position);
1306 let step = self
1307 .store
1308 .create_step(NewStep {
1309 run_id: self.run_id,
1310 trace_id,
1311 name: name.to_string(),
1312 kind: StepKind::Approval,
1313 position,
1314 input: Some(serde_json::to_value(&config)?),
1315 is_error_handler: false,
1316 })
1317 .await?;
1318
1319 self.start_step(step.id, Utc::now()).await?;
1320
1321 self.store
1324 .update_step(
1325 step.id,
1326 StepUpdate {
1327 status: Some(StepStatus::AwaitingApproval),
1328 ..StepUpdate::default()
1329 },
1330 )
1331 .await?;
1332
1333 self.last_step_ids = vec![step.id];
1334
1335 if let Some(ref bus) = self.event_bus {
1336 bus.publish(
1337 self.run_id,
1338 crate::notify::WorkflowEvent::ApprovalRequired {
1339 step_name: name.to_string(),
1340 step_index: position,
1341 approval_id: step.id,
1342 },
1343 );
1344 }
1345
1346 Err(EngineError::ApprovalRequired {
1347 run_id: self.run_id,
1348 step_id: step.id,
1349 message: config.message().to_string(),
1350 })
1351 }
1352
1353 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1382 let position = self.position;
1383 self.position += 1;
1384
1385 let trace_id = step_trace_id(self.run_id, name, position);
1386 let step = self
1387 .store
1388 .create_step(NewStep {
1389 run_id: self.run_id,
1390 trace_id,
1391 name: name.to_string(),
1392 kind: StepKind::Custom("skip".to_string()),
1393 position,
1394 input: None,
1395 is_error_handler: false,
1396 })
1397 .await?;
1398
1399 if !self.last_step_ids.is_empty() {
1400 let deps: Vec<NewStepDependency> = self
1401 .last_step_ids
1402 .iter()
1403 .map(|&depends_on| NewStepDependency {
1404 step_id: step.id,
1405 depends_on,
1406 })
1407 .collect();
1408 self.store.create_step_dependencies(deps).await?;
1409 }
1410
1411 let now = Utc::now();
1412 self.store
1413 .update_step(
1414 step.id,
1415 StepUpdate {
1416 status: Some(StepStatus::Skipped),
1417 output: Some(serde_json::json!({"reason": reason})),
1418 completed_at: Some(now),
1419 ..StepUpdate::default()
1420 },
1421 )
1422 .await?;
1423
1424 self.last_step_ids = vec![step.id];
1425
1426 info!(
1427 run_id = %self.run_id,
1428 step = %name,
1429 reason,
1430 "step skipped"
1431 );
1432
1433 Ok(())
1434 }
1435
1436 pub async fn operation(
1474 &mut self,
1475 name: &str,
1476 op: &dyn Operation,
1477 ) -> Result<StepOutput, EngineError> {
1478 let kind = StepKind::Custom(op.kind().to_string());
1479 let position = self.position;
1480 self.position += 1;
1481
1482 let trace_id = step_trace_id(self.run_id, name, position);
1483 let step = self
1484 .store
1485 .create_step(NewStep {
1486 run_id: self.run_id,
1487 trace_id,
1488 name: name.to_string(),
1489 kind,
1490 position,
1491 input: op.input(),
1492 is_error_handler: false,
1493 })
1494 .await?;
1495
1496 self.start_step(step.id, Utc::now()).await?;
1497
1498 let start = Instant::now();
1499
1500 match op.execute().await {
1501 Ok(output_value) => {
1502 let duration_ms = start.elapsed().as_millis() as u64;
1503 self.total_duration_ms += duration_ms;
1504
1505 let completed_at = Utc::now();
1506 self.store
1507 .update_step(
1508 step.id,
1509 StepUpdate {
1510 status: Some(StepStatus::Completed),
1511 output: Some(output_value.clone()),
1512 duration_ms: Some(duration_ms),
1513 cost_usd: Some(Decimal::ZERO),
1514 completed_at: Some(completed_at),
1515 ..StepUpdate::default()
1516 },
1517 )
1518 .await?;
1519
1520 info!(
1521 run_id = %self.run_id,
1522 step = %name,
1523 kind = op.kind(),
1524 duration_ms,
1525 "operation step completed"
1526 );
1527
1528 self.last_step_ids = vec![step.id];
1529
1530 Ok(StepOutput {
1531 output: output_value,
1532 duration_ms,
1533 cost_usd: Decimal::ZERO,
1534 input_tokens: None,
1535 output_tokens: None,
1536 model: None,
1537 debug_messages: None,
1538 })
1539 }
1540 Err(err) => {
1541 let completed_at = Utc::now();
1542 if let Err(store_err) = self
1543 .store
1544 .update_step(
1545 step.id,
1546 StepUpdate {
1547 status: Some(StepStatus::Failed),
1548 error: Some(err.to_string()),
1549 completed_at: Some(completed_at),
1550 ..StepUpdate::default()
1551 },
1552 )
1553 .await
1554 {
1555 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1556 }
1557
1558 Err(err)
1559 }
1560 }
1561 }
1562
1563 pub async fn workflow(
1590 &mut self,
1591 handler: &dyn WorkflowHandler,
1592 payload: Value,
1593 ) -> Result<StepOutput, EngineError> {
1594 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1596 let state = guard_state
1597 .lock()
1598 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1599 state.check(guard_config, handler.name())?;
1600 }
1601
1602 let config = WorkflowStepConfig::new(handler.name(), payload);
1603 let position = self.position;
1604 self.position += 1;
1605
1606 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1607 let step = self
1608 .store
1609 .create_step(NewStep {
1610 run_id: self.run_id,
1611 trace_id,
1612 name: config.workflow_name.clone(),
1613 kind: StepKind::Workflow,
1614 position,
1615 input: Some(serde_json::to_value(&config)?),
1616 is_error_handler: false,
1617 })
1618 .await?;
1619
1620 self.start_step(step.id, Utc::now()).await?;
1621
1622 if let Some(guard_state) = &self.guard_state {
1624 let mut state = guard_state
1625 .lock()
1626 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1627 state.record_invocation(handler.name());
1628 }
1629
1630 match self.execute_child_workflow(&config).await {
1631 Ok((output, child_had_allowed_failure)) => {
1632 self.total_cost_usd += output.cost_usd;
1633 self.total_duration_ms += output.duration_ms;
1634 if child_had_allowed_failure {
1635 self.has_allowed_failure = true;
1636 }
1637
1638 let completed_at = Utc::now();
1639 self.store
1640 .update_step(
1641 step.id,
1642 StepUpdate {
1643 status: Some(StepStatus::Completed),
1644 output: Some(output.output.clone()),
1645 duration_ms: Some(output.duration_ms),
1646 cost_usd: Some(output.cost_usd),
1647 completed_at: Some(completed_at),
1648 ..StepUpdate::default()
1649 },
1650 )
1651 .await?;
1652
1653 info!(
1654 run_id = %self.run_id,
1655 child_workflow = %config.workflow_name,
1656 duration_ms = output.duration_ms,
1657 "workflow step completed"
1658 );
1659
1660 self.last_step_ids = vec![step.id];
1661
1662 self.guard_record_return();
1663 Ok(output)
1664 }
1665 Err(err) => {
1666 let completed_at = Utc::now();
1667 if let Err(store_err) = self
1668 .store
1669 .update_step(
1670 step.id,
1671 StepUpdate {
1672 status: Some(StepStatus::Failed),
1673 error: Some(err.to_string()),
1674 completed_at: Some(completed_at),
1675 ..StepUpdate::default()
1676 },
1677 )
1678 .await
1679 {
1680 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1681 }
1682
1683 self.guard_record_return();
1684 Err(err)
1685 }
1686 }
1687 }
1688
1689 fn guard_record_return(&self) {
1694 if let Some(guard_state) = &self.guard_state {
1695 match guard_state.lock() {
1696 Ok(mut state) => state.record_return(),
1697 Err(_) => {
1698 error!(
1699 run_id = %self.run_id,
1700 "guard state mutex poisoned in record_return"
1701 );
1702 }
1703 }
1704 }
1705 }
1706
1707 async fn execute_with_guard_timeout(
1711 &self,
1712 config: &StepConfig,
1713 step_log_sender: Option<StepLogSender>,
1714 ) -> Result<StepOutput, EngineError> {
1715 let remaining = self.guard_remaining_timeout();
1716 match remaining {
1717 Some(dur) => {
1718 use tokio::time::timeout;
1719 match timeout(
1720 dur,
1721 execute_step_config(config, &self.provider, step_log_sender),
1722 )
1723 .await
1724 {
1725 Ok(result) => result,
1726 Err(_elapsed) => {
1727 let config_secs = self
1728 .guard_config
1729 .as_ref()
1730 .map_or(0, |c| c.workflow_timeout_secs);
1731 Err(WorkflowRejection::WorkflowTimeout {
1732 elapsed_secs: config_secs,
1733 max: config_secs,
1734 }
1735 .into())
1736 }
1737 }
1738 }
1739 None => execute_step_config(config, &self.provider, step_log_sender).await,
1740 }
1741 }
1742
1743 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1745 let config = self.guard_config.as_ref()?;
1746 let guard_state = self.guard_state.as_ref()?;
1747 let state = guard_state.lock().ok()?;
1748 let elapsed = state.elapsed_secs();
1749 let max = config.workflow_timeout_secs;
1750 if elapsed >= max {
1751 Some(std::time::Duration::ZERO)
1752 } else {
1753 Some(std::time::Duration::from_secs(max - elapsed))
1754 }
1755 }
1756
1757 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1759 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1760 let state = guard_state
1761 .lock()
1762 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1763 let elapsed = state.elapsed_secs();
1764 if elapsed >= config.workflow_timeout_secs {
1765 return Err(WorkflowRejection::WorkflowTimeout {
1766 elapsed_secs: elapsed,
1767 max: config.workflow_timeout_secs,
1768 }
1769 .into());
1770 }
1771 }
1772 Ok(())
1773 }
1774
1775 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1777 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1778 let mut state = guard_state
1779 .lock()
1780 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1781 state.record_tokens(config, tokens)?;
1782 }
1783 Ok(())
1784 }
1785
1786 async fn execute_child_workflow(
1789 &self,
1790 config: &WorkflowStepConfig,
1791 ) -> Result<(StepOutput, bool), EngineError> {
1792 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1793 EngineError::InvalidWorkflow(
1794 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1795 )
1796 })?;
1797
1798 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1799 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1800 })?;
1801
1802 let parent = self.store.get_run(self.run_id).await?;
1805 let (parent_labels, parent_author) =
1806 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1807
1808 let child_run = self
1809 .store
1810 .create_run(NewRun {
1811 workflow_name: config.workflow_name.clone(),
1812 trigger: TriggerKind::Workflow,
1813 payload: config.payload.clone(),
1814 max_retries: 0,
1815 handler_version: None,
1816 labels: parent_labels,
1817 scheduled_at: None,
1818 created_by: parent_author,
1819 idempotency_key: None,
1820 max_cost_usd: self.max_cost_usd,
1822 })
1823 .await?
1824 .into_run();
1825
1826 let child_run_id = child_run.id;
1827 info!(
1828 parent_run_id = %self.run_id,
1829 child_run_id = %child_run_id,
1830 workflow = %config.workflow_name,
1831 "child run created"
1832 );
1833
1834 self.store
1835 .update_run_status(child_run_id, RunStatus::Running)
1836 .await?;
1837
1838 let run_start = Instant::now();
1839 let mut child_ctx = WorkflowContext {
1840 run_id: child_run_id,
1841 workflow_name: config.workflow_name.clone(),
1842 store: self.store.clone(),
1843 provider: self.provider.clone(),
1844 handler_resolver: self.handler_resolver.clone(),
1845 position: 0,
1846 last_step_ids: Vec::new(),
1847 total_cost_usd: Decimal::ZERO,
1848 total_duration_ms: 0,
1849 max_cost_usd: self.max_cost_usd,
1850 inherited_cost_usd: self.charged_cost_usd(),
1853 replay_steps: HashMap::new(),
1854 granted_approvals: HashMap::new(),
1855 attempt: 1,
1857 carried_duration_ms: 0,
1858 log_sender: self.log_sender.clone(),
1859 artifact_sink: self.artifact_sink.clone(),
1862 has_allowed_failure: false,
1863 error_handlers: Vec::new(),
1864 guard_state: self.guard_state.clone(),
1865 guard_config: self.guard_config.clone(),
1866 step_results: Vec::new(),
1867 event_bus: self.event_bus.clone(),
1868 trace_context: self.trace_context.child(),
1869 };
1870
1871 let result = handler.execute(&mut child_ctx).await;
1872 let total_duration = run_start.elapsed().as_millis() as u64;
1873 let completed_at = Utc::now();
1874
1875 match result {
1876 Ok(()) => {
1877 let child_status = if child_ctx.has_allowed_failure {
1878 RunStatus::Warning
1879 } else {
1880 RunStatus::Completed
1881 };
1882 self.store
1883 .update_run(
1884 child_run_id,
1885 RunUpdate {
1886 status: Some(child_status),
1887 cost_usd: Some(child_ctx.total_cost_usd),
1888 duration_ms: Some(total_duration),
1889 completed_at: Some(completed_at),
1890 ..RunUpdate::default()
1891 },
1892 )
1893 .await?;
1894
1895 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1896 Ok((
1897 StepOutput {
1898 output: serde_json::json!({
1899 "run_id": child_run_id,
1900 "workflow_name": config.workflow_name,
1901 "status": child_status,
1902 "cost_usd": child_ctx.total_cost_usd,
1903 "duration_ms": total_duration,
1904 }),
1905 duration_ms: total_duration,
1906 cost_usd: child_ctx.total_cost_usd,
1907 input_tokens: None,
1908 output_tokens: None,
1909 model: None,
1910 debug_messages: None,
1911 },
1912 child_had_allowed_failure,
1913 ))
1914 }
1915 Err(err) => {
1916 if let Err(store_err) = self
1917 .store
1918 .update_run(
1919 child_run_id,
1920 RunUpdate {
1921 status: Some(RunStatus::Failed),
1922 error: Some(err.to_string()),
1923 cost_usd: Some(child_ctx.total_cost_usd),
1924 duration_ms: Some(total_duration),
1925 completed_at: Some(completed_at),
1926 ..RunUpdate::default()
1927 },
1928 )
1929 .await
1930 {
1931 error!(
1932 child_run_id = %child_run_id,
1933 store_error = %store_err,
1934 "failed to persist child run failure"
1935 );
1936 }
1937
1938 Err(err)
1939 }
1940 }
1941 }
1942
1943 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1948 let step = self.replay_steps.get(&position)?;
1949 if step.status.state != StepStatus::Completed {
1950 return None;
1951 }
1952 let output = StepOutput {
1953 output: step.output.clone().unwrap_or(Value::Null),
1954 duration_ms: step.duration_ms,
1955 cost_usd: step.cost_usd,
1956 input_tokens: step.input_tokens,
1957 output_tokens: step.output_tokens,
1958 model: None,
1959 debug_messages: None,
1960 };
1961 self.total_cost_usd += output.cost_usd;
1962 self.total_duration_ms += output.duration_ms;
1963 self.last_step_ids = vec![step.id];
1964 info!(
1965 run_id = %self.run_id,
1966 step = %step.name,
1967 position,
1968 "step replayed from previous execution"
1969 );
1970 Some(output)
1971 }
1972
1973 #[tracing::instrument(
1975 name = "context.execute_step",
1976 skip_all,
1977 fields(
1978 run_id = %self.run_id,
1979 step.name = %name,
1980 step.kind,
1981 step.position = self.position,
1982 step.trace_id,
1983 )
1984 )]
1985 async fn execute_step(
1986 &mut self,
1987 name: &str,
1988 kind: StepKind,
1989 config: StepConfig,
1990 ) -> Result<StepOutput, EngineError> {
1991 let kind_str: &'static str = match kind {
1992 StepKind::Shell => "shell",
1993 StepKind::Http => "http",
1994 StepKind::Agent => "agent",
1995 StepKind::Workflow => "workflow",
1996 StepKind::Approval => "approval",
1997 StepKind::Custom(_) => "custom",
1998 };
1999 Span::current().record("step.kind", kind_str);
2000
2001 self.check_guard_timeout()?;
2003
2004 let position = self.position;
2005 self.position += 1;
2006
2007 if let Some(output) = self.try_replay_step(position) {
2009 return Ok(output);
2010 }
2011
2012 if let StepConfig::Agent(ref agent_config) = config {
2015 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2016 }
2017
2018 let trace_id = step_trace_id(self.run_id, name, position);
2020 Span::current().record("step.trace_id", trace_id.to_string().as_str());
2021 let step = self
2022 .store
2023 .create_step(NewStep {
2024 run_id: self.run_id,
2025 trace_id,
2026 name: name.to_string(),
2027 kind,
2028 position,
2029 input: Some(serde_json::to_value(&config)?),
2030 is_error_handler: false,
2031 })
2032 .await?;
2033
2034 self.start_step(step.id, Utc::now()).await?;
2035
2036 if let Some(ref bus) = self.event_bus {
2037 bus.publish(
2038 self.run_id,
2039 crate::notify::WorkflowEvent::StepStarted {
2040 step_name: name.to_string(),
2041 step_index: position,
2042 timestamp: Utc::now(),
2043 },
2044 );
2045 }
2046
2047 if let Err(err) = self.prepare_step_inputs(&config, position).await {
2050 self.fail_step(step.id, &err).await;
2051 if config.allow_failure() {
2052 self.has_allowed_failure = true;
2053 self.last_step_ids = vec![step.id];
2054 info!(
2055 run_id = %self.run_id,
2056 step = %name,
2057 error = %err,
2058 "step input preparation failed but allow_failure is set, continuing"
2059 );
2060 return Ok(StepOutput {
2061 output: json!({"error": err.to_string()}),
2062 duration_ms: 0,
2063 cost_usd: Decimal::ZERO,
2064 input_tokens: None,
2065 output_tokens: None,
2066 model: None,
2067 debug_messages: None,
2068 });
2069 }
2070 return Err(err);
2071 }
2072
2073 let mut config = config;
2074 let step_trace = self.trace_context.child();
2075 match config {
2076 StepConfig::Agent(ref mut agent_config) => {
2077 agent_config.trace_context = Some(step_trace);
2078 }
2079 StepConfig::Http(ref mut http_config) => {
2080 http_config.trace_context = Some(step_trace);
2081 }
2082 _ => {}
2083 }
2084
2085 let step_log_sender = self
2086 .log_sender
2087 .as_ref()
2088 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
2089
2090 let execution = self
2091 .execute_with_guard_timeout(&config, step_log_sender)
2092 .await;
2093
2094 let execution = self
2095 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
2096 .await;
2097
2098 if let Err(err) = self
2099 .store_step_outputs(&config, step.id, name, execution.is_ok())
2100 .await
2101 {
2102 self.fail_step(step.id, &err).await;
2103 return Err(err);
2104 }
2105
2106 match execution {
2107 Ok(output) => {
2108 self.total_cost_usd += output.cost_usd;
2109 self.total_duration_ms += output.duration_ms;
2110
2111 if matches!(config, StepConfig::Agent(_)) {
2113 let tokens = output
2114 .input_tokens
2115 .unwrap_or(0)
2116 .saturating_add(output.output_tokens.unwrap_or(0));
2117 if tokens > 0 {
2118 self.guard_record_tokens(tokens)?;
2119 }
2120 }
2121
2122 let debug_messages_json = output.debug_messages_json();
2123
2124 let completed_at = Utc::now();
2125 self.store
2126 .update_step(
2127 step.id,
2128 StepUpdate {
2129 status: Some(StepStatus::Completed),
2130 output: Some(output.output.clone()),
2131 duration_ms: Some(output.duration_ms),
2132 cost_usd: Some(output.cost_usd),
2133 input_tokens: output.input_tokens,
2134 output_tokens: output.output_tokens,
2135 completed_at: Some(completed_at),
2136 debug_messages: debug_messages_json,
2137 ..StepUpdate::default()
2138 },
2139 )
2140 .await?;
2141
2142 self.step_results
2143 .push(StepResult::from_success(trace_id, name, &output));
2144 self.persist_progress().await;
2145
2146 info!(
2147 run_id = %self.run_id,
2148 step = %name,
2149 trace_id = %trace_id,
2150 duration_ms = output.duration_ms,
2151 "step completed"
2152 );
2153
2154 if let Some(ref bus) = self.event_bus {
2155 bus.publish(
2156 self.run_id,
2157 crate::notify::WorkflowEvent::StepCompleted {
2158 step_name: name.to_string(),
2159 step_index: position,
2160 duration_ms: output.duration_ms,
2161 output_summary: None,
2162 },
2163 );
2164
2165 if matches!(config, StepConfig::Agent(_)) {
2166 let tokens = output
2167 .input_tokens
2168 .unwrap_or(0)
2169 .saturating_add(output.output_tokens.unwrap_or(0));
2170 bus.publish(
2171 self.run_id,
2172 crate::notify::WorkflowEvent::AgentStepTokensUsed {
2173 step_name: name.to_string(),
2174 tokens,
2175 cost_usd: output.cost_usd,
2176 },
2177 );
2178 }
2179 }
2180
2181 self.last_step_ids = vec![step.id];
2182
2183 Ok(output)
2184 }
2185 Err(err) => {
2186 let completed_at = Utc::now();
2187 let debug_messages_json = extract_debug_messages_from_error(&err);
2188 let partial = extract_partial_usage_from_error(&err);
2189 let raw_response_output = extract_raw_response_from_error(&err);
2190
2191 if let Some(ref usage) = partial {
2192 if let Some(cost) = usage.cost_usd {
2193 self.total_cost_usd += cost;
2194 }
2195 if let Some(dur) = usage.duration_ms {
2196 self.total_duration_ms += dur;
2197 }
2198 }
2199
2200 if let Err(store_err) = self
2201 .store
2202 .update_step(
2203 step.id,
2204 StepUpdate {
2205 status: Some(StepStatus::Failed),
2206 error: Some(err.to_string()),
2207 output: raw_response_output.clone(),
2208 completed_at: Some(completed_at),
2209 debug_messages: debug_messages_json,
2210 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2211 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2212 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2213 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2214 ..StepUpdate::default()
2215 },
2216 )
2217 .await
2218 {
2219 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2220 }
2221
2222 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2223 let err_cost = partial
2224 .as_ref()
2225 .and_then(|p| p.cost_usd)
2226 .unwrap_or(Decimal::ZERO);
2227 self.step_results.push(StepResult::from_failure(
2228 trace_id,
2229 name,
2230 &err.to_string(),
2231 err_duration,
2232 err_cost,
2233 ));
2234 self.persist_progress().await;
2235
2236 if let Some(ref bus) = self.event_bus {
2237 bus.publish(
2238 self.run_id,
2239 crate::notify::WorkflowEvent::StepFailed {
2240 step_name: name.to_string(),
2241 step_index: position,
2242 error: err.to_string(),
2243 duration_ms: err_duration,
2244 },
2245 );
2246 }
2247
2248 self.fire_error_handlers(name, &err.to_string(), err_duration)
2249 .await;
2250
2251 if config.allow_failure() {
2252 self.has_allowed_failure = true;
2253 self.last_step_ids = vec![step.id];
2254 info!(
2255 run_id = %self.run_id,
2256 step = %name,
2257 error = %err,
2258 "step failed but allow_failure is set, continuing"
2259 );
2260 Ok(allowed_failure_output(
2261 &err.to_string(),
2262 raw_response_output,
2263 partial.as_ref(),
2264 ))
2265 } else {
2266 Err(err)
2267 }
2268 }
2269 }
2270 }
2271
2272 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2278 async fn retry_step_if_configured(
2279 &self,
2280 name: &str,
2281 kind_str: &str,
2282 config: &StepConfig,
2283 step_id: Uuid,
2284 first_result: Result<StepOutput, EngineError>,
2285 ) -> Result<StepOutput, EngineError> {
2286 let policy = match config.retry() {
2287 Some(p) => p,
2288 None => return first_result,
2289 };
2290
2291 let mut last_result = match first_result {
2292 Ok(output) => return Ok(output),
2293 Err(err) if !is_step_retryable(&err) => return Err(err),
2294 Err(err) => Err(err),
2295 };
2296
2297 let step_log_sender = self
2298 .log_sender
2299 .as_ref()
2300 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2301
2302 for attempt in 0..policy.max_retries() {
2303 if let StepConfig::Agent(agent_config) = config {
2304 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2305 }
2306
2307 let delay = policy.delay_for_attempt(attempt);
2308 info!(
2309 run_id = %self.run_id,
2310 step = %name,
2311 attempt = attempt + 1,
2312 max_retries = policy.max_retries(),
2313 delay_ms = delay.as_millis() as u64,
2314 "retrying step after transient failure"
2315 );
2316 tokio::time::sleep(delay).await;
2317
2318 record_retry_metric(kind_str, "retry");
2319
2320 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2321 Ok(output) => return Ok(output),
2322 Err(err) if !is_step_retryable(&err) => return Err(err),
2323 err => last_result = err,
2324 }
2325 }
2326
2327 record_retry_metric(kind_str, "exhausted");
2328
2329 info!(
2330 run_id = %self.run_id,
2331 step = %name,
2332 max_retries = policy.max_retries(),
2333 "step retries exhausted"
2334 );
2335
2336 last_result
2337 }
2338
2339 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
2344 if !self.last_step_ids.is_empty() {
2345 let deps: Vec<NewStepDependency> = self
2346 .last_step_ids
2347 .iter()
2348 .map(|&depends_on| NewStepDependency {
2349 step_id,
2350 depends_on,
2351 })
2352 .collect();
2353 self.store.create_step_dependencies(deps).await?;
2354 }
2355
2356 self.store
2357 .update_step(
2358 step_id,
2359 StepUpdate {
2360 status: Some(StepStatus::Running),
2361 started_at: Some(now),
2362 ..StepUpdate::default()
2363 },
2364 )
2365 .await?;
2366
2367 Ok(())
2368 }
2369
2370 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2377 if let Err(store_err) = self
2378 .store
2379 .update_step(
2380 step_id,
2381 StepUpdate {
2382 status: Some(StepStatus::Failed),
2383 error: Some(err.to_string()),
2384 completed_at: Some(Utc::now()),
2385 ..StepUpdate::default()
2386 },
2387 )
2388 .await
2389 {
2390 error!(
2391 step_id = %step_id,
2392 error = %store_err,
2393 "failed to persist step failure"
2394 );
2395 }
2396 }
2397
2398 pub fn store(&self) -> &Arc<dyn Store> {
2400 &self.store
2401 }
2402
2403 pub async fn payload(&self) -> Result<Value, EngineError> {
2411 let run = self
2412 .store
2413 .get_run(self.run_id)
2414 .await?
2415 .ok_or(EngineError::Store(
2416 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2417 ))?;
2418 Ok(run.payload)
2419 }
2420
2421 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2449 let payload = self.payload().await?;
2450 serde_json::from_value(payload).map_err(EngineError::Serialization)
2451 }
2452
2453 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2476 self.error_handlers.push(OnErrorHandler {
2477 name: name.to_string(),
2478 config: config.into(),
2479 });
2480 }
2481
2482 pub fn clear_error_handlers(&mut self) {
2501 self.error_handlers.clear();
2502 }
2503
2504 async fn fire_error_handlers(
2510 &mut self,
2511 failed_step_name: &str,
2512 error_msg: &str,
2513 duration_ms: u64,
2514 ) {
2515 let handlers = std::mem::take(&mut self.error_handlers);
2516 if handlers.is_empty() {
2517 return;
2518 }
2519
2520 let error_context = json!({
2521 "failed_step": failed_step_name,
2522 "error": error_msg,
2523 "duration_ms": duration_ms,
2524 });
2525
2526 for handler in handlers {
2527 let mut config = handler.config.clone();
2528 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2529
2530 let position = self.position;
2531 self.position += 1;
2532
2533 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2534 let step = match self
2535 .store
2536 .create_step(NewStep {
2537 run_id: self.run_id,
2538 trace_id,
2539 name: handler.name.clone(),
2540 kind: config.kind(),
2541 position,
2542 input: Some(error_context.clone()),
2543 is_error_handler: true,
2544 })
2545 .await
2546 {
2547 Ok(step) => step,
2548 Err(err) => {
2549 warn!(
2550 run_id = %self.run_id,
2551 handler = %handler.name,
2552 error = %err,
2553 "failed to create error handler step"
2554 );
2555 continue;
2556 }
2557 };
2558
2559 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2560 warn!(
2561 run_id = %self.run_id,
2562 handler = %handler.name,
2563 error = %err,
2564 "failed to start error handler step"
2565 );
2566 continue;
2567 }
2568
2569 let step_log_sender = self
2570 .log_sender
2571 .as_ref()
2572 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2573
2574 let start = Instant::now();
2575 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2576 let handler_duration = start.elapsed().as_millis() as u64;
2577 let completed_at = Utc::now();
2578
2579 match result {
2580 Ok(output) => {
2581 if let Err(store_err) = self
2582 .store
2583 .update_step(
2584 step.id,
2585 StepUpdate {
2586 status: Some(StepStatus::Completed),
2587 output: Some(output.output),
2588 duration_ms: Some(handler_duration),
2589 cost_usd: Some(output.cost_usd),
2590 completed_at: Some(completed_at),
2591 ..StepUpdate::default()
2592 },
2593 )
2594 .await
2595 {
2596 warn!(
2597 run_id = %self.run_id,
2598 handler = %handler.name,
2599 error = %store_err,
2600 "failed to persist error handler completion"
2601 );
2602 }
2603
2604 info!(
2605 run_id = %self.run_id,
2606 handler = %handler.name,
2607 duration_ms = handler_duration,
2608 "error handler completed"
2609 );
2610 }
2611 Err(err) => {
2612 if let Err(store_err) = self
2613 .store
2614 .update_step(
2615 step.id,
2616 StepUpdate {
2617 status: Some(StepStatus::Failed),
2618 error: Some(err.to_string()),
2619 duration_ms: Some(handler_duration),
2620 completed_at: Some(completed_at),
2621 ..StepUpdate::default()
2622 },
2623 )
2624 .await
2625 {
2626 warn!(
2627 run_id = %self.run_id,
2628 handler = %handler.name,
2629 error = %store_err,
2630 "failed to persist error handler failure"
2631 );
2632 }
2633
2634 warn!(
2635 run_id = %self.run_id,
2636 handler = %handler.name,
2637 error = %err,
2638 "error handler failed (original error preserved)"
2639 );
2640 }
2641 }
2642 }
2643 }
2644}
2645
2646fn inject_error_context(
2648 config: &mut StepConfig,
2649 failed_step: &str,
2650 error_msg: &str,
2651 duration_ms: u64,
2652) {
2653 match config {
2654 StepConfig::Shell(shell) => {
2655 shell
2656 .env
2657 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2658 shell
2659 .env
2660 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2661 shell.env.push((
2662 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2663 duration_ms.to_string(),
2664 ));
2665 }
2666 StepConfig::Agent(agent) => {
2667 agent.prompt = format!(
2668 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2669 failed_step, duration_ms, error_msg, agent.prompt
2670 );
2671 }
2672 StepConfig::Http(http) => {
2673 http.headers
2674 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2675 http.headers.push((
2676 "X-Ironflow-Error-Message".to_string(),
2677 error_msg.to_string(),
2678 ));
2679 }
2680 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2681 }
2682}
2683
2684#[cfg(feature = "prometheus")]
2685fn record_retry_metric(kind: &str, outcome: &str) {
2686 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2687 use metrics::counter;
2688 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2689 .increment(1);
2690}
2691
2692#[cfg(not(feature = "prometheus"))]
2693fn record_retry_metric(_kind: &str, _outcome: &str) {}
2694
2695fn is_step_retryable(err: &EngineError) -> bool {
2699 use ironflow_core::error::{AgentError, OperationError};
2700
2701 match err {
2702 EngineError::Operation(op) => match op {
2703 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2704 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2705 OperationError::Deserialize { .. } => false,
2706 OperationError::Http {
2707 status: Some(code), ..
2708 } if (400..500).contains(code) && *code != 429 => false,
2709 _ => true,
2710 },
2711 _ => false,
2712 }
2713}
2714
2715fn allowed_failure_output(
2716 error_msg: &str,
2717 raw_response: Option<Value>,
2718 partial: Option<&StepPartialUsage>,
2719) -> StepOutput {
2720 StepOutput {
2721 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2722 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2723 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2724 input_tokens: partial.and_then(|p| p.input_tokens),
2725 output_tokens: partial.and_then(|p| p.output_tokens),
2726 model: None,
2727 debug_messages: None,
2728 }
2729}
2730
2731impl fmt::Debug for WorkflowContext {
2732 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2733 f.debug_struct("WorkflowContext")
2734 .field("run_id", &self.run_id)
2735 .field("position", &self.position)
2736 .field("total_cost_usd", &self.total_cost_usd)
2737 .field("inherited_cost_usd", &self.inherited_cost_usd)
2738 .field("max_cost_usd", &self.max_cost_usd)
2739 .finish_non_exhaustive()
2740 }
2741}
2742
2743fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2746 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2747 debug_messages,
2748 ..
2749 })) = err
2750 && !debug_messages.is_empty()
2751 {
2752 return serde_json::to_value(debug_messages).ok();
2753 }
2754 None
2755}
2756
2757struct StepPartialUsage {
2763 cost_usd: Option<Decimal>,
2764 duration_ms: Option<u64>,
2765 input_tokens: Option<u64>,
2766 output_tokens: Option<u64>,
2767}
2768
2769fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2775 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2776 raw_response: Some(text),
2777 ..
2778 })) = err
2779 {
2780 return Some(Value::String(text.clone()));
2781 }
2782 None
2783}
2784
2785fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2786 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2787 partial_usage,
2788 ..
2789 })) = err
2790 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2791 {
2792 return Some(StepPartialUsage {
2793 cost_usd: partial_usage
2794 .cost_usd
2795 .and_then(|c| Decimal::try_from(c).ok()),
2796 duration_ms: partial_usage.duration_ms,
2797 input_tokens: partial_usage.input_tokens,
2798 output_tokens: partial_usage.output_tokens,
2799 });
2800 }
2801 None
2802}
2803
2804#[cfg(test)]
2805mod tests {
2806 use super::*;
2807 use ironflow_core::providers::claude::ClaudeCodeProvider;
2808 use ironflow_core::providers::record_replay::RecordReplayProvider;
2809 use ironflow_store::memory::InMemoryStore;
2810 use ironflow_store::models::{Run, RunActor, RunFilter};
2811 use ironflow_store::store::RunStore;
2812 use serde_json::json;
2813 use std::sync::Arc;
2814 use std::sync::atomic::{AtomicBool, Ordering};
2815 use uuid::Uuid;
2816
2817 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2819 let inner = ClaudeCodeProvider::new();
2820 Arc::new(RecordReplayProvider::replay(
2821 inner,
2822 "/tmp/ironflow-fixtures",
2823 ))
2824 }
2825
2826 fn create_test_context() -> WorkflowContext {
2828 let store = Arc::new(InMemoryStore::new());
2829 let provider = create_test_provider();
2830 let run_id = Uuid::now_v7();
2831 WorkflowContext::new(run_id, "test".to_string(), store, provider)
2832 }
2833
2834 #[test]
2835 fn context_new_initializes_correctly() {
2836 let ctx = create_test_context();
2837 assert_eq!(ctx.position, 0);
2838 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2839 assert_eq!(ctx.total_duration_ms, 0);
2840 assert!(ctx.last_step_ids.is_empty());
2841 assert!(ctx.replay_steps.is_empty());
2842 assert!(ctx.log_sender.is_none());
2843 }
2844
2845 #[test]
2846 fn context_run_id_returns_correct_id() {
2847 let run_id = Uuid::now_v7();
2848 let store = Arc::new(InMemoryStore::new());
2849 let provider = create_test_provider();
2850 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
2851 assert_eq!(ctx.run_id(), run_id);
2852 }
2853
2854 #[test]
2855 fn context_total_cost_usd_initially_zero() {
2856 let ctx = create_test_context();
2857 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2858 }
2859
2860 #[test]
2861 fn context_total_duration_ms_initially_zero() {
2862 let ctx = create_test_context();
2863 assert_eq!(ctx.total_duration_ms(), 0);
2864 }
2865
2866 #[test]
2867 fn context_with_handler_resolver_creates_context_with_resolver() {
2868 let store = Arc::new(InMemoryStore::new());
2869 let provider = create_test_provider();
2870 let run_id = Uuid::now_v7();
2871
2872 let called = Arc::new(AtomicBool::new(false));
2873 let called_clone = called.clone();
2874
2875 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2876 called_clone.store(true, Ordering::SeqCst);
2877 None
2878 });
2879
2880 let ctx = WorkflowContext::with_handler_resolver(
2881 run_id,
2882 "test".to_string(),
2883 store,
2884 provider,
2885 resolver,
2886 );
2887
2888 assert_eq!(ctx.run_id(), run_id);
2889 assert!(ctx.handler_resolver.is_some());
2890 }
2891
2892 #[tokio::test]
2893 async fn context_set_log_sender_attaches_sender() {
2894 let mut ctx = create_test_context();
2895 let (sender, _receiver) = crate::log_sender::channel();
2896 ctx.set_log_sender(sender);
2897 assert!(ctx.log_sender.is_some());
2898 }
2899
2900 #[tokio::test]
2901 async fn context_skip_creates_skipped_step() {
2902 let store = Arc::new(InMemoryStore::new());
2903 let provider = create_test_provider();
2904
2905 store
2907 .create_run(NewRun {
2908 created_by: None,
2909 workflow_name: "test".to_string(),
2910 trigger: TriggerKind::Manual,
2911 payload: json!({}),
2912 max_retries: 0,
2913 handler_version: None,
2914 labels: Default::default(),
2915 scheduled_at: None,
2916 idempotency_key: None,
2917 max_cost_usd: None,
2918 })
2919 .await
2920 .expect("failed to create run")
2921 .into_run();
2922
2923 let runs = store
2925 .list_runs(RunFilter::default(), 1, 10)
2926 .await
2927 .expect("failed to list runs");
2928 let created_run_id = runs.items[0].id;
2929
2930 let mut ctx =
2931 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
2932 let initial_position = ctx.position;
2933
2934 ctx.skip("skip-step", "condition not met")
2935 .await
2936 .expect("skip failed");
2937
2938 assert_eq!(ctx.position, initial_position + 1);
2939 assert!(!ctx.last_step_ids.is_empty());
2940
2941 let steps = store
2943 .list_steps(created_run_id)
2944 .await
2945 .expect("failed to list steps");
2946 assert_eq!(steps.len(), 1);
2947 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2948 }
2949
2950 struct NoopSubWorkflow;
2953
2954 impl WorkflowHandler for NoopSubWorkflow {
2955 fn name(&self) -> &str {
2956 "noop-sub"
2957 }
2958
2959 fn execute<'a>(
2960 &'a self,
2961 _ctx: &'a mut WorkflowContext,
2962 ) -> crate::handler::HandlerFuture<'a> {
2963 Box::pin(async move { Ok(()) })
2964 }
2965 }
2966
2967 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2970 let store = Arc::new(InMemoryStore::new());
2971 let provider = create_test_provider();
2972
2973 let parent = store
2974 .create_run(NewRun {
2975 workflow_name: "parent".to_string(),
2976 trigger: TriggerKind::Api,
2977 payload: json!({}),
2978 max_retries: 0,
2979 handler_version: None,
2980 labels: Default::default(),
2981 scheduled_at: None,
2982 created_by,
2983 idempotency_key: None,
2984 max_cost_usd: None,
2985 })
2986 .await
2987 .expect("failed to create parent run")
2988 .into_run();
2989
2990 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2991 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2992 _ => None,
2993 });
2994
2995 let mut ctx = WorkflowContext::with_handler_resolver(
2996 parent.id,
2997 "parent".to_string(),
2998 store.clone(),
2999 provider,
3000 resolver,
3001 );
3002 ctx.workflow(&NoopSubWorkflow, json!({}))
3003 .await
3004 .expect("sub-workflow failed");
3005
3006 let runs = store
3007 .list_runs(RunFilter::default(), 1, 10)
3008 .await
3009 .expect("failed to list runs");
3010 runs.items
3011 .into_iter()
3012 .find(|r| r.workflow_name == "noop-sub")
3013 .expect("child run was created")
3014 }
3015
3016 #[tokio::test]
3017 async fn child_run_inherits_the_parent_author() {
3018 let user_id = Uuid::now_v7();
3019 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
3020
3021 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
3022 }
3023
3024 #[tokio::test]
3025 async fn child_run_of_an_unattributed_parent_has_no_author() {
3026 let child = child_run_of_parent_authored_by(None).await;
3027
3028 assert!(child.created_by.is_none());
3029 }
3030
3031 #[tokio::test]
3032 async fn context_parallel_empty_steps_returns_empty_vec() {
3033 let mut ctx = create_test_context();
3034 let results = ctx
3035 .parallel(vec![], true)
3036 .await
3037 .expect("parallel should not fail on empty input");
3038 assert!(results.is_empty());
3039 }
3040
3041 #[tokio::test]
3042 async fn context_approval_first_execution_returns_error() {
3043 let store = Arc::new(InMemoryStore::new());
3044 let provider = create_test_provider();
3045
3046 store
3048 .create_run(NewRun {
3049 created_by: None,
3050 workflow_name: "test".to_string(),
3051 trigger: TriggerKind::Manual,
3052 payload: json!({}),
3053 max_retries: 0,
3054 handler_version: None,
3055 labels: Default::default(),
3056 scheduled_at: None,
3057 idempotency_key: None,
3058 max_cost_usd: None,
3059 })
3060 .await
3061 .expect("failed to create run")
3062 .into_run();
3063
3064 let runs = store
3066 .list_runs(RunFilter::default(), 1, 10)
3067 .await
3068 .expect("failed to list runs");
3069 let created_run_id = runs.items[0].id;
3070
3071 let mut ctx =
3072 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3073
3074 let result = ctx
3075 .approval(
3076 "approve-step",
3077 crate::config::ApprovalConfig::new("Continue?"),
3078 )
3079 .await;
3080
3081 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
3083
3084 assert_eq!(ctx.position, 1);
3086
3087 let steps = store
3089 .list_steps(created_run_id)
3090 .await
3091 .expect("failed to list steps");
3092 assert_eq!(steps.len(), 1);
3093 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
3094 }
3095
3096 #[tokio::test]
3097 async fn context_approval_replay_returns_ok() {
3098 let store = Arc::new(InMemoryStore::new());
3099 let provider = create_test_provider();
3100
3101 store
3103 .create_run(NewRun {
3104 created_by: None,
3105 workflow_name: "test".to_string(),
3106 trigger: TriggerKind::Manual,
3107 payload: json!({}),
3108 max_retries: 0,
3109 handler_version: None,
3110 labels: Default::default(),
3111 scheduled_at: None,
3112 idempotency_key: None,
3113 max_cost_usd: None,
3114 })
3115 .await
3116 .expect("failed to create run")
3117 .into_run();
3118
3119 let runs = store
3121 .list_runs(RunFilter::default(), 1, 10)
3122 .await
3123 .expect("failed to list runs");
3124 let created_run_id = runs.items[0].id;
3125
3126 let step = store
3128 .create_step(NewStep {
3129 run_id: created_run_id,
3130 trace_id: step_trace_id(created_run_id, "approval", 0),
3131 name: "approval".to_string(),
3132 kind: StepKind::Approval,
3133 position: 0,
3134 input: None,
3135 is_error_handler: false,
3136 })
3137 .await
3138 .expect("failed to create step");
3139
3140 store
3142 .update_step(
3143 step.id,
3144 StepUpdate {
3145 status: Some(StepStatus::Running),
3146 started_at: Some(Utc::now()),
3147 ..StepUpdate::default()
3148 },
3149 )
3150 .await
3151 .expect("failed to update step to Running");
3152
3153 store
3154 .update_step(
3155 step.id,
3156 StepUpdate {
3157 status: Some(StepStatus::AwaitingApproval),
3158 ..StepUpdate::default()
3159 },
3160 )
3161 .await
3162 .expect("failed to update step to AwaitingApproval");
3163
3164 let mut ctx =
3166 WorkflowContext::new(created_run_id, "test".to_string(), store.clone(), provider);
3167 ctx.load_replay_steps()
3168 .await
3169 .expect("failed to load replay steps");
3170
3171 let result = ctx
3173 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3174 .await;
3175
3176 assert!(result.is_ok());
3177
3178 let steps = store
3180 .list_steps(created_run_id)
3181 .await
3182 .expect("failed to list steps");
3183 assert_eq!(steps.len(), 1);
3184 assert_eq!(steps[0].status.state, StepStatus::Completed);
3185 }
3186
3187 #[tokio::test]
3188 async fn context_load_replay_steps_loads_completed_steps() {
3189 let store = Arc::new(InMemoryStore::new());
3190 let provider = create_test_provider();
3191
3192 store
3194 .create_run(NewRun {
3195 created_by: None,
3196 workflow_name: "test".to_string(),
3197 trigger: TriggerKind::Manual,
3198 payload: json!({}),
3199 max_retries: 0,
3200 handler_version: None,
3201 labels: Default::default(),
3202 scheduled_at: None,
3203 idempotency_key: None,
3204 max_cost_usd: None,
3205 })
3206 .await
3207 .expect("failed to create run")
3208 .into_run();
3209
3210 let runs = store
3212 .list_runs(RunFilter::default(), 1, 10)
3213 .await
3214 .expect("failed to list runs");
3215 let created_run_id = runs.items[0].id;
3216
3217 let completed_step = store
3219 .create_step(NewStep {
3220 run_id: created_run_id,
3221 trace_id: step_trace_id(created_run_id, "completed", 0),
3222 name: "completed".to_string(),
3223 kind: StepKind::Shell,
3224 position: 0,
3225 input: None,
3226 is_error_handler: false,
3227 })
3228 .await
3229 .expect("failed to create step");
3230
3231 store
3233 .update_step(
3234 completed_step.id,
3235 StepUpdate {
3236 status: Some(StepStatus::Running),
3237 started_at: Some(Utc::now()),
3238 ..StepUpdate::default()
3239 },
3240 )
3241 .await
3242 .expect("failed to update step to Running");
3243
3244 store
3245 .update_step(
3246 completed_step.id,
3247 StepUpdate {
3248 status: Some(StepStatus::Completed),
3249 completed_at: Some(Utc::now()),
3250 ..StepUpdate::default()
3251 },
3252 )
3253 .await
3254 .expect("failed to update step to Completed");
3255
3256 let _pending_step = store
3257 .create_step(NewStep {
3258 run_id: created_run_id,
3259 trace_id: step_trace_id(created_run_id, "pending", 1),
3260 name: "pending".to_string(),
3261 kind: StepKind::Shell,
3262 position: 1,
3263 input: None,
3264 is_error_handler: false,
3265 })
3266 .await
3267 .expect("failed to create step");
3268
3269 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3271 ctx.load_replay_steps()
3272 .await
3273 .expect("failed to load replay steps");
3274
3275 assert_eq!(ctx.replay_steps.len(), 1);
3277 assert!(ctx.replay_steps.contains_key(&0));
3278 assert!(!ctx.replay_steps.contains_key(&1));
3279 }
3280
3281 #[tokio::test]
3282 async fn context_payload_returns_run_payload() {
3283 let store = Arc::new(InMemoryStore::new());
3284 let provider = create_test_provider();
3285 let test_payload = json!({"key": "value", "number": 42});
3286
3287 store
3289 .create_run(NewRun {
3290 created_by: None,
3291 workflow_name: "test".to_string(),
3292 trigger: TriggerKind::Manual,
3293 payload: test_payload.clone(),
3294 max_retries: 0,
3295 handler_version: None,
3296 labels: Default::default(),
3297 scheduled_at: None,
3298 idempotency_key: None,
3299 max_cost_usd: None,
3300 })
3301 .await
3302 .expect("failed to create run")
3303 .into_run();
3304
3305 let runs = store
3307 .list_runs(RunFilter::default(), 1, 10)
3308 .await
3309 .expect("failed to list runs");
3310 let created_run_id = runs.items[0].id;
3311
3312 let ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3313 let payload = ctx.payload().await.expect("failed to get payload");
3314
3315 assert_eq!(payload, test_payload);
3316 }
3317
3318 #[tokio::test]
3319 async fn context_payload_returns_error_for_nonexistent_run() {
3320 let store = Arc::new(InMemoryStore::new());
3321 let provider = create_test_provider();
3322 let run_id = Uuid::now_v7();
3323
3324 let ctx = WorkflowContext::new(run_id, "test".to_string(), store, provider);
3325 let result = ctx.payload().await;
3326
3327 assert!(result.is_err());
3328 }
3329
3330 #[tokio::test]
3331 async fn context_store_returns_reference() {
3332 let ctx = create_test_context();
3333 let _store = ctx.store();
3334 }
3336
3337 #[test]
3338 fn context_debug_formatting() {
3339 let ctx = create_test_context();
3340 let debug_str = format!("{:?}", ctx);
3341 assert!(debug_str.contains("WorkflowContext"));
3342 assert!(debug_str.contains("run_id"));
3343 }
3344
3345 #[tokio::test]
3346 async fn context_last_step_ids_tracks_executed_steps() {
3347 let store = Arc::new(InMemoryStore::new());
3348 let provider = create_test_provider();
3349
3350 store
3352 .create_run(NewRun {
3353 created_by: None,
3354 workflow_name: "test".to_string(),
3355 trigger: TriggerKind::Manual,
3356 payload: json!({}),
3357 max_retries: 0,
3358 handler_version: None,
3359 labels: Default::default(),
3360 scheduled_at: None,
3361 idempotency_key: None,
3362 max_cost_usd: None,
3363 })
3364 .await
3365 .expect("failed to create run")
3366 .into_run();
3367
3368 let runs = store
3370 .list_runs(RunFilter::default(), 1, 10)
3371 .await
3372 .expect("failed to list runs");
3373 let created_run_id = runs.items[0].id;
3374
3375 let mut ctx = WorkflowContext::new(created_run_id, "test".to_string(), store, provider);
3376 assert!(ctx.last_step_ids.is_empty());
3377
3378 ctx.skip("step1", "reason").await.expect("skip failed");
3379
3380 assert_eq!(ctx.last_step_ids.len(), 1);
3381
3382 ctx.skip("step2", "reason").await.expect("skip failed");
3383
3384 assert_eq!(ctx.last_step_ids.len(), 1);
3386 }
3387}