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;
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 log_sender: Option<LogSender>,
100}
101
102impl WorkflowContext {
103 pub fn new(run_id: Uuid, store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self {
108 Self {
109 run_id,
110 store,
111 provider,
112 handler_resolver: None,
113 position: 0,
114 last_step_ids: Vec::new(),
115 total_cost_usd: Decimal::ZERO,
116 total_duration_ms: 0,
117 max_cost_usd: None,
118 inherited_cost_usd: Decimal::ZERO,
119 replay_steps: HashMap::new(),
120 log_sender: None,
121 }
122 }
123
124 pub(crate) fn with_handler_resolver(
129 run_id: Uuid,
130 store: Arc<dyn Store>,
131 provider: Arc<dyn AgentProvider>,
132 resolver: HandlerResolver,
133 ) -> Self {
134 Self {
135 run_id,
136 store,
137 provider,
138 handler_resolver: Some(resolver),
139 position: 0,
140 last_step_ids: Vec::new(),
141 total_cost_usd: Decimal::ZERO,
142 total_duration_ms: 0,
143 max_cost_usd: None,
144 inherited_cost_usd: Decimal::ZERO,
145 replay_steps: HashMap::new(),
146 log_sender: None,
147 }
148 }
149
150 pub fn set_log_sender(&mut self, sender: LogSender) {
152 self.log_sender = Some(sender);
153 }
154
155 pub fn set_max_cost_usd(&mut self, cap: Option<Decimal>) {
171 self.max_cost_usd = cap;
172 }
173
174 pub fn max_cost_usd(&self) -> Option<Decimal> {
176 self.max_cost_usd
177 }
178
179 pub fn charged_cost_usd(&self) -> Decimal {
184 self.inherited_cost_usd + self.total_cost_usd
185 }
186
187 fn check_run_budget(&self, step_budget: Decimal) -> Result<(), EngineError> {
198 let Some(limit) = self.max_cost_usd else {
199 return Ok(());
200 };
201
202 let spent = self.charged_cost_usd();
203 if spent + step_budget <= limit {
204 return Ok(());
205 }
206
207 error!(
208 run_id = %self.run_id,
209 limit_usd = %limit,
210 spent_usd = %spent,
211 step_budget_usd = %step_budget,
212 "run cost cap reached, refusing agent step"
213 );
214
215 Err(EngineError::RunBudgetExceeded {
216 run_id: self.run_id,
217 limit_usd: limit,
218 spent_usd: spent,
219 step_budget_usd: step_budget,
220 })
221 }
222
223 pub(crate) async fn load_replay_steps(&mut self) -> Result<(), EngineError> {
229 let steps = self.store.list_steps(self.run_id).await?;
230 for step in steps {
231 let dominated = matches!(
232 step.status.state,
233 StepStatus::Completed | StepStatus::Running | StepStatus::AwaitingApproval
234 );
235 if dominated {
236 self.replay_steps.insert(step.position, step);
237 }
238 }
239 Ok(())
240 }
241
242 pub fn run_id(&self) -> Uuid {
244 self.run_id
245 }
246
247 pub fn total_cost_usd(&self) -> Decimal {
249 self.total_cost_usd
250 }
251
252 pub fn total_duration_ms(&self) -> u64 {
254 self.total_duration_ms
255 }
256
257 pub async fn parallel(
294 &mut self,
295 steps: Vec<(&str, StepConfig)>,
296 fail_fast: bool,
297 ) -> Result<Vec<ParallelStepResult>, EngineError> {
298 if steps.is_empty() {
299 return Ok(Vec::new());
300 }
301
302 let wave_budget: Decimal = steps
305 .iter()
306 .filter_map(|(_, config)| match config {
307 StepConfig::Agent(agent_config) => Some(agent_config.max_budget_usd),
308 _ => None,
309 })
310 .map(step_budget_usd)
311 .sum();
312 self.check_run_budget(wave_budget)?;
313
314 let wave_position = self.position;
315 self.position += 1;
316
317 let now = Utc::now();
318 let mut step_records: Vec<(Uuid, String, StepConfig)> = Vec::with_capacity(steps.len());
319
320 for (name, config) in &steps {
321 let kind = config.kind();
322 let step = self
323 .store
324 .create_step(NewStep {
325 run_id: self.run_id,
326 name: name.to_string(),
327 kind,
328 position: wave_position,
329 input: Some(serde_json::to_value(config)?),
330 })
331 .await?;
332
333 self.start_step(step.id, now).await?;
334
335 step_records.push((step.id, name.to_string(), config.clone()));
336 }
337
338 let mut join_set = JoinSet::new();
339 let mut task_index: HashMap<Id, usize> = HashMap::new();
340 for (idx, (step_id, step_name, config)) in step_records.iter().enumerate() {
341 let provider = self.provider.clone();
342 let config = config.clone();
343 let step_log_sender = self
344 .log_sender
345 .as_ref()
346 .map(|s| StepLogSender::new(s.clone(), self.run_id, *step_id, step_name.clone()));
347 let handle = join_set.spawn(async move {
348 (
349 idx,
350 execute_step_config(&config, &provider, step_log_sender).await,
351 )
352 });
353 task_index.insert(handle.id(), idx);
354 }
355
356 let mut indexed_results: Vec<Option<Result<StepOutput, String>>> =
358 vec![None; step_records.len()];
359 let mut first_error: Option<EngineError> = None;
360
361 while let Some(join_result) = join_set.join_next().await {
362 let (idx, step_result) = match join_result {
363 Ok(r) => r,
364 Err(e) => {
365 let error_msg = format!("join error: {e}");
366 if let Some(&idx) = task_index.get(&e.id()) {
367 let (step_id, step_name, _) = &step_records[idx];
368 let completed_at = Utc::now();
369 error!(
370 run_id = %self.run_id,
371 step = %step_name,
372 error = %error_msg,
373 "parallel step panicked or was cancelled"
374 );
375 if let Err(store_err) = self
376 .store
377 .update_step(
378 *step_id,
379 StepUpdate {
380 status: Some(StepStatus::Failed),
381 error: Some(error_msg.clone()),
382 completed_at: Some(completed_at),
383 ..StepUpdate::default()
384 },
385 )
386 .await
387 {
388 error!(
389 run_id = %self.run_id,
390 step_id = %step_id,
391 error = %store_err,
392 "failed to persist JoinError for step"
393 );
394 }
395 indexed_results[idx] = Some(Err(error_msg.clone()));
396 }
397 if first_error.is_none() {
398 first_error = Some(EngineError::StepConfig(error_msg));
399 }
400 if fail_fast {
401 join_set.abort_all();
402 }
403 continue;
404 }
405 };
406
407 let (step_id, step_name, _) = &step_records[idx];
408 let completed_at = Utc::now();
409
410 match step_result {
411 Ok(output) => {
412 self.total_cost_usd += output.cost_usd;
413 self.total_duration_ms += output.duration_ms;
414
415 let debug_messages_json = output.debug_messages_json();
416
417 self.store
418 .update_step(
419 *step_id,
420 StepUpdate {
421 status: Some(StepStatus::Completed),
422 output: Some(output.output.clone()),
423 duration_ms: Some(output.duration_ms),
424 cost_usd: Some(output.cost_usd),
425 input_tokens: output.input_tokens,
426 output_tokens: output.output_tokens,
427 completed_at: Some(completed_at),
428 debug_messages: debug_messages_json,
429 ..StepUpdate::default()
430 },
431 )
432 .await?;
433
434 info!(
435 run_id = %self.run_id,
436 step = %step_name,
437 duration_ms = output.duration_ms,
438 "parallel step completed"
439 );
440
441 indexed_results[idx] = Some(Ok(output));
442 }
443 Err(err) => {
444 let err_msg = err.to_string();
445 let debug_messages_json = extract_debug_messages_from_error(&err);
446 let partial = extract_partial_usage_from_error(&err);
447 let raw_response_output = extract_raw_response_from_error(&err);
448
449 if let Some(ref usage) = partial {
450 if let Some(cost) = usage.cost_usd {
451 self.total_cost_usd += cost;
452 }
453 if let Some(dur) = usage.duration_ms {
454 self.total_duration_ms += dur;
455 }
456 }
457
458 if let Err(store_err) = self
459 .store
460 .update_step(
461 *step_id,
462 StepUpdate {
463 status: Some(StepStatus::Failed),
464 error: Some(err_msg.clone()),
465 output: raw_response_output,
466 completed_at: Some(completed_at),
467 debug_messages: debug_messages_json,
468 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
469 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
470 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
471 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
472 ..StepUpdate::default()
473 },
474 )
475 .await
476 {
477 tracing::error!(
478 step_id = %step_id,
479 error = %store_err,
480 "failed to persist parallel step failure"
481 );
482 }
483
484 indexed_results[idx] = Some(Err(err_msg.clone()));
485
486 if first_error.is_none() {
487 first_error = Some(err);
488 }
489
490 if fail_fast {
491 join_set.abort_all();
492 }
493 }
494 }
495 }
496
497 if let Some(err) = first_error {
498 return Err(err);
499 }
500
501 self.last_step_ids = step_records.iter().map(|(id, _, _)| *id).collect();
502
503 let results: Vec<ParallelStepResult> = step_records
505 .iter()
506 .enumerate()
507 .map(|(idx, (step_id, name, _))| {
508 let output = match indexed_results[idx].take() {
509 Some(Ok(o)) => o,
510 _ => unreachable!("all steps succeeded if no error returned"),
511 };
512 ParallelStepResult {
513 name: name.clone(),
514 output,
515 step_id: *step_id,
516 }
517 })
518 .collect();
519
520 Ok(results)
521 }
522
523 pub async fn shell(
546 &mut self,
547 name: &str,
548 config: ShellConfig,
549 ) -> Result<StepOutput, EngineError> {
550 self.execute_step(name, StepKind::Shell, StepConfig::Shell(config))
551 .await
552 }
553
554 pub async fn http(
574 &mut self,
575 name: &str,
576 config: HttpConfig,
577 ) -> Result<StepOutput, EngineError> {
578 self.execute_step(name, StepKind::Http, StepConfig::Http(config))
579 .await
580 }
581
582 pub async fn agent(
602 &mut self,
603 name: &str,
604 config: impl Into<AgentStepConfig>,
605 ) -> Result<StepOutput, EngineError> {
606 self.execute_step(name, StepKind::Agent, StepConfig::Agent(config.into()))
607 .await
608 }
609
610 pub async fn approval(
641 &mut self,
642 name: &str,
643 config: ApprovalConfig,
644 ) -> Result<(), EngineError> {
645 let position = self.position;
646 self.position += 1;
647
648 if let Some(existing) = self.replay_steps.get(&position)
651 && existing.kind == StepKind::Approval
652 {
653 if existing.status.state == StepStatus::AwaitingApproval {
654 self.store
655 .update_step(
656 existing.id,
657 StepUpdate {
658 status: Some(StepStatus::Completed),
659 completed_at: Some(Utc::now()),
660 ..StepUpdate::default()
661 },
662 )
663 .await?;
664 }
665
666 self.last_step_ids = vec![existing.id];
667 info!(
668 run_id = %self.run_id,
669 step = %name,
670 position,
671 "approval step replayed (approved)"
672 );
673 return Ok(());
674 }
675
676 let step = self
678 .store
679 .create_step(NewStep {
680 run_id: self.run_id,
681 name: name.to_string(),
682 kind: StepKind::Approval,
683 position,
684 input: Some(serde_json::to_value(&config)?),
685 })
686 .await?;
687
688 self.start_step(step.id, Utc::now()).await?;
689
690 self.store
693 .update_step(
694 step.id,
695 StepUpdate {
696 status: Some(StepStatus::AwaitingApproval),
697 ..StepUpdate::default()
698 },
699 )
700 .await?;
701
702 self.last_step_ids = vec![step.id];
703
704 Err(EngineError::ApprovalRequired {
705 run_id: self.run_id,
706 step_id: step.id,
707 message: config.message().to_string(),
708 })
709 }
710
711 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
740 let position = self.position;
741 self.position += 1;
742
743 let step = self
744 .store
745 .create_step(NewStep {
746 run_id: self.run_id,
747 name: name.to_string(),
748 kind: StepKind::Custom("skip".to_string()),
749 position,
750 input: None,
751 })
752 .await?;
753
754 if !self.last_step_ids.is_empty() {
755 let deps: Vec<NewStepDependency> = self
756 .last_step_ids
757 .iter()
758 .map(|&depends_on| NewStepDependency {
759 step_id: step.id,
760 depends_on,
761 })
762 .collect();
763 self.store.create_step_dependencies(deps).await?;
764 }
765
766 let now = Utc::now();
767 self.store
768 .update_step(
769 step.id,
770 StepUpdate {
771 status: Some(StepStatus::Skipped),
772 output: Some(serde_json::json!({"reason": reason})),
773 completed_at: Some(now),
774 ..StepUpdate::default()
775 },
776 )
777 .await?;
778
779 self.last_step_ids = vec![step.id];
780
781 info!(
782 run_id = %self.run_id,
783 step = %name,
784 reason,
785 "step skipped"
786 );
787
788 Ok(())
789 }
790
791 pub async fn operation(
829 &mut self,
830 name: &str,
831 op: &dyn Operation,
832 ) -> Result<StepOutput, EngineError> {
833 let kind = StepKind::Custom(op.kind().to_string());
834 let position = self.position;
835 self.position += 1;
836
837 let step = self
838 .store
839 .create_step(NewStep {
840 run_id: self.run_id,
841 name: name.to_string(),
842 kind,
843 position,
844 input: op.input(),
845 })
846 .await?;
847
848 self.start_step(step.id, Utc::now()).await?;
849
850 let start = Instant::now();
851
852 match op.execute().await {
853 Ok(output_value) => {
854 let duration_ms = start.elapsed().as_millis() as u64;
855 self.total_duration_ms += duration_ms;
856
857 let completed_at = Utc::now();
858 self.store
859 .update_step(
860 step.id,
861 StepUpdate {
862 status: Some(StepStatus::Completed),
863 output: Some(output_value.clone()),
864 duration_ms: Some(duration_ms),
865 cost_usd: Some(Decimal::ZERO),
866 completed_at: Some(completed_at),
867 ..StepUpdate::default()
868 },
869 )
870 .await?;
871
872 info!(
873 run_id = %self.run_id,
874 step = %name,
875 kind = op.kind(),
876 duration_ms,
877 "operation step completed"
878 );
879
880 self.last_step_ids = vec![step.id];
881
882 Ok(StepOutput {
883 output: output_value,
884 duration_ms,
885 cost_usd: Decimal::ZERO,
886 input_tokens: None,
887 output_tokens: None,
888 model: None,
889 debug_messages: None,
890 })
891 }
892 Err(err) => {
893 let completed_at = Utc::now();
894 if let Err(store_err) = self
895 .store
896 .update_step(
897 step.id,
898 StepUpdate {
899 status: Some(StepStatus::Failed),
900 error: Some(err.to_string()),
901 completed_at: Some(completed_at),
902 ..StepUpdate::default()
903 },
904 )
905 .await
906 {
907 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
908 }
909
910 Err(err)
911 }
912 }
913 }
914
915 pub async fn workflow(
942 &mut self,
943 handler: &dyn WorkflowHandler,
944 payload: Value,
945 ) -> Result<StepOutput, EngineError> {
946 let config = WorkflowStepConfig::new(handler.name(), payload);
947 let position = self.position;
948 self.position += 1;
949
950 let step = self
951 .store
952 .create_step(NewStep {
953 run_id: self.run_id,
954 name: config.workflow_name.clone(),
955 kind: StepKind::Workflow,
956 position,
957 input: Some(serde_json::to_value(&config)?),
958 })
959 .await?;
960
961 self.start_step(step.id, Utc::now()).await?;
962
963 match self.execute_child_workflow(&config).await {
964 Ok(output) => {
965 self.total_cost_usd += output.cost_usd;
966 self.total_duration_ms += output.duration_ms;
967
968 let completed_at = Utc::now();
969 self.store
970 .update_step(
971 step.id,
972 StepUpdate {
973 status: Some(StepStatus::Completed),
974 output: Some(output.output.clone()),
975 duration_ms: Some(output.duration_ms),
976 cost_usd: Some(output.cost_usd),
977 completed_at: Some(completed_at),
978 ..StepUpdate::default()
979 },
980 )
981 .await?;
982
983 info!(
984 run_id = %self.run_id,
985 child_workflow = %config.workflow_name,
986 duration_ms = output.duration_ms,
987 "workflow step completed"
988 );
989
990 self.last_step_ids = vec![step.id];
991
992 Ok(output)
993 }
994 Err(err) => {
995 let completed_at = Utc::now();
996 if let Err(store_err) = self
997 .store
998 .update_step(
999 step.id,
1000 StepUpdate {
1001 status: Some(StepStatus::Failed),
1002 error: Some(err.to_string()),
1003 completed_at: Some(completed_at),
1004 ..StepUpdate::default()
1005 },
1006 )
1007 .await
1008 {
1009 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1010 }
1011
1012 Err(err)
1013 }
1014 }
1015 }
1016
1017 async fn execute_child_workflow(
1019 &self,
1020 config: &WorkflowStepConfig,
1021 ) -> Result<StepOutput, EngineError> {
1022 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
1023 EngineError::InvalidWorkflow(
1024 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
1025 )
1026 })?;
1027
1028 let handler = resolver(&config.workflow_name).ok_or_else(|| {
1029 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
1030 })?;
1031
1032 let parent_labels = self
1033 .store
1034 .get_run(self.run_id)
1035 .await?
1036 .map(|r| r.labels)
1037 .unwrap_or_default();
1038
1039 let child_run = self
1040 .store
1041 .create_run(NewRun {
1042 workflow_name: config.workflow_name.clone(),
1043 trigger: TriggerKind::Workflow,
1044 payload: config.payload.clone(),
1045 max_retries: 0,
1046 handler_version: None,
1047 labels: parent_labels,
1048 scheduled_at: None,
1049 idempotency_key: None,
1050 max_cost_usd: self.max_cost_usd,
1052 })
1053 .await?
1054 .into_run();
1055
1056 let child_run_id = child_run.id;
1057 info!(
1058 parent_run_id = %self.run_id,
1059 child_run_id = %child_run_id,
1060 workflow = %config.workflow_name,
1061 "child run created"
1062 );
1063
1064 self.store
1065 .update_run_status(child_run_id, RunStatus::Running)
1066 .await?;
1067
1068 let run_start = Instant::now();
1069 let mut child_ctx = WorkflowContext {
1070 run_id: child_run_id,
1071 store: self.store.clone(),
1072 provider: self.provider.clone(),
1073 handler_resolver: self.handler_resolver.clone(),
1074 position: 0,
1075 last_step_ids: Vec::new(),
1076 total_cost_usd: Decimal::ZERO,
1077 total_duration_ms: 0,
1078 max_cost_usd: self.max_cost_usd,
1079 inherited_cost_usd: self.charged_cost_usd(),
1082 replay_steps: HashMap::new(),
1083 log_sender: self.log_sender.clone(),
1084 };
1085
1086 let result = handler.execute(&mut child_ctx).await;
1087 let total_duration = run_start.elapsed().as_millis() as u64;
1088 let completed_at = Utc::now();
1089
1090 match result {
1091 Ok(()) => {
1092 self.store
1093 .update_run(
1094 child_run_id,
1095 RunUpdate {
1096 status: Some(RunStatus::Completed),
1097 cost_usd: Some(child_ctx.total_cost_usd),
1098 duration_ms: Some(total_duration),
1099 completed_at: Some(completed_at),
1100 ..RunUpdate::default()
1101 },
1102 )
1103 .await?;
1104
1105 Ok(StepOutput {
1106 output: serde_json::json!({
1107 "run_id": child_run_id,
1108 "workflow_name": config.workflow_name,
1109 "status": RunStatus::Completed,
1110 "cost_usd": child_ctx.total_cost_usd,
1111 "duration_ms": total_duration,
1112 }),
1113 duration_ms: total_duration,
1114 cost_usd: child_ctx.total_cost_usd,
1115 input_tokens: None,
1116 output_tokens: None,
1117 model: None,
1118 debug_messages: None,
1119 })
1120 }
1121 Err(err) => {
1122 if let Err(store_err) = self
1123 .store
1124 .update_run(
1125 child_run_id,
1126 RunUpdate {
1127 status: Some(RunStatus::Failed),
1128 error: Some(err.to_string()),
1129 cost_usd: Some(child_ctx.total_cost_usd),
1130 duration_ms: Some(total_duration),
1131 completed_at: Some(completed_at),
1132 ..RunUpdate::default()
1133 },
1134 )
1135 .await
1136 {
1137 error!(
1138 child_run_id = %child_run_id,
1139 store_error = %store_err,
1140 "failed to persist child run failure"
1141 );
1142 }
1143
1144 Err(err)
1145 }
1146 }
1147 }
1148
1149 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1154 let step = self.replay_steps.get(&position)?;
1155 if step.status.state != StepStatus::Completed {
1156 return None;
1157 }
1158 let output = StepOutput {
1159 output: step.output.clone().unwrap_or(Value::Null),
1160 duration_ms: step.duration_ms,
1161 cost_usd: step.cost_usd,
1162 input_tokens: step.input_tokens,
1163 output_tokens: step.output_tokens,
1164 model: None,
1165 debug_messages: None,
1166 };
1167 self.total_cost_usd += output.cost_usd;
1168 self.total_duration_ms += output.duration_ms;
1169 self.last_step_ids = vec![step.id];
1170 info!(
1171 run_id = %self.run_id,
1172 step = %step.name,
1173 position,
1174 "step replayed from previous execution"
1175 );
1176 Some(output)
1177 }
1178
1179 async fn execute_step(
1181 &mut self,
1182 name: &str,
1183 kind: StepKind,
1184 config: StepConfig,
1185 ) -> Result<StepOutput, EngineError> {
1186 let position = self.position;
1187 self.position += 1;
1188
1189 if let Some(output) = self.try_replay_step(position) {
1191 return Ok(output);
1192 }
1193
1194 if let StepConfig::Agent(ref agent_config) = config {
1197 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1198 }
1199
1200 let step = self
1202 .store
1203 .create_step(NewStep {
1204 run_id: self.run_id,
1205 name: name.to_string(),
1206 kind,
1207 position,
1208 input: Some(serde_json::to_value(&config)?),
1209 })
1210 .await?;
1211
1212 self.start_step(step.id, Utc::now()).await?;
1213
1214 let step_log_sender = self
1215 .log_sender
1216 .as_ref()
1217 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1218
1219 match execute_step_config(&config, &self.provider, step_log_sender).await {
1220 Ok(output) => {
1221 self.total_cost_usd += output.cost_usd;
1222 self.total_duration_ms += output.duration_ms;
1223
1224 let debug_messages_json = output.debug_messages_json();
1225
1226 let completed_at = Utc::now();
1227 self.store
1228 .update_step(
1229 step.id,
1230 StepUpdate {
1231 status: Some(StepStatus::Completed),
1232 output: Some(output.output.clone()),
1233 duration_ms: Some(output.duration_ms),
1234 cost_usd: Some(output.cost_usd),
1235 input_tokens: output.input_tokens,
1236 output_tokens: output.output_tokens,
1237 completed_at: Some(completed_at),
1238 debug_messages: debug_messages_json,
1239 ..StepUpdate::default()
1240 },
1241 )
1242 .await?;
1243
1244 info!(
1245 run_id = %self.run_id,
1246 step = %name,
1247 duration_ms = output.duration_ms,
1248 "step completed"
1249 );
1250
1251 self.last_step_ids = vec![step.id];
1252
1253 Ok(output)
1254 }
1255 Err(err) => {
1256 let completed_at = Utc::now();
1257 let debug_messages_json = extract_debug_messages_from_error(&err);
1258 let partial = extract_partial_usage_from_error(&err);
1259 let raw_response_output = extract_raw_response_from_error(&err);
1260
1261 if let Some(ref usage) = partial {
1262 if let Some(cost) = usage.cost_usd {
1263 self.total_cost_usd += cost;
1264 }
1265 if let Some(dur) = usage.duration_ms {
1266 self.total_duration_ms += dur;
1267 }
1268 }
1269
1270 if let Err(store_err) = self
1271 .store
1272 .update_step(
1273 step.id,
1274 StepUpdate {
1275 status: Some(StepStatus::Failed),
1276 error: Some(err.to_string()),
1277 output: raw_response_output,
1278 completed_at: Some(completed_at),
1279 debug_messages: debug_messages_json,
1280 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1281 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1282 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1283 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1284 ..StepUpdate::default()
1285 },
1286 )
1287 .await
1288 {
1289 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1290 }
1291
1292 Err(err)
1293 }
1294 }
1295 }
1296
1297 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1302 if !self.last_step_ids.is_empty() {
1303 let deps: Vec<NewStepDependency> = self
1304 .last_step_ids
1305 .iter()
1306 .map(|&depends_on| NewStepDependency {
1307 step_id,
1308 depends_on,
1309 })
1310 .collect();
1311 self.store.create_step_dependencies(deps).await?;
1312 }
1313
1314 self.store
1315 .update_step(
1316 step_id,
1317 StepUpdate {
1318 status: Some(StepStatus::Running),
1319 started_at: Some(now),
1320 ..StepUpdate::default()
1321 },
1322 )
1323 .await?;
1324
1325 Ok(())
1326 }
1327
1328 pub fn store(&self) -> &Arc<dyn Store> {
1330 &self.store
1331 }
1332
1333 pub async fn payload(&self) -> Result<Value, EngineError> {
1341 let run = self
1342 .store
1343 .get_run(self.run_id)
1344 .await?
1345 .ok_or(EngineError::Store(
1346 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1347 ))?;
1348 Ok(run.payload)
1349 }
1350}
1351
1352impl fmt::Debug for WorkflowContext {
1353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1354 f.debug_struct("WorkflowContext")
1355 .field("run_id", &self.run_id)
1356 .field("position", &self.position)
1357 .field("total_cost_usd", &self.total_cost_usd)
1358 .field("inherited_cost_usd", &self.inherited_cost_usd)
1359 .field("max_cost_usd", &self.max_cost_usd)
1360 .finish_non_exhaustive()
1361 }
1362}
1363
1364fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
1367 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1368 debug_messages,
1369 ..
1370 })) = err
1371 && !debug_messages.is_empty()
1372 {
1373 return serde_json::to_value(debug_messages).ok();
1374 }
1375 None
1376}
1377
1378struct StepPartialUsage {
1384 cost_usd: Option<Decimal>,
1385 duration_ms: Option<u64>,
1386 input_tokens: Option<u64>,
1387 output_tokens: Option<u64>,
1388}
1389
1390fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
1396 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1397 raw_response: Some(text),
1398 ..
1399 })) = err
1400 {
1401 return Some(Value::String(text.clone()));
1402 }
1403 None
1404}
1405
1406fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
1407 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1408 partial_usage,
1409 ..
1410 })) = err
1411 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
1412 {
1413 return Some(StepPartialUsage {
1414 cost_usd: partial_usage
1415 .cost_usd
1416 .and_then(|c| Decimal::try_from(c).ok()),
1417 duration_ms: partial_usage.duration_ms,
1418 input_tokens: partial_usage.input_tokens,
1419 output_tokens: partial_usage.output_tokens,
1420 });
1421 }
1422 None
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427 use super::*;
1428 use ironflow_core::providers::claude::ClaudeCodeProvider;
1429 use ironflow_core::providers::record_replay::RecordReplayProvider;
1430 use ironflow_store::memory::InMemoryStore;
1431 use ironflow_store::models::RunFilter;
1432 use ironflow_store::store::RunStore;
1433 use serde_json::json;
1434 use std::sync::Arc;
1435 use std::sync::atomic::{AtomicBool, Ordering};
1436 use uuid::Uuid;
1437
1438 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
1440 let inner = ClaudeCodeProvider::new();
1441 Arc::new(RecordReplayProvider::replay(
1442 inner,
1443 "/tmp/ironflow-fixtures",
1444 ))
1445 }
1446
1447 fn create_test_context() -> WorkflowContext {
1449 let store = Arc::new(InMemoryStore::new());
1450 let provider = create_test_provider();
1451 let run_id = Uuid::now_v7();
1452 WorkflowContext::new(run_id, store, provider)
1453 }
1454
1455 #[test]
1456 fn context_new_initializes_correctly() {
1457 let ctx = create_test_context();
1458 assert_eq!(ctx.position, 0);
1459 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
1460 assert_eq!(ctx.total_duration_ms, 0);
1461 assert!(ctx.last_step_ids.is_empty());
1462 assert!(ctx.replay_steps.is_empty());
1463 assert!(ctx.log_sender.is_none());
1464 }
1465
1466 #[test]
1467 fn context_run_id_returns_correct_id() {
1468 let run_id = Uuid::now_v7();
1469 let store = Arc::new(InMemoryStore::new());
1470 let provider = create_test_provider();
1471 let ctx = WorkflowContext::new(run_id, store, provider);
1472 assert_eq!(ctx.run_id(), run_id);
1473 }
1474
1475 #[test]
1476 fn context_total_cost_usd_initially_zero() {
1477 let ctx = create_test_context();
1478 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
1479 }
1480
1481 #[test]
1482 fn context_total_duration_ms_initially_zero() {
1483 let ctx = create_test_context();
1484 assert_eq!(ctx.total_duration_ms(), 0);
1485 }
1486
1487 #[test]
1488 fn context_with_handler_resolver_creates_context_with_resolver() {
1489 let store = Arc::new(InMemoryStore::new());
1490 let provider = create_test_provider();
1491 let run_id = Uuid::now_v7();
1492
1493 let called = Arc::new(AtomicBool::new(false));
1494 let called_clone = called.clone();
1495
1496 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
1497 called_clone.store(true, Ordering::SeqCst);
1498 None
1499 });
1500
1501 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
1502
1503 assert_eq!(ctx.run_id(), run_id);
1504 assert!(ctx.handler_resolver.is_some());
1505 }
1506
1507 #[tokio::test]
1508 async fn context_set_log_sender_attaches_sender() {
1509 let mut ctx = create_test_context();
1510 let (sender, _receiver) = crate::log_sender::channel();
1511 ctx.set_log_sender(sender);
1512 assert!(ctx.log_sender.is_some());
1513 }
1514
1515 #[tokio::test]
1516 async fn context_skip_creates_skipped_step() {
1517 let store = Arc::new(InMemoryStore::new());
1518 let provider = create_test_provider();
1519
1520 store
1522 .create_run(NewRun {
1523 workflow_name: "test".to_string(),
1524 trigger: TriggerKind::Manual,
1525 payload: json!({}),
1526 max_retries: 0,
1527 handler_version: None,
1528 labels: Default::default(),
1529 scheduled_at: None,
1530 idempotency_key: None,
1531 max_cost_usd: None,
1532 })
1533 .await
1534 .expect("failed to create run")
1535 .into_run();
1536
1537 let runs = store
1539 .list_runs(RunFilter::default(), 1, 10)
1540 .await
1541 .expect("failed to list runs");
1542 let created_run_id = runs.items[0].id;
1543
1544 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1545 let initial_position = ctx.position;
1546
1547 ctx.skip("skip-step", "condition not met")
1548 .await
1549 .expect("skip failed");
1550
1551 assert_eq!(ctx.position, initial_position + 1);
1552 assert!(!ctx.last_step_ids.is_empty());
1553
1554 let steps = store
1556 .list_steps(created_run_id)
1557 .await
1558 .expect("failed to list steps");
1559 assert_eq!(steps.len(), 1);
1560 assert_eq!(steps[0].status.state, StepStatus::Skipped);
1561 }
1562
1563 #[tokio::test]
1564 async fn context_parallel_empty_steps_returns_empty_vec() {
1565 let mut ctx = create_test_context();
1566 let results = ctx
1567 .parallel(vec![], true)
1568 .await
1569 .expect("parallel should not fail on empty input");
1570 assert!(results.is_empty());
1571 }
1572
1573 #[tokio::test]
1574 async fn context_approval_first_execution_returns_error() {
1575 let store = Arc::new(InMemoryStore::new());
1576 let provider = create_test_provider();
1577
1578 store
1580 .create_run(NewRun {
1581 workflow_name: "test".to_string(),
1582 trigger: TriggerKind::Manual,
1583 payload: json!({}),
1584 max_retries: 0,
1585 handler_version: None,
1586 labels: Default::default(),
1587 scheduled_at: None,
1588 idempotency_key: None,
1589 max_cost_usd: None,
1590 })
1591 .await
1592 .expect("failed to create run")
1593 .into_run();
1594
1595 let runs = store
1597 .list_runs(RunFilter::default(), 1, 10)
1598 .await
1599 .expect("failed to list runs");
1600 let created_run_id = runs.items[0].id;
1601
1602 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1603
1604 let result = ctx
1605 .approval(
1606 "approve-step",
1607 crate::config::ApprovalConfig::new("Continue?"),
1608 )
1609 .await;
1610
1611 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
1613
1614 assert_eq!(ctx.position, 1);
1616
1617 let steps = store
1619 .list_steps(created_run_id)
1620 .await
1621 .expect("failed to list steps");
1622 assert_eq!(steps.len(), 1);
1623 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
1624 }
1625
1626 #[tokio::test]
1627 async fn context_approval_replay_returns_ok() {
1628 let store = Arc::new(InMemoryStore::new());
1629 let provider = create_test_provider();
1630
1631 store
1633 .create_run(NewRun {
1634 workflow_name: "test".to_string(),
1635 trigger: TriggerKind::Manual,
1636 payload: json!({}),
1637 max_retries: 0,
1638 handler_version: None,
1639 labels: Default::default(),
1640 scheduled_at: None,
1641 idempotency_key: None,
1642 max_cost_usd: None,
1643 })
1644 .await
1645 .expect("failed to create run")
1646 .into_run();
1647
1648 let runs = store
1650 .list_runs(RunFilter::default(), 1, 10)
1651 .await
1652 .expect("failed to list runs");
1653 let created_run_id = runs.items[0].id;
1654
1655 let step = store
1657 .create_step(NewStep {
1658 run_id: created_run_id,
1659 name: "approval".to_string(),
1660 kind: StepKind::Approval,
1661 position: 0,
1662 input: None,
1663 })
1664 .await
1665 .expect("failed to create step");
1666
1667 store
1669 .update_step(
1670 step.id,
1671 StepUpdate {
1672 status: Some(StepStatus::Running),
1673 started_at: Some(Utc::now()),
1674 ..StepUpdate::default()
1675 },
1676 )
1677 .await
1678 .expect("failed to update step to Running");
1679
1680 store
1681 .update_step(
1682 step.id,
1683 StepUpdate {
1684 status: Some(StepStatus::AwaitingApproval),
1685 ..StepUpdate::default()
1686 },
1687 )
1688 .await
1689 .expect("failed to update step to AwaitingApproval");
1690
1691 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1693 ctx.load_replay_steps()
1694 .await
1695 .expect("failed to load replay steps");
1696
1697 let result = ctx
1699 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
1700 .await;
1701
1702 assert!(result.is_ok());
1703
1704 let steps = store
1706 .list_steps(created_run_id)
1707 .await
1708 .expect("failed to list steps");
1709 assert_eq!(steps.len(), 1);
1710 assert_eq!(steps[0].status.state, StepStatus::Completed);
1711 }
1712
1713 #[tokio::test]
1714 async fn context_load_replay_steps_loads_completed_steps() {
1715 let store = Arc::new(InMemoryStore::new());
1716 let provider = create_test_provider();
1717
1718 store
1720 .create_run(NewRun {
1721 workflow_name: "test".to_string(),
1722 trigger: TriggerKind::Manual,
1723 payload: json!({}),
1724 max_retries: 0,
1725 handler_version: None,
1726 labels: Default::default(),
1727 scheduled_at: None,
1728 idempotency_key: None,
1729 max_cost_usd: None,
1730 })
1731 .await
1732 .expect("failed to create run")
1733 .into_run();
1734
1735 let runs = store
1737 .list_runs(RunFilter::default(), 1, 10)
1738 .await
1739 .expect("failed to list runs");
1740 let created_run_id = runs.items[0].id;
1741
1742 let completed_step = store
1744 .create_step(NewStep {
1745 run_id: created_run_id,
1746 name: "completed".to_string(),
1747 kind: StepKind::Shell,
1748 position: 0,
1749 input: None,
1750 })
1751 .await
1752 .expect("failed to create step");
1753
1754 store
1756 .update_step(
1757 completed_step.id,
1758 StepUpdate {
1759 status: Some(StepStatus::Running),
1760 started_at: Some(Utc::now()),
1761 ..StepUpdate::default()
1762 },
1763 )
1764 .await
1765 .expect("failed to update step to Running");
1766
1767 store
1768 .update_step(
1769 completed_step.id,
1770 StepUpdate {
1771 status: Some(StepStatus::Completed),
1772 completed_at: Some(Utc::now()),
1773 ..StepUpdate::default()
1774 },
1775 )
1776 .await
1777 .expect("failed to update step to Completed");
1778
1779 let _pending_step = store
1780 .create_step(NewStep {
1781 run_id: created_run_id,
1782 name: "pending".to_string(),
1783 kind: StepKind::Shell,
1784 position: 1,
1785 input: None,
1786 })
1787 .await
1788 .expect("failed to create step");
1789
1790 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
1792 ctx.load_replay_steps()
1793 .await
1794 .expect("failed to load replay steps");
1795
1796 assert_eq!(ctx.replay_steps.len(), 1);
1798 assert!(ctx.replay_steps.contains_key(&0));
1799 assert!(!ctx.replay_steps.contains_key(&1));
1800 }
1801
1802 #[tokio::test]
1803 async fn context_payload_returns_run_payload() {
1804 let store = Arc::new(InMemoryStore::new());
1805 let provider = create_test_provider();
1806 let test_payload = json!({"key": "value", "number": 42});
1807
1808 store
1810 .create_run(NewRun {
1811 workflow_name: "test".to_string(),
1812 trigger: TriggerKind::Manual,
1813 payload: test_payload.clone(),
1814 max_retries: 0,
1815 handler_version: None,
1816 labels: Default::default(),
1817 scheduled_at: None,
1818 idempotency_key: None,
1819 max_cost_usd: None,
1820 })
1821 .await
1822 .expect("failed to create run")
1823 .into_run();
1824
1825 let runs = store
1827 .list_runs(RunFilter::default(), 1, 10)
1828 .await
1829 .expect("failed to list runs");
1830 let created_run_id = runs.items[0].id;
1831
1832 let ctx = WorkflowContext::new(created_run_id, store, provider);
1833 let payload = ctx.payload().await.expect("failed to get payload");
1834
1835 assert_eq!(payload, test_payload);
1836 }
1837
1838 #[tokio::test]
1839 async fn context_payload_returns_error_for_nonexistent_run() {
1840 let store = Arc::new(InMemoryStore::new());
1841 let provider = create_test_provider();
1842 let run_id = Uuid::now_v7();
1843
1844 let ctx = WorkflowContext::new(run_id, store, provider);
1845 let result = ctx.payload().await;
1846
1847 assert!(result.is_err());
1848 }
1849
1850 #[tokio::test]
1851 async fn context_store_returns_reference() {
1852 let ctx = create_test_context();
1853 let _store = ctx.store();
1854 }
1856
1857 #[test]
1858 fn context_debug_formatting() {
1859 let ctx = create_test_context();
1860 let debug_str = format!("{:?}", ctx);
1861 assert!(debug_str.contains("WorkflowContext"));
1862 assert!(debug_str.contains("run_id"));
1863 }
1864
1865 #[tokio::test]
1866 async fn context_last_step_ids_tracks_executed_steps() {
1867 let store = Arc::new(InMemoryStore::new());
1868 let provider = create_test_provider();
1869
1870 store
1872 .create_run(NewRun {
1873 workflow_name: "test".to_string(),
1874 trigger: TriggerKind::Manual,
1875 payload: json!({}),
1876 max_retries: 0,
1877 handler_version: None,
1878 labels: Default::default(),
1879 scheduled_at: None,
1880 idempotency_key: None,
1881 max_cost_usd: None,
1882 })
1883 .await
1884 .expect("failed to create run")
1885 .into_run();
1886
1887 let runs = store
1889 .list_runs(RunFilter::default(), 1, 10)
1890 .await
1891 .expect("failed to list runs");
1892 let created_run_id = runs.items[0].id;
1893
1894 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
1895 assert!(ctx.last_step_ids.is_empty());
1896
1897 ctx.skip("step1", "reason").await.expect("skip failed");
1898
1899 assert_eq!(ctx.last_step_ids.len(), 1);
1900
1901 ctx.skip("step2", "reason").await.expect("skip failed");
1902
1903 assert_eq!(ctx.last_step_ids.len(), 1);
1905 }
1906}