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 max_cost_usd: self.max_cost_usd,
1051 })
1052 .await?;
1053
1054 let child_run_id = child_run.id;
1055 info!(
1056 parent_run_id = %self.run_id,
1057 child_run_id = %child_run_id,
1058 workflow = %config.workflow_name,
1059 "child run created"
1060 );
1061
1062 self.store
1063 .update_run_status(child_run_id, RunStatus::Running)
1064 .await?;
1065
1066 let run_start = Instant::now();
1067 let mut child_ctx = WorkflowContext {
1068 run_id: child_run_id,
1069 store: self.store.clone(),
1070 provider: self.provider.clone(),
1071 handler_resolver: self.handler_resolver.clone(),
1072 position: 0,
1073 last_step_ids: Vec::new(),
1074 total_cost_usd: Decimal::ZERO,
1075 total_duration_ms: 0,
1076 max_cost_usd: self.max_cost_usd,
1077 inherited_cost_usd: self.charged_cost_usd(),
1080 replay_steps: HashMap::new(),
1081 log_sender: self.log_sender.clone(),
1082 };
1083
1084 let result = handler.execute(&mut child_ctx).await;
1085 let total_duration = run_start.elapsed().as_millis() as u64;
1086 let completed_at = Utc::now();
1087
1088 match result {
1089 Ok(()) => {
1090 self.store
1091 .update_run(
1092 child_run_id,
1093 RunUpdate {
1094 status: Some(RunStatus::Completed),
1095 cost_usd: Some(child_ctx.total_cost_usd),
1096 duration_ms: Some(total_duration),
1097 completed_at: Some(completed_at),
1098 ..RunUpdate::default()
1099 },
1100 )
1101 .await?;
1102
1103 Ok(StepOutput {
1104 output: serde_json::json!({
1105 "run_id": child_run_id,
1106 "workflow_name": config.workflow_name,
1107 "status": RunStatus::Completed,
1108 "cost_usd": child_ctx.total_cost_usd,
1109 "duration_ms": total_duration,
1110 }),
1111 duration_ms: total_duration,
1112 cost_usd: child_ctx.total_cost_usd,
1113 input_tokens: None,
1114 output_tokens: None,
1115 model: None,
1116 debug_messages: None,
1117 })
1118 }
1119 Err(err) => {
1120 if let Err(store_err) = self
1121 .store
1122 .update_run(
1123 child_run_id,
1124 RunUpdate {
1125 status: Some(RunStatus::Failed),
1126 error: Some(err.to_string()),
1127 cost_usd: Some(child_ctx.total_cost_usd),
1128 duration_ms: Some(total_duration),
1129 completed_at: Some(completed_at),
1130 ..RunUpdate::default()
1131 },
1132 )
1133 .await
1134 {
1135 error!(
1136 child_run_id = %child_run_id,
1137 store_error = %store_err,
1138 "failed to persist child run failure"
1139 );
1140 }
1141
1142 Err(err)
1143 }
1144 }
1145 }
1146
1147 fn try_replay_step(&mut self, position: u32) -> Option<StepOutput> {
1152 let step = self.replay_steps.get(&position)?;
1153 if step.status.state != StepStatus::Completed {
1154 return None;
1155 }
1156 let output = StepOutput {
1157 output: step.output.clone().unwrap_or(Value::Null),
1158 duration_ms: step.duration_ms,
1159 cost_usd: step.cost_usd,
1160 input_tokens: step.input_tokens,
1161 output_tokens: step.output_tokens,
1162 model: None,
1163 debug_messages: None,
1164 };
1165 self.total_cost_usd += output.cost_usd;
1166 self.total_duration_ms += output.duration_ms;
1167 self.last_step_ids = vec![step.id];
1168 info!(
1169 run_id = %self.run_id,
1170 step = %step.name,
1171 position,
1172 "step replayed from previous execution"
1173 );
1174 Some(output)
1175 }
1176
1177 async fn execute_step(
1179 &mut self,
1180 name: &str,
1181 kind: StepKind,
1182 config: StepConfig,
1183 ) -> Result<StepOutput, EngineError> {
1184 let position = self.position;
1185 self.position += 1;
1186
1187 if let Some(output) = self.try_replay_step(position) {
1189 return Ok(output);
1190 }
1191
1192 if let StepConfig::Agent(ref agent_config) = config {
1195 self.check_run_budget(step_budget_usd(agent_config.max_budget_usd))?;
1196 }
1197
1198 let step = self
1200 .store
1201 .create_step(NewStep {
1202 run_id: self.run_id,
1203 name: name.to_string(),
1204 kind,
1205 position,
1206 input: Some(serde_json::to_value(&config)?),
1207 })
1208 .await?;
1209
1210 self.start_step(step.id, Utc::now()).await?;
1211
1212 let step_log_sender = self
1213 .log_sender
1214 .as_ref()
1215 .map(|s| StepLogSender::new(s.clone(), self.run_id, step.id, name.to_string()));
1216
1217 match execute_step_config(&config, &self.provider, step_log_sender).await {
1218 Ok(output) => {
1219 self.total_cost_usd += output.cost_usd;
1220 self.total_duration_ms += output.duration_ms;
1221
1222 let debug_messages_json = output.debug_messages_json();
1223
1224 let completed_at = Utc::now();
1225 self.store
1226 .update_step(
1227 step.id,
1228 StepUpdate {
1229 status: Some(StepStatus::Completed),
1230 output: Some(output.output.clone()),
1231 duration_ms: Some(output.duration_ms),
1232 cost_usd: Some(output.cost_usd),
1233 input_tokens: output.input_tokens,
1234 output_tokens: output.output_tokens,
1235 completed_at: Some(completed_at),
1236 debug_messages: debug_messages_json,
1237 ..StepUpdate::default()
1238 },
1239 )
1240 .await?;
1241
1242 info!(
1243 run_id = %self.run_id,
1244 step = %name,
1245 duration_ms = output.duration_ms,
1246 "step completed"
1247 );
1248
1249 self.last_step_ids = vec![step.id];
1250
1251 Ok(output)
1252 }
1253 Err(err) => {
1254 let completed_at = Utc::now();
1255 let debug_messages_json = extract_debug_messages_from_error(&err);
1256 let partial = extract_partial_usage_from_error(&err);
1257 let raw_response_output = extract_raw_response_from_error(&err);
1258
1259 if let Some(ref usage) = partial {
1260 if let Some(cost) = usage.cost_usd {
1261 self.total_cost_usd += cost;
1262 }
1263 if let Some(dur) = usage.duration_ms {
1264 self.total_duration_ms += dur;
1265 }
1266 }
1267
1268 if let Err(store_err) = self
1269 .store
1270 .update_step(
1271 step.id,
1272 StepUpdate {
1273 status: Some(StepStatus::Failed),
1274 error: Some(err.to_string()),
1275 output: raw_response_output,
1276 completed_at: Some(completed_at),
1277 debug_messages: debug_messages_json,
1278 duration_ms: partial.as_ref().and_then(|p| p.duration_ms),
1279 cost_usd: partial.as_ref().and_then(|p| p.cost_usd),
1280 input_tokens: partial.as_ref().and_then(|p| p.input_tokens),
1281 output_tokens: partial.as_ref().and_then(|p| p.output_tokens),
1282 ..StepUpdate::default()
1283 },
1284 )
1285 .await
1286 {
1287 tracing::error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
1288 }
1289
1290 Err(err)
1291 }
1292 }
1293 }
1294
1295 async fn start_step(&self, step_id: Uuid, now: DateTime<Utc>) -> Result<(), EngineError> {
1300 if !self.last_step_ids.is_empty() {
1301 let deps: Vec<NewStepDependency> = self
1302 .last_step_ids
1303 .iter()
1304 .map(|&depends_on| NewStepDependency {
1305 step_id,
1306 depends_on,
1307 })
1308 .collect();
1309 self.store.create_step_dependencies(deps).await?;
1310 }
1311
1312 self.store
1313 .update_step(
1314 step_id,
1315 StepUpdate {
1316 status: Some(StepStatus::Running),
1317 started_at: Some(now),
1318 ..StepUpdate::default()
1319 },
1320 )
1321 .await?;
1322
1323 Ok(())
1324 }
1325
1326 pub fn store(&self) -> &Arc<dyn Store> {
1328 &self.store
1329 }
1330
1331 pub async fn payload(&self) -> Result<Value, EngineError> {
1339 let run = self
1340 .store
1341 .get_run(self.run_id)
1342 .await?
1343 .ok_or(EngineError::Store(
1344 ironflow_store::error::StoreError::RunNotFound(self.run_id),
1345 ))?;
1346 Ok(run.payload)
1347 }
1348}
1349
1350impl fmt::Debug for WorkflowContext {
1351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1352 f.debug_struct("WorkflowContext")
1353 .field("run_id", &self.run_id)
1354 .field("position", &self.position)
1355 .field("total_cost_usd", &self.total_cost_usd)
1356 .field("inherited_cost_usd", &self.inherited_cost_usd)
1357 .field("max_cost_usd", &self.max_cost_usd)
1358 .finish_non_exhaustive()
1359 }
1360}
1361
1362fn extract_debug_messages_from_error(err: &EngineError) -> Option<Value> {
1365 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1366 debug_messages,
1367 ..
1368 })) = err
1369 && !debug_messages.is_empty()
1370 {
1371 return serde_json::to_value(debug_messages).ok();
1372 }
1373 None
1374}
1375
1376struct StepPartialUsage {
1382 cost_usd: Option<Decimal>,
1383 duration_ms: Option<u64>,
1384 input_tokens: Option<u64>,
1385 output_tokens: Option<u64>,
1386}
1387
1388fn extract_raw_response_from_error(err: &EngineError) -> Option<Value> {
1394 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1395 raw_response: Some(text),
1396 ..
1397 })) = err
1398 {
1399 return Some(Value::String(text.clone()));
1400 }
1401 None
1402}
1403
1404fn extract_partial_usage_from_error(err: &EngineError) -> Option<StepPartialUsage> {
1405 if let EngineError::Operation(OperationError::Agent(AgentError::SchemaValidation {
1406 partial_usage,
1407 ..
1408 })) = err
1409 && (partial_usage.cost_usd.is_some() || partial_usage.duration_ms.is_some())
1410 {
1411 return Some(StepPartialUsage {
1412 cost_usd: partial_usage
1413 .cost_usd
1414 .and_then(|c| Decimal::try_from(c).ok()),
1415 duration_ms: partial_usage.duration_ms,
1416 input_tokens: partial_usage.input_tokens,
1417 output_tokens: partial_usage.output_tokens,
1418 });
1419 }
1420 None
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425 use super::*;
1426 use ironflow_core::providers::claude::ClaudeCodeProvider;
1427 use ironflow_core::providers::record_replay::RecordReplayProvider;
1428 use ironflow_store::memory::InMemoryStore;
1429 use ironflow_store::models::RunFilter;
1430 use ironflow_store::store::RunStore;
1431 use serde_json::json;
1432 use std::sync::Arc;
1433 use std::sync::atomic::{AtomicBool, Ordering};
1434 use uuid::Uuid;
1435
1436 fn create_test_provider() -> Arc<dyn ironflow_core::provider::AgentProvider> {
1438 let inner = ClaudeCodeProvider::new();
1439 Arc::new(RecordReplayProvider::replay(
1440 inner,
1441 "/tmp/ironflow-fixtures",
1442 ))
1443 }
1444
1445 fn create_test_context() -> WorkflowContext {
1447 let store = Arc::new(InMemoryStore::new());
1448 let provider = create_test_provider();
1449 let run_id = Uuid::now_v7();
1450 WorkflowContext::new(run_id, store, provider)
1451 }
1452
1453 #[test]
1454 fn context_new_initializes_correctly() {
1455 let ctx = create_test_context();
1456 assert_eq!(ctx.position, 0);
1457 assert_eq!(ctx.total_cost_usd, Decimal::ZERO);
1458 assert_eq!(ctx.total_duration_ms, 0);
1459 assert!(ctx.last_step_ids.is_empty());
1460 assert!(ctx.replay_steps.is_empty());
1461 assert!(ctx.log_sender.is_none());
1462 }
1463
1464 #[test]
1465 fn context_run_id_returns_correct_id() {
1466 let run_id = Uuid::now_v7();
1467 let store = Arc::new(InMemoryStore::new());
1468 let provider = create_test_provider();
1469 let ctx = WorkflowContext::new(run_id, store, provider);
1470 assert_eq!(ctx.run_id(), run_id);
1471 }
1472
1473 #[test]
1474 fn context_total_cost_usd_initially_zero() {
1475 let ctx = create_test_context();
1476 assert_eq!(ctx.total_cost_usd(), Decimal::ZERO);
1477 }
1478
1479 #[test]
1480 fn context_total_duration_ms_initially_zero() {
1481 let ctx = create_test_context();
1482 assert_eq!(ctx.total_duration_ms(), 0);
1483 }
1484
1485 #[test]
1486 fn context_with_handler_resolver_creates_context_with_resolver() {
1487 let store = Arc::new(InMemoryStore::new());
1488 let provider = create_test_provider();
1489 let run_id = Uuid::now_v7();
1490
1491 let called = Arc::new(AtomicBool::new(false));
1492 let called_clone = called.clone();
1493
1494 let resolver: HandlerResolver = Arc::new(move |_name: &str| {
1495 called_clone.store(true, Ordering::SeqCst);
1496 None
1497 });
1498
1499 let ctx = WorkflowContext::with_handler_resolver(run_id, store, provider, resolver);
1500
1501 assert_eq!(ctx.run_id(), run_id);
1502 assert!(ctx.handler_resolver.is_some());
1503 }
1504
1505 #[tokio::test]
1506 async fn context_set_log_sender_attaches_sender() {
1507 let mut ctx = create_test_context();
1508 let (sender, _receiver) = crate::log_sender::channel();
1509 ctx.set_log_sender(sender);
1510 assert!(ctx.log_sender.is_some());
1511 }
1512
1513 #[tokio::test]
1514 async fn context_skip_creates_skipped_step() {
1515 let store = Arc::new(InMemoryStore::new());
1516 let provider = create_test_provider();
1517
1518 store
1520 .create_run(NewRun {
1521 workflow_name: "test".to_string(),
1522 trigger: TriggerKind::Manual,
1523 payload: json!({}),
1524 max_retries: 0,
1525 handler_version: None,
1526 labels: Default::default(),
1527 scheduled_at: None,
1528 max_cost_usd: None,
1529 })
1530 .await
1531 .expect("failed to create run");
1532
1533 let runs = store
1535 .list_runs(RunFilter::default(), 1, 10)
1536 .await
1537 .expect("failed to list runs");
1538 let created_run_id = runs.items[0].id;
1539
1540 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1541 let initial_position = ctx.position;
1542
1543 ctx.skip("skip-step", "condition not met")
1544 .await
1545 .expect("skip failed");
1546
1547 assert_eq!(ctx.position, initial_position + 1);
1548 assert!(!ctx.last_step_ids.is_empty());
1549
1550 let steps = store
1552 .list_steps(created_run_id)
1553 .await
1554 .expect("failed to list steps");
1555 assert_eq!(steps.len(), 1);
1556 assert_eq!(steps[0].status.state, StepStatus::Skipped);
1557 }
1558
1559 #[tokio::test]
1560 async fn context_parallel_empty_steps_returns_empty_vec() {
1561 let mut ctx = create_test_context();
1562 let results = ctx
1563 .parallel(vec![], true)
1564 .await
1565 .expect("parallel should not fail on empty input");
1566 assert!(results.is_empty());
1567 }
1568
1569 #[tokio::test]
1570 async fn context_approval_first_execution_returns_error() {
1571 let store = Arc::new(InMemoryStore::new());
1572 let provider = create_test_provider();
1573
1574 store
1576 .create_run(NewRun {
1577 workflow_name: "test".to_string(),
1578 trigger: TriggerKind::Manual,
1579 payload: json!({}),
1580 max_retries: 0,
1581 handler_version: None,
1582 labels: Default::default(),
1583 scheduled_at: None,
1584 max_cost_usd: None,
1585 })
1586 .await
1587 .expect("failed to create run");
1588
1589 let runs = store
1591 .list_runs(RunFilter::default(), 1, 10)
1592 .await
1593 .expect("failed to list runs");
1594 let created_run_id = runs.items[0].id;
1595
1596 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1597
1598 let result = ctx
1599 .approval(
1600 "approve-step",
1601 crate::config::ApprovalConfig::new("Continue?"),
1602 )
1603 .await;
1604
1605 assert!(matches!(result, Err(EngineError::ApprovalRequired { .. })));
1607
1608 assert_eq!(ctx.position, 1);
1610
1611 let steps = store
1613 .list_steps(created_run_id)
1614 .await
1615 .expect("failed to list steps");
1616 assert_eq!(steps.len(), 1);
1617 assert_eq!(steps[0].status.state, StepStatus::AwaitingApproval);
1618 }
1619
1620 #[tokio::test]
1621 async fn context_approval_replay_returns_ok() {
1622 let store = Arc::new(InMemoryStore::new());
1623 let provider = create_test_provider();
1624
1625 store
1627 .create_run(NewRun {
1628 workflow_name: "test".to_string(),
1629 trigger: TriggerKind::Manual,
1630 payload: json!({}),
1631 max_retries: 0,
1632 handler_version: None,
1633 labels: Default::default(),
1634 scheduled_at: None,
1635 max_cost_usd: None,
1636 })
1637 .await
1638 .expect("failed to create run");
1639
1640 let runs = store
1642 .list_runs(RunFilter::default(), 1, 10)
1643 .await
1644 .expect("failed to list runs");
1645 let created_run_id = runs.items[0].id;
1646
1647 let step = store
1649 .create_step(NewStep {
1650 run_id: created_run_id,
1651 name: "approval".to_string(),
1652 kind: StepKind::Approval,
1653 position: 0,
1654 input: None,
1655 })
1656 .await
1657 .expect("failed to create step");
1658
1659 store
1661 .update_step(
1662 step.id,
1663 StepUpdate {
1664 status: Some(StepStatus::Running),
1665 started_at: Some(Utc::now()),
1666 ..StepUpdate::default()
1667 },
1668 )
1669 .await
1670 .expect("failed to update step to Running");
1671
1672 store
1673 .update_step(
1674 step.id,
1675 StepUpdate {
1676 status: Some(StepStatus::AwaitingApproval),
1677 ..StepUpdate::default()
1678 },
1679 )
1680 .await
1681 .expect("failed to update step to AwaitingApproval");
1682
1683 let mut ctx = WorkflowContext::new(created_run_id, store.clone(), provider);
1685 ctx.load_replay_steps()
1686 .await
1687 .expect("failed to load replay steps");
1688
1689 let result = ctx
1691 .approval("approval", crate::config::ApprovalConfig::new("Continue?"))
1692 .await;
1693
1694 assert!(result.is_ok());
1695
1696 let steps = store
1698 .list_steps(created_run_id)
1699 .await
1700 .expect("failed to list steps");
1701 assert_eq!(steps.len(), 1);
1702 assert_eq!(steps[0].status.state, StepStatus::Completed);
1703 }
1704
1705 #[tokio::test]
1706 async fn context_load_replay_steps_loads_completed_steps() {
1707 let store = Arc::new(InMemoryStore::new());
1708 let provider = create_test_provider();
1709
1710 store
1712 .create_run(NewRun {
1713 workflow_name: "test".to_string(),
1714 trigger: TriggerKind::Manual,
1715 payload: json!({}),
1716 max_retries: 0,
1717 handler_version: None,
1718 labels: Default::default(),
1719 scheduled_at: None,
1720 max_cost_usd: None,
1721 })
1722 .await
1723 .expect("failed to create run");
1724
1725 let runs = store
1727 .list_runs(RunFilter::default(), 1, 10)
1728 .await
1729 .expect("failed to list runs");
1730 let created_run_id = runs.items[0].id;
1731
1732 let completed_step = store
1734 .create_step(NewStep {
1735 run_id: created_run_id,
1736 name: "completed".to_string(),
1737 kind: StepKind::Shell,
1738 position: 0,
1739 input: None,
1740 })
1741 .await
1742 .expect("failed to create step");
1743
1744 store
1746 .update_step(
1747 completed_step.id,
1748 StepUpdate {
1749 status: Some(StepStatus::Running),
1750 started_at: Some(Utc::now()),
1751 ..StepUpdate::default()
1752 },
1753 )
1754 .await
1755 .expect("failed to update step to Running");
1756
1757 store
1758 .update_step(
1759 completed_step.id,
1760 StepUpdate {
1761 status: Some(StepStatus::Completed),
1762 completed_at: Some(Utc::now()),
1763 ..StepUpdate::default()
1764 },
1765 )
1766 .await
1767 .expect("failed to update step to Completed");
1768
1769 let _pending_step = store
1770 .create_step(NewStep {
1771 run_id: created_run_id,
1772 name: "pending".to_string(),
1773 kind: StepKind::Shell,
1774 position: 1,
1775 input: None,
1776 })
1777 .await
1778 .expect("failed to create step");
1779
1780 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
1782 ctx.load_replay_steps()
1783 .await
1784 .expect("failed to load replay steps");
1785
1786 assert_eq!(ctx.replay_steps.len(), 1);
1788 assert!(ctx.replay_steps.contains_key(&0));
1789 assert!(!ctx.replay_steps.contains_key(&1));
1790 }
1791
1792 #[tokio::test]
1793 async fn context_payload_returns_run_payload() {
1794 let store = Arc::new(InMemoryStore::new());
1795 let provider = create_test_provider();
1796 let test_payload = json!({"key": "value", "number": 42});
1797
1798 store
1800 .create_run(NewRun {
1801 workflow_name: "test".to_string(),
1802 trigger: TriggerKind::Manual,
1803 payload: test_payload.clone(),
1804 max_retries: 0,
1805 handler_version: None,
1806 labels: Default::default(),
1807 scheduled_at: None,
1808 max_cost_usd: None,
1809 })
1810 .await
1811 .expect("failed to create run");
1812
1813 let runs = store
1815 .list_runs(RunFilter::default(), 1, 10)
1816 .await
1817 .expect("failed to list runs");
1818 let created_run_id = runs.items[0].id;
1819
1820 let ctx = WorkflowContext::new(created_run_id, store, provider);
1821 let payload = ctx.payload().await.expect("failed to get payload");
1822
1823 assert_eq!(payload, test_payload);
1824 }
1825
1826 #[tokio::test]
1827 async fn context_payload_returns_error_for_nonexistent_run() {
1828 let store = Arc::new(InMemoryStore::new());
1829 let provider = create_test_provider();
1830 let run_id = Uuid::now_v7();
1831
1832 let ctx = WorkflowContext::new(run_id, store, provider);
1833 let result = ctx.payload().await;
1834
1835 assert!(result.is_err());
1836 }
1837
1838 #[tokio::test]
1839 async fn context_store_returns_reference() {
1840 let ctx = create_test_context();
1841 let _store = ctx.store();
1842 }
1844
1845 #[test]
1846 fn context_debug_formatting() {
1847 let ctx = create_test_context();
1848 let debug_str = format!("{:?}", ctx);
1849 assert!(debug_str.contains("WorkflowContext"));
1850 assert!(debug_str.contains("run_id"));
1851 }
1852
1853 #[tokio::test]
1854 async fn context_last_step_ids_tracks_executed_steps() {
1855 let store = Arc::new(InMemoryStore::new());
1856 let provider = create_test_provider();
1857
1858 store
1860 .create_run(NewRun {
1861 workflow_name: "test".to_string(),
1862 trigger: TriggerKind::Manual,
1863 payload: json!({}),
1864 max_retries: 0,
1865 handler_version: None,
1866 labels: Default::default(),
1867 scheduled_at: None,
1868 max_cost_usd: None,
1869 })
1870 .await
1871 .expect("failed to create run");
1872
1873 let runs = store
1875 .list_runs(RunFilter::default(), 1, 10)
1876 .await
1877 .expect("failed to list runs");
1878 let created_run_id = runs.items[0].id;
1879
1880 let mut ctx = WorkflowContext::new(created_run_id, store, provider);
1881 assert!(ctx.last_step_ids.is_empty());
1882
1883 ctx.skip("step1", "reason").await.expect("skip failed");
1884
1885 assert_eq!(ctx.last_step_ids.len(), 1);
1886
1887 ctx.skip("step2", "reason").await.expect("skip failed");
1888
1889 assert_eq!(ctx.last_step_ids.len(), 1);
1891 }
1892}