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