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 if let Some(ref bus) = self.event_bus
900 && matches!(step_config, StepConfig::Agent(_))
901 {
902 let tokens = output
903 .input_tokens
904 .unwrap_or(0)
905 .saturating_add(output.output_tokens.unwrap_or(0));
906 bus.publish(
907 self.run_id,
908 crate::notify::WorkflowEvent::AgentStepTokensUsed {
909 step_name: step_name.clone(),
910 tokens,
911 cost_usd: output.cost_usd,
912 },
913 );
914 }
915
916 info!(
917 run_id = %self.run_id,
918 step = %step_name,
919 trace_id = %step_trace,
920 duration_ms = output.duration_ms,
921 "parallel step completed"
922 );
923
924 indexed_results[idx] = Some(Ok(output));
925 }
926 Err(err) => {
927 let err_msg = err.to_string();
928 let debug_messages_json = extract_debug_messages_from_error(&err);
929 let partial = extract_partial_usage_from_error(&err);
930 let raw_response_output = extract_raw_response_from_error(&err);
931
932 if let Some(ref usage) = partial {
933 if let Some(cost) = usage.cost_usd {
934 self.total_cost_usd += cost;
935 }
936 if let Some(dur) = usage.duration_ms {
937 self.total_duration_ms += dur;
938 }
939 }
940
941 if let Err(store_err) = self
942 .store
943 .update_step(
944 *step_id,
945 StepUpdate {
946 status: Some(StepStatus::Failed),
947 error: Some(err_msg.clone()),
948 output: raw_response_output.clone(),
949 completed_at: Some(completed_at),
950 debug_messages: debug_messages_json,
951 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
952 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
953 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
954 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
955 ..StepUpdate::default()
956 },
957 )
958 .await
959 {
960 tracing::error!(
961 step_id = %step_id,
962 error = %store_err,
963 "failed to persist parallel step failure"
964 );
965 }
966
967 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
968 let err_cost = partial
969 .as_ref()
970 .and_then(|p| p.cost_usd)
971 .unwrap_or(Decimal::ZERO);
972 self.step_results.push(StepResult::from_failure(
973 *step_trace,
974 step_name,
975 &err_msg,
976 err_duration,
977 err_cost,
978 ));
979
980 if step_config.allow_failure() {
981 self.has_allowed_failure = true;
982 info!(
983 run_id = %self.run_id,
984 step = %step_name,
985 error = %err_msg,
986 "parallel step failed but allow_failure is set, continuing"
987 );
988 indexed_results[idx] = Some(Ok(allowed_failure_output(
989 &err_msg,
990 raw_response_output,
991 partial.as_ref(),
992 )));
993 } else {
994 indexed_results[idx] = Some(Err(err_msg.clone()));
995
996 if first_error.is_none() {
997 first_error = Some(err);
998 }
999
1000 if fail_fast {
1001 join_set.abort_all();
1002 }
1003 }
1004 }
1005 }
1006 }
1007
1008 if let Some(err) = first_error {
1009 return Err(err);
1010 }
1011
1012 self.persist_progress().await;
1013
1014 self.last_step_ids = step_records.iter().map(|(id, _, _, _)| *id).collect();
1015
1016 let results: Vec<ParallelStepResult> = step_records
1018 .iter()
1019 .enumerate()
1020 .map(|(idx, (step_id, _trace_id, name, _))| {
1021 let output = match indexed_results[idx].take() {
1022 Some(Ok(o)) => o,
1023 _ => unreachable!("all steps succeeded if no error returned"),
1024 };
1025 ParallelStepResult {
1026 name: name.clone(),
1027 output,
1028 step_id: *step_id,
1029 }
1030 })
1031 .collect();
1032
1033 Ok(results)
1034 }
1035
1036 pub async fn shell(
1059 &mut self,
1060 name: &str,
1061 config: ShellConfig,
1062 ) -> Result<StepOutput, EngineError> {
1063 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
1064 .await
1065 }
1066
1067 pub async fn http(
1087 &mut self,
1088 name: &str,
1089 config: HttpConfig,
1090 ) -> Result<StepOutput, EngineError> {
1091 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
1092 .await
1093 }
1094
1095 pub async fn agent(
1115 &mut self,
1116 name: &str,
1117 config: impl Into<AgentStepConfig>,
1118 ) -> Result<StepOutput, EngineError> {
1119 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
1120 .await
1121 }
1122
1123 pub async fn approval(
1154 &mut self,
1155 name: &str,
1156 config: ApprovalConfig,
1157 ) -> Result<(), EngineError> {
1158 let position = self.position;
1159 self.position += 1;
1160
1161 if let Some(existing) = self.replay_steps.get(&position)
1164 && existing.kind == StepKind::Approval
1165 {
1166 if existing.status.state == StepStatus::AwaitingApproval {
1167 self.store
1168 .update_step(
1169 existing.id,
1170 StepUpdate {
1171 status: Some(StepStatus::Completed),
1172 completed_at: Some(Utc::now()),
1173 ..StepUpdate::default()
1174 },
1175 )
1176 .await?;
1177 }
1178
1179 self.last_step_ids = vec![existing.id];
1180 info!(
1181 run_id = %self.run_id,
1182 step = %name,
1183 position,
1184 "approval step replayed (approved)"
1185 );
1186 return Ok(());
1187 }
1188
1189 if let Some(&granted_in) = self.granted_approvals.get(&position) {
1193 let trace_id = step_trace_id(self.run_id, name, position);
1194 let step = self
1195 .store
1196 .create_step(NewStep {
1197 run_id: self.run_id,
1198 trace_id,
1199 name: name.to_string(),
1200 kind: StepKind::Approval,
1201 position,
1202 input: Some(serde_json::to_value(&config)?),
1203 is_error_handler: false,
1204 })
1205 .await?;
1206
1207 let now = Utc::now();
1208 self.start_step(step.id, now).await?;
1209 self.store
1210 .update_step(
1211 step.id,
1212 StepUpdate {
1213 status: Some(StepStatus::Completed),
1214 output: Some(json!({"approved_in_attempt": granted_in})),
1215 completed_at: Some(now),
1216 ..StepUpdate::default()
1217 },
1218 )
1219 .await?;
1220
1221 self.last_step_ids = vec![step.id];
1222 info!(
1223 run_id = %self.run_id,
1224 step = %name,
1225 position,
1226 granted_in_attempt = granted_in,
1227 attempt = self.attempt,
1228 "approval carried over from a previous attempt"
1229 );
1230 return Ok(());
1231 }
1232
1233 let trace_id = step_trace_id(self.run_id, name, position);
1235 let step = self
1236 .store
1237 .create_step(NewStep {
1238 run_id: self.run_id,
1239 trace_id,
1240 name: name.to_string(),
1241 kind: StepKind::Approval,
1242 position,
1243 input: Some(serde_json::to_value(&config)?),
1244 is_error_handler: false,
1245 })
1246 .await?;
1247
1248 self.start_step(step.id, Utc::now()).await?;
1249
1250 self.store
1253 .update_step(
1254 step.id,
1255 StepUpdate {
1256 status: Some(StepStatus::AwaitingApproval),
1257 ..StepUpdate::default()
1258 },
1259 )
1260 .await?;
1261
1262 self.last_step_ids = vec![step.id];
1263
1264 if let Some(ref bus) = self.event_bus {
1265 bus.publish(
1266 self.run_id,
1267 crate::notify::WorkflowEvent::ApprovalRequired {
1268 step_name: name.to_string(),
1269 step_index: position,
1270 approval_id: step.id,
1271 },
1272 );
1273 }
1274
1275 Err(EngineError::ApprovalRequired {
1276 run_id: self.run_id,
1277 step_id: step.id,
1278 message: config.message().to_string(),
1279 })
1280 }
1281
1282 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1311 let position = self.position;
1312 self.position += 1;
1313
1314 let trace_id = step_trace_id(self.run_id, name, position);
1315 let step = self
1316 .store
1317 .create_step(NewStep {
1318 run_id: self.run_id,
1319 trace_id,
1320 name: name.to_string(),
1321 kind: StepKind::Custom("skip".to_string()),
1322 position,
1323 input: None,
1324 is_error_handler: false,
1325 })
1326 .await?;
1327
1328 if !self.last_step_ids.is_empty() {
1329 let deps: Vec<NewStepDependency> = self
1330 .last_step_ids
1331 .iter()
1332 .map(|&depends_on| NewStepDependency {
1333 step_id: step.id,
1334 depends_on,
1335 })
1336 .collect();
1337 self.store.create_step_dependencies(deps).await?;
1338 }
1339
1340 let now = Utc::now();
1341 self.store
1342 .update_step(
1343 step.id,
1344 StepUpdate {
1345 status: Some(StepStatus::Skipped),
1346 output: Some(serde_json::json!({"reason": reason})),
1347 completed_at: Some(now),
1348 ..StepUpdate::default()
1349 },
1350 )
1351 .await?;
1352
1353 self.last_step_ids = vec![step.id];
1354
1355 info!(
1356 run_id = %self.run_id,
1357 step = %name,
1358 reason,
1359 "step skipped"
1360 );
1361
1362 Ok(())
1363 }
1364
1365 pub async fn operation(
1403 &mut self,
1404 name: &str,
1405 op: &dyn Operation,
1406 ) -> Result<StepOutput, EngineError> {
1407 let kind = StepKind::Custom(op.kind().to_string());
1408 let position = self.position;
1409 self.position += 1;
1410
1411 let trace_id = step_trace_id(self.run_id, name, position);
1412 let step = self
1413 .store
1414 .create_step(NewStep {
1415 run_id: self.run_id,
1416 trace_id,
1417 name: name.to_string(),
1418 kind,
1419 position,
1420 input: op.input(),
1421 is_error_handler: false,
1422 })
1423 .await?;
1424
1425 self.start_step(step.id, Utc::now()).await?;
1426
1427 let start = Instant::now();
1428
1429 match op.execute().await {
1430 Ok(output_value) => {
1431 let duration_ms = start.elapsed().as_millis() as u64;
1432 self.total_duration_ms += duration_ms;
1433
1434 let completed_at = Utc::now();
1435 self.store
1436 .update_step(
1437 step.id,
1438 StepUpdate {
1439 status: Some(StepStatus::Completed),
1440 output: Some(output_value.clone()),
1441 duration_ms: Some(duration_ms),
1442 cost_usd: Some(Decimal::ZERO),
1443 completed_at: Some(completed_at),
1444 ..StepUpdate::default()
1445 },
1446 )
1447 .await?;
1448
1449 info!(
1450 run_id = %self.run_id,
1451 step = %name,
1452 kind = op.kind(),
1453 duration_ms,
1454 "operation step completed"
1455 );
1456
1457 self.last_step_ids = vec![step.id];
1458
1459 Ok(StepOutput {
1460 output: output_value,
1461 duration_ms,
1462 cost_usd: Decimal::ZERO,
1463 input_tokens: None,
1464 output_tokens: None,
1465 model: None,
1466 debug_messages: None,
1467 })
1468 }
1469 Err(err) => {
1470 let completed_at = Utc::now();
1471 if let Err(store_err) = self
1472 .store
1473 .update_step(
1474 step.id,
1475 StepUpdate {
1476 status: Some(StepStatus::Failed),
1477 error: Some(err.to_string()),
1478 completed_at: Some(completed_at),
1479 ..StepUpdate::default()
1480 },
1481 )
1482 .await
1483 {
1484 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1485 }
1486
1487 Err(err)
1488 }
1489 }
1490 }
1491
1492 pub async fn workflow(
1519 &mut self,
1520 handler: &dyn WorkflowHandler,
1521 payload: Value,
1522 ) -> Result<StepOutput, EngineError> {
1523 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1525 let state = guard_state
1526 .lock()
1527 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1528 state.check(guard_config, handler.name())?;
1529 }
1530
1531 let config = WorkflowStepConfig::new(handler.name(), payload);
1532 let position = self.position;
1533 self.position += 1;
1534
1535 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
1536 let step = self
1537 .store
1538 .create_step(NewStep {
1539 run_id: self.run_id,
1540 trace_id,
1541 name: config.workflow_name.clone(),
1542 kind: StepKind::Workflow,
1543 position,
1544 input: Some(serde_json::to_value(&config)?),
1545 is_error_handler: false,
1546 })
1547 .await?;
1548
1549 self.start_step(step.id, Utc::now()).await?;
1550
1551 if let Some(guard_state) = &self.guard_state {
1553 let mut state = guard_state
1554 .lock()
1555 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1556 state.record_invocation(handler.name());
1557 }
1558
1559 match self.execute_child_workflow(&config).await {
1560 Ok((output, child_had_allowed_failure)) => {
1561 self.total_cost_usd += output.cost_usd;
1562 self.total_duration_ms += output.duration_ms;
1563 if child_had_allowed_failure {
1564 self.has_allowed_failure = true;
1565 }
1566
1567 let completed_at = Utc::now();
1568 self.store
1569 .update_step(
1570 step.id,
1571 StepUpdate {
1572 status: Some(StepStatus::Completed),
1573 output: Some(output.output.clone()),
1574 duration_ms: Some(output.duration_ms),
1575 cost_usd: Some(output.cost_usd),
1576 completed_at: Some(completed_at),
1577 ..StepUpdate::default()
1578 },
1579 )
1580 .await?;
1581
1582 info!(
1583 run_id = %self.run_id,
1584 child_workflow = %config.workflow_name,
1585 duration_ms = output.duration_ms,
1586 "workflow step completed"
1587 );
1588
1589 self.last_step_ids = vec![step.id];
1590
1591 self.guard_record_return();
1592 Ok(output)
1593 }
1594 Err(err) => {
1595 let completed_at = Utc::now();
1596 if let Err(store_err) = self
1597 .store
1598 .update_step(
1599 step.id,
1600 StepUpdate {
1601 status: Some(StepStatus::Failed),
1602 error: Some(err.to_string()),
1603 completed_at: Some(completed_at),
1604 ..StepUpdate::default()
1605 },
1606 )
1607 .await
1608 {
1609 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1610 }
1611
1612 self.guard_record_return();
1613 Err(err)
1614 }
1615 }
1616 }
1617
1618 fn guard_record_return(&self) {
1623 if let Some(guard_state) = &self.guard_state {
1624 match guard_state.lock() {
1625 Ok(mut state) => state.record_return(),
1626 Err(_) => {
1627 error!(
1628 run_id = %self.run_id,
1629 "guard state mutex poisoned in record_return"
1630 );
1631 }
1632 }
1633 }
1634 }
1635
1636 async fn execute_with_guard_timeout(
1640 &self,
1641 config: &StepConfig,
1642 step_log_sender: Option<StepLogSender>,
1643 ) -> Result<StepOutput, EngineError> {
1644 let remaining = self.guard_remaining_timeout();
1645 match remaining {
1646 Some(dur) => {
1647 use tokio::time::timeout;
1648 match timeout(
1649 dur,
1650 execute_step_config(config, &self.provider, step_log_sender),
1651 )
1652 .await
1653 {
1654 Ok(result) => result,
1655 Err(_elapsed) => {
1656 let config_secs = self
1657 .guard_config
1658 .as_ref()
1659 .map_or(0, |c| c.workflow_timeout_secs);
1660 Err(WorkflowRejection::WorkflowTimeout {
1661 elapsed_secs: config_secs,
1662 max: config_secs,
1663 }
1664 .into())
1665 }
1666 }
1667 }
1668 None => execute_step_config(config, &self.provider, step_log_sender).await,
1669 }
1670 }
1671
1672 fn guard_remaining_timeout(&self) -> Option<std::time::Duration> {
1674 let config = self.guard_config.as_ref()?;
1675 let guard_state = self.guard_state.as_ref()?;
1676 let state = guard_state.lock().ok()?;
1677 let elapsed = state.elapsed_secs();
1678 let max = config.workflow_timeout_secs;
1679 if elapsed >= max {
1680 Some(std::time::Duration::ZERO)
1681 } else {
1682 Some(std::time::Duration::from_secs(max - elapsed))
1683 }
1684 }
1685
1686 fn check_guard_timeout(&self) -> Result<(), EngineError> {
1688 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1689 let state = guard_state
1690 .lock()
1691 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1692 let elapsed = state.elapsed_secs();
1693 if elapsed >= config.workflow_timeout_secs {
1694 return Err(WorkflowRejection::WorkflowTimeout {
1695 elapsed_secs: elapsed,
1696 max: config.workflow_timeout_secs,
1697 }
1698 .into());
1699 }
1700 }
1701 Ok(())
1702 }
1703
1704 fn guard_record_tokens(&self, tokens: u64) -> Result<(), EngineError> {
1706 if let (Some(config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
1707 let mut state = guard_state
1708 .lock()
1709 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
1710 state.record_tokens(config, tokens)?;
1711 }
1712 Ok(())
1713 }
1714
1715 async fn execute_child_workflow(
1718 &self,
1719 config: &WorkflowStepConfig,
1720 ) -> Result<(StepOutput, bool), EngineError> {
1721 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1722 EngineError::InvalidWorkflow(
1723 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1724 )
1725 })?;
1726
1727 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1728 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1729 })?;
1730
1731 let parent = self.store.get_run(self.run_id).await?;
1734 let (parent_labels, parent_author) =
1735 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1736
1737 let child_run = self
1738 .store
1739 .create_run(NewRun {
1740 workflow_name: config.workflow_name.clone(),
1741 trigger: TriggerKind::Workflow,
1742 payload: config.payload.clone(),
1743 max_retries: 0,
1744 handler_version: None,
1745 labels: parent_labels,
1746 scheduled_at: None,
1747 created_by: parent_author,
1748 idempotency_key: None,
1749 max_cost_usd: self.max_cost_usd,
1751 })
1752 .await?
1753 .into_run();
1754
1755 let child_run_id = child_run.id;
1756 info!(
1757 parent_run_id = %self.run_id,
1758 child_run_id = %child_run_id,
1759 workflow = %config.workflow_name,
1760 "child run created"
1761 );
1762
1763 self.store
1764 .update_run_status(child_run_id, RunStatus::Running)
1765 .await?;
1766
1767 let run_start = Instant::now();
1768 let mut child_ctx = WorkflowContext {
1769 run_id: child_run_id,
1770 store: self.store.clone(),
1771 provider: self.provider.clone(),
1772 handler_resolver: self.handler_resolver.clone(),
1773 position: 0,
1774 last_step_ids: Vec::new(),
1775 total_cost_usd: Decimal::ZERO,
1776 total_duration_ms: 0,
1777 max_cost_usd: self.max_cost_usd,
1778 inherited_cost_usd: self.charged_cost_usd(),
1781 replay_steps: HashMap::new(),
1782 granted_approvals: HashMap::new(),
1783 attempt: 1,
1785 carried_duration_ms: 0,
1786 log_sender: self.log_sender.clone(),
1787 artifact_sink: self.artifact_sink.clone(),
1790 has_allowed_failure: false,
1791 error_handlers: Vec::new(),
1792 guard_state: self.guard_state.clone(),
1793 guard_config: self.guard_config.clone(),
1794 step_results: Vec::new(),
1795 event_bus: self.event_bus.clone(),
1796 };
1797
1798 let result = handler.execute(&mut child_ctx).await;
1799 let total_duration = run_start.elapsed().as_millis() as u64;
1800 let completed_at = Utc::now();
1801
1802 match result {
1803 Ok(()) => {
1804 let child_status = if child_ctx.has_allowed_failure {
1805 RunStatus::Warning
1806 } else {
1807 RunStatus::Completed
1808 };
1809 self.store
1810 .update_run(
1811 child_run_id,
1812 RunUpdate {
1813 status: Some(child_status),
1814 cost_usd: Some(child_ctx.total_cost_usd),
1815 duration_ms: Some(total_duration),
1816 completed_at: Some(completed_at),
1817 ..RunUpdate::default()
1818 },
1819 )
1820 .await?;
1821
1822 let child_had_allowed_failure = child_ctx.has_allowed_failure;
1823 Ok((
1824 StepOutput {
1825 output: serde_json::json!({
1826 "run_id": child_run_id,
1827 "workflow_name": config.workflow_name,
1828 "status": child_status,
1829 "cost_usd": child_ctx.total_cost_usd,
1830 "duration_ms": total_duration,
1831 }),
1832 duration_ms: total_duration,
1833 cost_usd: child_ctx.total_cost_usd,
1834 input_tokens: None,
1835 output_tokens: None,
1836 model: None,
1837 debug_messages: None,
1838 },
1839 child_had_allowed_failure,
1840 ))
1841 }
1842 Err(err) => {
1843 if let Err(store_err) = self
1844 .store
1845 .update_run(
1846 child_run_id,
1847 RunUpdate {
1848 status: Some(RunStatus::Failed),
1849 error: Some(err.to_string()),
1850 cost_usd: Some(child_ctx.total_cost_usd),
1851 duration_ms: Some(total_duration),
1852 completed_at: Some(completed_at),
1853 ..RunUpdate::default()
1854 },
1855 )
1856 .await
1857 {
1858 error!(
1859 child_run_id = %child_run_id,
1860 store_error = %store_err,
1861 "failed to persist child run failure"
1862 );
1863 }
1864
1865 Err(err)
1866 }
1867 }
1868 }
1869
1870 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1875 let step = self.replay_steps.get(&position)?;
1876 if step.status.state != StepStatus::Completed {
1877 return None;
1878 }
1879 let output = StepOutput {
1880 output: step.output.clone().unwrap_or(Value::Null),
1881 duration_ms: step.duration_ms,
1882 cost_usd: step.cost_usd,
1883 input_tokens: step.input_tokens,
1884 output_tokens: step.output_tokens,
1885 model: None,
1886 debug_messages: None,
1887 };
1888 self.total_cost_usd += output.cost_usd;
1889 self.total_duration_ms += output.duration_ms;
1890 self.last_step_ids = vec![step.id];
1891 info!(
1892 run_id = %self.run_id,
1893 step = %step.name,
1894 position,
1895 "step replayed from previous execution"
1896 );
1897 Some(output)
1898 }
1899
1900 #[tracing::instrument(
1902 name = "context.execute_step",
1903 skip_all,
1904 fields(
1905 run_id = %self.run_id,
1906 step.name = %name,
1907 step.kind,
1908 step.position = self.position,
1909 step.trace_id,
1910 )
1911 )]
1912 async fn execute_step(
1913 &mut self,
1914 name: &str,
1915 kind: StepKind,
1916 config: StepConfig,
1917 ) -> Result<StepOutput, EngineError> {
1918 let kind_str: &'static str = match kind {
1919 StepKind::Shell => "shell",
1920 StepKind::Http => "http",
1921 StepKind::Agent => "agent",
1922 StepKind::Workflow => "workflow",
1923 StepKind::Approval => "approval",
1924 StepKind::Custom(_) => "custom",
1925 };
1926 Span::current().record("step.kind", kind_str);
1927
1928 self.check_guard_timeout()?;
1930
1931 let position = self.position;
1932 self.position += 1;
1933
1934 if let Some(output) = self.try_replay_step(position) {
1936 return Ok(output);
1937 }
1938
1939 if let StepConfig::Agent(ref agent_config) = config {
1942 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1943 }
1944
1945 let trace_id = step_trace_id(self.run_id, name, position);
1947 Span::current().record("step.trace_id", trace_id.to_string().as_str());
1948 let step = self
1949 .store
1950 .create_step(NewStep {
1951 run_id: self.run_id,
1952 trace_id,
1953 name: name.to_string(),
1954 kind,
1955 position,
1956 input: Some(serde_json::to_value(&config)?),
1957 is_error_handler: false,
1958 })
1959 .await?;
1960
1961 self.start_step(step.id, Utc::now()).await?;
1962
1963 if let Some(ref bus) = self.event_bus {
1964 bus.publish(
1965 self.run_id,
1966 crate::notify::WorkflowEvent::StepStarted {
1967 step_name: name.to_string(),
1968 step_index: position,
1969 timestamp: Utc::now(),
1970 },
1971 );
1972 }
1973
1974 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1977 self.fail_step(step.id, &err).await;
1978 if config.allow_failure() {
1979 self.has_allowed_failure = true;
1980 self.last_step_ids = vec![step.id];
1981 info!(
1982 run_id = %self.run_id,
1983 step = %name,
1984 error = %err,
1985 "step input preparation failed but allow_failure is set, continuing"
1986 );
1987 return Ok(StepOutput {
1988 output: json!({"error": err.to_string()}),
1989 duration_ms: 0,
1990 cost_usd: Decimal::ZERO,
1991 input_tokens: None,
1992 output_tokens: None,
1993 model: None,
1994 debug_messages: None,
1995 });
1996 }
1997 return Err(err);
1998 }
1999
2000 let step_log_sender = self
2001 .log_sender
2002 .as_ref()
2003 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
2004
2005 let execution = self
2006 .execute_with_guard_timeout(&config, step_log_sender)
2007 .await;
2008
2009 let execution = self
2010 .retry_step_if_configured(name, kind_str, &config, step.id, execution)
2011 .await;
2012
2013 if let Err(err) = self
2014 .store_step_outputs(&config, step.id, name, execution.is_ok())
2015 .await
2016 {
2017 self.fail_step(step.id, &err).await;
2018 return Err(err);
2019 }
2020
2021 match execution {
2022 Ok(output) => {
2023 self.total_cost_usd += output.cost_usd;
2024 self.total_duration_ms += output.duration_ms;
2025
2026 if matches!(config, StepConfig::Agent(_)) {
2028 let tokens = output
2029 .input_tokens
2030 .unwrap_or(0)
2031 .saturating_add(output.output_tokens.unwrap_or(0));
2032 if tokens > 0 {
2033 self.guard_record_tokens(tokens)?;
2034 }
2035 }
2036
2037 let debug_messages_json = output.debug_messages_json();
2038
2039 let completed_at = Utc::now();
2040 self.store
2041 .update_step(
2042 step.id,
2043 StepUpdate {
2044 status: Some(StepStatus::Completed),
2045 output: Some(output.output.clone()),
2046 duration_ms: Some(output.duration_ms),
2047 cost_usd: Some(output.cost_usd),
2048 input_tokens: output.input_tokens,
2049 output_tokens: output.output_tokens,
2050 completed_at: Some(completed_at),
2051 debug_messages: debug_messages_json,
2052 ..StepUpdate::default()
2053 },
2054 )
2055 .await?;
2056
2057 self.step_results
2058 .push(StepResult::from_success(trace_id, name, &output));
2059 self.persist_progress().await;
2060
2061 info!(
2062 run_id = %self.run_id,
2063 step = %name,
2064 trace_id = %trace_id,
2065 duration_ms = output.duration_ms,
2066 "step completed"
2067 );
2068
2069 if let Some(ref bus) = self.event_bus {
2070 bus.publish(
2071 self.run_id,
2072 crate::notify::WorkflowEvent::StepCompleted {
2073 step_name: name.to_string(),
2074 step_index: position,
2075 duration_ms: output.duration_ms,
2076 output_summary: None,
2077 },
2078 );
2079
2080 if matches!(config, StepConfig::Agent(_)) {
2081 let tokens = output
2082 .input_tokens
2083 .unwrap_or(0)
2084 .saturating_add(output.output_tokens.unwrap_or(0));
2085 bus.publish(
2086 self.run_id,
2087 crate::notify::WorkflowEvent::AgentStepTokensUsed {
2088 step_name: name.to_string(),
2089 tokens,
2090 cost_usd: output.cost_usd,
2091 },
2092 );
2093 }
2094 }
2095
2096 self.last_step_ids = vec![step.id];
2097
2098 Ok(output)
2099 }
2100 Err(err) => {
2101 let completed_at = Utc::now();
2102 let debug_messages_json = extract_debug_messages_from_error(&err);
2103 let partial = extract_partial_usage_from_error(&err);
2104 let raw_response_output = extract_raw_response_from_error(&err);
2105
2106 if let Some(ref usage) = partial {
2107 if let Some(cost) = usage.cost_usd {
2108 self.total_cost_usd += cost;
2109 }
2110 if let Some(dur) = usage.duration_ms {
2111 self.total_duration_ms += dur;
2112 }
2113 }
2114
2115 if let Err(store_err) = self
2116 .store
2117 .update_step(
2118 step.id,
2119 StepUpdate {
2120 status: Some(StepStatus::Failed),
2121 error: Some(err.to_string()),
2122 output: raw_response_output.clone(),
2123 completed_at: Some(completed_at),
2124 debug_messages: debug_messages_json,
2125 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
2126 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
2127 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
2128 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
2129 ..StepUpdate::default()
2130 },
2131 )
2132 .await
2133 {
2134 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
2135 }
2136
2137 let err_duration = partial.as_ref().and_then(|p| p.duration_ms).unwrap_or(0);
2138 let err_cost = partial
2139 .as_ref()
2140 .and_then(|p| p.cost_usd)
2141 .unwrap_or(Decimal::ZERO);
2142 self.step_results.push(StepResult::from_failure(
2143 trace_id,
2144 name,
2145 &err.to_string(),
2146 err_duration,
2147 err_cost,
2148 ));
2149 self.persist_progress().await;
2150
2151 if let Some(ref bus) = self.event_bus {
2152 bus.publish(
2153 self.run_id,
2154 crate::notify::WorkflowEvent::StepFailed {
2155 step_name: name.to_string(),
2156 step_index: position,
2157 error: err.to_string(),
2158 duration_ms: err_duration,
2159 },
2160 );
2161 }
2162
2163 self.fire_error_handlers(name, &err.to_string(), err_duration)
2164 .await;
2165
2166 if config.allow_failure() {
2167 self.has_allowed_failure = true;
2168 self.last_step_ids = vec![step.id];
2169 info!(
2170 run_id = %self.run_id,
2171 step = %name,
2172 error = %err,
2173 "step failed but allow_failure is set, continuing"
2174 );
2175 Ok(allowed_failure_output(
2176 &err.to_string(),
2177 raw_response_output,
2178 partial.as_ref(),
2179 ))
2180 } else {
2181 Err(err)
2182 }
2183 }
2184 }
2185 }
2186
2187 #[cfg_attr(not(feature = "prometheus"), allow(unused_variables))]
2193 async fn retry_step_if_configured(
2194 &self,
2195 name: &str,
2196 kind_str: &str,
2197 config: &StepConfig,
2198 step_id: Uuid,
2199 first_result: Result<StepOutput, EngineError>,
2200 ) -> Result<StepOutput, EngineError> {
2201 let policy = match config.retry() {
2202 Some(p) => p,
2203 None => return first_result,
2204 };
2205
2206 let mut last_result = match first_result {
2207 Ok(output) => return Ok(output),
2208 Err(err) if !is_step_retryable(&err) => return Err(err),
2209 Err(err) => Err(err),
2210 };
2211
2212 let step_log_sender = self
2213 .log_sender
2214 .as_ref()
2215 .map(|s| StepLogSender::new(s.clone(), self.run_id, step_id, name.to_string()));
2216
2217 for attempt in 0..policy.max_retries() {
2218 if let StepConfig::Agent(agent_config) = config {
2219 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
2220 }
2221
2222 let delay = policy.delay_for_attempt(attempt);
2223 info!(
2224 run_id = %self.run_id,
2225 step = %name,
2226 attempt = attempt + 1,
2227 max_retries = policy.max_retries(),
2228 delay_ms = delay.as_millis() as u64,
2229 "retrying step after transient failure"
2230 );
2231 tokio::time::sleep(delay).await;
2232
2233 record_retry_metric(kind_str, "retry");
2234
2235 match execute_step_config(config, &self.provider, step_log_sender.clone()).await {
2236 Ok(output) => return Ok(output),
2237 Err(err) if !is_step_retryable(&err) => return Err(err),
2238 err => last_result = err,
2239 }
2240 }
2241
2242 record_retry_metric(kind_str, "exhausted");
2243
2244 info!(
2245 run_id = %self.run_id,
2246 step = %name,
2247 max_retries = policy.max_retries(),
2248 "step retries exhausted"
2249 );
2250
2251 last_result
2252 }
2253
2254 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
2259 if !self.last_step_ids.is_empty() {
2260 let deps: Vec<NewStepDependency> = self
2261 .last_step_ids
2262 .iter()
2263 .map(|&depends_on| NewStepDependency {
2264 step_id,
2265 depends_on,
2266 })
2267 .collect();
2268 self.store.create_step_dependencies(deps).await?;
2269 }
2270
2271 self.store
2272 .update_step(
2273 step_id,
2274 StepUpdate {
2275 status: Some(StepStatus::Running),
2276 started_at: Some(now),
2277 ..StepUpdate::default()
2278 },
2279 )
2280 .await?;
2281
2282 Ok(())
2283 }
2284
2285 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
2292 if let Err(store_err) = self
2293 .store
2294 .update_step(
2295 step_id,
2296 StepUpdate {
2297 status: Some(StepStatus::Failed),
2298 error: Some(err.to_string()),
2299 completed_at: Some(Utc::now()),
2300 ..StepUpdate::default()
2301 },
2302 )
2303 .await
2304 {
2305 error!(
2306 step_id = %step_id,
2307 error = %store_err,
2308 "failed to persist step failure"
2309 );
2310 }
2311 }
2312
2313 pub fn store(&self) -> &Arc<dyn Store> {
2315 &self.store
2316 }
2317
2318 pub async fn payload(&self) -> Result<Value, EngineError> {
2326 let run = self
2327 .store
2328 .get_run(self.run_id)
2329 .await?
2330 .ok_or(EngineError::Store(
2331 ironflow_store::error::StoreError::RunNotFound(self.run_id),
2332 ))?;
2333 Ok(run.payload)
2334 }
2335
2336 pub async fn input<T: serde::de::DeserializeOwned>(&self) -> Result<T, EngineError> {
2364 let payload = self.payload().await?;
2365 serde_json::from_value(payload).map_err(EngineError::Serialization)
2366 }
2367
2368 pub fn on_error(&mut self, name: &str, config: impl Into<StepConfig>) {
2391 self.error_handlers.push(OnErrorHandler {
2392 name: name.to_string(),
2393 config: config.into(),
2394 });
2395 }
2396
2397 pub fn clear_error_handlers(&mut self) {
2416 self.error_handlers.clear();
2417 }
2418
2419 async fn fire_error_handlers(
2425 &mut self,
2426 failed_step_name: &str,
2427 error_msg: &str,
2428 duration_ms: u64,
2429 ) {
2430 let handlers = std::mem::take(&mut self.error_handlers);
2431 if handlers.is_empty() {
2432 return;
2433 }
2434
2435 let error_context = json!({
2436 "failed_step": failed_step_name,
2437 "error": error_msg,
2438 "duration_ms": duration_ms,
2439 });
2440
2441 for handler in handlers {
2442 let mut config = handler.config.clone();
2443 inject_error_context(&mut config, failed_step_name, error_msg, duration_ms);
2444
2445 let position = self.position;
2446 self.position += 1;
2447
2448 let trace_id = step_trace_id(self.run_id, &handler.name, position);
2449 let step = match self
2450 .store
2451 .create_step(NewStep {
2452 run_id: self.run_id,
2453 trace_id,
2454 name: handler.name.clone(),
2455 kind: config.kind(),
2456 position,
2457 input: Some(error_context.clone()),
2458 is_error_handler: true,
2459 })
2460 .await
2461 {
2462 Ok(step) => step,
2463 Err(err) => {
2464 warn!(
2465 run_id = %self.run_id,
2466 handler = %handler.name,
2467 error = %err,
2468 "failed to create error handler step"
2469 );
2470 continue;
2471 }
2472 };
2473
2474 if let Err(err) = self.start_step(step.id, Utc::now()).await {
2475 warn!(
2476 run_id = %self.run_id,
2477 handler = %handler.name,
2478 error = %err,
2479 "failed to start error handler step"
2480 );
2481 continue;
2482 }
2483
2484 let step_log_sender = self
2485 .log_sender
2486 .as_ref()
2487 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, handler.name.clone()));
2488
2489 let start = Instant::now();
2490 let result = execute_step_config(&config, &self.provider, step_log_sender).await;
2491 let handler_duration = start.elapsed().as_millis() as u64;
2492 let completed_at = Utc::now();
2493
2494 match result {
2495 Ok(output) => {
2496 if let Err(store_err) = self
2497 .store
2498 .update_step(
2499 step.id,
2500 StepUpdate {
2501 status: Some(StepStatus::Completed),
2502 output: Some(output.output),
2503 duration_ms: Some(handler_duration),
2504 cost_usd: Some(output.cost_usd),
2505 completed_at: Some(completed_at),
2506 ..StepUpdate::default()
2507 },
2508 )
2509 .await
2510 {
2511 warn!(
2512 run_id = %self.run_id,
2513 handler = %handler.name,
2514 error = %store_err,
2515 "failed to persist error handler completion"
2516 );
2517 }
2518
2519 info!(
2520 run_id = %self.run_id,
2521 handler = %handler.name,
2522 duration_ms = handler_duration,
2523 "error handler completed"
2524 );
2525 }
2526 Err(err) => {
2527 if let Err(store_err) = self
2528 .store
2529 .update_step(
2530 step.id,
2531 StepUpdate {
2532 status: Some(StepStatus::Failed),
2533 error: Some(err.to_string()),
2534 duration_ms: Some(handler_duration),
2535 completed_at: Some(completed_at),
2536 ..StepUpdate::default()
2537 },
2538 )
2539 .await
2540 {
2541 warn!(
2542 run_id = %self.run_id,
2543 handler = %handler.name,
2544 error = %store_err,
2545 "failed to persist error handler failure"
2546 );
2547 }
2548
2549 warn!(
2550 run_id = %self.run_id,
2551 handler = %handler.name,
2552 error = %err,
2553 "error handler failed (original error preserved)"
2554 );
2555 }
2556 }
2557 }
2558 }
2559}
2560
2561fn inject_error_context(
2563 config: &mut StepConfig,
2564 failed_step: &str,
2565 error_msg: &str,
2566 duration_ms: u64,
2567) {
2568 match config {
2569 StepConfig::Shell(shell) => {
2570 shell
2571 .env
2572 .push(("IRONFLOW_ERROR_STEP".to_string(), failed_step.to_string()));
2573 shell
2574 .env
2575 .push(("IRONFLOW_ERROR_MESSAGE".to_string(), error_msg.to_string()));
2576 shell.env.push((
2577 "IRONFLOW_ERROR_DURATION_MS".to_string(),
2578 duration_ms.to_string(),
2579 ));
2580 }
2581 StepConfig::Agent(agent) => {
2582 agent.prompt = format!(
2583 "[Error Context]\nStep \"{}\" failed after {}ms:\n{}\n\n{}",
2584 failed_step, duration_ms, error_msg, agent.prompt
2585 );
2586 }
2587 StepConfig::Http(http) => {
2588 http.headers
2589 .push(("X-Ironflow-Error-Step".to_string(), failed_step.to_string()));
2590 http.headers.push((
2591 "X-Ironflow-Error-Message".to_string(),
2592 error_msg.to_string(),
2593 ));
2594 }
2595 StepConfig::Workflow(_) | StepConfig::Approval(_) => {}
2596 }
2597}
2598
2599#[cfg(feature = "prometheus")]
2600fn record_retry_metric(kind: &str, outcome: &str) {
2601 use ironflow_core::metric_names::STEP_RETRIES_TOTAL;
2602 use metrics::counter;
2603 counter!(STEP_RETRIES_TOTAL, "kind" => kind.to_string(), "outcome" => outcome.to_string())
2604 .increment(1);
2605}
2606
2607#[cfg(not(feature = "prometheus"))]
2608fn record_retry_metric(_kind: &str, _outcome: &str) {}
2609
2610fn is_step_retryable(err: &EngineError) -> bool {
2614 use ironflow_core::error::{AgentError, OperationError};
2615
2616 match err {
2617 EngineError::Operation(op) => match op {
2618 OperationError::Agent(AgentError::PromptTooLarge { .. }) => false,
2619 OperationError::Agent(AgentError::BudgetExceeded { .. }) => false,
2620 OperationError::Deserialize { .. } => false,
2621 OperationError::Http {
2622 status: Some(code), ..
2623 } if (400..500).contains(code) && *code != 429 => false,
2624 _ => true,
2625 },
2626 _ => false,
2627 }
2628}
2629
2630fn allowed_failure_output(
2631 error_msg: &str,
2632 raw_response: Option<Value>,
2633 partial: Option<&StepPartialUsage>,
2634) -> StepOutput {
2635 StepOutput {
2636 output: raw_response.unwrap_or_else(|| json!({"error": error_msg})),
2637 duration_ms: partial.and_then(|p| p.duration_ms).unwrap_or(0),
2638 cost_usd: partial.and_then(|p| p.cost_usd).unwrap_or(Decimal::ZERO),
2639 input_tokens: partial.and_then(|p| p.input_tokens),
2640 output_tokens: partial.and_then(|p| p.output_tokens),
2641 model: None,
2642 debug_messages: None,
2643 }
2644}
2645
2646impl fmt::Debug for WorkflowContext {
2647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2648 f.debug_struct("WorkflowContext")
2649 .field("run_id", &self.run_id)
2650 .field("position", &self.position)
2651 .field("total_cost_usd", &self.total_cost_usd)
2652 .field("inherited_cost_usd", &self.inherited_cost_usd)
2653 .field("max_cost_usd", &self.max_cost_usd)
2654 .finish_non_exhaustive()
2655 }
2656}
2657
2658fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
2661 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2662 debug_messages,
2663 ..
2664 })) = err
2665 && !debug_messages.is_empty()
2666 {
2667 return serde_json::to_value(debug_messages).ok();
2668 }
2669 None
2670}
2671
2672struct StepPartialUsage {
2678 cost_usd: Option<Decimal>,
2679 duration_ms: Option<u64>,
2680 input_tokens: Option<u64>,
2681 output_tokens: Option<u64>,
2682}
2683
2684fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
2690 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2691 raw_response: Some(text),
2692 ..
2693 })) = err
2694 {
2695 return Some(Value::String(text.clone()));
2696 }
2697 None
2698}
2699
2700fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
2701 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
2702 partial_usage,
2703 ..
2704 })) = err
2705 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
2706 {
2707 return Some(StepPartialUsage {
2708 cost_usd: partial_usage
2709 .cost_usd
2710 .and_then(|c| Decimal::try_from(c).ok()),
2711 duration_ms: partial_usage.duration_ms,
2712 input_tokens: partial_usage.input_tokens,
2713 output_tokens: partial_usage.output_tokens,
2714 });
2715 }
2716 None
2717}
2718
2719#[cfg(test)]
2720mod tests {
2721 use super::*;
2722 use ironflow_core::providers::claude::ClaudeCodeProvider;
2723 use ironflow_core::providers::record_replay::RecordReplayProvider;
2724 use ironflow_store::memory::InMemoryStore;
2725 use ironflow_store::models::{Run, RunActor, RunFilter};
2726 use ironflow_store::store::RunStore;
2727 use serde_json::json;
2728 use std::sync::Arc;
2729 use std::sync::atomic::{AtomicBool, Ordering};
2730 use uuid::Uuid;
2731
2732 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
2734 let inner = ClaudeCodeProvider::new();
2735 Arc::new(RecordReplayProvider::replay(
2736 inner,
2737 "/tmp/ironflow-fixtures",
2738 ))
2739 }
2740
2741 fn create_test_context() -> WorkflowContext {
2743 let store = Arc::new(InMemoryStore::new());
2744 let provider = create_test_provider();
2745 let run_id = Uuid::now_v7();
2746 WorkflowContext::new(run_id, store, provider)
2747 }
2748
2749 #[test]
2750 fn context_new_initializes_correctly() {
2751 let ctx = create_test_context();
2752 assert_eq!(ctx.position, 0);
2753 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
2754 assert_eq!(ctx.total_duration_ms, 0);
2755 assert!(ctx.last_step_ids.is_empty());
2756 assert!(ctx.replay_steps.is_empty());
2757 assert!(ctx.log_sender.is_none());
2758 }
2759
2760 #[test]
2761 fn context_run_id_returns_correct_id() {
2762 let run_id = Uuid::now_v7();
2763 let store = Arc::new(InMemoryStore::new());
2764 let provider = create_test_provider();
2765 let ctx = WorkflowContext::new(run_id, store, provider);
2766 assert_eq!(ctx.run_id(), run_id);
2767 }
2768
2769 #[test]
2770 fn context_total_cost_usd_initially_zero() {
2771 let ctx = create_test_context();
2772 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
2773 }
2774
2775 #[test]
2776 fn context_total_duration_ms_initially_zero() {
2777 let ctx = create_test_context();
2778 assert_eq!(ctx.total_duration_ms(), 0);
2779 }
2780
2781 #[test]
2782 fn context_with_handler_resolver_creates_context_with_resolver() {
2783 let store = Arc::new(InMemoryStore::new());
2784 let provider = create_test_provider();
2785 let run_id = Uuid::now_v7();
2786
2787 let called = Arc::new(AtomicBool::new(false));
2788 let called_clone = called.clone();
2789
2790 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
2791 called_clone.store(true, Ordering::SeqCst);
2792 None
2793 });
2794
2795 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
2796
2797 assert_eq!(ctx.run_id(), run_id);
2798 assert!(ctx.handler_resolver.is_some());
2799 }
2800
2801 #[tokio::test]
2802 async fn context_set_log_sender_attaches_sender() {
2803 let mut ctx = create_test_context();
2804 let (sender, _receiver) = crate::log_sender::channel();
2805 ctx.set_log_sender(sender);
2806 assert!(ctx.log_sender.is_some());
2807 }
2808
2809 #[tokio::test]
2810 async fn context_skip_creates_skipped_step() {
2811 let store = Arc::new(InMemoryStore::new());
2812 let provider = create_test_provider();
2813
2814 store
2816 .create_run(NewRun {
2817 created_by: None,
2818 workflow_name: "test".to_string(),
2819 trigger: TriggerKind::Manual,
2820 payload: json!({}),
2821 max_retries: 0,
2822 handler_version: None,
2823 labels: Default::default(),
2824 scheduled_at: None,
2825 idempotency_key: None,
2826 max_cost_usd: None,
2827 })
2828 .await
2829 .expect("failed to create run")
2830 .into_run();
2831
2832 let runs = store
2834 .list_runs(RunFilter::default(), 1, 10)
2835 .await
2836 .expect("failed to list runs");
2837 let created_run_id = runs.items[0].id;
2838
2839 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2840 let initial_position = ctx.position;
2841
2842 ctx.skip("skip-step", "condition not met")
2843 .await
2844 .expect("skip failed");
2845
2846 assert_eq!(ctx.position, initial_position + 1);
2847 assert!(!ctx.last_step_ids.is_empty());
2848
2849 let steps = store
2851 .list_steps(created_run_id)
2852 .await
2853 .expect("failed to list steps");
2854 assert_eq!(steps.len(), 1);
2855 assert_eq!(steps[0].status.state, StepStatus::Skipped);
2856 }
2857
2858 struct NoopSubWorkflow;
2861
2862 impl WorkflowHandler for NoopSubWorkflow {
2863 fn name(&self) -> &str {
2864 "noop-sub"
2865 }
2866
2867 fn execute<'a>(
2868 &'a self,
2869 _ctx: &'a mut WorkflowContext,
2870 ) -> crate::handler::HandlerFuture<'a> {
2871 Box::pin(async move { Ok(()) })
2872 }
2873 }
2874
2875 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
2878 let store = Arc::new(InMemoryStore::new());
2879 let provider = create_test_provider();
2880
2881 let parent = store
2882 .create_run(NewRun {
2883 workflow_name: "parent".to_string(),
2884 trigger: TriggerKind::Api,
2885 payload: json!({}),
2886 max_retries: 0,
2887 handler_version: None,
2888 labels: Default::default(),
2889 scheduled_at: None,
2890 created_by,
2891 idempotency_key: None,
2892 max_cost_usd: None,
2893 })
2894 .await
2895 .expect("failed to create parent run")
2896 .into_run();
2897
2898 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2899 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2900 _ => None,
2901 });
2902
2903 let mut ctx =
2904 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2905 ctx.workflow(&NoopSubWorkflow, json!({}))
2906 .await
2907 .expect("sub-workflow failed");
2908
2909 let runs = store
2910 .list_runs(RunFilter::default(), 1, 10)
2911 .await
2912 .expect("failed to list runs");
2913 runs.items
2914 .into_iter()
2915 .find(|r| r.workflow_name == "noop-sub")
2916 .expect("child run was created")
2917 }
2918
2919 #[tokio::test]
2920 async fn child_run_inherits_the_parent_author() {
2921 let user_id = Uuid::now_v7();
2922 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2923
2924 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2925 }
2926
2927 #[tokio::test]
2928 async fn child_run_of_an_unattributed_parent_has_no_author() {
2929 let child = child_run_of_parent_authored_by(None).await;
2930
2931 assert!(child.created_by.is_none());
2932 }
2933
2934 #[tokio::test]
2935 async fn context_parallel_empty_steps_returns_empty_vec() {
2936 let mut ctx = create_test_context();
2937 let results = ctx
2938 .parallel(vec![], true)
2939 .await
2940 .expect("parallel should not fail on empty input");
2941 assert!(results.is_empty());
2942 }
2943
2944 #[tokio::test]
2945 async fn context_approval_first_execution_returns_error() {
2946 let store = Arc::new(InMemoryStore::new());
2947 let provider = create_test_provider();
2948
2949 store
2951 .create_run(NewRun {
2952 created_by: None,
2953 workflow_name: "test".to_string(),
2954 trigger: TriggerKind::Manual,
2955 payload: json!({}),
2956 max_retries: 0,
2957 handler_version: None,
2958 labels: Default::default(),
2959 scheduled_at: None,
2960 idempotency_key: None,
2961 max_cost_usd: None,
2962 })
2963 .await
2964 .expect("failed to create run")
2965 .into_run();
2966
2967 let runs = store
2969 .list_runs(RunFilter::default(), 1, 10)
2970 .await
2971 .expect("failed to list runs");
2972 let created_run_id = runs.items[0].id;
2973
2974 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2975
2976 let result = ctx
2977 .approval(
2978 "approve-step",
2979 crate::config::ApprovalConfig::new("Continue?"),
2980 )
2981 .await;
2982
2983 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2985
2986 assert_eq!(ctx.position, 1);
2988
2989 let steps = store
2991 .list_steps(created_run_id)
2992 .await
2993 .expect("failed to list steps");
2994 assert_eq!(steps.len(), 1);
2995 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2996 }
2997
2998 #[tokio::test]
2999 async fn context_approval_replay_returns_ok() {
3000 let store = Arc::new(InMemoryStore::new());
3001 let provider = create_test_provider();
3002
3003 store
3005 .create_run(NewRun {
3006 created_by: None,
3007 workflow_name: "test".to_string(),
3008 trigger: TriggerKind::Manual,
3009 payload: json!({}),
3010 max_retries: 0,
3011 handler_version: None,
3012 labels: Default::default(),
3013 scheduled_at: None,
3014 idempotency_key: None,
3015 max_cost_usd: None,
3016 })
3017 .await
3018 .expect("failed to create run")
3019 .into_run();
3020
3021 let runs = store
3023 .list_runs(RunFilter::default(), 1, 10)
3024 .await
3025 .expect("failed to list runs");
3026 let created_run_id = runs.items[0].id;
3027
3028 let step = store
3030 .create_step(NewStep {
3031 run_id: created_run_id,
3032 trace_id: step_trace_id(created_run_id, "approval", 0),
3033 name: "approval".to_string(),
3034 kind: StepKind::Approval,
3035 position: 0,
3036 input: None,
3037 is_error_handler: false,
3038 })
3039 .await
3040 .expect("failed to create step");
3041
3042 store
3044 .update_step(
3045 step.id,
3046 StepUpdate {
3047 status: Some(StepStatus::Running),
3048 started_at: Some(Utc::now()),
3049 ..StepUpdate::default()
3050 },
3051 )
3052 .await
3053 .expect("failed to update step to Running");
3054
3055 store
3056 .update_step(
3057 step.id,
3058 StepUpdate {
3059 status: Some(StepStatus::AwaitingApproval),
3060 ..StepUpdate::default()
3061 },
3062 )
3063 .await
3064 .expect("failed to update step to AwaitingApproval");
3065
3066 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
3068 ctx.load_replay_steps()
3069 .await
3070 .expect("failed to load replay steps");
3071
3072 let result = ctx
3074 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
3075 .await;
3076
3077 assert!(result.is_ok());
3078
3079 let steps = store
3081 .list_steps(created_run_id)
3082 .await
3083 .expect("failed to list steps");
3084 assert_eq!(steps.len(), 1);
3085 assert_eq!(steps[0].status.state, StepStatus::Completed);
3086 }
3087
3088 #[tokio::test]
3089 async fn context_load_replay_steps_loads_completed_steps() {
3090 let store = Arc::new(InMemoryStore::new());
3091 let provider = create_test_provider();
3092
3093 store
3095 .create_run(NewRun {
3096 created_by: None,
3097 workflow_name: "test".to_string(),
3098 trigger: TriggerKind::Manual,
3099 payload: json!({}),
3100 max_retries: 0,
3101 handler_version: None,
3102 labels: Default::default(),
3103 scheduled_at: None,
3104 idempotency_key: None,
3105 max_cost_usd: None,
3106 })
3107 .await
3108 .expect("failed to create run")
3109 .into_run();
3110
3111 let runs = store
3113 .list_runs(RunFilter::default(), 1, 10)
3114 .await
3115 .expect("failed to list runs");
3116 let created_run_id = runs.items[0].id;
3117
3118 let completed_step = store
3120 .create_step(NewStep {
3121 run_id: created_run_id,
3122 trace_id: step_trace_id(created_run_id, "completed", 0),
3123 name: "completed".to_string(),
3124 kind: StepKind::Shell,
3125 position: 0,
3126 input: None,
3127 is_error_handler: false,
3128 })
3129 .await
3130 .expect("failed to create step");
3131
3132 store
3134 .update_step(
3135 completed_step.id,
3136 StepUpdate {
3137 status: Some(StepStatus::Running),
3138 started_at: Some(Utc::now()),
3139 ..StepUpdate::default()
3140 },
3141 )
3142 .await
3143 .expect("failed to update step to Running");
3144
3145 store
3146 .update_step(
3147 completed_step.id,
3148 StepUpdate {
3149 status: Some(StepStatus::Completed),
3150 completed_at: Some(Utc::now()),
3151 ..StepUpdate::default()
3152 },
3153 )
3154 .await
3155 .expect("failed to update step to Completed");
3156
3157 let _pending_step = store
3158 .create_step(NewStep {
3159 run_id: created_run_id,
3160 trace_id: step_trace_id(created_run_id, "pending", 1),
3161 name: "pending".to_string(),
3162 kind: StepKind::Shell,
3163 position: 1,
3164 input: None,
3165 is_error_handler: false,
3166 })
3167 .await
3168 .expect("failed to create step");
3169
3170 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
3172 ctx.load_replay_steps()
3173 .await
3174 .expect("failed to load replay steps");
3175
3176 assert_eq!(ctx.replay_steps.len(), 1);
3178 assert!(ctx.replay_steps.contains_key(&0));
3179 assert!(!ctx.replay_steps.contains_key(&1));
3180 }
3181
3182 #[tokio::test]
3183 async fn context_payload_returns_run_payload() {
3184 let store = Arc::new(InMemoryStore::new());
3185 let provider = create_test_provider();
3186 let test_payload = json!({"key": "value", "number": 42});
3187
3188 store
3190 .create_run(NewRun {
3191 created_by: None,
3192 workflow_name: "test".to_string(),
3193 trigger: TriggerKind::Manual,
3194 payload: test_payload.clone(),
3195 max_retries: 0,
3196 handler_version: None,
3197 labels: Default::default(),
3198 scheduled_at: None,
3199 idempotency_key: None,
3200 max_cost_usd: None,
3201 })
3202 .await
3203 .expect("failed to create run")
3204 .into_run();
3205
3206 let runs = store
3208 .list_runs(RunFilter::default(), 1, 10)
3209 .await
3210 .expect("failed to list runs");
3211 let created_run_id = runs.items[0].id;
3212
3213 let ctx = WorkflowContext::new(created_run_id, store, provider);
3214 let payload = ctx.payload().await.expect("failed to get payload");
3215
3216 assert_eq!(payload, test_payload);
3217 }
3218
3219 #[tokio::test]
3220 async fn context_payload_returns_error_for_nonexistent_run() {
3221 let store = Arc::new(InMemoryStore::new());
3222 let provider = create_test_provider();
3223 let run_id = Uuid::now_v7();
3224
3225 let ctx = WorkflowContext::new(run_id, store, provider);
3226 let result = ctx.payload().await;
3227
3228 assert!(result.is_err());
3229 }
3230
3231 #[tokio::test]
3232 async fn context_store_returns_reference() {
3233 let ctx = create_test_context();
3234 let _store = ctx.store();
3235 }
3237
3238 #[test]
3239 fn context_debug_formatting() {
3240 let ctx = create_test_context();
3241 let debug_str = format!("{:?}", ctx);
3242 assert!(debug_str.contains("WorkflowContext"));
3243 assert!(debug_str.contains("run_id"));
3244 }
3245
3246 #[tokio::test]
3247 async fn context_last_step_ids_tracks_executed_steps() {
3248 let store = Arc::new(InMemoryStore::new());
3249 let provider = create_test_provider();
3250
3251 store
3253 .create_run(NewRun {
3254 created_by: None,
3255 workflow_name: "test".to_string(),
3256 trigger: TriggerKind::Manual,
3257 payload: json!({}),
3258 max_retries: 0,
3259 handler_version: None,
3260 labels: Default::default(),
3261 scheduled_at: None,
3262 idempotency_key: None,
3263 max_cost_usd: None,
3264 })
3265 .await
3266 .expect("failed to create run")
3267 .into_run();
3268
3269 let runs = store
3271 .list_runs(RunFilter::default(), 1, 10)
3272 .await
3273 .expect("failed to list runs");
3274 let created_run_id = runs.items[0].id;
3275
3276 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
3277 assert!(ctx.last_step_ids.is_empty());
3278
3279 ctx.skip("step1", "reason").await.expect("skip failed");
3280
3281 assert_eq!(ctx.last_step_ids.len(), 1);
3282
3283 ctx.skip("step2", "reason").await.expect("skip failed");
3284
3285 assert_eq!(ctx.last_step_ids.len(), 1);
3287 }
3288}