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::decision::DecisionAnswers;
24use crate::error::EngineError;
25use crate::executor::{DecisionExecution, StepArtifacts, StepOutput, StepResult, execute_decision};
26use crate::notify::{
27 WorkflowApprovalRequiredEvent, WorkflowEvent, WorkflowStepCompletedEvent,
28 WorkflowStepStartedEvent,
29};
30use crate::plan::lock_plan;
31
32impl WorkflowContext {
33 pub async fn decision<T: DecisionAnswers>(
88 &mut self,
89 name: &str,
90 config: DecisionConfig<T>,
91 ) -> Result<T, EngineError> {
92 let output = self.decision_output(name, config.erase()).await?;
93 Ok(T::from_output(&output)?)
94 }
95
96 async fn decision_output(
98 &mut self,
99 name: &str,
100 config: DecisionConfig,
101 ) -> Result<DecisionOutput, EngineError> {
102 if let Some(plan) = self.plan().cloned() {
105 self.position += 1;
106 let mut recorder = lock_plan(&plan);
107 if recorder.record(name, StepKind::Decision, &self.workflow_name, None) {
108 recorder.set_last(vec![name.to_string()]);
109 }
110 return Ok(DecisionOutput {
111 model: None,
112 answers: BTreeMap::new(),
113 usage: DecisionUsage::default(),
114 });
115 }
116
117 if let Some(output) = self.decision_replay(name, &config).await? {
118 return Ok(output);
119 }
120 self.decision_execute(name, config).await
121 }
122
123 async fn decision_replay(
130 &mut self,
131 name: &str,
132 _config: &DecisionConfig,
133 ) -> Result<Option<DecisionOutput>, EngineError> {
134 let position = self.position;
135
136 let Some(existing) = self.replay_steps.get(&position).cloned() else {
137 return Ok(None);
138 };
139 if existing.kind != StepKind::Decision {
140 return Ok(None);
141 }
142
143 self.position += 1;
144
145 let stored: DecisionOutput = existing
146 .output
147 .clone()
148 .ok_or_else(|| {
149 EngineError::StepConfig(format!(
150 "decision step '{name}' has no stored output to replay"
151 ))
152 })
153 .and_then(|v| from_value(v).map_err(EngineError::from))?;
154
155 if existing.status.state == StepStatus::AwaitingApproval {
158 self.store
159 .update_step(
160 existing.id,
161 StepUpdate {
162 status: Some(StepStatus::Completed),
163 completed_at: Some(Utc::now()),
164 ..StepUpdate::default()
165 },
166 )
167 .await?;
168 info!(
169 run_id = %self.run_id,
170 step = %name,
171 position,
172 "decision step replayed (approved after escalation)"
173 );
174 } else {
175 info!(
176 run_id = %self.run_id,
177 step = %name,
178 position,
179 "decision step replayed from previous execution"
180 );
181 }
182
183 self.last_step_ids = vec![existing.id];
189 Ok(Some(stored))
190 }
191
192 async fn decision_execute(
195 &mut self,
196 name: &str,
197 config: DecisionConfig,
198 ) -> Result<DecisionOutput, EngineError> {
199 self.check_guard_timeout()?;
200
201 let position = self.position;
202 self.position += 1;
203
204 let provider =
205 self.decision_provider
206 .clone()
207 .ok_or_else(|| EngineError::NoDecisionProvider {
208 step: name.to_string(),
209 })?;
210
211 let trace_id = step_trace_id(self.run_id, name, position);
212 let step = self
213 .store
214 .create_step(NewStep {
215 run_id: self.run_id,
216 trace_id,
217 name: name.to_string(),
218 kind: StepKind::Decision,
219 position,
220 input: Some(to_value(&config)?),
221 is_error_handler: false,
222 })
223 .await?;
224 self.start_step(step.id, Utc::now()).await?;
225
226 if let Some(ref bus) = self.event_bus {
227 bus.publish(
228 self.run_id,
229 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
230 step_name: name.to_string(),
231 step_index: position,
232 timestamp: Utc::now(),
233 }),
234 );
235 }
236
237 let execution = match execute_decision(&provider, &config).await {
238 Ok(execution) => execution,
239 Err(err) => {
240 self.fail_step(step.id, &err).await;
241 return Err(err);
242 }
243 };
244
245 self.total_cost_usd += execution.cost_usd;
247 self.total_duration_ms += execution.duration_ms;
248
249 let output_value = to_value(&execution.output)?;
250
251 let escalated = config
252 .escalate_below
253 .zip(execution.output.min_confidence())
254 .map(|(threshold, min)| min < threshold)
255 .unwrap_or(false);
256
257 if escalated {
258 return self
259 .decision_escalate(name, position, step.id, &config, &execution, output_value)
260 .await;
261 }
262
263 let step_output = StepOutput {
264 output: output_value.clone(),
265 duration_ms: execution.duration_ms,
266 cost_usd: execution.cost_usd,
267 input_tokens: Some(execution.input_tokens),
268 cache_read_input_tokens: None,
269 cache_creation_input_tokens: None,
270 output_tokens: Some(execution.output_tokens),
271 model: execution.output.model.as_ref().map(ToString::to_string),
272 debug_messages: None,
273 artifacts: StepArtifacts::default(),
274 };
275
276 let completed_at = Utc::now();
277 self.store
278 .update_step(
279 step.id,
280 StepUpdate {
281 status: Some(StepStatus::Completed),
282 output: Some(output_value),
283 duration_ms: Some(execution.duration_ms),
284 cost_usd: Some(execution.cost_usd),
285 input_tokens: Some(execution.input_tokens),
286 output_tokens: Some(execution.output_tokens),
287 completed_at: Some(completed_at),
288 ..StepUpdate::default()
289 },
290 )
291 .await?;
292
293 self.step_results
294 .push(StepResult::from_success(trace_id, name, &step_output));
295 self.persist_progress().await;
296 self.last_step_ids = vec![step.id];
297
298 info!(
299 run_id = %self.run_id,
300 step = %name,
301 trace_id = %trace_id,
302 cost_usd = %execution.cost_usd,
303 "decision step completed"
304 );
305
306 if let Some(ref bus) = self.event_bus {
307 bus.publish(
308 self.run_id,
309 WorkflowEvent::StepCompleted(WorkflowStepCompletedEvent {
310 step_name: name.to_string(),
311 step_index: position,
312 duration_ms: execution.duration_ms,
313 output_summary: None,
314 }),
315 );
316 }
317
318 Ok(execution.output)
319 }
320
321 async fn decision_escalate(
324 &mut self,
325 name: &str,
326 position: u32,
327 step_id: Uuid,
328 config: &DecisionConfig,
329 execution: &DecisionExecution,
330 output_value: Value,
331 ) -> Result<DecisionOutput, EngineError> {
332 let threshold = config.escalate_below.unwrap_or_default();
333 let min = execution.output.min_confidence().unwrap_or_default();
334
335 self.store
336 .update_step(
337 step_id,
338 StepUpdate {
339 status: Some(StepStatus::AwaitingApproval),
340 output: Some(output_value),
341 duration_ms: Some(execution.duration_ms),
342 cost_usd: Some(execution.cost_usd),
343 input_tokens: Some(execution.input_tokens),
344 output_tokens: Some(execution.output_tokens),
345 ..StepUpdate::default()
346 },
347 )
348 .await?;
349 self.last_step_ids = vec![step_id];
350
351 info!(
352 run_id = %self.run_id,
353 step = %name,
354 position,
355 confidence = min,
356 threshold,
357 "decision escalated to human approval"
358 );
359
360 if let Some(ref bus) = self.event_bus {
361 bus.publish(
362 self.run_id,
363 WorkflowEvent::ApprovalRequired(WorkflowApprovalRequiredEvent {
364 step_name: name.to_string(),
365 step_index: position,
366 approval_id: step_id,
367 }),
368 );
369 }
370
371 Err(EngineError::ApprovalRequired {
372 run_id: self.run_id,
373 step_id,
374 message: format!(
375 "decision '{name}' escalated: confidence {min:.3} below threshold {threshold:.3}"
376 ),
377 })
378 }
379}