ironflow_engine/context/steps/
decision.rs1use std::collections::BTreeMap;
12
13use chrono::Utc;
14use serde_json::{Value, from_value, to_value};
15use tracing::info;
16use uuid::Uuid;
17
18use ironflow_core::decision::{DecisionOutput, DecisionUsage};
19use ironflow_store::models::{NewStep, StepKind, StepStatus, StepUpdate, step_trace_id};
20
21use crate::config::DecisionConfig;
22use crate::context::WorkflowContext;
23use crate::error::EngineError;
24use crate::executor::{DecisionExecution, StepOutput, StepResult, execute_decision};
25use crate::notify::{
26 WorkflowApprovalRequiredEvent, WorkflowEvent, WorkflowStepCompletedEvent,
27 WorkflowStepStartedEvent,
28};
29use crate::plan::lock_plan;
30
31impl WorkflowContext {
32 pub async fn decision(
45 &mut self,
46 name: &str,
47 config: DecisionConfig,
48 ) -> Result<DecisionOutput, EngineError> {
49 if let Some(plan) = self.plan().cloned() {
52 self.position += 1;
53 let mut recorder = lock_plan(&plan);
54 if recorder.record(name, StepKind::Decision, &self.workflow_name, None) {
55 recorder.set_last(vec![name.to_string()]);
56 }
57 return Ok(DecisionOutput {
58 model: None,
59 answers: BTreeMap::new(),
60 usage: DecisionUsage::default(),
61 });
62 }
63
64 if let Some(output) = self.decision_replay(name, &config).await? {
65 return Ok(output);
66 }
67 self.decision_execute(name, config).await
68 }
69
70 async fn decision_replay(
77 &mut self,
78 name: &str,
79 _config: &DecisionConfig,
80 ) -> Result<Option<DecisionOutput>, EngineError> {
81 let position = self.position;
82
83 let Some(existing) = self.replay_steps.get(&position).cloned() else {
84 return Ok(None);
85 };
86 if existing.kind != StepKind::Decision {
87 return Ok(None);
88 }
89
90 self.position += 1;
91
92 let stored: DecisionOutput = existing
93 .output
94 .clone()
95 .ok_or_else(|| {
96 EngineError::StepConfig(format!(
97 "decision step '{name}' has no stored output to replay"
98 ))
99 })
100 .and_then(|v| from_value(v).map_err(EngineError::from))?;
101
102 if existing.status.state == StepStatus::AwaitingApproval {
105 self.store
106 .update_step(
107 existing.id,
108 StepUpdate {
109 status: Some(StepStatus::Completed),
110 completed_at: Some(Utc::now()),
111 ..StepUpdate::default()
112 },
113 )
114 .await?;
115 info!(
116 run_id = %self.run_id,
117 step = %name,
118 position,
119 "decision step replayed (approved after escalation)"
120 );
121 } else {
122 info!(
123 run_id = %self.run_id,
124 step = %name,
125 position,
126 "decision step replayed from previous execution"
127 );
128 }
129
130 self.last_step_ids = vec![existing.id];
136 Ok(Some(stored))
137 }
138
139 async fn decision_execute(
142 &mut self,
143 name: &str,
144 config: DecisionConfig,
145 ) -> Result<DecisionOutput, EngineError> {
146 self.check_guard_timeout()?;
147
148 let position = self.position;
149 self.position += 1;
150
151 let provider =
152 self.decision_provider
153 .clone()
154 .ok_or_else(|| EngineError::NoDecisionProvider {
155 step: name.to_string(),
156 })?;
157
158 let trace_id = step_trace_id(self.run_id, name, position);
159 let step = self
160 .store
161 .create_step(NewStep {
162 run_id: self.run_id,
163 trace_id,
164 name: name.to_string(),
165 kind: StepKind::Decision,
166 position,
167 input: Some(to_value(&config)?),
168 is_error_handler: false,
169 })
170 .await?;
171 self.start_step(step.id, Utc::now()).await?;
172
173 if let Some(ref bus) = self.event_bus {
174 bus.publish(
175 self.run_id,
176 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
177 step_name: name.to_string(),
178 step_index: position,
179 timestamp: Utc::now(),
180 }),
181 );
182 }
183
184 let execution = match execute_decision(&provider, &config).await {
185 Ok(execution) => execution,
186 Err(err) => {
187 self.fail_step(step.id, &err).await;
188 return Err(err);
189 }
190 };
191
192 self.total_cost_usd += execution.cost_usd;
194 self.total_duration_ms += execution.duration_ms;
195
196 let output_value = to_value(&execution.output)?;
197
198 let escalated = config
199 .escalate_below
200 .zip(execution.output.min_confidence())
201 .map(|(threshold, min)| min < threshold)
202 .unwrap_or(false);
203
204 if escalated {
205 return self
206 .decision_escalate(name, position, step.id, &config, &execution, output_value)
207 .await;
208 }
209
210 let step_output = StepOutput {
211 output: output_value.clone(),
212 duration_ms: execution.duration_ms,
213 cost_usd: execution.cost_usd,
214 input_tokens: Some(execution.input_tokens),
215 output_tokens: Some(execution.output_tokens),
216 model: execution.output.model.as_ref().map(ToString::to_string),
217 debug_messages: None,
218 };
219
220 let completed_at = Utc::now();
221 self.store
222 .update_step(
223 step.id,
224 StepUpdate {
225 status: Some(StepStatus::Completed),
226 output: Some(output_value),
227 duration_ms: Some(execution.duration_ms),
228 cost_usd: Some(execution.cost_usd),
229 input_tokens: Some(execution.input_tokens),
230 output_tokens: Some(execution.output_tokens),
231 completed_at: Some(completed_at),
232 ..StepUpdate::default()
233 },
234 )
235 .await?;
236
237 self.step_results
238 .push(StepResult::from_success(trace_id, name, &step_output));
239 self.persist_progress().await;
240 self.last_step_ids = vec![step.id];
241
242 info!(
243 run_id = %self.run_id,
244 step = %name,
245 trace_id = %trace_id,
246 cost_usd = %execution.cost_usd,
247 "decision step completed"
248 );
249
250 if let Some(ref bus) = self.event_bus {
251 bus.publish(
252 self.run_id,
253 WorkflowEvent::StepCompleted(WorkflowStepCompletedEvent {
254 step_name: name.to_string(),
255 step_index: position,
256 duration_ms: execution.duration_ms,
257 output_summary: None,
258 }),
259 );
260 }
261
262 Ok(execution.output)
263 }
264
265 async fn decision_escalate(
268 &mut self,
269 name: &str,
270 position: u32,
271 step_id: Uuid,
272 config: &DecisionConfig,
273 execution: &DecisionExecution,
274 output_value: Value,
275 ) -> Result<DecisionOutput, EngineError> {
276 let threshold = config.escalate_below.unwrap_or_default();
277 let min = execution.output.min_confidence().unwrap_or_default();
278
279 self.store
280 .update_step(
281 step_id,
282 StepUpdate {
283 status: Some(StepStatus::AwaitingApproval),
284 output: Some(output_value),
285 duration_ms: Some(execution.duration_ms),
286 cost_usd: Some(execution.cost_usd),
287 input_tokens: Some(execution.input_tokens),
288 output_tokens: Some(execution.output_tokens),
289 ..StepUpdate::default()
290 },
291 )
292 .await?;
293 self.last_step_ids = vec![step_id];
294
295 info!(
296 run_id = %self.run_id,
297 step = %name,
298 position,
299 confidence = min,
300 threshold,
301 "decision escalated to human approval"
302 );
303
304 if let Some(ref bus) = self.event_bus {
305 bus.publish(
306 self.run_id,
307 WorkflowEvent::ApprovalRequired(WorkflowApprovalRequiredEvent {
308 step_name: name.to_string(),
309 step_index: position,
310 approval_id: step_id,
311 }),
312 );
313 }
314
315 Err(EngineError::ApprovalRequired {
316 run_id: self.run_id,
317 step_id,
318 message: format!(
319 "decision '{name}' escalated: confidence {min:.3} below threshold {threshold:.3}"
320 ),
321 })
322 }
323}