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::{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,
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, execute_step_config};
60use crate::handler::WorkflowHandler;
61use crate::log_sender::{LogSender, StepLogSender};
62use crate::operation::Operation;
63
64pub(crate) type HandlerResolver =
66 Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
67
68pub struct WorkflowContext {
87 run_id: Uuid,
88 store: Arc<dyn Store>,
89 provider: Arc<dyn AgentProvider>,
90 handler_resolver: Option<HandlerResolver>,
91 position: u32,
92 last_step_ids: Vec<Uuid>,
94 total_cost_usd: Decimal,
96 total_duration_ms: u64,
98 max_cost_usd: Option<Decimal>,
100 inherited_cost_usd: Decimal,
103 replay_steps: HashMap<u32, Step>,
106 granted_approvals: HashMap<u32, u32>,
110 attempt: u32,
112 carried_duration_ms: u64,
115 log_sender: Option<LogSender>,
117 artifact_sink: Option<Arc<dyn ArtifactSink>>,
121}
122
123impl WorkflowContext {
124 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
129 Self {
130 run_id,
131 store,
132 provider,
133 handler_resolver: None,
134 position: 0,
135 last_step_ids: Vec::new(),
136 total_cost_usd: Decimal::ZERO,
137 total_duration_ms: 0,
138 max_cost_usd: None,
139 inherited_cost_usd: Decimal::ZERO,
140 replay_steps: HashMap::new(),
141 granted_approvals: HashMap::new(),
142 attempt: 1,
143 carried_duration_ms: 0,
144 log_sender: None,
145 artifact_sink: None,
146 }
147 }
148
149 pub(crate) fn with_handler_resolver(
154 run_id: Uuid,
155 store: Arc<dyn Store>,
156 provider: Arc<dyn AgentProvider>,
157 resolver: HandlerResolver,
158 ) -> Self {
159 Self {
160 run_id,
161 store,
162 provider,
163 handler_resolver: Some(resolver),
164 position: 0,
165 last_step_ids: Vec::new(),
166 total_cost_usd: Decimal::ZERO,
167 total_duration_ms: 0,
168 max_cost_usd: None,
169 inherited_cost_usd: Decimal::ZERO,
170 replay_steps: HashMap::new(),
171 granted_approvals: HashMap::new(),
172 attempt: 1,
173 carried_duration_ms: 0,
174 log_sender: None,
175 artifact_sink: None,
176 }
177 }
178
179 pub fn set_log_sender(&mut self, sender: LogSender) {
181 self.log_sender = Some(sender);
182 }
183
184 pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>) {
204 self.artifact_sink = Some(sink);
205 }
206
207 fn artifact_sink(&self) -> Result<&Arc<dyn ArtifactSink>, EngineError> {
209 self.artifact_sink.as_ref().ok_or_else(|| {
210 EngineError::ArtifactsUnavailable(
211 "no artifact storage is attached to this run".to_string(),
212 )
213 })
214 }
215
216 pub async fn put_artifact(
246 &self,
247 step_id: Uuid,
248 name: &str,
249 content_type: Option<&str>,
250 content: Vec<u8>,
251 ) -> Result<Artifact, EngineError> {
252 let sink = self.artifact_sink()?;
253 sink.put(
254 ArtifactUpload {
255 run_id: self.run_id,
256 step_id,
257 name: name.to_string(),
258 content_type: content_type
259 .map(str::to_string)
260 .unwrap_or_else(|| guess_content_type(name)),
261 },
262 stream_from_bytes(content),
263 )
264 .await
265 }
266
267 pub async fn get_artifact(&self, step: &str, name: &str) -> Result<Vec<u8>, EngineError> {
292 let sink = self.artifact_sink()?;
293
294 let artifact = self
295 .store
296 .find_artifact_for_input(ArtifactLookup {
297 run_id: self.run_id,
298 attempt: self.attempt,
299 before_position: self.position,
300 step_name: step.to_string(),
301 name: name.to_string(),
302 })
303 .await?
304 .ok_or_else(|| EngineError::ArtifactNotFound {
305 step: step.to_string(),
306 name: name.to_string(),
307 })?;
308
309 let mut content = sink.get(&artifact).await?;
310 let mut buffer = Vec::with_capacity(artifact.size_bytes as usize);
311 while let Some(chunk) = content.next().await {
312 let chunk = chunk?;
313 buffer.extend_from_slice(chunk.as_ref());
314 }
315
316 Ok(buffer)
317 }
318
319 async fn prepare_step_inputs(
324 &self,
325 config: &StepConfig,
326 position: u32,
327 ) -> Result<(), EngineError> {
328 let StepConfig::Shell(shell) = config else {
329 return Ok(());
330 };
331 if shell.inputs.is_empty() {
332 return Ok(());
333 }
334
335 materialize_inputs(
336 self.artifact_sink()?,
337 &self.store,
338 shell,
339 StepLocation {
340 run_id: self.run_id,
341 attempt: self.attempt,
342 position,
343 },
344 )
345 .await
346 }
347
348 async fn store_step_outputs(
353 &self,
354 config: &StepConfig,
355 step_id: Uuid,
356 step_name: &str,
357 step_succeeded: bool,
358 ) -> Result<(), EngineError> {
359 let StepConfig::Shell(shell) = config else {
360 return Ok(());
361 };
362 if shell.outputs.is_empty() {
363 return Ok(());
364 }
365
366 let sink = match self.artifact_sink() {
367 Ok(sink) => sink,
368 Err(err) if step_succeeded => return Err(err),
369 Err(err) => {
370 warn!(
371 run_id = %self.run_id,
372 step = %step_name,
373 error = %err,
374 "cannot collect outputs of a failed step"
375 );
376 return Ok(());
377 }
378 };
379
380 let collected =
381 collect_outputs(sink, shell, self.run_id, step_id, step_name, step_succeeded).await;
382
383 match collected {
384 Ok(()) => Ok(()),
385 Err(err) if step_succeeded => Err(err),
386 Err(err) => {
387 warn!(
388 run_id = %self.run_id,
389 step = %step_name,
390 error = %err,
391 "failed to collect outputs of a failed step"
392 );
393 Ok(())
394 }
395 }
396 }
397
398 pub(crate) fn carry_over_run_totals(
405 &mut self,
406 attempt: u32,
407 cost_usd: Decimal,
408 duration_ms: u64,
409 ) {
410 self.attempt = attempt;
411 self.total_cost_usd = cost_usd;
412 self.carried_duration_ms = duration_ms;
413 }
414
415 pub(crate) fn carried_duration_ms(&self) -> u64 {
417 self.carried_duration_ms
418 }
419
420 pub fn attempt(&self) -> u32 {
422 self.attempt
423 }
424
425 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
441 self.max_cost_usd = cap;
442 }
443
444 pub fn max_cost_usd(&self) -> Option<Decimal> {
446 self.max_cost_usd
447 }
448
449 pub fn charged_cost_usd(&self) -> Decimal {
454 self.inherited_cost_usd + self.total_cost_usd
455 }
456
457 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
468 let Some(limit) = self.max_cost_usd else {
469 return Ok(());
470 };
471
472 let spent = self.charged_cost_usd();
473 if spent + step_budget <= limit {
474 return Ok(());
475 }
476
477 error!(
478 run_id = %self.run_id,
479 limit_usd = %limit,
480 spent_usd = %spent,
481 step_budget_usd = %step_budget,
482 "run cost cap reached, refusing agent step"
483 );
484
485 Err(EngineError::RunBudgetExceeded {
486 run_id: self.run_id,
487 limit_usd: limit,
488 spent_usd: spent,
489 step_budget_usd: step_budget,
490 })
491 }
492
493 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
505 let steps = self.store.list_steps(self.run_id).await?;
506 for step in steps {
507 let dominated = matches!(
508 step.status.state,
509 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
510 );
511 if !dominated {
512 continue;
513 }
514
515 if step.attempt == self.attempt {
516 self.replay_steps.insert(step.position, step);
517 } else if step.kind == StepKind::Approval && step.status.state == StepStatus::Completed
518 {
519 self.granted_approvals.insert(step.position, step.attempt);
520 }
521 }
522 Ok(())
523 }
524
525 pub fn run_id(&self) -> Uuid {
527 self.run_id
528 }
529
530 pub fn total_cost_usd(&self) -> Decimal {
532 self.total_cost_usd
533 }
534
535 pub fn total_duration_ms(&self) -> u64 {
537 self.total_duration_ms
538 }
539
540 pub async fn parallel(
577 &mut self,
578 steps: Vec<(&str, StepConfig)>,
579 fail_fast: bool,
580 ) -> Result<Vec<ParallelStepResult>, EngineError> {
581 if steps.is_empty() {
582 return Ok(Vec::new());
583 }
584
585 let wave_budget: Decimal = steps
588 .iter()
589 .filter_map(|(_, config)| match config {
590 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
591 _ => None,
592 })
593 .map(step_budget_usd)
594 .sum();
595 self.check_run_budget(wave_budget)?;
596
597 let wave_position = self.position;
598 self.position += 1;
599
600 let now = Utc::now();
601 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
602
603 for (name, config) in &steps {
604 let kind = config.kind();
605 let step = self
606 .store
607 .create_step(NewStep {
608 run_id: self.run_id,
609 name: name.to_string(),
610 kind,
611 position: wave_position,
612 input: Some(serde_json::to_value(config)?),
613 })
614 .await?;
615
616 self.start_step(step.id, now).await?;
617
618 if let Err(err) = self.prepare_step_inputs(config, wave_position).await {
621 self.fail_step(step.id, &err).await;
622 return Err(err);
623 }
624
625 step_records.push((step.id, name.to_string(), config.clone()));
626 }
627
628 let mut join_set = JoinSet::new();
629 let mut task_index: HashMap<Id, usize> = HashMap::new();
630 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
631 let provider = self.provider.clone();
632 let config = config.clone();
633 let step_log_sender = self
634 .log_sender
635 .as_ref()
636 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
637 let handle = join_set.spawn(async move {
638 (
639 idx,
640 execute_step_config(&config, &provider, step_log_sender).await,
641 )
642 });
643 task_index.insert(handle.id(), idx);
644 }
645
646 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
648 vec![None; step_records.len()];
649 let mut first_error: Option<EngineError> = None;
650
651 while let Some(join_result) = join_set.join_next().await {
652 let (idx, step_result) = match join_result {
653 Ok(r) => r,
654 Err(e) => {
655 let error_msg = format!("join error: {e}");
656 if let Some(&idx) = task_index.get(&e.id()) {
657 let (step_id, step_name, _) = &step_records[idx];
658 let completed_at = Utc::now();
659 error!(
660 run_id = %self.run_id,
661 step = %step_name,
662 error = %error_msg,
663 "parallel step panicked or was cancelled"
664 );
665 if let Err(store_err) = self
666 .store
667 .update_step(
668 *step_id,
669 StepUpdate {
670 status: Some(StepStatus::Failed),
671 error: Some(error_msg.clone()),
672 completed_at: Some(completed_at),
673 ..StepUpdate::default()
674 },
675 )
676 .await
677 {
678 error!(
679 run_id = %self.run_id,
680 step_id = %step_id,
681 error = %store_err,
682 "failed to persist JoinError for step"
683 );
684 }
685 indexed_results[idx] = Some(Err(error_msg.clone()));
686 }
687 if first_error.is_none() {
688 first_error = Some(EngineError::StepConfig(error_msg));
689 }
690 if fail_fast {
691 join_set.abort_all();
692 }
693 continue;
694 }
695 };
696
697 let (step_id, step_name, step_config) = &step_records[idx];
698 let completed_at = Utc::now();
699
700 if let Err(err) = self
701 .store_step_outputs(step_config, *step_id, step_name, step_result.is_ok())
702 .await
703 {
704 self.fail_step(*step_id, &err).await;
705 indexed_results[idx] = Some(Err(err.to_string()));
706 if first_error.is_none() {
707 first_error = Some(err);
708 }
709 if fail_fast {
710 join_set.abort_all();
711 }
712 continue;
713 }
714
715 match step_result {
716 Ok(output) => {
717 self.total_cost_usd += output.cost_usd;
718 self.total_duration_ms += output.duration_ms;
719
720 let debug_messages_json = output.debug_messages_json();
721
722 self.store
723 .update_step(
724 *step_id,
725 StepUpdate {
726 status: Some(StepStatus::Completed),
727 output: Some(output.output.clone()),
728 duration_ms: Some(output.duration_ms),
729 cost_usd: Some(output.cost_usd),
730 input_tokens: output.input_tokens,
731 output_tokens: output.output_tokens,
732 completed_at: Some(completed_at),
733 debug_messages: debug_messages_json,
734 ..StepUpdate::default()
735 },
736 )
737 .await?;
738
739 info!(
740 run_id = %self.run_id,
741 step = %step_name,
742 duration_ms = output.duration_ms,
743 "parallel step completed"
744 );
745
746 indexed_results[idx] = Some(Ok(output));
747 }
748 Err(err) => {
749 let err_msg = err.to_string();
750 let debug_messages_json = extract_debug_messages_from_error(&err);
751 let partial = extract_partial_usage_from_error(&err);
752 let raw_response_output = extract_raw_response_from_error(&err);
753
754 if let Some(ref usage) = partial {
755 if let Some(cost) = usage.cost_usd {
756 self.total_cost_usd += cost;
757 }
758 if let Some(dur) = usage.duration_ms {
759 self.total_duration_ms += dur;
760 }
761 }
762
763 if let Err(store_err) = self
764 .store
765 .update_step(
766 *step_id,
767 StepUpdate {
768 status: Some(StepStatus::Failed),
769 error: Some(err_msg.clone()),
770 output: raw_response_output,
771 completed_at: Some(completed_at),
772 debug_messages: debug_messages_json,
773 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
774 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
775 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
776 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
777 ..StepUpdate::default()
778 },
779 )
780 .await
781 {
782 tracing::error!(
783 step_id = %step_id,
784 error = %store_err,
785 "failed to persist parallel step failure"
786 );
787 }
788
789 indexed_results[idx] = Some(Err(err_msg.clone()));
790
791 if first_error.is_none() {
792 first_error = Some(err);
793 }
794
795 if fail_fast {
796 join_set.abort_all();
797 }
798 }
799 }
800 }
801
802 if let Some(err) = first_error {
803 return Err(err);
804 }
805
806 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
807
808 let results: Vec<ParallelStepResult> = step_records
810 .iter()
811 .enumerate()
812 .map(|(idx, (step_id, name, _))| {
813 let output = match indexed_results[idx].take() {
814 Some(Ok(o)) => o,
815 _ => unreachable!("all steps succeeded if no error returned"),
816 };
817 ParallelStepResult {
818 name: name.clone(),
819 output,
820 step_id: *step_id,
821 }
822 })
823 .collect();
824
825 Ok(results)
826 }
827
828 pub async fn shell(
851 &mut self,
852 name: &str,
853 config: ShellConfig,
854 ) -> Result<StepOutput, EngineError> {
855 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
856 .await
857 }
858
859 pub async fn http(
879 &mut self,
880 name: &str,
881 config: HttpConfig,
882 ) -> Result<StepOutput, EngineError> {
883 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
884 .await
885 }
886
887 pub async fn agent(
907 &mut self,
908 name: &str,
909 config: impl Into<AgentStepConfig>,
910 ) -> Result<StepOutput, EngineError> {
911 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
912 .await
913 }
914
915 pub async fn approval(
946 &mut self,
947 name: &str,
948 config: ApprovalConfig,
949 ) -> Result<(), EngineError> {
950 let position = self.position;
951 self.position += 1;
952
953 if let Some(existing) = self.replay_steps.get(&position)
956 && existing.kind == StepKind::Approval
957 {
958 if existing.status.state == StepStatus::AwaitingApproval {
959 self.store
960 .update_step(
961 existing.id,
962 StepUpdate {
963 status: Some(StepStatus::Completed),
964 completed_at: Some(Utc::now()),
965 ..StepUpdate::default()
966 },
967 )
968 .await?;
969 }
970
971 self.last_step_ids = vec![existing.id];
972 info!(
973 run_id = %self.run_id,
974 step = %name,
975 position,
976 "approval step replayed (approved)"
977 );
978 return Ok(());
979 }
980
981 if let Some(&granted_in) = self.granted_approvals.get(&position) {
985 let step = self
986 .store
987 .create_step(NewStep {
988 run_id: self.run_id,
989 name: name.to_string(),
990 kind: StepKind::Approval,
991 position,
992 input: Some(serde_json::to_value(&config)?),
993 })
994 .await?;
995
996 let now = Utc::now();
997 self.start_step(step.id, now).await?;
998 self.store
999 .update_step(
1000 step.id,
1001 StepUpdate {
1002 status: Some(StepStatus::Completed),
1003 output: Some(json!({"approved_in_attempt": granted_in})),
1004 completed_at: Some(now),
1005 ..StepUpdate::default()
1006 },
1007 )
1008 .await?;
1009
1010 self.last_step_ids = vec![step.id];
1011 info!(
1012 run_id = %self.run_id,
1013 step = %name,
1014 position,
1015 granted_in_attempt = granted_in,
1016 attempt = self.attempt,
1017 "approval carried over from a previous attempt"
1018 );
1019 return Ok(());
1020 }
1021
1022 let step = self
1024 .store
1025 .create_step(NewStep {
1026 run_id: self.run_id,
1027 name: name.to_string(),
1028 kind: StepKind::Approval,
1029 position,
1030 input: Some(serde_json::to_value(&config)?),
1031 })
1032 .await?;
1033
1034 self.start_step(step.id, Utc::now()).await?;
1035
1036 self.store
1039 .update_step(
1040 step.id,
1041 StepUpdate {
1042 status: Some(StepStatus::AwaitingApproval),
1043 ..StepUpdate::default()
1044 },
1045 )
1046 .await?;
1047
1048 self.last_step_ids = vec![step.id];
1049
1050 Err(EngineError::ApprovalRequired {
1051 run_id: self.run_id,
1052 step_id: step.id,
1053 message: config.message().to_string(),
1054 })
1055 }
1056
1057 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
1086 let position = self.position;
1087 self.position += 1;
1088
1089 let step = self
1090 .store
1091 .create_step(NewStep {
1092 run_id: self.run_id,
1093 name: name.to_string(),
1094 kind: StepKind::Custom("skip".to_string()),
1095 position,
1096 input: None,
1097 })
1098 .await?;
1099
1100 if !self.last_step_ids.is_empty() {
1101 let deps: Vec<NewStepDependency> = self
1102 .last_step_ids
1103 .iter()
1104 .map(|&depends_on| NewStepDependency {
1105 step_id: step.id,
1106 depends_on,
1107 })
1108 .collect();
1109 self.store.create_step_dependencies(deps).await?;
1110 }
1111
1112 let now = Utc::now();
1113 self.store
1114 .update_step(
1115 step.id,
1116 StepUpdate {
1117 status: Some(StepStatus::Skipped),
1118 output: Some(serde_json::json!({"reason": reason})),
1119 completed_at: Some(now),
1120 ..StepUpdate::default()
1121 },
1122 )
1123 .await?;
1124
1125 self.last_step_ids = vec![step.id];
1126
1127 info!(
1128 run_id = %self.run_id,
1129 step = %name,
1130 reason,
1131 "step skipped"
1132 );
1133
1134 Ok(())
1135 }
1136
1137 pub async fn operation(
1175 &mut self,
1176 name: &str,
1177 op: &dyn Operation,
1178 ) -> Result<StepOutput, EngineError> {
1179 let kind = StepKind::Custom(op.kind().to_string());
1180 let position = self.position;
1181 self.position += 1;
1182
1183 let step = self
1184 .store
1185 .create_step(NewStep {
1186 run_id: self.run_id,
1187 name: name.to_string(),
1188 kind,
1189 position,
1190 input: op.input(),
1191 })
1192 .await?;
1193
1194 self.start_step(step.id, Utc::now()).await?;
1195
1196 let start = Instant::now();
1197
1198 match op.execute().await {
1199 Ok(output_value) => {
1200 let duration_ms = start.elapsed().as_millis() as u64;
1201 self.total_duration_ms += duration_ms;
1202
1203 let completed_at = Utc::now();
1204 self.store
1205 .update_step(
1206 step.id,
1207 StepUpdate {
1208 status: Some(StepStatus::Completed),
1209 output: Some(output_value.clone()),
1210 duration_ms: Some(duration_ms),
1211 cost_usd: Some(Decimal::ZERO),
1212 completed_at: Some(completed_at),
1213 ..StepUpdate::default()
1214 },
1215 )
1216 .await?;
1217
1218 info!(
1219 run_id = %self.run_id,
1220 step = %name,
1221 kind = op.kind(),
1222 duration_ms,
1223 "operation step completed"
1224 );
1225
1226 self.last_step_ids = vec![step.id];
1227
1228 Ok(StepOutput {
1229 output: output_value,
1230 duration_ms,
1231 cost_usd: Decimal::ZERO,
1232 input_tokens: None,
1233 output_tokens: None,
1234 model: None,
1235 debug_messages: None,
1236 })
1237 }
1238 Err(err) => {
1239 let completed_at = Utc::now();
1240 if let Err(store_err) = self
1241 .store
1242 .update_step(
1243 step.id,
1244 StepUpdate {
1245 status: Some(StepStatus::Failed),
1246 error: Some(err.to_string()),
1247 completed_at: Some(completed_at),
1248 ..StepUpdate::default()
1249 },
1250 )
1251 .await
1252 {
1253 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1254 }
1255
1256 Err(err)
1257 }
1258 }
1259 }
1260
1261 pub async fn workflow(
1288 &mut self,
1289 handler: &dyn WorkflowHandler,
1290 payload: Value,
1291 ) -> Result<StepOutput, EngineError> {
1292 let config = WorkflowStepConfig::new(handler.name(), payload);
1293 let position = self.position;
1294 self.position += 1;
1295
1296 let step = self
1297 .store
1298 .create_step(NewStep {
1299 run_id: self.run_id,
1300 name: config.workflow_name.clone(),
1301 kind: StepKind::Workflow,
1302 position,
1303 input: Some(serde_json::to_value(&config)?),
1304 })
1305 .await?;
1306
1307 self.start_step(step.id, Utc::now()).await?;
1308
1309 match self.execute_child_workflow(&config).await {
1310 Ok(output) => {
1311 self.total_cost_usd += output.cost_usd;
1312 self.total_duration_ms += output.duration_ms;
1313
1314 let completed_at = Utc::now();
1315 self.store
1316 .update_step(
1317 step.id,
1318 StepUpdate {
1319 status: Some(StepStatus::Completed),
1320 output: Some(output.output.clone()),
1321 duration_ms: Some(output.duration_ms),
1322 cost_usd: Some(output.cost_usd),
1323 completed_at: Some(completed_at),
1324 ..StepUpdate::default()
1325 },
1326 )
1327 .await?;
1328
1329 info!(
1330 run_id = %self.run_id,
1331 child_workflow = %config.workflow_name,
1332 duration_ms = output.duration_ms,
1333 "workflow step completed"
1334 );
1335
1336 self.last_step_ids = vec![step.id];
1337
1338 Ok(output)
1339 }
1340 Err(err) => {
1341 let completed_at = Utc::now();
1342 if let Err(store_err) = self
1343 .store
1344 .update_step(
1345 step.id,
1346 StepUpdate {
1347 status: Some(StepStatus::Failed),
1348 error: Some(err.to_string()),
1349 completed_at: Some(completed_at),
1350 ..StepUpdate::default()
1351 },
1352 )
1353 .await
1354 {
1355 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1356 }
1357
1358 Err(err)
1359 }
1360 }
1361 }
1362
1363 async fn execute_child_workflow(
1365 &self,
1366 config: &WorkflowStepConfig,
1367 ) -> Result<StepOutput, EngineError> {
1368 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1369 EngineError::InvalidWorkflow(
1370 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1371 )
1372 })?;
1373
1374 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1375 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1376 })?;
1377
1378 let parent = self.store.get_run(self.run_id).await?;
1381 let (parent_labels, parent_author) =
1382 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
1383
1384 let child_run = self
1385 .store
1386 .create_run(NewRun {
1387 workflow_name: config.workflow_name.clone(),
1388 trigger: TriggerKind::Workflow,
1389 payload: config.payload.clone(),
1390 max_retries: 0,
1391 handler_version: None,
1392 labels: parent_labels,
1393 scheduled_at: None,
1394 created_by: parent_author,
1395 idempotency_key: None,
1396 max_cost_usd: self.max_cost_usd,
1398 })
1399 .await?
1400 .into_run();
1401
1402 let child_run_id = child_run.id;
1403 info!(
1404 parent_run_id = %self.run_id,
1405 child_run_id = %child_run_id,
1406 workflow = %config.workflow_name,
1407 "child run created"
1408 );
1409
1410 self.store
1411 .update_run_status(child_run_id, RunStatus::Running)
1412 .await?;
1413
1414 let run_start = Instant::now();
1415 let mut child_ctx = WorkflowContext {
1416 run_id: child_run_id,
1417 store: self.store.clone(),
1418 provider: self.provider.clone(),
1419 handler_resolver: self.handler_resolver.clone(),
1420 position: 0,
1421 last_step_ids: Vec::new(),
1422 total_cost_usd: Decimal::ZERO,
1423 total_duration_ms: 0,
1424 max_cost_usd: self.max_cost_usd,
1425 inherited_cost_usd: self.charged_cost_usd(),
1428 replay_steps: HashMap::new(),
1429 granted_approvals: HashMap::new(),
1430 attempt: 1,
1432 carried_duration_ms: 0,
1433 log_sender: self.log_sender.clone(),
1434 artifact_sink: self.artifact_sink.clone(),
1437 };
1438
1439 let result = handler.execute(&mut child_ctx).await;
1440 let total_duration = run_start.elapsed().as_millis() as u64;
1441 let completed_at = Utc::now();
1442
1443 match result {
1444 Ok(()) => {
1445 self.store
1446 .update_run(
1447 child_run_id,
1448 RunUpdate {
1449 status: Some(RunStatus::Completed),
1450 cost_usd: Some(child_ctx.total_cost_usd),
1451 duration_ms: Some(total_duration),
1452 completed_at: Some(completed_at),
1453 ..RunUpdate::default()
1454 },
1455 )
1456 .await?;
1457
1458 Ok(StepOutput {
1459 output: serde_json::json!({
1460 "run_id": child_run_id,
1461 "workflow_name": config.workflow_name,
1462 "status": RunStatus::Completed,
1463 "cost_usd": child_ctx.total_cost_usd,
1464 "duration_ms": total_duration,
1465 }),
1466 duration_ms: total_duration,
1467 cost_usd: child_ctx.total_cost_usd,
1468 input_tokens: None,
1469 output_tokens: None,
1470 model: None,
1471 debug_messages: None,
1472 })
1473 }
1474 Err(err) => {
1475 if let Err(store_err) = self
1476 .store
1477 .update_run(
1478 child_run_id,
1479 RunUpdate {
1480 status: Some(RunStatus::Failed),
1481 error: Some(err.to_string()),
1482 cost_usd: Some(child_ctx.total_cost_usd),
1483 duration_ms: Some(total_duration),
1484 completed_at: Some(completed_at),
1485 ..RunUpdate::default()
1486 },
1487 )
1488 .await
1489 {
1490 error!(
1491 child_run_id = %child_run_id,
1492 store_error = %store_err,
1493 "failed to persist child run failure"
1494 );
1495 }
1496
1497 Err(err)
1498 }
1499 }
1500 }
1501
1502 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1507 let step = self.replay_steps.get(&position)?;
1508 if step.status.state != StepStatus::Completed {
1509 return None;
1510 }
1511 let output = StepOutput {
1512 output: step.output.clone().unwrap_or(Value::Null),
1513 duration_ms: step.duration_ms,
1514 cost_usd: step.cost_usd,
1515 input_tokens: step.input_tokens,
1516 output_tokens: step.output_tokens,
1517 model: None,
1518 debug_messages: None,
1519 };
1520 self.total_cost_usd += output.cost_usd;
1521 self.total_duration_ms += output.duration_ms;
1522 self.last_step_ids = vec![step.id];
1523 info!(
1524 run_id = %self.run_id,
1525 step = %step.name,
1526 position,
1527 "step replayed from previous execution"
1528 );
1529 Some(output)
1530 }
1531
1532 async fn execute_step(
1534 &mut self,
1535 name: &str,
1536 kind: StepKind,
1537 config: StepConfig,
1538 ) -> Result<StepOutput, EngineError> {
1539 let position = self.position;
1540 self.position += 1;
1541
1542 if let Some(output) = self.try_replay_step(position) {
1544 return Ok(output);
1545 }
1546
1547 if let StepConfig::Agent(ref agent_config) = config {
1550 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1551 }
1552
1553 let step = self
1555 .store
1556 .create_step(NewStep {
1557 run_id: self.run_id,
1558 name: name.to_string(),
1559 kind,
1560 position,
1561 input: Some(serde_json::to_value(&config)?),
1562 })
1563 .await?;
1564
1565 self.start_step(step.id, Utc::now()).await?;
1566
1567 if let Err(err) = self.prepare_step_inputs(&config, position).await {
1570 self.fail_step(step.id, &err).await;
1571 return Err(err);
1572 }
1573
1574 let step_log_sender = self
1575 .log_sender
1576 .as_ref()
1577 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1578
1579 let execution = execute_step_config(&config, &self.provider, step_log_sender).await;
1580
1581 if let Err(err) = self
1582 .store_step_outputs(&config, step.id, name, execution.is_ok())
1583 .await
1584 {
1585 self.fail_step(step.id, &err).await;
1586 return Err(err);
1587 }
1588
1589 match execution {
1590 Ok(output) => {
1591 self.total_cost_usd += output.cost_usd;
1592 self.total_duration_ms += output.duration_ms;
1593
1594 let debug_messages_json = output.debug_messages_json();
1595
1596 let completed_at = Utc::now();
1597 self.store
1598 .update_step(
1599 step.id,
1600 StepUpdate {
1601 status: Some(StepStatus::Completed),
1602 output: Some(output.output.clone()),
1603 duration_ms: Some(output.duration_ms),
1604 cost_usd: Some(output.cost_usd),
1605 input_tokens: output.input_tokens,
1606 output_tokens: output.output_tokens,
1607 completed_at: Some(completed_at),
1608 debug_messages: debug_messages_json,
1609 ..StepUpdate::default()
1610 },
1611 )
1612 .await?;
1613
1614 info!(
1615 run_id = %self.run_id,
1616 step = %name,
1617 duration_ms = output.duration_ms,
1618 "step completed"
1619 );
1620
1621 self.last_step_ids = vec![step.id];
1622
1623 Ok(output)
1624 }
1625 Err(err) => {
1626 let completed_at = Utc::now();
1627 let debug_messages_json = extract_debug_messages_from_error(&err);
1628 let partial = extract_partial_usage_from_error(&err);
1629 let raw_response_output = extract_raw_response_from_error(&err);
1630
1631 if let Some(ref usage) = partial {
1632 if let Some(cost) = usage.cost_usd {
1633 self.total_cost_usd += cost;
1634 }
1635 if let Some(dur) = usage.duration_ms {
1636 self.total_duration_ms += dur;
1637 }
1638 }
1639
1640 if let Err(store_err) = self
1641 .store
1642 .update_step(
1643 step.id,
1644 StepUpdate {
1645 status: Some(StepStatus::Failed),
1646 error: Some(err.to_string()),
1647 output: raw_response_output,
1648 completed_at: Some(completed_at),
1649 debug_messages: debug_messages_json,
1650 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1651 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1652 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1653 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1654 ..StepUpdate::default()
1655 },
1656 )
1657 .await
1658 {
1659 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1660 }
1661
1662 Err(err)
1663 }
1664 }
1665 }
1666
1667 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1672 if !self.last_step_ids.is_empty() {
1673 let deps: Vec<NewStepDependency> = self
1674 .last_step_ids
1675 .iter()
1676 .map(|&depends_on| NewStepDependency {
1677 step_id,
1678 depends_on,
1679 })
1680 .collect();
1681 self.store.create_step_dependencies(deps).await?;
1682 }
1683
1684 self.store
1685 .update_step(
1686 step_id,
1687 StepUpdate {
1688 status: Some(StepStatus::Running),
1689 started_at: Some(now),
1690 ..StepUpdate::default()
1691 },
1692 )
1693 .await?;
1694
1695 Ok(())
1696 }
1697
1698 async fn fail_step(&self, step_id: Uuid, err: &EngineError) {
1705 if let Err(store_err) = self
1706 .store
1707 .update_step(
1708 step_id,
1709 StepUpdate {
1710 status: Some(StepStatus::Failed),
1711 error: Some(err.to_string()),
1712 completed_at: Some(Utc::now()),
1713 ..StepUpdate::default()
1714 },
1715 )
1716 .await
1717 {
1718 error!(
1719 step_id = %step_id,
1720 error = %store_err,
1721 "failed to persist step failure"
1722 );
1723 }
1724 }
1725
1726 pub fn store(&self) -> &Arc<dyn Store> {
1728 &self.store
1729 }
1730
1731 pub async fn payload(&self) -> Result<Value, EngineError> {
1739 let run = self
1740 .store
1741 .get_run(self.run_id)
1742 .await?
1743 .ok_or(EngineError::Store(
1744 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1745 ))?;
1746 Ok(run.payload)
1747 }
1748}
1749
1750impl fmt::Debug for WorkflowContext {
1751 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1752 f.debug_struct("WorkflowContext")
1753 .field("run_id", &self.run_id)
1754 .field("position", &self.position)
1755 .field("total_cost_usd", &self.total_cost_usd)
1756 .field("inherited_cost_usd", &self.inherited_cost_usd)
1757 .field("max_cost_usd", &self.max_cost_usd)
1758 .finish_non_exhaustive()
1759 }
1760}
1761
1762fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
1765 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1766 debug_messages,
1767 ..
1768 })) = err
1769 && !debug_messages.is_empty()
1770 {
1771 return serde_json::to_value(debug_messages).ok();
1772 }
1773 None
1774}
1775
1776struct StepPartialUsage {
1782 cost_usd: Option<Decimal>,
1783 duration_ms: Option<u64>,
1784 input_tokens: Option<u64>,
1785 output_tokens: Option<u64>,
1786}
1787
1788fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
1794 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1795 raw_response: Some(text),
1796 ..
1797 })) = err
1798 {
1799 return Some(Value::String(text.clone()));
1800 }
1801 None
1802}
1803
1804fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
1805 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1806 partial_usage,
1807 ..
1808 })) = err
1809 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
1810 {
1811 return Some(StepPartialUsage {
1812 cost_usd: partial_usage
1813 .cost_usd
1814 .and_then(|c| Decimal::try_from(c).ok()),
1815 duration_ms: partial_usage.duration_ms,
1816 input_tokens: partial_usage.input_tokens,
1817 output_tokens: partial_usage.output_tokens,
1818 });
1819 }
1820 None
1821}
1822
1823#[cfg(test)]
1824mod tests {
1825 use super::*;
1826 use ironflow_core::providers::claude::ClaudeCodeProvider;
1827 use ironflow_core::providers::record_replay::RecordReplayProvider;
1828 use ironflow_store::memory::InMemoryStore;
1829 use ironflow_store::models::{Run, RunActor, RunFilter};
1830 use ironflow_store::store::RunStore;
1831 use serde_json::json;
1832 use std::sync::Arc;
1833 use std::sync::atomic::{AtomicBool, Ordering};
1834 use uuid::Uuid;
1835
1836 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
1838 let inner = ClaudeCodeProvider::new();
1839 Arc::new(RecordReplayProvider::replay(
1840 inner,
1841 "/tmp/ironflow-fixtures",
1842 ))
1843 }
1844
1845 fn create_test_context() -> WorkflowContext {
1847 let store = Arc::new(InMemoryStore::new());
1848 let provider = create_test_provider();
1849 let run_id = Uuid::now_v7();
1850 WorkflowContext::new(run_id, store, provider)
1851 }
1852
1853 #[test]
1854 fn context_new_initializes_correctly() {
1855 let ctx = create_test_context();
1856 assert_eq!(ctx.position, 0);
1857 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
1858 assert_eq!(ctx.total_duration_ms, 0);
1859 assert!(ctx.last_step_ids.is_empty());
1860 assert!(ctx.replay_steps.is_empty());
1861 assert!(ctx.log_sender.is_none());
1862 }
1863
1864 #[test]
1865 fn context_run_id_returns_correct_id() {
1866 let run_id = Uuid::now_v7();
1867 let store = Arc::new(InMemoryStore::new());
1868 let provider = create_test_provider();
1869 let ctx = WorkflowContext::new(run_id, store, provider);
1870 assert_eq!(ctx.run_id(), run_id);
1871 }
1872
1873 #[test]
1874 fn context_total_cost_usd_initially_zero() {
1875 let ctx = create_test_context();
1876 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
1877 }
1878
1879 #[test]
1880 fn context_total_duration_ms_initially_zero() {
1881 let ctx = create_test_context();
1882 assert_eq!(ctx.total_duration_ms(), 0);
1883 }
1884
1885 #[test]
1886 fn context_with_handler_resolver_creates_context_with_resolver() {
1887 let store = Arc::new(InMemoryStore::new());
1888 let provider = create_test_provider();
1889 let run_id = Uuid::now_v7();
1890
1891 let called = Arc::new(AtomicBool::new(false));
1892 let called_clone = called.clone();
1893
1894 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
1895 called_clone.store(true, Ordering::SeqCst);
1896 None
1897 });
1898
1899 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
1900
1901 assert_eq!(ctx.run_id(), run_id);
1902 assert!(ctx.handler_resolver.is_some());
1903 }
1904
1905 #[tokio::test]
1906 async fn context_set_log_sender_attaches_sender() {
1907 let mut ctx = create_test_context();
1908 let (sender, _receiver) = crate::log_sender::channel();
1909 ctx.set_log_sender(sender);
1910 assert!(ctx.log_sender.is_some());
1911 }
1912
1913 #[tokio::test]
1914 async fn context_skip_creates_skipped_step() {
1915 let store = Arc::new(InMemoryStore::new());
1916 let provider = create_test_provider();
1917
1918 store
1920 .create_run(NewRun {
1921 created_by: None,
1922 workflow_name: "test".to_string(),
1923 trigger: TriggerKind::Manual,
1924 payload: json!({}),
1925 max_retries: 0,
1926 handler_version: None,
1927 labels: Default::default(),
1928 scheduled_at: None,
1929 idempotency_key: None,
1930 max_cost_usd: None,
1931 })
1932 .await
1933 .expect("failed to create run")
1934 .into_run();
1935
1936 let runs = store
1938 .list_runs(RunFilter::default(), 1, 10)
1939 .await
1940 .expect("failed to list runs");
1941 let created_run_id = runs.items[0].id;
1942
1943 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1944 let initial_position = ctx.position;
1945
1946 ctx.skip("skip-step", "condition not met")
1947 .await
1948 .expect("skip failed");
1949
1950 assert_eq!(ctx.position, initial_position + 1);
1951 assert!(!ctx.last_step_ids.is_empty());
1952
1953 let steps = store
1955 .list_steps(created_run_id)
1956 .await
1957 .expect("failed to list steps");
1958 assert_eq!(steps.len(), 1);
1959 assert_eq!(steps[0].status.state, StepStatus::Skipped);
1960 }
1961
1962 struct NoopSubWorkflow;
1965
1966 impl WorkflowHandler for NoopSubWorkflow {
1967 fn name(&self) -> &str {
1968 "noop-sub"
1969 }
1970
1971 fn execute<'a>(
1972 &'a self,
1973 _ctx: &'a mut WorkflowContext,
1974 ) -> crate::handler::HandlerFuture<'a> {
1975 Box::pin(async move { Ok(()) })
1976 }
1977 }
1978
1979 async fn child_run_of_parent_authored_by(created_by: Option<RunActor>) -> Run {
1982 let store = Arc::new(InMemoryStore::new());
1983 let provider = create_test_provider();
1984
1985 let parent = store
1986 .create_run(NewRun {
1987 workflow_name: "parent".to_string(),
1988 trigger: TriggerKind::Api,
1989 payload: json!({}),
1990 max_retries: 0,
1991 handler_version: None,
1992 labels: Default::default(),
1993 scheduled_at: None,
1994 created_by,
1995 idempotency_key: None,
1996 max_cost_usd: None,
1997 })
1998 .await
1999 .expect("failed to create parent run")
2000 .into_run();
2001
2002 let resolver: HandlerResolver = Arc::new(|name: &str| match name {
2003 "noop-sub" => Some(Arc::new(NoopSubWorkflow) as Arc<dyn WorkflowHandler>),
2004 _ => None,
2005 });
2006
2007 let mut ctx =
2008 WorkflowContext::with_handler_resolver(parent.id, store.clone(), provider, resolver);
2009 ctx.workflow(&NoopSubWorkflow, json!({}))
2010 .await
2011 .expect("sub-workflow failed");
2012
2013 let runs = store
2014 .list_runs(RunFilter::default(), 1, 10)
2015 .await
2016 .expect("failed to list runs");
2017 runs.items
2018 .into_iter()
2019 .find(|r| r.workflow_name == "noop-sub")
2020 .expect("child run was created")
2021 }
2022
2023 #[tokio::test]
2024 async fn child_run_inherits_the_parent_author() {
2025 let user_id = Uuid::now_v7();
2026 let child = child_run_of_parent_authored_by(Some(RunActor::User { user_id })).await;
2027
2028 assert_eq!(child.created_by, Some(RunActor::User { user_id }));
2029 }
2030
2031 #[tokio::test]
2032 async fn child_run_of_an_unattributed_parent_has_no_author() {
2033 let child = child_run_of_parent_authored_by(None).await;
2034
2035 assert!(child.created_by.is_none());
2036 }
2037
2038 #[tokio::test]
2039 async fn context_parallel_empty_steps_returns_empty_vec() {
2040 let mut ctx = create_test_context();
2041 let results = ctx
2042 .parallel(vec![], true)
2043 .await
2044 .expect("parallel should not fail on empty input");
2045 assert!(results.is_empty());
2046 }
2047
2048 #[tokio::test]
2049 async fn context_approval_first_execution_returns_error() {
2050 let store = Arc::new(InMemoryStore::new());
2051 let provider = create_test_provider();
2052
2053 store
2055 .create_run(NewRun {
2056 created_by: None,
2057 workflow_name: "test".to_string(),
2058 trigger: TriggerKind::Manual,
2059 payload: json!({}),
2060 max_retries: 0,
2061 handler_version: None,
2062 labels: Default::default(),
2063 scheduled_at: None,
2064 idempotency_key: None,
2065 max_cost_usd: None,
2066 })
2067 .await
2068 .expect("failed to create run")
2069 .into_run();
2070
2071 let runs = store
2073 .list_runs(RunFilter::default(), 1, 10)
2074 .await
2075 .expect("failed to list runs");
2076 let created_run_id = runs.items[0].id;
2077
2078 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2079
2080 let result = ctx
2081 .approval(
2082 "approve-step",
2083 crate::config::ApprovalConfig::new("Continue?"),
2084 )
2085 .await;
2086
2087 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
2089
2090 assert_eq!(ctx.position, 1);
2092
2093 let steps = store
2095 .list_steps(created_run_id)
2096 .await
2097 .expect("failed to list steps");
2098 assert_eq!(steps.len(), 1);
2099 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
2100 }
2101
2102 #[tokio::test]
2103 async fn context_approval_replay_returns_ok() {
2104 let store = Arc::new(InMemoryStore::new());
2105 let provider = create_test_provider();
2106
2107 store
2109 .create_run(NewRun {
2110 created_by: None,
2111 workflow_name: "test".to_string(),
2112 trigger: TriggerKind::Manual,
2113 payload: json!({}),
2114 max_retries: 0,
2115 handler_version: None,
2116 labels: Default::default(),
2117 scheduled_at: None,
2118 idempotency_key: None,
2119 max_cost_usd: None,
2120 })
2121 .await
2122 .expect("failed to create run")
2123 .into_run();
2124
2125 let runs = store
2127 .list_runs(RunFilter::default(), 1, 10)
2128 .await
2129 .expect("failed to list runs");
2130 let created_run_id = runs.items[0].id;
2131
2132 let step = store
2134 .create_step(NewStep {
2135 run_id: created_run_id,
2136 name: "approval".to_string(),
2137 kind: StepKind::Approval,
2138 position: 0,
2139 input: None,
2140 })
2141 .await
2142 .expect("failed to create step");
2143
2144 store
2146 .update_step(
2147 step.id,
2148 StepUpdate {
2149 status: Some(StepStatus::Running),
2150 started_at: Some(Utc::now()),
2151 ..StepUpdate::default()
2152 },
2153 )
2154 .await
2155 .expect("failed to update step to Running");
2156
2157 store
2158 .update_step(
2159 step.id,
2160 StepUpdate {
2161 status: Some(StepStatus::AwaitingApproval),
2162 ..StepUpdate::default()
2163 },
2164 )
2165 .await
2166 .expect("failed to update step to AwaitingApproval");
2167
2168 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
2170 ctx.load_replay_steps()
2171 .await
2172 .expect("failed to load replay steps");
2173
2174 let result = ctx
2176 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
2177 .await;
2178
2179 assert!(result.is_ok());
2180
2181 let steps = store
2183 .list_steps(created_run_id)
2184 .await
2185 .expect("failed to list steps");
2186 assert_eq!(steps.len(), 1);
2187 assert_eq!(steps[0].status.state, StepStatus::Completed);
2188 }
2189
2190 #[tokio::test]
2191 async fn context_load_replay_steps_loads_completed_steps() {
2192 let store = Arc::new(InMemoryStore::new());
2193 let provider = create_test_provider();
2194
2195 store
2197 .create_run(NewRun {
2198 created_by: None,
2199 workflow_name: "test".to_string(),
2200 trigger: TriggerKind::Manual,
2201 payload: json!({}),
2202 max_retries: 0,
2203 handler_version: None,
2204 labels: Default::default(),
2205 scheduled_at: None,
2206 idempotency_key: None,
2207 max_cost_usd: None,
2208 })
2209 .await
2210 .expect("failed to create run")
2211 .into_run();
2212
2213 let runs = store
2215 .list_runs(RunFilter::default(), 1, 10)
2216 .await
2217 .expect("failed to list runs");
2218 let created_run_id = runs.items[0].id;
2219
2220 let completed_step = store
2222 .create_step(NewStep {
2223 run_id: created_run_id,
2224 name: "completed".to_string(),
2225 kind: StepKind::Shell,
2226 position: 0,
2227 input: None,
2228 })
2229 .await
2230 .expect("failed to create step");
2231
2232 store
2234 .update_step(
2235 completed_step.id,
2236 StepUpdate {
2237 status: Some(StepStatus::Running),
2238 started_at: Some(Utc::now()),
2239 ..StepUpdate::default()
2240 },
2241 )
2242 .await
2243 .expect("failed to update step to Running");
2244
2245 store
2246 .update_step(
2247 completed_step.id,
2248 StepUpdate {
2249 status: Some(StepStatus::Completed),
2250 completed_at: Some(Utc::now()),
2251 ..StepUpdate::default()
2252 },
2253 )
2254 .await
2255 .expect("failed to update step to Completed");
2256
2257 let _pending_step = store
2258 .create_step(NewStep {
2259 run_id: created_run_id,
2260 name: "pending".to_string(),
2261 kind: StepKind::Shell,
2262 position: 1,
2263 input: None,
2264 })
2265 .await
2266 .expect("failed to create step");
2267
2268 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2270 ctx.load_replay_steps()
2271 .await
2272 .expect("failed to load replay steps");
2273
2274 assert_eq!(ctx.replay_steps.len(), 1);
2276 assert!(ctx.replay_steps.contains_key(&0));
2277 assert!(!ctx.replay_steps.contains_key(&1));
2278 }
2279
2280 #[tokio::test]
2281 async fn context_payload_returns_run_payload() {
2282 let store = Arc::new(InMemoryStore::new());
2283 let provider = create_test_provider();
2284 let test_payload = json!({"key": "value", "number": 42});
2285
2286 store
2288 .create_run(NewRun {
2289 created_by: None,
2290 workflow_name: "test".to_string(),
2291 trigger: TriggerKind::Manual,
2292 payload: test_payload.clone(),
2293 max_retries: 0,
2294 handler_version: None,
2295 labels: Default::default(),
2296 scheduled_at: None,
2297 idempotency_key: None,
2298 max_cost_usd: None,
2299 })
2300 .await
2301 .expect("failed to create run")
2302 .into_run();
2303
2304 let runs = store
2306 .list_runs(RunFilter::default(), 1, 10)
2307 .await
2308 .expect("failed to list runs");
2309 let created_run_id = runs.items[0].id;
2310
2311 let ctx = WorkflowContext::new(created_run_id, store, provider);
2312 let payload = ctx.payload().await.expect("failed to get payload");
2313
2314 assert_eq!(payload, test_payload);
2315 }
2316
2317 #[tokio::test]
2318 async fn context_payload_returns_error_for_nonexistent_run() {
2319 let store = Arc::new(InMemoryStore::new());
2320 let provider = create_test_provider();
2321 let run_id = Uuid::now_v7();
2322
2323 let ctx = WorkflowContext::new(run_id, store, provider);
2324 let result = ctx.payload().await;
2325
2326 assert!(result.is_err());
2327 }
2328
2329 #[tokio::test]
2330 async fn context_store_returns_reference() {
2331 let ctx = create_test_context();
2332 let _store = ctx.store();
2333 }
2335
2336 #[test]
2337 fn context_debug_formatting() {
2338 let ctx = create_test_context();
2339 let debug_str = format!("{:?}", ctx);
2340 assert!(debug_str.contains("WorkflowContext"));
2341 assert!(debug_str.contains("run_id"));
2342 }
2343
2344 #[tokio::test]
2345 async fn context_last_step_ids_tracks_executed_steps() {
2346 let store = Arc::new(InMemoryStore::new());
2347 let provider = create_test_provider();
2348
2349 store
2351 .create_run(NewRun {
2352 created_by: None,
2353 workflow_name: "test".to_string(),
2354 trigger: TriggerKind::Manual,
2355 payload: json!({}),
2356 max_retries: 0,
2357 handler_version: None,
2358 labels: Default::default(),
2359 scheduled_at: None,
2360 idempotency_key: None,
2361 max_cost_usd: None,
2362 })
2363 .await
2364 .expect("failed to create run")
2365 .into_run();
2366
2367 let runs = store
2369 .list_runs(RunFilter::default(), 1, 10)
2370 .await
2371 .expect("failed to list runs");
2372 let created_run_id = runs.items[0].id;
2373
2374 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
2375 assert!(ctx.last_step_ids.is_empty());
2376
2377 ctx.skip("step1", "reason").await.expect("skip failed");
2378
2379 assert_eq!(ctx.last_step_ids.len(), 1);
2380
2381 ctx.skip("step2", "reason").await.expect("skip failed");
2382
2383 assert_eq!(ctx.last_step_ids.len(), 1);
2385 }
2386}