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_store::models::{
42 ArtifactLookup, NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind,
43 StepStatus, StepUpdate, TriggerKind, step_trace_id,
44};
45use ironflow_store::store::Store;
46
47use ironflow_artifacts::name::guess_content_type;
48use ironflow_artifacts::stream_from_bytes;
49use ironflow_store::entities::Artifact;
50
51use crate::artifact::{
52 ArtifactSink, ArtifactUpload, StepLocation, collect_outputs, materialize_inputs,
53};
54use crate::budget::step_budget_usd;
55use crate::config::{
56 AgentStepConfig, ApprovalConfig, HttpConfig, ShellConfig, StepConfig, WorkflowStepConfig,
57};
58use crate::error::EngineError;
59use crate::executor::{ParallelStepResult, StepOutput, StepResult, execute_step_config};
60use crate::guard::{SharedGuardState, WorkflowGuardConfig, WorkflowRejection};
61use crate::handler::WorkflowHandler;
62use crate::log_sender::{LogSender, StepLogSender};
63use crate::operation::Operation;
64
65pub(crate) type HandlerResolver =
67 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
68
69pub struct WorkflowContext {
88 run_id: Uuid,
89 store: Arc<dyn Store>,
90 provider: Arc<dyn AgentProvider>,
91 handler_resolver: Option<HandlerResolver>,
92 position: u32,
93 last_step_ids: Vec<Uuid>,
95 total_cost_usd: Decimal,
97 total_duration_ms: u64,
99 max_cost_usd: Option<Decimal>,
101 inherited_cost_usd: Decimal,
104 replay_steps: HashMap<u32, Step>,
107 granted_approvals: HashMap<u32, u32>,
111 attempt: u32,
113 carried_duration_ms: u64,
116 log_sender: Option<LogSender>,
118 artifact_sink: Option<Arc<dyn ArtifactSink>>,
122 has_allowed_failure: bool,
124 error_handlers: Vec<OnErrorHandler>,
126 guard_state: Option<SharedGuardState>,
128 guard_config: Option<WorkflowGuardConfig>,
130 step_results: Vec<StepResult>,
132 event_bus: Option<crate::notify::WorkflowEventBus>,
134}
135
136struct OnErrorHandler {
138 name: String,
139 config: StepConfig,
140}
141
142impl WorkflowContext {
143 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
148 Self {
149 run_id,
150 store,
151 provider,
152 handler_resolver: None,
153 position: 0,
154 last_step_ids: Vec::new(),
155 total_cost_usd: Decimal::ZERO,
156 total_duration_ms: 0,
157 max_cost_usd: None,
158 inherited_cost_usd: Decimal::ZERO,
159 replay_steps: HashMap::new(),
160 granted_approvals: HashMap::new(),
161 attempt: 1,
162 carried_duration_ms: 0,
163 log_sender: None,
164 artifact_sink: None,
165 has_allowed_failure: false,
166 error_handlers: Vec::new(),
167 guard_state: None,
168 guard_config: None,
169 step_results: Vec::new(),
170 event_bus: None,
171 }
172 }
173
174 pub(crate) fn with_handler_resolver(
179 run_id: Uuid,
180 store: Arc<dyn Store>,
181 provider: Arc<dyn AgentProvider>,
182 resolver: HandlerResolver,
183 ) -> Self {
184 Self {
185 run_id,
186 store,
187 provider,
188 handler_resolver: Some(resolver),
189 position: 0,
190 last_step_ids: Vec::new(),
191 total_cost_usd: Decimal::ZERO,
192 total_duration_ms: 0,
193 max_cost_usd: None,
194 inherited_cost_usd: Decimal::ZERO,
195 replay_steps: HashMap::new(),
196 granted_approvals: HashMap::new(),
197 attempt: 1,
198 carried_duration_ms: 0,
199 log_sender: None,
200 artifact_sink: None,
201 has_allowed_failure: false,
202 error_handlers: Vec::new(),
203 guard_state: None,
204 guard_config: None,
205 step_results: Vec::new(),
206 event_bus: None,
207 }
208 }
209
210 pub fn set_log_sender(&mut self, sender: LogSender) {
212 self.log_sender = Some(sender);
213 }
214
215 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
235 self.artifact_sink = Some(sink);
236 }
237
238 pub fn set_guard(&mut self, config: WorkflowGuardConfig, state: SharedGuardState) {
255 self.guard_config = Some(config);
256 self.guard_state = Some(state);
257 }
258
259 pub fn guard_config(&self) -> Option<&WorkflowGuardConfig> {
261 self.guard_config.as_ref()
262 }
263
264 pub fn set_event_bus(&mut self, bus: crate::notify::WorkflowEventBus) {
270 self.event_bus = Some(bus);
271 }
272
273 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
275 self.artifact_sink.as_ref().ok_or_else(|| {
276 EngineError::ArtifactsUnavailable(
277 "no artifact storage is attached to this run".to_string(),
278 )
279 })
280 }
281
282 pub async fn put_artifact(
312 &self,
313 step_id: Uuid,
314 name: &str,
315 content_type: Option<&str>,
316 content: Vec<u8>,
317 ) -> Result<Artifact, EngineError> {
318 let sink = self.artifact_sink()?;
319 sink.put(
320 ArtifactUpload {
321 run_id: self.run_id,
322 step_id,
323 name: name.to_string(),
324 content_type: content_type
325 .map(str::to_string)
326 .unwrap_or_else(|| guess_content_type(name)),
327 },
328 stream_from_bytes(content),
329 )
330 .await
331 }
332
333 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
358 let sink = self.artifact_sink()?;
359
360 let artifact = self
361 .store
362 .find_artifact_for_input(ArtifactLookup {
363 run_id: self.run_id,
364 attempt: self.attempt,
365 before_position: self.position,
366 step_name: step.to_string(),
367 name: name.to_string(),
368 })
369 .await?
370 .ok_or_else(|| EngineError::ArtifactNotFound {
371 step: step.to_string(),
372 name: name.to_string(),
373 })?;
374
375 let mut content = sink.get(&artifact).await?;
376 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
377 while let Some(chunk) = content.next().await {
378 let chunk = chunk?;
379 buffer.extend_from_slice(chunk.as_ref());
380 }
381
382 Ok(buffer)
383 }
384
385 async fn prepare_step_inputs(
390 &self,
391 config: &StepConfig,
392 position: u32,
393 ) -> Result<(), EngineError> {
394 let StepConfig::Shell(shell) = config else {
395 return Ok(());
396 };
397 if shell.inputs.is_empty() {
398 return Ok(());
399 }
400
401 materialize_inputs(
402 self.artifact_sink()?,
403 &self.store,
404 shell,
405 StepLocation {
406 run_id: self.run_id,
407 attempt: self.attempt,
408 position,
409 },
410 )
411 .await
412 }
413
414 async fn store_step_outputs(
419 &self,
420 config: &StepConfig,
421 step_id: Uuid,
422 step_name: &str,
423 step_succeeded: bool,
424 ) -> Result<(), EngineError> {
425 let StepConfig::Shell(shell) = config else {
426 return Ok(());
427 };
428 if shell.outputs.is_empty() {
429 return Ok(());
430 }
431
432 let sink = match self.artifact_sink() {
433 Ok(sink) => sink,
434 Err(err) if step_succeeded => return Err(err),
435 Err(err) => {
436 warn!(
437 run_id = %self.run_id,
438 step = %step_name,
439 error = %err,
440 "cannot collect outputs of a failed step"
441 );
442 return Ok(());
443 }
444 };
445
446 let collected =
447 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
448
449 match collected {
450 Ok(()) => Ok(()),
451 Err(err) if step_succeeded => Err(err),
452 Err(err) => {
453 warn!(
454 run_id = %self.run_id,
455 step = %step_name,
456 error = %err,
457 "failed to collect outputs of a failed step"
458 );
459 Ok(())
460 }
461 }
462 }
463
464 pub(crate) fn carry_over_run_totals(
471 &mut self,
472 attempt: u32,
473 cost_usd: Decimal,
474 duration_ms: u64,
475 ) {
476 self.attempt = attempt;
477 self.total_cost_usd = cost_usd;
478 self.carried_duration_ms = duration_ms;
479 }
480
481 pub(crate) fn carried_duration_ms(&self) -> u64 {
483 self.carried_duration_ms
484 }
485
486 pub fn attempt(&self) -> u32 {
488 self.attempt
489 }
490
491 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
507 self.max_cost_usd = cap;
508 }
509
510 pub fn max_cost_usd(&self) -> Option<Decimal> {
512 self.max_cost_usd
513 }
514
515 pub fn charged_cost_usd(&self) -> Decimal {
520 self.inherited_cost_usd + self.total_cost_usd
521 }
522
523 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
534 let Some(limit) = self.max_cost_usd else {
535 return Ok(());
536 };
537
538 let spent = self.charged_cost_usd();
539 if spent + step_budget <= limit {
540 return Ok(());
541 }
542
543 error!(
544 run_id = %self.run_id,
545 limit_usd = %limit,
546 spent_usd = %spent,
547 step_budget_usd = %step_budget,
548 "run cost cap reached, refusing agent step"
549 );
550
551 Err(EngineError::RunBudgetExceeded {
552 run_id: self.run_id,
553 limit_usd: limit,
554 spent_usd: spent,
555 step_budget_usd: step_budget,
556 })
557 }
558
559 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
571 let steps = self.store.list_steps(self.run_id).await?;
572 for step in steps {
573 let dominated = matches!(
574 step.status.state,
575 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
576 );
577 if !dominated {
578 continue;
579 }
580
581 if step.attempt == self.attempt {
582 self.replay_steps.insert(step.position, step);
583 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
584 {
585 self.granted_approvals.insert(step.position, step.attempt);
586 }
587 }
588 Ok(())
589 }
590
591 pub fn run_id(&self) -> Uuid {
593 self.run_id
594 }
595
596 pub fn total_cost_usd(&self) -> Decimal {
598 self.total_cost_usd
599 }
600
601 pub fn has_allowed_failure(&self) -> bool {
603 self.has_allowed_failure
604 }
605
606 pub fn total_duration_ms(&self) -> u64 {
608 self.total_duration_ms
609 }
610
611 pub fn step_results(&self) -> &[StepResult] {
613 &self.step_results
614 }
615
616 async fn persist_progress(&self) {
622 if let Err(err) = self
623 .store
624 .update_run(
625 self.run_id,
626 RunUpdate {
627 cost_usd: Some(self.total_cost_usd),
628 duration_ms: Some(self.total_duration_ms),
629 ..RunUpdate::default()
630 },
631 )
632 .await
633 {
634 warn!(
635 run_id = %self.run_id,
636 error = %err,
637 "failed to persist run progress snapshot"
638 );
639 }
640 }
641
642 pub async fn parallel(
679 &mut self,
680 steps: Vec<(&str, StepConfig)>,
681 fail_fast: bool,
682 ) -> Result<Vec<ParallelStepResult>, EngineError> {
683 if steps.is_empty() {
684 return Ok(Vec::new());
685 }
686
687 self.check_guard_timeout()?;
689
690 let wave_budget: Decimal = steps
693 .iter()
694 .filter_map(|(_, config)| match config {
695 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
696 _ => None,
697 })
698 .map(step_budget_usd)
699 .sum();
700 self.check_run_budget(wave_budget)?;
701
702 let wave_position = self.position;
703 self.position += 1;
704
705 let now = Utc::now();
706 let mut step_records: Vec<(Uuid, Uuid, String, StepConfig)> =
707 Vec::with_capacity(steps.len());
708
709 for (name, config) in &steps {
710 let kind = config.kind();
711 let trace_id = step_trace_id(self.run_id, name, wave_position);
712 let step = self
713 .store
714 .create_step(NewStep {
715 run_id: self.run_id,
716 trace_id,
717 name: name.to_string(),
718 kind,
719 position: wave_position,
720 input: Some(serde_json::to_value(config)?),
721 is_error_handler: false,
722 })
723 .await?;
724
725 self.start_step(step.id, now).await?;
726
727 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
730 self.fail_step(step.id, &err).await;
731 if !config.allow_failure() {
732 return Err(err);
733 }
734 self.has_allowed_failure = true;
735 info!(
736 run_id = %self.run_id,
737 step = %name,
738 error = %err,
739 "parallel step input preparation failed but allow_failure is set, skipping"
740 );
741 continue;
742 }
743
744 step_records.push((step.id, trace_id, name.to_string(), config.clone()));
745 }
746
747 let mut join_set = JoinSet::new();
748 let mut task_index: HashMap<Id, usize> = HashMap::new();
749 let parallel_timeout = self.guard_remaining_timeout();
750 for (idx, (step_id, _trace_id, step_name, config)) in step_records.iter().enumerate() {
751 let provider = self.provider.clone();
752 let config = config.clone();
753 let step_log_sender = self
754 .log_sender
755 .as_ref()
756 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
757 let handle = join_set.spawn(async move {
758 let result = match parallel_timeout {
759 Some(dur) => {
760 match tokio::time::timeout(
761 dur,
762 execute_step_config(&config, &provider, step_log_sender),
763 )
764 .await
765 {
766 Ok(r) => r,
767 Err(_elapsed) => {
768 Err(EngineError::from(WorkflowRejection::WorkflowTimeout {
769 elapsed_secs: 0,
770 max: 0,
771 }))
772 }
773 }
774 }
775 None => execute_step_config(&config, &provider, step_log_sender).await,
776 };
777 (idx, result)
778 });
779 task_index.insert(handle.id(), idx);
780 }
781
782 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
784 vec![None; step_records.len()];
785 let mut first_error: Option<EngineError> = None;
786
787 while let Some(join_result) = join_set.join_next().await {
788 let (idx, step_result) = match join_result {
789 Ok(r) => r,
790 Err(e) => {
791 let error_msg = format!("join error: {e}");
792 if let Some(&idx) = task_index.get(&e.id()) {
793 let (step_id, _, step_name, _) = &step_records[idx];
794 let completed_at = Utc::now();
795 error!(
796 run_id = %self.run_id,
797 step = %step_name,
798 error = %error_msg,
799 "parallel step panicked or was cancelled"
800 );
801 if let Err(store_err) = self
802 .store
803 .update_step(
804 *step_id,
805 StepUpdate {
806 status: Some(StepStatus::Failed),
807 error: Some(error_msg.clone()),
808 completed_at: Some(completed_at),
809 ..StepUpdate::default()
810 },
811 )
812 .await
813 {
814 error!(
815 run_id = %self.run_id,
816 step_id = %step_id,
817 error = %store_err,
818 "failed to persist JoinError for step"
819 );
820 }
821 indexed_results[idx] = Some(Err(error_msg.clone()));
822 }
823 if first_error.is_none() {
824 first_error = Some(EngineError::StepConfig(error_msg));
825 }
826 if fail_fast {
827 join_set.abort_all();
828 }
829 continue;
830 }
831 };
832
833 let (step_id, step_trace, step_name, step_config) = &step_records[idx];
834 let completed_at = Utc::now();
835
836 if let Err(err) = self
837 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
838 .await
839 {
840 self.fail_step(*step_id, &err).await;
841 indexed_results[idx] = Some(Err(err.to_string()));
842 if first_error.is_none() {
843 first_error = Some(err);
844 }
845 if fail_fast {
846 join_set.abort_all();
847 }
848 continue;
849 }
850
851 match step_result {
852 Ok(output) => {
853 self.total_cost_usd += output.cost_usd;
854 self.total_duration_ms += output.duration_ms;
855
856 if matches!(step_config, StepConfig::Agent(_)) {
858 let tokens = output
859 .input_tokens
860 .unwrap_or(0)
861 .saturating_add(output.output_tokens.unwrap_or(0));
862 if tokens > 0
863 && let Err(guard_err) = self.guard_record_tokens(tokens)
864 {
865 if first_error.is_none() {
866 first_error = Some(guard_err);
867 }
868 if fail_fast {
869 join_set.abort_all();
870 }
871 }
872 }
873
874 let debug_messages_json = output.debug_messages_json();
875
876 self.store
877 .update_step(
878 *step_id,
879 StepUpdate {
880 status: Some(StepStatus::Completed),
881 output: Some(output.output.clone()),
882 duration_ms: Some(output.duration_ms),
883 cost_usd: Some(output.cost_usd),
884 input_tokens: output.input_tokens,
885 output_tokens: output.output_tokens,
886 completed_at: Some(completed_at),
887 debug_messages: debug_messages_json,
888 ..StepUpdate::default()
889 },
890 )
891 .await?;
892
893 self.step_results.push(StepResult::from_success(
894 *step_trace,
895 step_name,
896 &output,
897 ));
898
899 info!(
900 run_id = %self.run_id,
901 step = %step_name,
902 trace_id = %step_trace,
903 duration_ms = output.duration_ms,
904 "parallel step completed"
905 );
906
907 indexed_results[idx] = Some(Ok(output));
908 }
909 Err(err) => {
910 let err_msg = err.to_string();
911 let debug_messages_json = extract_debug_messages_from_error(&err);
912 let partial = extract_partial_usage_from_error(&err);
913 let raw_response_output = extract_raw_response_from_error(&err);
914
915 if let Some(ref usage) = partial {
916 if let Some(cost) = usage.cost_usd {
917 self.total_cost_usd += cost;
918 }
919 if let Some(dur) = usage.duration_ms {
920 self.total_duration_ms += dur;
921 }
922 }
923
924 if let Err(store_err) = self
925 .store
926 .update_step(
927 *step_id,
928 StepUpdate {
929 status: Some(StepStatus::Failed),
930 error: Some(err_msg.clone()),
931 output: raw_response_output.clone(),
932 completed_at: Some(completed_at),
933 debug_messages: debug_messages_json,
934 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
935 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
936 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
937 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
938 ..StepUpdate::default()
939 },
940 )
941 .await
942 {
943 tracing::error!(
944 step_id = %step_id,
945 error = %store_err,
946 "failed to persist parallel step failure"
947 );
948 }
949
950 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
951 let err_cost = partial
952 .as_ref()
953 .and_then(|p| p.cost_usd)
954 .unwrap_or(Decimal::ZERO);
955 self.step_results.push(StepResult::from_failure(
956 *step_trace,
957 step_name,
958 &err_msg,
959 err_duration,
960 err_cost,
961 ));
962
963 if step_config.allow_failure() {
964 self.has_allowed_failure = true;
965 info!(
966 run_id = %self.run_id,
967 step = %step_name,
968 error = %err_msg,
969 "parallel step failed but allow_failure is set, continuing"
970 );
971 indexed_results[idx] = Some(Ok(allowed_failure_output(
972 &err_msg,
973 raw_response_output,
974 partial.as_ref(),
975 )));
976 } else {
977 indexed_results[idx] = Some(Err(err_msg.clone()));
978
979 if first_error.is_none() {
980 first_error = Some(err);
981 }
982
983 if fail_fast {
984 join_set.abort_all();
985 }
986 }
987 }
988 }
989 }
990
991 if let Some(err) = first_error {
992 return Err(err);
993 }
994
995 self.persist_progress().await;
996
997 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
998
999 let results: Vec<ParallelStepResult> = step_records
1001 .iter()
1002 .enumerate()
1003 .map(|(idx, (step_id, _trace_id, name, _))| {
1004 let output = match indexed_results[idx].take() {
1005 Some(Ok(o)) => o,
1006 _ => unreachable!("all steps succeeded if no error returned"),
1007 };
1008 ParallelStepResult {
1009 name: name.clone(),
1010 output,
1011 step_id: *step_id,
1012 }
1013 })
1014 .collect();
1015
1016 Ok(results)
1017 }
1018
1019 pub async fn shell(
1042 &mut self,
1043 name: &str,
1044 config: ShellConfig,
1045 ) -> Result<StepOutput, EngineError> {
1046 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1047 .await
1048 }
1049
1050 pub async fn http(
1070 &mut self,
1071 name: &str,
1072 config: HttpConfig,
1073 ) -> Result<StepOutput, EngineError> {
1074 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1075 .await
1076 }
1077
1078 pub async fn agent(
1098 &mut self,
1099 name: &str,
1100 config: impl Into<AgentStepConfig>,
1101 ) -> Result<StepOutput, EngineError> {
1102 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1103 .await
1104 }
1105
1106 pub async fn approval(
1137 &mut self,
1138 name: &str,
1139 config: ApprovalConfig,
1140 ) -> Result<(), EngineError> {
1141 let position = self.position;
1142 self.position += 1;
1143
1144 if let Some(existing) = self.replay_steps.get(&position)
1147 && existing.kind == StepKind::Approval
1148 {
1149 if existing.status.state == StepStatus::AwaitingApproval {
1150 self.store
1151 .update_step(
1152 existing.id,
1153 StepUpdate {
1154 status: Some(StepStatus::Completed),
1155 completed_at: Some(Utc::now()),
1156 ..StepUpdate::default()
1157 },
1158 )
1159 .await?;
1160 }
1161
1162 self.last_step_ids = vec![existing.id];
1163 info!(
1164 run_id = %self.run_id,
1165 step = %name,
1166 position,
1167 "approval step replayed (approved)"
1168 );
1169 return Ok(());
1170 }
1171
1172 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1176 let trace_id = step_trace_id(self.run_id, name, position);
1177 let step = self
1178 .store
1179 .create_step(NewStep {
1180 run_id: self.run_id,
1181 trace_id,
1182 name: name.to_string(),
1183 kind: StepKind::Approval,
1184 position,
1185 input: Some(serde_json::to_value(&config)?),
1186 is_error_handler: false,
1187 })
1188 .await?;
1189
1190 let now = Utc::now();
1191 self.start_step(step.id, now).await?;
1192 self.store
1193 .update_step(
1194 step.id,
1195 StepUpdate {
1196 status: Some(StepStatus::Completed),
1197 output: Some(json!({"approved_in_attempt": granted_in})),
1198 completed_at: Some(now),
1199 ..StepUpdate::default()
1200 },
1201 )
1202 .await?;
1203
1204 self.last_step_ids = vec![step.id];
1205 info!(
1206 run_id = %self.run_id,
1207 step = %name,
1208 position,
1209 granted_in_attempt = granted_in,
1210 attempt = self.attempt,
1211 "approval carried over from a previous attempt"
1212 );
1213 return Ok(());
1214 }
1215
1216 let trace_id = step_trace_id(self.run_id, name, position);
1218 let step = self
1219 .store
1220 .create_step(NewStep {
1221 run_id: self.run_id,
1222 trace_id,
1223 name: name.to_string(),
1224 kind: StepKind::Approval,
1225 position,
1226 input: Some(serde_json::to_value(&config)?),
1227 is_error_handler: false,
1228 })
1229 .await?;
1230
1231 self.start_step(step.id, Utc::now()).await?;
1232
1233 self.store
1236 .update_step(
1237 step.id,
1238 StepUpdate {
1239 status: Some(StepStatus::AwaitingApproval),
1240 ..StepUpdate::default()
1241 },
1242 )
1243 .await?;
1244
1245 self.last_step_ids = vec![step.id];
1246
1247 Err(EngineError::ApprovalRequired {
1248 run_id: self.run_id,
1249 step_id: step.id,
1250 message: config.message().to_string(),
1251 })
1252 }
1253
1254 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1283 let position = self.position;
1284 self.position += 1;
1285
1286 let trace_id = step_trace_id(self.run_id, name, position);
1287 let step = self
1288 .store
1289 .create_step(NewStep {
1290 run_id: self.run_id,
1291 trace_id,
1292 name: name.to_string(),
1293 kind: StepKind::Custom("skip".to_string()),
1294 position,
1295 input: None,
1296 is_error_handler: false,
1297 })
1298 .await?;
1299
1300 if !self.last_step_ids.is_empty() {
1301 let deps: Vec<NewStepDependency> = self
1302 .last_step_ids
1303 .iter()
1304 .map(|&depends_on| NewStepDependency {
1305 step_id: step.id,
1306 depends_on,
1307 })
1308 .collect();
1309 self.store.create_step_dependencies(deps).await?;
1310 }
1311
1312 let now = Utc::now();
1313 self.store
1314 .update_step(
1315 step.id,
1316 StepUpdate {
1317 status: Some(StepStatus::Skipped),
1318 output: Some(serde_json::json!({"reason": reason})),
1319 completed_at: Some(now),
1320 ..StepUpdate::default()
1321 },
1322 )
1323 .await?;
1324
1325 self.last_step_ids = vec![step.id];
1326
1327 info!(
1328 run_id = %self.run_id,
1329 step = %name,
1330 reason,
1331 "step skipped"
1332 );
1333
1334 Ok(())
1335 }
1336
1337 pub async fn operation(
1375 &mut self,
1376 name: &str,
1377 op: &dyn Operation,
1378 ) -> Result<StepOutput, EngineError> {
1379 let kind = StepKind::Custom(op.kind().to_string());
1380 let position = self.position;
1381 self.position += 1;
1382
1383 let trace_id = step_trace_id(self.run_id, name, position);
1384 let step = self
1385 .store
1386 .create_step(NewStep {
1387 run_id: self.run_id,
1388 trace_id,
1389 name: name.to_string(),
1390 kind,
1391 position,
1392 input: op.input(),
1393 is_error_handler: false,
1394 })
1395 .await?;
1396
1397 self.start_step(step.id, Utc::now()).await?;
1398
1399 let start = Instant::now();
1400
1401 match op.execute().await {
1402 Ok(output_value) => {
1403 let duration_ms = start.elapsed().as_millis() as u64;
1404 self.total_duration_ms += duration_ms;
1405
1406 let completed_at = Utc::now();
1407 self.store
1408 .update_step(
1409 step.id,
1410 StepUpdate {
1411 status: Some(StepStatus::Completed),
1412 output: Some(output_value.clone()),
1413 duration_ms: Some(duration_ms),
1414 cost_usd: Some(Decimal::ZERO),
1415 completed_at: Some(completed_at),
1416 ..StepUpdate::default()
1417 },
1418 )
1419 .await?;
1420
1421 info!(
1422 run_id = %self.run_id,
1423 step = %name,
1424 kind = op.kind(),
1425 duration_ms,
1426 "operation step completed"
1427 );
1428
1429 self.last_step_ids = vec![step.id];
1430
1431 Ok(StepOutput {
1432 output: output_value,
1433 duration_ms,
1434 cost_usd: Decimal::ZERO,
1435 input_tokens: None,
1436 output_tokens: None,
1437 model: None,
1438 debug_messages: None,
1439 })
1440 }
1441 Err(err) => {
1442 let completed_at = Utc::now();
1443 if let Err(store_err) = self
1444 .store
1445 .update_step(
1446 step.id,
1447 StepUpdate {
1448 status: Some(StepStatus::Failed),
1449 error: Some(err.to_string()),
1450 completed_at: Some(completed_at),
1451 ..StepUpdate::default()
1452 },
1453 )
1454 .await
1455 {
1456 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1457 }
1458
1459 Err(err)
1460 }
1461 }
1462 }
1463
1464 pub async fn workflow(
1491 &mut self,
1492 handler: &dyn WorkflowHandler,
1493 payload: Value,
1494 ) -> Result<StepOutput, EngineError> {
1495 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1497 let state = guard_state
1498 .lock()
1499 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1500 state.check(guard_config, handler.name())?;
1501 }
1502
1503 let config = WorkflowStepConfig::new(handler.name(), payload);
1504 let position = self.position;
1505 self.position += 1;
1506
1507 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1508 let step = self
1509 .store
1510 .create_step(NewStep {
1511 run_id: self.run_id,
1512 trace_id,
1513 name: config.workflow_name.clone(),
1514 kind: StepKind::Workflow,
1515 position,
1516 input: Some(serde_json::to_value(&config)?),
1517 is_error_handler: false,
1518 })
1519 .await?;
1520
1521 self.start_step(step.id, Utc::now()).await?;
1522
1523 if let Some(guard_state) = &self.guard_state {
1525 let mut state = guard_state
1526 .lock()
1527 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1528 state.record_invocation(handler.name());
1529 }
1530
1531 match self.execute_child_workflow(&config).await {
1532 Ok((output, child_had_allowed_failure)) => {
1533 self.total_cost_usd += output.cost_usd;
1534 self.total_duration_ms += output.duration_ms;
1535 if child_had_allowed_failure {
1536 self.has_allowed_failure = true;
1537 }
1538
1539 let completed_at = Utc::now();
1540 self.store
1541 .update_step(
1542 step.id,
1543 StepUpdate {
1544 status: Some(StepStatus::Completed),
1545 output: Some(output.output.clone()),
1546 duration_ms: Some(output.duration_ms),
1547 cost_usd: Some(output.cost_usd),
1548 completed_at: Some(completed_at),
1549 ..StepUpdate::default()
1550 },
1551 )
1552 .await?;
1553
1554 info!(
1555 run_id = %self.run_id,
1556 child_workflow = %config.workflow_name,
1557 duration_ms = output.duration_ms,
1558 "workflow step completed"
1559 );
1560
1561 self.last_step_ids = vec![step.id];
1562
1563 self.guard_record_return();
1564 Ok(output)
1565 }
1566 Err(err) => {
1567 let completed_at = Utc::now();
1568 if let Err(store_err) = self
1569 .store
1570 .update_step(
1571 step.id,
1572 StepUpdate {
1573 status: Some(StepStatus::Failed),
1574 error: Some(err.to_string()),
1575 completed_at: Some(completed_at),
1576 ..StepUpdate::default()
1577 },
1578 )
1579 .await
1580 {
1581 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1582 }
1583
1584 self.guard_record_return();
1585 Err(err)
1586 }
1587 }
1588 }
1589
1590 fn guard_record_return(&self) {
1595 if let Some(guard_state) = &self.guard_state {
1596 match guard_state.lock() {
1597 Ok(mut state) => state.record_return(),
1598 Err(_) => {
1599 error!(
1600 run_id = %self.run_id,
1601 "guard state mutex poisoned in record_return"
1602 );
1603 }
1604 }
1605 }
1606 }
1607
1608 async fn execute_with_guard_timeout(
1612 &self,
1613 config: &StepConfig,
1614 step_log_sender: Option<StepLogSender>,
1615 ) -> Result<StepOutput, EngineError> {
1616 let remaining = self.guard_remaining_timeout();
1617 match remaining {
1618 Some(dur) => {
1619 use tokio::time::timeout;
1620 match timeout(
1621 dur,
1622 execute_step_config(config, &self.provider, step_log_sender),
1623 )
1624 .await
1625 {
1626 Ok(result) => result,
1627 Err(_elapsed) => {
1628 let config_secs = self
1629 .guard_config
1630 .as_ref()
1631 .map_or(0, |c| c.workflow_timeout_secs);
1632 Err(WorkflowRejection::WorkflowTimeout {
1633 elapsed_secs: config_secs,
1634 max: config_secs,
1635 }
1636 .into())
1637 }
1638 }
1639 }
1640 None => execute_step_config(config, &self.provider, step_log_sender).await,
1641 }
1642 }
1643
1644 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1646 let config = self.guard_config.as_ref()?;
1647 let guard_state = self.guard_state.as_ref()?;
1648 let state = guard_state.lock().ok()?;
1649 let elapsed = state.elapsed_secs();
1650 let max = config.workflow_timeout_secs;
1651 if elapsed >= max {
1652 Some(std::time::Duration::ZERO)
1653 } else {
1654 Some(std::time::Duration::from_secs(max - elapsed))
1655 }
1656 }
1657
1658 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1660 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1661 let state = guard_state
1662 .lock()
1663 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1664 let elapsed = state.elapsed_secs();
1665 if elapsed >= config.workflow_timeout_secs {
1666 return Err(WorkflowRejection::WorkflowTimeout {
1667 elapsed_secs: elapsed,
1668 max: config.workflow_timeout_secs,
1669 }
1670 .into());
1671 }
1672 }
1673 Ok(())
1674 }
1675
1676 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1678 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1679 let mut state = guard_state
1680 .lock()
1681 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1682 state.record_tokens(config, tokens)?;
1683 }
1684 Ok(())
1685 }
1686
1687 async fn execute_child_workflow(
1690 &self,
1691 config: &WorkflowStepConfig,
1692 ) -> Result<(StepOutput, bool), EngineError> {
1693 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1694 EngineError::InvalidWorkflow(
1695 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1696 )
1697 })?;
1698
1699 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1700 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1701 })?;
1702
1703 let parent = self.store.get_run(self.run_id).await?;
1706 let (parent_labels, parent_author) =
1707 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1708
1709 let child_run = self
1710 .store
1711 .create_run(NewRun {
1712 workflow_name: config.workflow_name.clone(),
1713 trigger: TriggerKind::Workflow,
1714 payload: config.payload.clone(),
1715 max_retries: 0,
1716 handler_version: None,
1717 labels: parent_labels,
1718 scheduled_at: None,
1719 created_by: parent_author,
1720 idempotency_key: None,
1721 max_cost_usd: self.max_cost_usd,
1723 })
1724 .await?
1725 .into_run();
1726
1727 let child_run_id = child_run.id;
1728 info!(
1729 parent_run_id = %self.run_id,
1730 child_run_id = %child_run_id,
1731 workflow = %config.workflow_name,
1732 "child run created"
1733 );
1734
1735 self.store
1736 .update_run_status(child_run_id, RunStatus::Running)
1737 .await?;
1738
1739 let run_start = Instant::now();
1740 let mut child_ctx = WorkflowContext {
1741 run_id: child_run_id,
1742 store: self.store.clone(),
1743 provider: self.provider.clone(),
1744 handler_resolver: self.handler_resolver.clone(),
1745 position: 0,
1746 last_step_ids: Vec::new(),
1747 total_cost_usd: Decimal::ZERO,
1748 total_duration_ms: 0,
1749 max_cost_usd: self.max_cost_usd,
1750 inherited_cost_usd: self.charged_cost_usd(),
1753 replay_steps: HashMap::new(),
1754 granted_approvals: HashMap::new(),
1755 attempt: 1,
1757 carried_duration_ms: 0,
1758 log_sender: self.log_sender.clone(),
1759 artifact_sink: self.artifact_sink.clone(),
1762 has_allowed_failure: false,
1763 error_handlers: Vec::new(),
1764 guard_state: self.guard_state.clone(),
1765 guard_config: self.guard_config.clone(),
1766 step_results: Vec::new(),
1767 event_bus: self.event_bus.clone(),
1768 };
1769
1770 let result = handler.execute(&mut child_ctx).await;
1771 let total_duration = run_start.elapsed().as_millis() as u64;
1772 let completed_at = Utc::now();
1773
1774 match result {
1775 Ok(()) => {
1776 let child_status = if child_ctx.has_allowed_failure {
1777 RunStatus::Warning
1778 } else {
1779 RunStatus::Completed
1780 };
1781 self.store
1782 .update_run(
1783 child_run_id,
1784 RunUpdate {
1785 status: Some(child_status),
1786 cost_usd: Some(child_ctx.total_cost_usd),
1787 duration_ms: Some(total_duration),
1788 completed_at: Some(completed_at),
1789 ..RunUpdate::default()
1790 },
1791 )
1792 .await?;
1793
1794 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1795 Ok((
1796 StepOutput {
1797 output: serde_json::json!({
1798 "run_id": child_run_id,
1799 "workflow_name": config.workflow_name,
1800 "status": child_status,
1801 "cost_usd": child_ctx.total_cost_usd,
1802 "duration_ms": total_duration,
1803 }),
1804 duration_ms: total_duration,
1805 cost_usd: child_ctx.total_cost_usd,
1806 input_tokens: None,
1807 output_tokens: None,
1808 model: None,
1809 debug_messages: None,
1810 },
1811 child_had_allowed_failure,
1812 ))
1813 }
1814 Err(err) => {
1815 if let Err(store_err) = self
1816 .store
1817 .update_run(
1818 child_run_id,
1819 RunUpdate {
1820 status: Some(RunStatus::Failed),
1821 error: Some(err.to_string()),
1822 cost_usd: Some(child_ctx.total_cost_usd),
1823 duration_ms: Some(total_duration),
1824 completed_at: Some(completed_at),
1825 ..RunUpdate::default()
1826 },
1827 )
1828 .await
1829 {
1830 error!(
1831 child_run_id = %child_run_id,
1832 store_error = %store_err,
1833 "failed to persist child run failure"
1834 );
1835 }
1836
1837 Err(err)
1838 }
1839 }
1840 }
1841
1842 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1847 let step = self.replay_steps.get(&position)?;
1848 if step.status.state != StepStatus::Completed {
1849 return None;
1850 }
1851 let output = StepOutput {
1852 output: step.output.clone().unwrap_or(Value::Null),
1853 duration_ms: step.duration_ms,
1854 cost_usd: step.cost_usd,
1855 input_tokens: step.input_tokens,
1856 output_tokens: step.output_tokens,
1857 model: None,
1858 debug_messages: None,
1859 };
1860 self.total_cost_usd += output.cost_usd;
1861 self.total_duration_ms += output.duration_ms;
1862 self.last_step_ids = vec![step.id];
1863 info!(
1864 run_id = %self.run_id,
1865 step = %step.name,
1866 position,
1867 "step replayed from previous execution"
1868 );
1869 Some(output)
1870 }
1871
1872 #[tracing::instrument(
1874 name = "context.execute_step",
1875 skip_all,
1876 fields(
1877 run_id = %self.run_id,
1878 step.name = %name,
1879 step.kind,
1880 step.position = self.position,
1881 step.trace_id,
1882 )
1883 )]
1884 async fn execute_step(
1885 &mut self,
1886 name: &str,
1887 kind: StepKind,
1888 config: StepConfig,
1889 ) -> Result<StepOutput, EngineError> {
1890 let kind_str: &'static str = match kind {
1891 StepKind::Shell => "shell",
1892 StepKind::Http => "http",
1893 StepKind::Agent => "agent",
1894 StepKind::Workflow => "workflow",
1895 StepKind::Approval => "approval",
1896 StepKind::Custom(_) => "custom",
1897 };
1898 Span::current().record("step.kind", kind_str);
1899
1900 self.check_guard_timeout()?;
1902
1903 let position = self.position;
1904 self.position += 1;
1905
1906 if let Some(output) = self.try_replay_step(position) {
1908 return Ok(output);
1909 }
1910
1911 if let StepConfig::Agent(ref agent_config) = config {
1914 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1915 }
1916
1917 let trace_id = step_trace_id(self.run_id, name, position);
1919 Span::current().record("step.trace_id", trace_id.to_string().as_str());
1920 let step = self
1921 .store
1922 .create_step(NewStep {
1923 run_id: self.run_id,
1924 trace_id,
1925 name: name.to_string(),
1926 kind,
1927 position,
1928 input: Some(serde_json::to_value(&config)?),
1929 is_error_handler: false,
1930 })
1931 .await?;
1932
1933 self.start_step(step.id, Utc::now()).await?;
1934
1935 if let Some(ref bus) = self.event_bus {
1936 bus.publish(
1937 self.run_id,
1938 crate::notify::WorkflowEvent::StepStarted {
1939 step_name: name.to_string(),
1940 step_index: position,
1941 timestamp: Utc::now(),
1942 },
1943 );
1944 }
1945
1946 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1949 self.fail_step(step.id, &err).await;
1950 if config.allow_failure() {
1951 self.has_allowed_failure = true;
1952 self.last_step_ids = vec![step.id];
1953 info!(
1954 run_id = %self.run_id,
1955 step = %name,
1956 error = %err,
1957 "step input preparation failed but allow_failure is set, continuing"
1958 );
1959 return Ok(StepOutput {
1960 output: json!({"error": err.to_string()}),
1961 duration_ms: 0,
1962 cost_usd: Decimal::ZERO,
1963 input_tokens: None,
1964 output_tokens: None,
1965 model: None,
1966 debug_messages: None,
1967 });
1968 }
1969 return Err(err);
1970 }
1971
1972 let step_log_sender = self
1973 .log_sender
1974 .as_ref()
1975 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1976
1977 let execution = self
1978 .execute_with_guard_timeout(&config, step_log_sender)
1979 .await;
1980
1981 let execution = self
1982 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
1983 .await;
1984
1985 if let Err(err) = self
1986 .store_step_outputs(&config, step.id, name, execution.is_ok())
1987 .await
1988 {
1989 self.fail_step(step.id, &err).await;
1990 return Err(err);
1991 }
1992
1993 match execution {
1994 Ok(output) => {
1995 self.total_cost_usd += output.cost_usd;
1996 self.total_duration_ms += output.duration_ms;
1997
1998 if matches!(config, StepConfig::Agent(_)) {
2000 let tokens = output
2001 .input_tokens
2002 .unwrap_or(0)
2003 .saturating_add(output.output_tokens.unwrap_or(0));
2004 if tokens > 0 {
2005 self.guard_record_tokens(tokens)?;
2006 }
2007 }
2008
2009 let debug_messages_json = output.debug_messages_json();
2010
2011 let completed_at = Utc::now();
2012 self.store
2013 .update_step(
2014 step.id,
2015 StepUpdate {
2016 status: Some(StepStatus::Completed),
2017 output: Some(output.output.clone()),
2018 duration_ms: Some(output.duration_ms),
2019 cost_usd: Some(output.cost_usd),
2020 input_tokens: output.input_tokens,
2021 output_tokens: output.output_tokens,
2022 completed_at: Some(completed_at),
2023 debug_messages: debug_messages_json,
2024 ..StepUpdate::default()
2025 },
2026 )
2027 .await?;
2028
2029 self.step_results
2030 .push(StepResult::from_success(trace_id, name, &output));
2031 self.persist_progress().await;
2032
2033 info!(
2034 run_id = %self.run_id,
2035 step = %name,
2036 trace_id = %trace_id,
2037 duration_ms = output.duration_ms,
2038 "step completed"
2039 );
2040
2041 if let Some(ref bus) = self.event_bus {
2042 bus.publish(
2043 self.run_id,
2044 crate::notify::WorkflowEvent::StepCompleted {
2045 step_name: name.to_string(),
2046 step_index: position,
2047 duration_ms: output.duration_ms,
2048 output_summary: None,
2049 },
2050 );
2051 }
2052
2053 self.last_step_ids = vec![step.id];
2054
2055 Ok(output)
2056 }
2057 Err(err) => {
2058 let completed_at = Utc::now();
2059 let debug_messages_json = extract_debug_messages_from_error(&err);
2060 let partial = extract_partial_usage_from_error(&err);
2061 let raw_response_output = extract_raw_response_from_error(&err);
2062
2063 if let Some(ref usage) = partial {
2064 if let Some(cost) = usage.cost_usd {
2065 self.total_cost_usd += cost;
2066 }
2067 if let Some(dur) = usage.duration_ms {
2068 self.total_duration_ms += dur;
2069 }
2070 }
2071
2072 if let Err(store_err) = self
2073 .store
2074 .update_step(
2075 step.id,
2076 StepUpdate {
2077 status: Some(StepStatus::Failed),
2078 error: Some(err.to_string()),
2079 output: raw_response_output.clone(),
2080 completed_at: Some(completed_at),
2081 debug_messages: debug_messages_json,
2082 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2083 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2084 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2085 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2086 ..StepUpdate::default()
2087 },
2088 )
2089 .await
2090 {
2091 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2092 }
2093
2094 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2095 let err_cost = partial
2096 .as_ref()
2097 .and_then(|p| p.cost_usd)
2098 .unwrap_or(Decimal::ZERO);
2099 self.step_results.push(StepResult::from_failure(
2100 trace_id,
2101 name,
2102 &err.to_string(),
2103 err_duration,
2104 err_cost,
2105 ));
2106 self.persist_progress().await;
2107
2108 if let Some(ref bus) = self.event_bus {
2109 bus.publish(
2110 self.run_id,
2111 crate::notify::WorkflowEvent::StepFailed {
2112 step_name: name.to_string(),
2113 step_index: position,
2114 error: err.to_string(),
2115 duration_ms: err_duration,
2116 },
2117 );
2118 }
2119
2120 self.fire_error_handlers(name, &err.to_string(), err_duration)
2121 .await;
2122
2123 if config.allow_failure() {
2124 self.has_allowed_failure = true;
2125 self.last_step_ids = vec![step.id];
2126 info!(
2127 run_id = %self.run_id,
2128 step = %name,
2129 error = %err,
2130 "step failed but allow_failure is set, continuing"
2131 );
2132 Ok(allowed_failure_output(
2133 &err.to_string(),
2134 raw_response_output,
2135 partial.as_ref(),
2136 ))
2137 } else {
2138 Err(err)
2139 }
2140 }
2141 }
2142 }
2143
2144 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2150 async fn retry_step_if_configured(
2151 &self,
2152 name: &str,
2153 kind_str: &str,
2154 config: &StepConfig,
2155 step_id: Uuid,
2156 first_result: Result<StepOutput, EngineError>,
2157 ) -> Result<StepOutput, EngineError> {
2158 let policy = match config.retry() {
2159 Some(p) => p,
2160 None => return first_result,
2161 };
2162
2163 let mut last_result = match first_result {
2164 Ok(output) => return Ok(output),
2165 Err(err) if !is_step_retryable(&err) => return Err(err),
2166 Err(err) => Err(err),
2167 };
2168
2169 let step_log_sender = self
2170 .log_sender
2171 .as_ref()
2172 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2173
2174 for attempt in 0..policy.max_retries() {
2175 if let StepConfig::Agent(agent_config) = config {
2176 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2177 }
2178
2179 let delay = policy.delay_for_attempt(attempt);
2180 info!(
2181 run_id = %self.run_id,
2182 step = %name,
2183 attempt = attempt + 1,
2184 max_retries = policy.max_retries(),
2185 delay_ms = delay.as_millis() as u64,
2186 "retrying step after transient failure"
2187 );
2188 tokio::time::sleep(delay).await;
2189
2190 record_retry_metric(kind_str, "retry");
2191
2192 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2193 Ok(output) => return Ok(output),
2194 Err(err) if !is_step_retryable(&err) => return Err(err),
2195 err => last_result = err,
2196 }
2197 }
2198
2199 record_retry_metric(kind_str, "exhausted");
2200
2201 info!(
2202 run_id = %self.run_id,
2203 step = %name,
2204 max_retries = policy.max_retries(),
2205 "step retries exhausted"
2206 );
2207
2208 last_result
2209 }
2210
2211 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
2216 if !self.last_step_ids.is_empty() {
2217 let deps: Vec<NewStepDependency> = self
2218 .last_step_ids
2219 .iter()
2220 .map(|&depends_on| NewStepDependency {
2221 step_id,
2222 depends_on,
2223 })
2224 .collect();
2225 self.store.create_step_dependencies(deps).await?;
2226 }
2227
2228 self.store
2229 .update_step(
2230 step_id,
2231 StepUpdate {
2232 status: Some(StepStatus::Running),
2233 started_at: Some(now),
2234 ..StepUpdate::default()
2235 },
2236 )
2237 .await?;
2238
2239 Ok(())
2240 }
2241
2242 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2249 if let Err(store_err) = self
2250 .store
2251 .update_step(
2252 step_id,
2253 StepUpdate {
2254 status: Some(StepStatus::Failed),
2255 error: Some(err.to_string()),
2256 completed_at: Some(Utc::now()),
2257 ..StepUpdate::default()
2258 },
2259 )
2260 .await
2261 {
2262 error!(
2263 step_id = %step_id,
2264 error = %store_err,
2265 "failed to persist step failure"
2266 );
2267 }
2268 }
2269
2270 pub fn store(&self) -> &Arc<dyn Store> {
2272 &self.store
2273 }
2274
2275 pub async fn payload(&self) -> Result<Value, EngineError> {
2283 let run = self
2284 .store
2285 .get_run(self.run_id)
2286 .await?
2287 .ok_or(EngineError::Store(
2288 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2289 ))?;
2290 Ok(run.payload)
2291 }
2292
2293 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2321 let payload = self.payload().await?;
2322 serde_json::from_value(payload).map_err(EngineError::Serialization)
2323 }
2324
2325 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2348 self.error_handlers.push(OnErrorHandler {
2349 name: name.to_string(),
2350 config: config.into(),
2351 });
2352 }
2353
2354 pub fn clear_error_handlers(&mut self) {
2373 self.error_handlers.clear();
2374 }
2375
2376 async fn fire_error_handlers(
2382 &mut self,
2383 failed_step_name: &str,
2384 error_msg: &str,
2385 duration_ms: u64,
2386 ) {
2387 let handlers = std::mem::take(&mut self.error_handlers);
2388 if handlers.is_empty() {
2389 return;
2390 }
2391
2392 let error_context = json!({
2393 "failed_step": failed_step_name,
2394 "error": error_msg,
2395 "duration_ms": duration_ms,
2396 });
2397
2398 for handler in handlers {
2399 let mut config = handler.config.clone();
2400 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2401
2402 let position = self.position;
2403 self.position += 1;
2404
2405 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2406 let step = match self
2407 .store
2408 .create_step(NewStep {
2409 run_id: self.run_id,
2410 trace_id,
2411 name: handler.name.clone(),
2412 kind: config.kind(),
2413 position,
2414 input: Some(error_context.clone()),
2415 is_error_handler: true,
2416 })
2417 .await
2418 {
2419 Ok(step) => step,
2420 Err(err) => {
2421 warn!(
2422 run_id = %self.run_id,
2423 handler = %handler.name,
2424 error = %err,
2425 "failed to create error handler step"
2426 );
2427 continue;
2428 }
2429 };
2430
2431 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2432 warn!(
2433 run_id = %self.run_id,
2434 handler = %handler.name,
2435 error = %err,
2436 "failed to start error handler step"
2437 );
2438 continue;
2439 }
2440
2441 let step_log_sender = self
2442 .log_sender
2443 .as_ref()
2444 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2445
2446 let start = Instant::now();
2447 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2448 let handler_duration = start.elapsed().as_millis() as u64;
2449 let completed_at = Utc::now();
2450
2451 match result {
2452 Ok(output) => {
2453 if let Err(store_err) = self
2454 .store
2455 .update_step(
2456 step.id,
2457 StepUpdate {
2458 status: Some(StepStatus::Completed),
2459 output: Some(output.output),
2460 duration_ms: Some(handler_duration),
2461 cost_usd: Some(output.cost_usd),
2462 completed_at: Some(completed_at),
2463 ..StepUpdate::default()
2464 },
2465 )
2466 .await
2467 {
2468 warn!(
2469 run_id = %self.run_id,
2470 handler = %handler.name,
2471 error = %store_err,
2472 "failed to persist error handler completion"
2473 );
2474 }
2475
2476 info!(
2477 run_id = %self.run_id,
2478 handler = %handler.name,
2479 duration_ms = handler_duration,
2480 "error handler completed"
2481 );
2482 }
2483 Err(err) => {
2484 if let Err(store_err) = self
2485 .store
2486 .update_step(
2487 step.id,
2488 StepUpdate {
2489 status: Some(StepStatus::Failed),
2490 error: Some(err.to_string()),
2491 duration_ms: Some(handler_duration),
2492 completed_at: Some(completed_at),
2493 ..StepUpdate::default()
2494 },
2495 )
2496 .await
2497 {
2498 warn!(
2499 run_id = %self.run_id,
2500 handler = %handler.name,
2501 error = %store_err,
2502 "failed to persist error handler failure"
2503 );
2504 }
2505
2506 warn!(
2507 run_id = %self.run_id,
2508 handler = %handler.name,
2509 error = %err,
2510 "error handler failed (original error preserved)"
2511 );
2512 }
2513 }
2514 }
2515 }
2516}
2517
2518fn inject_error_context(
2520 config: &mut StepConfig,
2521 failed_step: &str,
2522 error_msg: &str,
2523 duration_ms: u64,
2524) {
2525 match config {
2526 StepConfig::Shell(shell) => {
2527 shell
2528 .env
2529 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2530 shell
2531 .env
2532 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2533 shell.env.push((
2534 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2535 duration_ms.to_string(),
2536 ));
2537 }
2538 StepConfig::Agent(agent) => {
2539 agent.prompt = format!(
2540 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2541 failed_step, duration_ms, error_msg, agent.prompt
2542 );
2543 }
2544 StepConfig::Http(http) => {
2545 http.headers
2546 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2547 http.headers.push((
2548 "X-Ironflow-Error-Message".to_string(),
2549 error_msg.to_string(),
2550 ));
2551 }
2552 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2553 }
2554}
2555
2556#[cfg(feature = "prometheus")]
2557fn record_retry_metric(kind: &str, outcome: &str) {
2558 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2559 use metrics::counter;
2560 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2561 .increment(1);
2562}
2563
2564#[cfg(not(feature = "prometheus"))]
2565fn record_retry_metric(_kind: &str, _outcome: &str) {}
2566
2567fn is_step_retryable(err: &EngineError) -> bool {
2571 use ironflow_core::error::{AgentError, OperationError};
2572
2573 match err {
2574 EngineError::Operation(op) => match op {
2575 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2576 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2577 OperationError::Deserialize { .. } => false,
2578 OperationError::Http {
2579 status: Some(code), ..
2580 } if (400..500).contains(code) && *code != 429 => false,
2581 _ => true,
2582 },
2583 _ => false,
2584 }
2585}
2586
2587fn allowed_failure_output(
2588 error_msg: &str,
2589 raw_response: Option<Value>,
2590 partial: Option<&StepPartialUsage>,
2591) -> StepOutput {
2592 StepOutput {
2593 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2594 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2595 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2596 input_tokens: partial.and_then(|p| p.input_tokens),
2597 output_tokens: partial.and_then(|p| p.output_tokens),
2598 model: None,
2599 debug_messages: None,
2600 }
2601}
2602
2603impl fmt::Debug for WorkflowContext {
2604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2605 f.debug_struct("WorkflowContext")
2606 .field("run_id", &self.run_id)
2607 .field("position", &self.position)
2608 .field("total_cost_usd", &self.total_cost_usd)
2609 .field("inherited_cost_usd", &self.inherited_cost_usd)
2610 .field("max_cost_usd", &self.max_cost_usd)
2611 .finish_non_exhaustive()
2612 }
2613}
2614
2615fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2618 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2619 debug_messages,
2620 ..
2621 })) = err
2622 && !debug_messages.is_empty()
2623 {
2624 return serde_json::to_value(debug_messages).ok();
2625 }
2626 None
2627}
2628
2629struct StepPartialUsage {
2635 cost_usd: Option<Decimal>,
2636 duration_ms: Option<u64>,
2637 input_tokens: Option<u64>,
2638 output_tokens: Option<u64>,
2639}
2640
2641fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2647 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2648 raw_response: Some(text),
2649 ..
2650 })) = err
2651 {
2652 return Some(Value::String(text.clone()));
2653 }
2654 None
2655}
2656
2657fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2658 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2659 partial_usage,
2660 ..
2661 })) = err
2662 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2663 {
2664 return Some(StepPartialUsage {
2665 cost_usd: partial_usage
2666 .cost_usd
2667 .and_then(|c| Decimal::try_from(c).ok()),
2668 duration_ms: partial_usage.duration_ms,
2669 input_tokens: partial_usage.input_tokens,
2670 output_tokens: partial_usage.output_tokens,
2671 });
2672 }
2673 None
2674}
2675
2676#[cfg(test)]
2677mod tests {
2678 use super::*;
2679 use ironflow_core::providers::claude::ClaudeCodeProvider;
2680 use ironflow_core::providers::record_replay::RecordReplayProvider;
2681 use ironflow_store::memory::InMemoryStore;
2682 use ironflow_store::models::{Run, RunActor, RunFilter};
2683 use ironflow_store::store::RunStore;
2684 use serde_json::json;
2685 use std::sync::Arc;
2686 use std::sync::atomic::{AtomicBool, Ordering};
2687 use uuid::Uuid;
2688
2689 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2691 let inner = ClaudeCodeProvider::new();
2692 Arc::new(RecordReplayProvider::replay(
2693 inner,
2694 "/tmp/ironflow-fixtures",
2695 ))
2696 }
2697
2698 fn create_test_context() -> WorkflowContext {
2700 let store = Arc::new(InMemoryStore::new());
2701 let provider = create_test_provider();
2702 let run_id = Uuid::now_v7();
2703 WorkflowContext::new(run_id, store, provider)
2704 }
2705
2706 #[test]
2707 fn context_new_initializes_correctly() {
2708 let ctx = create_test_context();
2709 assert_eq!(ctx.position, 0);
2710 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2711 assert_eq!(ctx.total_duration_ms, 0);
2712 assert!(ctx.last_step_ids.is_empty());
2713 assert!(ctx.replay_steps.is_empty());
2714 assert!(ctx.log_sender.is_none());
2715 }
2716
2717 #[test]
2718 fn context_run_id_returns_correct_id() {
2719 let run_id = Uuid::now_v7();
2720 let store = Arc::new(InMemoryStore::new());
2721 let provider = create_test_provider();
2722 let ctx = WorkflowContext::new(run_id, store, provider);
2723 assert_eq!(ctx.run_id(), run_id);
2724 }
2725
2726 #[test]
2727 fn context_total_cost_usd_initially_zero() {
2728 let ctx = create_test_context();
2729 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2730 }
2731
2732 #[test]
2733 fn context_total_duration_ms_initially_zero() {
2734 let ctx = create_test_context();
2735 assert_eq!(ctx.total_duration_ms(), 0);
2736 }
2737
2738 #[test]
2739 fn context_with_handler_resolver_creates_context_with_resolver() {
2740 let store = Arc::new(InMemoryStore::new());
2741 let provider = create_test_provider();
2742 let run_id = Uuid::now_v7();
2743
2744 let called = Arc::new(AtomicBool::new(false));
2745 let called_clone = called.clone();
2746
2747 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2748 called_clone.store(true, Ordering::SeqCst);
2749 None
2750 });
2751
2752 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2753
2754 assert_eq!(ctx.run_id(), run_id);
2755 assert!(ctx.handler_resolver.is_some());
2756 }
2757
2758 #[tokio::test]
2759 async fn context_set_log_sender_attaches_sender() {
2760 let mut ctx = create_test_context();
2761 let (sender, _receiver) = crate::log_sender::channel();
2762 ctx.set_log_sender(sender);
2763 assert!(ctx.log_sender.is_some());
2764 }
2765
2766 #[tokio::test]
2767 async fn context_skip_creates_skipped_step() {
2768 let store = Arc::new(InMemoryStore::new());
2769 let provider = create_test_provider();
2770
2771 store
2773 .create_run(NewRun {
2774 created_by: None,
2775 workflow_name: "test".to_string(),
2776 trigger: TriggerKind::Manual,
2777 payload: json!({}),
2778 max_retries: 0,
2779 handler_version: None,
2780 labels: Default::default(),
2781 scheduled_at: None,
2782 idempotency_key: None,
2783 max_cost_usd: None,
2784 })
2785 .await
2786 .expect("failed to create run")
2787 .into_run();
2788
2789 let runs = store
2791 .list_runs(RunFilter::default(), 1, 10)
2792 .await
2793 .expect("failed to list runs");
2794 let created_run_id = runs.items[0].id;
2795
2796 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2797 let initial_position = ctx.position;
2798
2799 ctx.skip("skip-step", "condition not met")
2800 .await
2801 .expect("skip failed");
2802
2803 assert_eq!(ctx.position, initial_position + 1);
2804 assert!(!ctx.last_step_ids.is_empty());
2805
2806 let steps = store
2808 .list_steps(created_run_id)
2809 .await
2810 .expect("failed to list steps");
2811 assert_eq!(steps.len(), 1);
2812 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2813 }
2814
2815 struct NoopSubWorkflow;
2818
2819 impl WorkflowHandler for NoopSubWorkflow {
2820 fn name(&self) -> &str {
2821 "noop-sub"
2822 }
2823
2824 fn execute<'a>(
2825 &'a self,
2826 _ctx: &'a mut WorkflowContext,
2827 ) -> crate::handler::HandlerFuture<'a> {
2828 Box::pin(async move { Ok(()) })
2829 }
2830 }
2831
2832 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2835 let store = Arc::new(InMemoryStore::new());
2836 let provider = create_test_provider();
2837
2838 let parent = store
2839 .create_run(NewRun {
2840 workflow_name: "parent".to_string(),
2841 trigger: TriggerKind::Api,
2842 payload: json!({}),
2843 max_retries: 0,
2844 handler_version: None,
2845 labels: Default::default(),
2846 scheduled_at: None,
2847 created_by,
2848 idempotency_key: None,
2849 max_cost_usd: None,
2850 })
2851 .await
2852 .expect("failed to create parent run")
2853 .into_run();
2854
2855 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2856 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2857 _ => None,
2858 });
2859
2860 let mut ctx =
2861 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2862 ctx.workflow(&NoopSubWorkflow, json!({}))
2863 .await
2864 .expect("sub-workflow failed");
2865
2866 let runs = store
2867 .list_runs(RunFilter::default(), 1, 10)
2868 .await
2869 .expect("failed to list runs");
2870 runs.items
2871 .into_iter()
2872 .find(|r| r.workflow_name == "noop-sub")
2873 .expect("child run was created")
2874 }
2875
2876 #[tokio::test]
2877 async fn child_run_inherits_the_parent_author() {
2878 let user_id = Uuid::now_v7();
2879 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2880
2881 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2882 }
2883
2884 #[tokio::test]
2885 async fn child_run_of_an_unattributed_parent_has_no_author() {
2886 let child = child_run_of_parent_authored_by(None).await;
2887
2888 assert!(child.created_by.is_none());
2889 }
2890
2891 #[tokio::test]
2892 async fn context_parallel_empty_steps_returns_empty_vec() {
2893 let mut ctx = create_test_context();
2894 let results = ctx
2895 .parallel(vec![], true)
2896 .await
2897 .expect("parallel should not fail on empty input");
2898 assert!(results.is_empty());
2899 }
2900
2901 #[tokio::test]
2902 async fn context_approval_first_execution_returns_error() {
2903 let store = Arc::new(InMemoryStore::new());
2904 let provider = create_test_provider();
2905
2906 store
2908 .create_run(NewRun {
2909 created_by: None,
2910 workflow_name: "test".to_string(),
2911 trigger: TriggerKind::Manual,
2912 payload: json!({}),
2913 max_retries: 0,
2914 handler_version: None,
2915 labels: Default::default(),
2916 scheduled_at: None,
2917 idempotency_key: None,
2918 max_cost_usd: None,
2919 })
2920 .await
2921 .expect("failed to create run")
2922 .into_run();
2923
2924 let runs = store
2926 .list_runs(RunFilter::default(), 1, 10)
2927 .await
2928 .expect("failed to list runs");
2929 let created_run_id = runs.items[0].id;
2930
2931 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2932
2933 let result = ctx
2934 .approval(
2935 "approve-step",
2936 crate::config::ApprovalConfig::new("Continue?"),
2937 )
2938 .await;
2939
2940 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2942
2943 assert_eq!(ctx.position, 1);
2945
2946 let steps = store
2948 .list_steps(created_run_id)
2949 .await
2950 .expect("failed to list steps");
2951 assert_eq!(steps.len(), 1);
2952 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2953 }
2954
2955 #[tokio::test]
2956 async fn context_approval_replay_returns_ok() {
2957 let store = Arc::new(InMemoryStore::new());
2958 let provider = create_test_provider();
2959
2960 store
2962 .create_run(NewRun {
2963 created_by: None,
2964 workflow_name: "test".to_string(),
2965 trigger: TriggerKind::Manual,
2966 payload: json!({}),
2967 max_retries: 0,
2968 handler_version: None,
2969 labels: Default::default(),
2970 scheduled_at: None,
2971 idempotency_key: None,
2972 max_cost_usd: None,
2973 })
2974 .await
2975 .expect("failed to create run")
2976 .into_run();
2977
2978 let runs = store
2980 .list_runs(RunFilter::default(), 1, 10)
2981 .await
2982 .expect("failed to list runs");
2983 let created_run_id = runs.items[0].id;
2984
2985 let step = store
2987 .create_step(NewStep {
2988 run_id: created_run_id,
2989 trace_id: step_trace_id(created_run_id, "approval", 0),
2990 name: "approval".to_string(),
2991 kind: StepKind::Approval,
2992 position: 0,
2993 input: None,
2994 is_error_handler: false,
2995 })
2996 .await
2997 .expect("failed to create step");
2998
2999 store
3001 .update_step(
3002 step.id,
3003 StepUpdate {
3004 status: Some(StepStatus::Running),
3005 started_at: Some(Utc::now()),
3006 ..StepUpdate::default()
3007 },
3008 )
3009 .await
3010 .expect("failed to update step to Running");
3011
3012 store
3013 .update_step(
3014 step.id,
3015 StepUpdate {
3016 status: Some(StepStatus::AwaitingApproval),
3017 ..StepUpdate::default()
3018 },
3019 )
3020 .await
3021 .expect("failed to update step to AwaitingApproval");
3022
3023 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
3025 ctx.load_replay_steps()
3026 .await
3027 .expect("failed to load replay steps");
3028
3029 let result = ctx
3031 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3032 .await;
3033
3034 assert!(result.is_ok());
3035
3036 let steps = store
3038 .list_steps(created_run_id)
3039 .await
3040 .expect("failed to list steps");
3041 assert_eq!(steps.len(), 1);
3042 assert_eq!(steps[0].status.state, StepStatus::Completed);
3043 }
3044
3045 #[tokio::test]
3046 async fn context_load_replay_steps_loads_completed_steps() {
3047 let store = Arc::new(InMemoryStore::new());
3048 let provider = create_test_provider();
3049
3050 store
3052 .create_run(NewRun {
3053 created_by: None,
3054 workflow_name: "test".to_string(),
3055 trigger: TriggerKind::Manual,
3056 payload: json!({}),
3057 max_retries: 0,
3058 handler_version: None,
3059 labels: Default::default(),
3060 scheduled_at: None,
3061 idempotency_key: None,
3062 max_cost_usd: None,
3063 })
3064 .await
3065 .expect("failed to create run")
3066 .into_run();
3067
3068 let runs = store
3070 .list_runs(RunFilter::default(), 1, 10)
3071 .await
3072 .expect("failed to list runs");
3073 let created_run_id = runs.items[0].id;
3074
3075 let completed_step = store
3077 .create_step(NewStep {
3078 run_id: created_run_id,
3079 trace_id: step_trace_id(created_run_id, "completed", 0),
3080 name: "completed".to_string(),
3081 kind: StepKind::Shell,
3082 position: 0,
3083 input: None,
3084 is_error_handler: false,
3085 })
3086 .await
3087 .expect("failed to create step");
3088
3089 store
3091 .update_step(
3092 completed_step.id,
3093 StepUpdate {
3094 status: Some(StepStatus::Running),
3095 started_at: Some(Utc::now()),
3096 ..StepUpdate::default()
3097 },
3098 )
3099 .await
3100 .expect("failed to update step to Running");
3101
3102 store
3103 .update_step(
3104 completed_step.id,
3105 StepUpdate {
3106 status: Some(StepStatus::Completed),
3107 completed_at: Some(Utc::now()),
3108 ..StepUpdate::default()
3109 },
3110 )
3111 .await
3112 .expect("failed to update step to Completed");
3113
3114 let _pending_step = store
3115 .create_step(NewStep {
3116 run_id: created_run_id,
3117 trace_id: step_trace_id(created_run_id, "pending", 1),
3118 name: "pending".to_string(),
3119 kind: StepKind::Shell,
3120 position: 1,
3121 input: None,
3122 is_error_handler: false,
3123 })
3124 .await
3125 .expect("failed to create step");
3126
3127 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
3129 ctx.load_replay_steps()
3130 .await
3131 .expect("failed to load replay steps");
3132
3133 assert_eq!(ctx.replay_steps.len(), 1);
3135 assert!(ctx.replay_steps.contains_key(&0));
3136 assert!(!ctx.replay_steps.contains_key(&1));
3137 }
3138
3139 #[tokio::test]
3140 async fn context_payload_returns_run_payload() {
3141 let store = Arc::new(InMemoryStore::new());
3142 let provider = create_test_provider();
3143 let test_payload = json!({"key": "value", "number": 42});
3144
3145 store
3147 .create_run(NewRun {
3148 created_by: None,
3149 workflow_name: "test".to_string(),
3150 trigger: TriggerKind::Manual,
3151 payload: test_payload.clone(),
3152 max_retries: 0,
3153 handler_version: None,
3154 labels: Default::default(),
3155 scheduled_at: None,
3156 idempotency_key: None,
3157 max_cost_usd: None,
3158 })
3159 .await
3160 .expect("failed to create run")
3161 .into_run();
3162
3163 let runs = store
3165 .list_runs(RunFilter::default(), 1, 10)
3166 .await
3167 .expect("failed to list runs");
3168 let created_run_id = runs.items[0].id;
3169
3170 let ctx = WorkflowContext::new(created_run_id, store, provider);
3171 let payload = ctx.payload().await.expect("failed to get payload");
3172
3173 assert_eq!(payload, test_payload);
3174 }
3175
3176 #[tokio::test]
3177 async fn context_payload_returns_error_for_nonexistent_run() {
3178 let store = Arc::new(InMemoryStore::new());
3179 let provider = create_test_provider();
3180 let run_id = Uuid::now_v7();
3181
3182 let ctx = WorkflowContext::new(run_id, store, provider);
3183 let result = ctx.payload().await;
3184
3185 assert!(result.is_err());
3186 }
3187
3188 #[tokio::test]
3189 async fn context_store_returns_reference() {
3190 let ctx = create_test_context();
3191 let _store = ctx.store();
3192 }
3194
3195 #[test]
3196 fn context_debug_formatting() {
3197 let ctx = create_test_context();
3198 let debug_str = format!("{:?}", ctx);
3199 assert!(debug_str.contains("WorkflowContext"));
3200 assert!(debug_str.contains("run_id"));
3201 }
3202
3203 #[tokio::test]
3204 async fn context_last_step_ids_tracks_executed_steps() {
3205 let store = Arc::new(InMemoryStore::new());
3206 let provider = create_test_provider();
3207
3208 store
3210 .create_run(NewRun {
3211 created_by: None,
3212 workflow_name: "test".to_string(),
3213 trigger: TriggerKind::Manual,
3214 payload: json!({}),
3215 max_retries: 0,
3216 handler_version: None,
3217 labels: Default::default(),
3218 scheduled_at: None,
3219 idempotency_key: None,
3220 max_cost_usd: None,
3221 })
3222 .await
3223 .expect("failed to create run")
3224 .into_run();
3225
3226 let runs = store
3228 .list_runs(RunFilter::default(), 1, 10)
3229 .await
3230 .expect("failed to list runs");
3231 let created_run_id = runs.items[0].id;
3232
3233 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
3234 assert!(ctx.last_step_ids.is_empty());
3235
3236 ctx.skip("step1", "reason").await.expect("skip failed");
3237
3238 assert_eq!(ctx.last_step_ids.len(), 1);
3239
3240 ctx.skip("step2", "reason").await.expect("skip failed");
3241
3242 assert_eq!(ctx.last_step_ids.len(), 1);
3244 }
3245}