1use std::collections::HashMap;
27use std::fmt;
28use std::sync::Arc;
29use std::time::Instant;
30
31use chrono::{DateTime, Utc};
32use rust_decimal::Decimal;
33use serde_json::{Value, json};
34use tokio::task::{Id, JoinSet};
35use tracing::{error, info};
36use uuid::Uuid;
37
38use ironflow_core::error::{AgentError, OperationError};
39use ironflow_core::provider::AgentProvider;
40use ironflow_store::models::{
41 NewRun, NewStep, NewStepDependency, RunStatus, RunUpdate, Step, StepKind, StepStatus,
42 StepUpdate, TriggerKind,
43};
44use ironflow_store::store::Store;
45
46use crate::budget::step_budget_usd;
47use crate::config::{
48 AgentStepConfig, ApprovalConfig, HttpConfig, ShellConfig, StepConfig, WorkflowStepConfig,
49};
50use crate::error::EngineError;
51use crate::executor::{ParallelStepResult, StepOutput, execute_step_config};
52use crate::handler::WorkflowHandler;
53use crate::log_sender::{LogSender, StepLogSender};
54use crate::operation::Operation;
55
56pub(crate) type HandlerResolver =
58 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
59
60pub struct WorkflowContext {
79 run_id: Uuid,
80 store: Arc<dyn Store>,
81 provider: Arc<dyn AgentProvider>,
82 handler_resolver: Option<HandlerResolver>,
83 position: u32,
84 last_step_ids: Vec<Uuid>,
86 total_cost_usd: Decimal,
88 total_duration_ms: u64,
90 max_cost_usd: Option<Decimal>,
92 inherited_cost_usd: Decimal,
95 replay_steps: HashMap<u32, Step>,
98 granted_approvals: HashMap<u32, u32>,
102 attempt: u32,
104 carried_duration_ms: u64,
107 log_sender: Option<LogSender>,
109}
110
111impl WorkflowContext {
112 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
117 Self {
118 run_id,
119 store,
120 provider,
121 handler_resolver: None,
122 position: 0,
123 last_step_ids: Vec::new(),
124 total_cost_usd: Decimal::ZERO,
125 total_duration_ms: 0,
126 max_cost_usd: None,
127 inherited_cost_usd: Decimal::ZERO,
128 replay_steps: HashMap::new(),
129 granted_approvals: HashMap::new(),
130 attempt: 1,
131 carried_duration_ms: 0,
132 log_sender: None,
133 }
134 }
135
136 pub(crate) fn with_handler_resolver(
141 run_id: Uuid,
142 store: Arc<dyn Store>,
143 provider: Arc<dyn AgentProvider>,
144 resolver: HandlerResolver,
145 ) -> Self {
146 Self {
147 run_id,
148 store,
149 provider,
150 handler_resolver: Some(resolver),
151 position: 0,
152 last_step_ids: Vec::new(),
153 total_cost_usd: Decimal::ZERO,
154 total_duration_ms: 0,
155 max_cost_usd: None,
156 inherited_cost_usd: Decimal::ZERO,
157 replay_steps: HashMap::new(),
158 granted_approvals: HashMap::new(),
159 attempt: 1,
160 carried_duration_ms: 0,
161 log_sender: None,
162 }
163 }
164
165 pub fn set_log_sender(&mut self, sender: LogSender) {
167 self.log_sender = Some(sender);
168 }
169
170 pub(crate) fn carry_over_run_totals(
177 &mut self,
178 attempt: u32,
179 cost_usd: Decimal,
180 duration_ms: u64,
181 ) {
182 self.attempt = attempt;
183 self.total_cost_usd = cost_usd;
184 self.carried_duration_ms = duration_ms;
185 }
186
187 pub(crate) fn carried_duration_ms(&self) -> u64 {
189 self.carried_duration_ms
190 }
191
192 pub fn attempt(&self) -> u32 {
194 self.attempt
195 }
196
197 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
213 self.max_cost_usd = cap;
214 }
215
216 pub fn max_cost_usd(&self) -> Option<Decimal> {
218 self.max_cost_usd
219 }
220
221 pub fn charged_cost_usd(&self) -> Decimal {
226 self.inherited_cost_usd + self.total_cost_usd
227 }
228
229 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
240 let Some(limit) = self.max_cost_usd else {
241 return Ok(());
242 };
243
244 let spent = self.charged_cost_usd();
245 if spent + step_budget <= limit {
246 return Ok(());
247 }
248
249 error!(
250 run_id = %self.run_id,
251 limit_usd = %limit,
252 spent_usd = %spent,
253 step_budget_usd = %step_budget,
254 "run cost cap reached, refusing agent step"
255 );
256
257 Err(EngineError::RunBudgetExceeded {
258 run_id: self.run_id,
259 limit_usd: limit,
260 spent_usd: spent,
261 step_budget_usd: step_budget,
262 })
263 }
264
265 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
277 let steps = self.store.list_steps(self.run_id).await?;
278 for step in steps {
279 let dominated = matches!(
280 step.status.state,
281 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
282 );
283 if !dominated {
284 continue;
285 }
286
287 if step.attempt == self.attempt {
288 self.replay_steps.insert(step.position, step);
289 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
290 {
291 self.granted_approvals.insert(step.position, step.attempt);
292 }
293 }
294 Ok(())
295 }
296
297 pub fn run_id(&self) -> Uuid {
299 self.run_id
300 }
301
302 pub fn total_cost_usd(&self) -> Decimal {
304 self.total_cost_usd
305 }
306
307 pub fn total_duration_ms(&self) -> u64 {
309 self.total_duration_ms
310 }
311
312 pub async fn parallel(
349 &mut self,
350 steps: Vec<(&str, StepConfig)>,
351 fail_fast: bool,
352 ) -> Result<Vec<ParallelStepResult>, EngineError> {
353 if steps.is_empty() {
354 return Ok(Vec::new());
355 }
356
357 let wave_budget: Decimal = steps
360 .iter()
361 .filter_map(|(_, config)| match config {
362 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
363 _ => None,
364 })
365 .map(step_budget_usd)
366 .sum();
367 self.check_run_budget(wave_budget)?;
368
369 let wave_position = self.position;
370 self.position += 1;
371
372 let now = Utc::now();
373 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
374
375 for (name, config) in &steps {
376 let kind = config.kind();
377 let step = self
378 .store
379 .create_step(NewStep {
380 run_id: self.run_id,
381 name: name.to_string(),
382 kind,
383 position: wave_position,
384 input: Some(serde_json::to_value(config)?),
385 })
386 .await?;
387
388 self.start_step(step.id, now).await?;
389
390 step_records.push((step.id, name.to_string(), config.clone()));
391 }
392
393 let mut join_set = JoinSet::new();
394 let mut task_index: HashMap<Id, usize> = HashMap::new();
395 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
396 let provider = self.provider.clone();
397 let config = config.clone();
398 let step_log_sender = self
399 .log_sender
400 .as_ref()
401 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
402 let handle = join_set.spawn(async move {
403 (
404 idx,
405 execute_step_config(&config, &provider, step_log_sender).await,
406 )
407 });
408 task_index.insert(handle.id(), idx);
409 }
410
411 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
413 vec![None; step_records.len()];
414 let mut first_error: Option<EngineError> = None;
415
416 while let Some(join_result) = join_set.join_next().await {
417 let (idx, step_result) = match join_result {
418 Ok(r) => r,
419 Err(e) => {
420 let error_msg = format!("join error: {e}");
421 if let Some(&idx) = task_index.get(&e.id()) {
422 let (step_id, step_name, _) = &step_records[idx];
423 let completed_at = Utc::now();
424 error!(
425 run_id = %self.run_id,
426 step = %step_name,
427 error = %error_msg,
428 "parallel step panicked or was cancelled"
429 );
430 if let Err(store_err) = self
431 .store
432 .update_step(
433 *step_id,
434 StepUpdate {
435 status: Some(StepStatus::Failed),
436 error: Some(error_msg.clone()),
437 completed_at: Some(completed_at),
438 ..StepUpdate::default()
439 },
440 )
441 .await
442 {
443 error!(
444 run_id = %self.run_id,
445 step_id = %step_id,
446 error = %store_err,
447 "failed to persist JoinError for step"
448 );
449 }
450 indexed_results[idx] = Some(Err(error_msg.clone()));
451 }
452 if first_error.is_none() {
453 first_error = Some(EngineError::StepConfig(error_msg));
454 }
455 if fail_fast {
456 join_set.abort_all();
457 }
458 continue;
459 }
460 };
461
462 let (step_id, step_name, _) = &step_records[idx];
463 let completed_at = Utc::now();
464
465 match step_result {
466 Ok(output) => {
467 self.total_cost_usd += output.cost_usd;
468 self.total_duration_ms += output.duration_ms;
469
470 let debug_messages_json = output.debug_messages_json();
471
472 self.store
473 .update_step(
474 *step_id,
475 StepUpdate {
476 status: Some(StepStatus::Completed),
477 output: Some(output.output.clone()),
478 duration_ms: Some(output.duration_ms),
479 cost_usd: Some(output.cost_usd),
480 input_tokens: output.input_tokens,
481 output_tokens: output.output_tokens,
482 completed_at: Some(completed_at),
483 debug_messages: debug_messages_json,
484 ..StepUpdate::default()
485 },
486 )
487 .await?;
488
489 info!(
490 run_id = %self.run_id,
491 step = %step_name,
492 duration_ms = output.duration_ms,
493 "parallel step completed"
494 );
495
496 indexed_results[idx] = Some(Ok(output));
497 }
498 Err(err) => {
499 let err_msg = err.to_string();
500 let debug_messages_json = extract_debug_messages_from_error(&err);
501 let partial = extract_partial_usage_from_error(&err);
502 let raw_response_output = extract_raw_response_from_error(&err);
503
504 if let Some(ref usage) = partial {
505 if let Some(cost) = usage.cost_usd {
506 self.total_cost_usd += cost;
507 }
508 if let Some(dur) = usage.duration_ms {
509 self.total_duration_ms += dur;
510 }
511 }
512
513 if let Err(store_err) = self
514 .store
515 .update_step(
516 *step_id,
517 StepUpdate {
518 status: Some(StepStatus::Failed),
519 error: Some(err_msg.clone()),
520 output: raw_response_output,
521 completed_at: Some(completed_at),
522 debug_messages: debug_messages_json,
523 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
524 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
525 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
526 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
527 ..StepUpdate::default()
528 },
529 )
530 .await
531 {
532 tracing::error!(
533 step_id = %step_id,
534 error = %store_err,
535 "failed to persist parallel step failure"
536 );
537 }
538
539 indexed_results[idx] = Some(Err(err_msg.clone()));
540
541 if first_error.is_none() {
542 first_error = Some(err);
543 }
544
545 if fail_fast {
546 join_set.abort_all();
547 }
548 }
549 }
550 }
551
552 if let Some(err) = first_error {
553 return Err(err);
554 }
555
556 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
557
558 let results: Vec<ParallelStepResult> = step_records
560 .iter()
561 .enumerate()
562 .map(|(idx, (step_id, name, _))| {
563 let output = match indexed_results[idx].take() {
564 Some(Ok(o)) => o,
565 _ => unreachable!("all steps succeeded if no error returned"),
566 };
567 ParallelStepResult {
568 name: name.clone(),
569 output,
570 step_id: *step_id,
571 }
572 })
573 .collect();
574
575 Ok(results)
576 }
577
578 pub async fn shell(
601 &mut self,
602 name: &str,
603 config: ShellConfig,
604 ) -> Result<StepOutput, EngineError> {
605 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
606 .await
607 }
608
609 pub async fn http(
629 &mut self,
630 name: &str,
631 config: HttpConfig,
632 ) -> Result<StepOutput, EngineError> {
633 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
634 .await
635 }
636
637 pub async fn agent(
657 &mut self,
658 name: &str,
659 config: impl Into<AgentStepConfig>,
660 ) -> Result<StepOutput, EngineError> {
661 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
662 .await
663 }
664
665 pub async fn approval(
696 &mut self,
697 name: &str,
698 config: ApprovalConfig,
699 ) -> Result<(), EngineError> {
700 let position = self.position;
701 self.position += 1;
702
703 if let Some(existing) = self.replay_steps.get(&position)
706 && existing.kind == StepKind::Approval
707 {
708 if existing.status.state == StepStatus::AwaitingApproval {
709 self.store
710 .update_step(
711 existing.id,
712 StepUpdate {
713 status: Some(StepStatus::Completed),
714 completed_at: Some(Utc::now()),
715 ..StepUpdate::default()
716 },
717 )
718 .await?;
719 }
720
721 self.last_step_ids = vec![existing.id];
722 info!(
723 run_id = %self.run_id,
724 step = %name,
725 position,
726 "approval step replayed (approved)"
727 );
728 return Ok(());
729 }
730
731 if let Some(&granted_in) = self.granted_approvals.get(&position) {
735 let step = self
736 .store
737 .create_step(NewStep {
738 run_id: self.run_id,
739 name: name.to_string(),
740 kind: StepKind::Approval,
741 position,
742 input: Some(serde_json::to_value(&config)?),
743 })
744 .await?;
745
746 let now = Utc::now();
747 self.start_step(step.id, now).await?;
748 self.store
749 .update_step(
750 step.id,
751 StepUpdate {
752 status: Some(StepStatus::Completed),
753 output: Some(json!({"approved_in_attempt": granted_in})),
754 completed_at: Some(now),
755 ..StepUpdate::default()
756 },
757 )
758 .await?;
759
760 self.last_step_ids = vec![step.id];
761 info!(
762 run_id = %self.run_id,
763 step = %name,
764 position,
765 granted_in_attempt = granted_in,
766 attempt = self.attempt,
767 "approval carried over from a previous attempt"
768 );
769 return Ok(());
770 }
771
772 let step = self
774 .store
775 .create_step(NewStep {
776 run_id: self.run_id,
777 name: name.to_string(),
778 kind: StepKind::Approval,
779 position,
780 input: Some(serde_json::to_value(&config)?),
781 })
782 .await?;
783
784 self.start_step(step.id, Utc::now()).await?;
785
786 self.store
789 .update_step(
790 step.id,
791 StepUpdate {
792 status: Some(StepStatus::AwaitingApproval),
793 ..StepUpdate::default()
794 },
795 )
796 .await?;
797
798 self.last_step_ids = vec![step.id];
799
800 Err(EngineError::ApprovalRequired {
801 run_id: self.run_id,
802 step_id: step.id,
803 message: config.message().to_string(),
804 })
805 }
806
807 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
836 let position = self.position;
837 self.position += 1;
838
839 let step = self
840 .store
841 .create_step(NewStep {
842 run_id: self.run_id,
843 name: name.to_string(),
844 kind: StepKind::Custom("skip".to_string()),
845 position,
846 input: None,
847 })
848 .await?;
849
850 if !self.last_step_ids.is_empty() {
851 let deps: Vec<NewStepDependency> = self
852 .last_step_ids
853 .iter()
854 .map(|&depends_on| NewStepDependency {
855 step_id: step.id,
856 depends_on,
857 })
858 .collect();
859 self.store.create_step_dependencies(deps).await?;
860 }
861
862 let now = Utc::now();
863 self.store
864 .update_step(
865 step.id,
866 StepUpdate {
867 status: Some(StepStatus::Skipped),
868 output: Some(serde_json::json!({"reason": reason})),
869 completed_at: Some(now),
870 ..StepUpdate::default()
871 },
872 )
873 .await?;
874
875 self.last_step_ids = vec![step.id];
876
877 info!(
878 run_id = %self.run_id,
879 step = %name,
880 reason,
881 "step skipped"
882 );
883
884 Ok(())
885 }
886
887 pub async fn operation(
925 &mut self,
926 name: &str,
927 op: &dyn Operation,
928 ) -> Result<StepOutput, EngineError> {
929 let kind = StepKind::Custom(op.kind().to_string());
930 let position = self.position;
931 self.position += 1;
932
933 let step = self
934 .store
935 .create_step(NewStep {
936 run_id: self.run_id,
937 name: name.to_string(),
938 kind,
939 position,
940 input: op.input(),
941 })
942 .await?;
943
944 self.start_step(step.id, Utc::now()).await?;
945
946 let start = Instant::now();
947
948 match op.execute().await {
949 Ok(output_value) => {
950 let duration_ms = start.elapsed().as_millis() as u64;
951 self.total_duration_ms += duration_ms;
952
953 let completed_at = Utc::now();
954 self.store
955 .update_step(
956 step.id,
957 StepUpdate {
958 status: Some(StepStatus::Completed),
959 output: Some(output_value.clone()),
960 duration_ms: Some(duration_ms),
961 cost_usd: Some(Decimal::ZERO),
962 completed_at: Some(completed_at),
963 ..StepUpdate::default()
964 },
965 )
966 .await?;
967
968 info!(
969 run_id = %self.run_id,
970 step = %name,
971 kind = op.kind(),
972 duration_ms,
973 "operation step completed"
974 );
975
976 self.last_step_ids = vec![step.id];
977
978 Ok(StepOutput {
979 output: output_value,
980 duration_ms,
981 cost_usd: Decimal::ZERO,
982 input_tokens: None,
983 output_tokens: None,
984 model: None,
985 debug_messages: None,
986 })
987 }
988 Err(err) => {
989 let completed_at = Utc::now();
990 if let Err(store_err) = self
991 .store
992 .update_step(
993 step.id,
994 StepUpdate {
995 status: Some(StepStatus::Failed),
996 error: Some(err.to_string()),
997 completed_at: Some(completed_at),
998 ..StepUpdate::default()
999 },
1000 )
1001 .await
1002 {
1003 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1004 }
1005
1006 Err(err)
1007 }
1008 }
1009 }
1010
1011 pub async fn workflow(
1038 &mut self,
1039 handler: &dyn WorkflowHandler,
1040 payload: Value,
1041 ) -> Result<StepOutput, EngineError> {
1042 let config = WorkflowStepConfig::new(handler.name(), payload);
1043 let position = self.position;
1044 self.position += 1;
1045
1046 let step = self
1047 .store
1048 .create_step(NewStep {
1049 run_id: self.run_id,
1050 name: config.workflow_name.clone(),
1051 kind: StepKind::Workflow,
1052 position,
1053 input: Some(serde_json::to_value(&config)?),
1054 })
1055 .await?;
1056
1057 self.start_step(step.id, Utc::now()).await?;
1058
1059 match self.execute_child_workflow(&config).await {
1060 Ok(output) => {
1061 self.total_cost_usd += output.cost_usd;
1062 self.total_duration_ms += output.duration_ms;
1063
1064 let completed_at = Utc::now();
1065 self.store
1066 .update_step(
1067 step.id,
1068 StepUpdate {
1069 status: Some(StepStatus::Completed),
1070 output: Some(output.output.clone()),
1071 duration_ms: Some(output.duration_ms),
1072 cost_usd: Some(output.cost_usd),
1073 completed_at: Some(completed_at),
1074 ..StepUpdate::default()
1075 },
1076 )
1077 .await?;
1078
1079 info!(
1080 run_id = %self.run_id,
1081 child_workflow = %config.workflow_name,
1082 duration_ms = output.duration_ms,
1083 "workflow step completed"
1084 );
1085
1086 self.last_step_ids = vec![step.id];
1087
1088 Ok(output)
1089 }
1090 Err(err) => {
1091 let completed_at = Utc::now();
1092 if let Err(store_err) = self
1093 .store
1094 .update_step(
1095 step.id,
1096 StepUpdate {
1097 status: Some(StepStatus::Failed),
1098 error: Some(err.to_string()),
1099 completed_at: Some(completed_at),
1100 ..StepUpdate::default()
1101 },
1102 )
1103 .await
1104 {
1105 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1106 }
1107
1108 Err(err)
1109 }
1110 }
1111 }
1112
1113 async fn execute_child_workflow(
1115 &self,
1116 config: &WorkflowStepConfig,
1117 ) -> Result<StepOutput, EngineError> {
1118 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1119 EngineError::InvalidWorkflow(
1120 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1121 )
1122 })?;
1123
1124 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1125 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1126 })?;
1127
1128 let parent = self.store.get_run(self.run_id).await?;
1131 let (parent_labels, parent_author) =
1132 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1133
1134 let child_run = self
1135 .store
1136 .create_run(NewRun {
1137 workflow_name: config.workflow_name.clone(),
1138 trigger: TriggerKind::Workflow,
1139 payload: config.payload.clone(),
1140 max_retries: 0,
1141 handler_version: None,
1142 labels: parent_labels,
1143 scheduled_at: None,
1144 created_by: parent_author,
1145 idempotency_key: None,
1146 max_cost_usd: self.max_cost_usd,
1148 })
1149 .await?
1150 .into_run();
1151
1152 let child_run_id = child_run.id;
1153 info!(
1154 parent_run_id = %self.run_id,
1155 child_run_id = %child_run_id,
1156 workflow = %config.workflow_name,
1157 "child run created"
1158 );
1159
1160 self.store
1161 .update_run_status(child_run_id, RunStatus::Running)
1162 .await?;
1163
1164 let run_start = Instant::now();
1165 let mut child_ctx = WorkflowContext {
1166 run_id: child_run_id,
1167 store: self.store.clone(),
1168 provider: self.provider.clone(),
1169 handler_resolver: self.handler_resolver.clone(),
1170 position: 0,
1171 last_step_ids: Vec::new(),
1172 total_cost_usd: Decimal::ZERO,
1173 total_duration_ms: 0,
1174 max_cost_usd: self.max_cost_usd,
1175 inherited_cost_usd: self.charged_cost_usd(),
1178 replay_steps: HashMap::new(),
1179 granted_approvals: HashMap::new(),
1180 attempt: 1,
1182 carried_duration_ms: 0,
1183 log_sender: self.log_sender.clone(),
1184 };
1185
1186 let result = handler.execute(&mut child_ctx).await;
1187 let total_duration = run_start.elapsed().as_millis() as u64;
1188 let completed_at = Utc::now();
1189
1190 match result {
1191 Ok(()) => {
1192 self.store
1193 .update_run(
1194 child_run_id,
1195 RunUpdate {
1196 status: Some(RunStatus::Completed),
1197 cost_usd: Some(child_ctx.total_cost_usd),
1198 duration_ms: Some(total_duration),
1199 completed_at: Some(completed_at),
1200 ..RunUpdate::default()
1201 },
1202 )
1203 .await?;
1204
1205 Ok(StepOutput {
1206 output: serde_json::json!({
1207 "run_id": child_run_id,
1208 "workflow_name": config.workflow_name,
1209 "status": RunStatus::Completed,
1210 "cost_usd": child_ctx.total_cost_usd,
1211 "duration_ms": total_duration,
1212 }),
1213 duration_ms: total_duration,
1214 cost_usd: child_ctx.total_cost_usd,
1215 input_tokens: None,
1216 output_tokens: None,
1217 model: None,
1218 debug_messages: None,
1219 })
1220 }
1221 Err(err) => {
1222 if let Err(store_err) = self
1223 .store
1224 .update_run(
1225 child_run_id,
1226 RunUpdate {
1227 status: Some(RunStatus::Failed),
1228 error: Some(err.to_string()),
1229 cost_usd: Some(child_ctx.total_cost_usd),
1230 duration_ms: Some(total_duration),
1231 completed_at: Some(completed_at),
1232 ..RunUpdate::default()
1233 },
1234 )
1235 .await
1236 {
1237 error!(
1238 child_run_id = %child_run_id,
1239 store_error = %store_err,
1240 "failed to persist child run failure"
1241 );
1242 }
1243
1244 Err(err)
1245 }
1246 }
1247 }
1248
1249 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1254 let step = self.replay_steps.get(&position)?;
1255 if step.status.state != StepStatus::Completed {
1256 return None;
1257 }
1258 let output = StepOutput {
1259 output: step.output.clone().unwrap_or(Value::Null),
1260 duration_ms: step.duration_ms,
1261 cost_usd: step.cost_usd,
1262 input_tokens: step.input_tokens,
1263 output_tokens: step.output_tokens,
1264 model: None,
1265 debug_messages: None,
1266 };
1267 self.total_cost_usd += output.cost_usd;
1268 self.total_duration_ms += output.duration_ms;
1269 self.last_step_ids = vec![step.id];
1270 info!(
1271 run_id = %self.run_id,
1272 step = %step.name,
1273 position,
1274 "step replayed from previous execution"
1275 );
1276 Some(output)
1277 }
1278
1279 async fn execute_step(
1281 &mut self,
1282 name: &str,
1283 kind: StepKind,
1284 config: StepConfig,
1285 ) -> Result<StepOutput, EngineError> {
1286 let position = self.position;
1287 self.position += 1;
1288
1289 if let Some(output) = self.try_replay_step(position) {
1291 return Ok(output);
1292 }
1293
1294 if let StepConfig::Agent(ref agent_config) = config {
1297 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1298 }
1299
1300 let step = self
1302 .store
1303 .create_step(NewStep {
1304 run_id: self.run_id,
1305 name: name.to_string(),
1306 kind,
1307 position,
1308 input: Some(serde_json::to_value(&config)?),
1309 })
1310 .await?;
1311
1312 self.start_step(step.id, Utc::now()).await?;
1313
1314 let step_log_sender = self
1315 .log_sender
1316 .as_ref()
1317 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1318
1319 match execute_step_config(&config, &self.provider, step_log_sender).await {
1320 Ok(output) => {
1321 self.total_cost_usd += output.cost_usd;
1322 self.total_duration_ms += output.duration_ms;
1323
1324 let debug_messages_json = output.debug_messages_json();
1325
1326 let completed_at = Utc::now();
1327 self.store
1328 .update_step(
1329 step.id,
1330 StepUpdate {
1331 status: Some(StepStatus::Completed),
1332 output: Some(output.output.clone()),
1333 duration_ms: Some(output.duration_ms),
1334 cost_usd: Some(output.cost_usd),
1335 input_tokens: output.input_tokens,
1336 output_tokens: output.output_tokens,
1337 completed_at: Some(completed_at),
1338 debug_messages: debug_messages_json,
1339 ..StepUpdate::default()
1340 },
1341 )
1342 .await?;
1343
1344 info!(
1345 run_id = %self.run_id,
1346 step = %name,
1347 duration_ms = output.duration_ms,
1348 "step completed"
1349 );
1350
1351 self.last_step_ids = vec![step.id];
1352
1353 Ok(output)
1354 }
1355 Err(err) => {
1356 let completed_at = Utc::now();
1357 let debug_messages_json = extract_debug_messages_from_error(&err);
1358 let partial = extract_partial_usage_from_error(&err);
1359 let raw_response_output = extract_raw_response_from_error(&err);
1360
1361 if let Some(ref usage) = partial {
1362 if let Some(cost) = usage.cost_usd {
1363 self.total_cost_usd += cost;
1364 }
1365 if let Some(dur) = usage.duration_ms {
1366 self.total_duration_ms += dur;
1367 }
1368 }
1369
1370 if let Err(store_err) = self
1371 .store
1372 .update_step(
1373 step.id,
1374 StepUpdate {
1375 status: Some(StepStatus::Failed),
1376 error: Some(err.to_string()),
1377 output: raw_response_output,
1378 completed_at: Some(completed_at),
1379 debug_messages: debug_messages_json,
1380 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1381 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1382 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1383 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1384 ..StepUpdate::default()
1385 },
1386 )
1387 .await
1388 {
1389 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1390 }
1391
1392 Err(err)
1393 }
1394 }
1395 }
1396
1397 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1402 if !self.last_step_ids.is_empty() {
1403 let deps: Vec<NewStepDependency> = self
1404 .last_step_ids
1405 .iter()
1406 .map(|&depends_on| NewStepDependency {
1407 step_id,
1408 depends_on,
1409 })
1410 .collect();
1411 self.store.create_step_dependencies(deps).await?;
1412 }
1413
1414 self.store
1415 .update_step(
1416 step_id,
1417 StepUpdate {
1418 status: Some(StepStatus::Running),
1419 started_at: Some(now),
1420 ..StepUpdate::default()
1421 },
1422 )
1423 .await?;
1424
1425 Ok(())
1426 }
1427
1428 pub fn store(&self) -> &Arc<dyn Store> {
1430 &self.store
1431 }
1432
1433 pub async fn payload(&self) -> Result<Value, EngineError> {
1441 let run = self
1442 .store
1443 .get_run(self.run_id)
1444 .await?
1445 .ok_or(EngineError::Store(
1446 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1447 ))?;
1448 Ok(run.payload)
1449 }
1450}
1451
1452impl fmt::Debug for WorkflowContext {
1453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1454 f.debug_struct("WorkflowContext")
1455 .field("run_id", &self.run_id)
1456 .field("position", &self.position)
1457 .field("total_cost_usd", &self.total_cost_usd)
1458 .field("inherited_cost_usd", &self.inherited_cost_usd)
1459 .field("max_cost_usd", &self.max_cost_usd)
1460 .finish_non_exhaustive()
1461 }
1462}
1463
1464fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
1467 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1468 debug_messages,
1469 ..
1470 })) = err
1471 && !debug_messages.is_empty()
1472 {
1473 return serde_json::to_value(debug_messages).ok();
1474 }
1475 None
1476}
1477
1478struct StepPartialUsage {
1484 cost_usd: Option<Decimal>,
1485 duration_ms: Option<u64>,
1486 input_tokens: Option<u64>,
1487 output_tokens: Option<u64>,
1488}
1489
1490fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
1496 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1497 raw_response: Some(text),
1498 ..
1499 })) = err
1500 {
1501 return Some(Value::String(text.clone()));
1502 }
1503 None
1504}
1505
1506fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
1507 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1508 partial_usage,
1509 ..
1510 })) = err
1511 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
1512 {
1513 return Some(StepPartialUsage {
1514 cost_usd: partial_usage
1515 .cost_usd
1516 .and_then(|c| Decimal::try_from(c).ok()),
1517 duration_ms: partial_usage.duration_ms,
1518 input_tokens: partial_usage.input_tokens,
1519 output_tokens: partial_usage.output_tokens,
1520 });
1521 }
1522 None
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527 use super::*;
1528 use ironflow_core::providers::claude::ClaudeCodeProvider;
1529 use ironflow_core::providers::record_replay::RecordReplayProvider;
1530 use ironflow_store::memory::InMemoryStore;
1531 use ironflow_store::models::{Run, RunActor, RunFilter};
1532 use ironflow_store::store::RunStore;
1533 use serde_json::json;
1534 use std::sync::Arc;
1535 use std::sync::atomic::{AtomicBool, Ordering};
1536 use uuid::Uuid;
1537
1538 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
1540 let inner = ClaudeCodeProvider::new();
1541 Arc::new(RecordReplayProvider::replay(
1542 inner,
1543 "/tmp/ironflow-fixtures",
1544 ))
1545 }
1546
1547 fn create_test_context() -> WorkflowContext {
1549 let store = Arc::new(InMemoryStore::new());
1550 let provider = create_test_provider();
1551 let run_id = Uuid::now_v7();
1552 WorkflowContext::new(run_id, store, provider)
1553 }
1554
1555 #[test]
1556 fn context_new_initializes_correctly() {
1557 let ctx = create_test_context();
1558 assert_eq!(ctx.position, 0);
1559 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
1560 assert_eq!(ctx.total_duration_ms, 0);
1561 assert!(ctx.last_step_ids.is_empty());
1562 assert!(ctx.replay_steps.is_empty());
1563 assert!(ctx.log_sender.is_none());
1564 }
1565
1566 #[test]
1567 fn context_run_id_returns_correct_id() {
1568 let run_id = Uuid::now_v7();
1569 let store = Arc::new(InMemoryStore::new());
1570 let provider = create_test_provider();
1571 let ctx = WorkflowContext::new(run_id, store, provider);
1572 assert_eq!(ctx.run_id(), run_id);
1573 }
1574
1575 #[test]
1576 fn context_total_cost_usd_initially_zero() {
1577 let ctx = create_test_context();
1578 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
1579 }
1580
1581 #[test]
1582 fn context_total_duration_ms_initially_zero() {
1583 let ctx = create_test_context();
1584 assert_eq!(ctx.total_duration_ms(), 0);
1585 }
1586
1587 #[test]
1588 fn context_with_handler_resolver_creates_context_with_resolver() {
1589 let store = Arc::new(InMemoryStore::new());
1590 let provider = create_test_provider();
1591 let run_id = Uuid::now_v7();
1592
1593 let called = Arc::new(AtomicBool::new(false));
1594 let called_clone = called.clone();
1595
1596 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
1597 called_clone.store(true, Ordering::SeqCst);
1598 None
1599 });
1600
1601 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
1602
1603 assert_eq!(ctx.run_id(), run_id);
1604 assert!(ctx.handler_resolver.is_some());
1605 }
1606
1607 #[tokio::test]
1608 async fn context_set_log_sender_attaches_sender() {
1609 let mut ctx = create_test_context();
1610 let (sender, _receiver) = crate::log_sender::channel();
1611 ctx.set_log_sender(sender);
1612 assert!(ctx.log_sender.is_some());
1613 }
1614
1615 #[tokio::test]
1616 async fn context_skip_creates_skipped_step() {
1617 let store = Arc::new(InMemoryStore::new());
1618 let provider = create_test_provider();
1619
1620 store
1622 .create_run(NewRun {
1623 created_by: None,
1624 workflow_name: "test".to_string(),
1625 trigger: TriggerKind::Manual,
1626 payload: json!({}),
1627 max_retries: 0,
1628 handler_version: None,
1629 labels: Default::default(),
1630 scheduled_at: None,
1631 idempotency_key: None,
1632 max_cost_usd: None,
1633 })
1634 .await
1635 .expect("failed to create run")
1636 .into_run();
1637
1638 let runs = store
1640 .list_runs(RunFilter::default(), 1, 10)
1641 .await
1642 .expect("failed to list runs");
1643 let created_run_id = runs.items[0].id;
1644
1645 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1646 let initial_position = ctx.position;
1647
1648 ctx.skip("skip-step", "condition not met")
1649 .await
1650 .expect("skip failed");
1651
1652 assert_eq!(ctx.position, initial_position + 1);
1653 assert!(!ctx.last_step_ids.is_empty());
1654
1655 let steps = store
1657 .list_steps(created_run_id)
1658 .await
1659 .expect("failed to list steps");
1660 assert_eq!(steps.len(), 1);
1661 assert_eq!(steps[0].status.state, StepStatus::Skipped);
1662 }
1663
1664 struct NoopSubWorkflow;
1667
1668 impl WorkflowHandler for NoopSubWorkflow {
1669 fn name(&self) -> &str {
1670 "noop-sub"
1671 }
1672
1673 fn execute<'a>(
1674 &'a self,
1675 _ctx: &'a mut WorkflowContext,
1676 ) -> crate::handler::HandlerFuture<'a> {
1677 Box::pin(async move { Ok(()) })
1678 }
1679 }
1680
1681 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
1684 let store = Arc::new(InMemoryStore::new());
1685 let provider = create_test_provider();
1686
1687 let parent = store
1688 .create_run(NewRun {
1689 workflow_name: "parent".to_string(),
1690 trigger: TriggerKind::Api,
1691 payload: json!({}),
1692 max_retries: 0,
1693 handler_version: None,
1694 labels: Default::default(),
1695 scheduled_at: None,
1696 created_by,
1697 idempotency_key: None,
1698 max_cost_usd: None,
1699 })
1700 .await
1701 .expect("failed to create parent run")
1702 .into_run();
1703
1704 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
1705 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
1706 _ => None,
1707 });
1708
1709 let mut ctx =
1710 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
1711 ctx.workflow(&NoopSubWorkflow, json!({}))
1712 .await
1713 .expect("sub-workflow failed");
1714
1715 let runs = store
1716 .list_runs(RunFilter::default(), 1, 10)
1717 .await
1718 .expect("failed to list runs");
1719 runs.items
1720 .into_iter()
1721 .find(|r| r.workflow_name == "noop-sub")
1722 .expect("child run was created")
1723 }
1724
1725 #[tokio::test]
1726 async fn child_run_inherits_the_parent_author() {
1727 let user_id = Uuid::now_v7();
1728 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
1729
1730 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
1731 }
1732
1733 #[tokio::test]
1734 async fn child_run_of_an_unattributed_parent_has_no_author() {
1735 let child = child_run_of_parent_authored_by(None).await;
1736
1737 assert!(child.created_by.is_none());
1738 }
1739
1740 #[tokio::test]
1741 async fn context_parallel_empty_steps_returns_empty_vec() {
1742 let mut ctx = create_test_context();
1743 let results = ctx
1744 .parallel(vec![], true)
1745 .await
1746 .expect("parallel should not fail on empty input");
1747 assert!(results.is_empty());
1748 }
1749
1750 #[tokio::test]
1751 async fn context_approval_first_execution_returns_error() {
1752 let store = Arc::new(InMemoryStore::new());
1753 let provider = create_test_provider();
1754
1755 store
1757 .create_run(NewRun {
1758 created_by: None,
1759 workflow_name: "test".to_string(),
1760 trigger: TriggerKind::Manual,
1761 payload: json!({}),
1762 max_retries: 0,
1763 handler_version: None,
1764 labels: Default::default(),
1765 scheduled_at: None,
1766 idempotency_key: None,
1767 max_cost_usd: None,
1768 })
1769 .await
1770 .expect("failed to create run")
1771 .into_run();
1772
1773 let runs = store
1775 .list_runs(RunFilter::default(), 1, 10)
1776 .await
1777 .expect("failed to list runs");
1778 let created_run_id = runs.items[0].id;
1779
1780 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1781
1782 let result = ctx
1783 .approval(
1784 "approve-step",
1785 crate::config::ApprovalConfig::new("Continue?"),
1786 )
1787 .await;
1788
1789 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
1791
1792 assert_eq!(ctx.position, 1);
1794
1795 let steps = store
1797 .list_steps(created_run_id)
1798 .await
1799 .expect("failed to list steps");
1800 assert_eq!(steps.len(), 1);
1801 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
1802 }
1803
1804 #[tokio::test]
1805 async fn context_approval_replay_returns_ok() {
1806 let store = Arc::new(InMemoryStore::new());
1807 let provider = create_test_provider();
1808
1809 store
1811 .create_run(NewRun {
1812 created_by: None,
1813 workflow_name: "test".to_string(),
1814 trigger: TriggerKind::Manual,
1815 payload: json!({}),
1816 max_retries: 0,
1817 handler_version: None,
1818 labels: Default::default(),
1819 scheduled_at: None,
1820 idempotency_key: None,
1821 max_cost_usd: None,
1822 })
1823 .await
1824 .expect("failed to create run")
1825 .into_run();
1826
1827 let runs = store
1829 .list_runs(RunFilter::default(), 1, 10)
1830 .await
1831 .expect("failed to list runs");
1832 let created_run_id = runs.items[0].id;
1833
1834 let step = store
1836 .create_step(NewStep {
1837 run_id: created_run_id,
1838 name: "approval".to_string(),
1839 kind: StepKind::Approval,
1840 position: 0,
1841 input: None,
1842 })
1843 .await
1844 .expect("failed to create step");
1845
1846 store
1848 .update_step(
1849 step.id,
1850 StepUpdate {
1851 status: Some(StepStatus::Running),
1852 started_at: Some(Utc::now()),
1853 ..StepUpdate::default()
1854 },
1855 )
1856 .await
1857 .expect("failed to update step to Running");
1858
1859 store
1860 .update_step(
1861 step.id,
1862 StepUpdate {
1863 status: Some(StepStatus::AwaitingApproval),
1864 ..StepUpdate::default()
1865 },
1866 )
1867 .await
1868 .expect("failed to update step to AwaitingApproval");
1869
1870 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1872 ctx.load_replay_steps()
1873 .await
1874 .expect("failed to load replay steps");
1875
1876 let result = ctx
1878 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
1879 .await;
1880
1881 assert!(result.is_ok());
1882
1883 let steps = store
1885 .list_steps(created_run_id)
1886 .await
1887 .expect("failed to list steps");
1888 assert_eq!(steps.len(), 1);
1889 assert_eq!(steps[0].status.state, StepStatus::Completed);
1890 }
1891
1892 #[tokio::test]
1893 async fn context_load_replay_steps_loads_completed_steps() {
1894 let store = Arc::new(InMemoryStore::new());
1895 let provider = create_test_provider();
1896
1897 store
1899 .create_run(NewRun {
1900 created_by: None,
1901 workflow_name: "test".to_string(),
1902 trigger: TriggerKind::Manual,
1903 payload: json!({}),
1904 max_retries: 0,
1905 handler_version: None,
1906 labels: Default::default(),
1907 scheduled_at: None,
1908 idempotency_key: None,
1909 max_cost_usd: None,
1910 })
1911 .await
1912 .expect("failed to create run")
1913 .into_run();
1914
1915 let runs = store
1917 .list_runs(RunFilter::default(), 1, 10)
1918 .await
1919 .expect("failed to list runs");
1920 let created_run_id = runs.items[0].id;
1921
1922 let completed_step = store
1924 .create_step(NewStep {
1925 run_id: created_run_id,
1926 name: "completed".to_string(),
1927 kind: StepKind::Shell,
1928 position: 0,
1929 input: None,
1930 })
1931 .await
1932 .expect("failed to create step");
1933
1934 store
1936 .update_step(
1937 completed_step.id,
1938 StepUpdate {
1939 status: Some(StepStatus::Running),
1940 started_at: Some(Utc::now()),
1941 ..StepUpdate::default()
1942 },
1943 )
1944 .await
1945 .expect("failed to update step to Running");
1946
1947 store
1948 .update_step(
1949 completed_step.id,
1950 StepUpdate {
1951 status: Some(StepStatus::Completed),
1952 completed_at: Some(Utc::now()),
1953 ..StepUpdate::default()
1954 },
1955 )
1956 .await
1957 .expect("failed to update step to Completed");
1958
1959 let _pending_step = store
1960 .create_step(NewStep {
1961 run_id: created_run_id,
1962 name: "pending".to_string(),
1963 kind: StepKind::Shell,
1964 position: 1,
1965 input: None,
1966 })
1967 .await
1968 .expect("failed to create step");
1969
1970 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
1972 ctx.load_replay_steps()
1973 .await
1974 .expect("failed to load replay steps");
1975
1976 assert_eq!(ctx.replay_steps.len(), 1);
1978 assert!(ctx.replay_steps.contains_key(&0));
1979 assert!(!ctx.replay_steps.contains_key(&1));
1980 }
1981
1982 #[tokio::test]
1983 async fn context_payload_returns_run_payload() {
1984 let store = Arc::new(InMemoryStore::new());
1985 let provider = create_test_provider();
1986 let test_payload = json!({"key": "value", "number": 42});
1987
1988 store
1990 .create_run(NewRun {
1991 created_by: None,
1992 workflow_name: "test".to_string(),
1993 trigger: TriggerKind::Manual,
1994 payload: test_payload.clone(),
1995 max_retries: 0,
1996 handler_version: None,
1997 labels: Default::default(),
1998 scheduled_at: None,
1999 idempotency_key: None,
2000 max_cost_usd: None,
2001 })
2002 .await
2003 .expect("failed to create run")
2004 .into_run();
2005
2006 let runs = store
2008 .list_runs(RunFilter::default(), 1, 10)
2009 .await
2010 .expect("failed to list runs");
2011 let created_run_id = runs.items[0].id;
2012
2013 let ctx = WorkflowContext::new(created_run_id, store, provider);
2014 let payload = ctx.payload().await.expect("failed to get payload");
2015
2016 assert_eq!(payload, test_payload);
2017 }
2018
2019 #[tokio::test]
2020 async fn context_payload_returns_error_for_nonexistent_run() {
2021 let store = Arc::new(InMemoryStore::new());
2022 let provider = create_test_provider();
2023 let run_id = Uuid::now_v7();
2024
2025 let ctx = WorkflowContext::new(run_id, store, provider);
2026 let result = ctx.payload().await;
2027
2028 assert!(result.is_err());
2029 }
2030
2031 #[tokio::test]
2032 async fn context_store_returns_reference() {
2033 let ctx = create_test_context();
2034 let _store = ctx.store();
2035 }
2037
2038 #[test]
2039 fn context_debug_formatting() {
2040 let ctx = create_test_context();
2041 let debug_str = format!("{:?}", ctx);
2042 assert!(debug_str.contains("WorkflowContext"));
2043 assert!(debug_str.contains("run_id"));
2044 }
2045
2046 #[tokio::test]
2047 async fn context_last_step_ids_tracks_executed_steps() {
2048 let store = Arc::new(InMemoryStore::new());
2049 let provider = create_test_provider();
2050
2051 store
2053 .create_run(NewRun {
2054 created_by: None,
2055 workflow_name: "test".to_string(),
2056 trigger: TriggerKind::Manual,
2057 payload: json!({}),
2058 max_retries: 0,
2059 handler_version: None,
2060 labels: Default::default(),
2061 scheduled_at: None,
2062 idempotency_key: None,
2063 max_cost_usd: None,
2064 })
2065 .await
2066 .expect("failed to create run")
2067 .into_run();
2068
2069 let runs = store
2071 .list_runs(RunFilter::default(), 1, 10)
2072 .await
2073 .expect("failed to list runs");
2074 let created_run_id = runs.items[0].id;
2075
2076 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2077 assert!(ctx.last_step_ids.is_empty());
2078
2079 ctx.skip("step1", "reason").await.expect("skip failed");
2080
2081 assert_eq!(ctx.last_step_ids.len(), 1);
2082
2083 ctx.skip("step2", "reason").await.expect("skip failed");
2084
2085 assert_eq!(ctx.last_step_ids.len(), 1);
2087 }
2088}