1use std::collections::HashMap;
16use std::sync::Arc;
17
18use serde::{Deserialize, Serialize};
19
20use oneai_core::error::{OneAIError, Result};
21use oneai_core::traits::{InteractionGate, LlmProvider, Tool};
22use oneai_core::{Conversation, InferenceRequest, Message};
23
24use crate::dag::WorkflowDag;
25use crate::config::{WorkflowConfig, RetryPolicy};
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum StepStatus {
31 Pending,
33 Running,
35 Completed,
37 Failed,
39 Skipped,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct StepResult {
46 pub step_id: String,
48
49 pub status: StepStatus,
51
52 pub output: Option<String>,
54
55 pub error: Option<String>,
57
58 pub retries_used: usize,
60
61 pub execution_time_ms: Option<u64>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct WorkflowResult {
68 pub name: String,
70
71 pub step_results: HashMap<String, StepResult>,
73
74 pub variables: HashMap<String, String>,
76
77 pub success: bool,
79
80 pub total_time_ms: u64,
82
83 pub completed_levels: usize,
85}
86
87impl WorkflowResult {
88 pub fn get_step_result(&self, step_id: &str) -> Option<&StepResult> {
90 self.step_results.get(step_id)
91 }
92
93 pub fn completed_steps(&self) -> Vec<String> {
95 self.step_results.iter()
96 .filter(|(_, r)| r.status == StepStatus::Completed)
97 .map(|(id, _)| id.clone())
98 .collect()
99 }
100
101 pub fn failed_steps(&self) -> Vec<String> {
103 self.step_results.iter()
104 .filter(|(_, r)| r.status == StepStatus::Failed)
105 .map(|(id, _)| id.clone())
106 .collect()
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct WorkflowContext {
113 pub variables: HashMap<String, String>,
115 pub step_outputs: HashMap<String, String>,
117}
118
119impl WorkflowContext {
120 pub fn from_config(config: &WorkflowConfig) -> Self {
122 Self {
123 variables: config.variables.clone(),
124 step_outputs: HashMap::new(),
125 }
126 }
127
128 pub fn set_variable(&mut self, key: impl Into<String>, value: impl Into<String>) {
130 self.variables.insert(key.into(), value.into());
131 }
132
133 pub fn get_variable(&self, key: &str) -> Option<&String> {
135 self.variables.get(key)
136 }
137
138 pub fn set_step_output(&mut self, step_id: impl Into<String>, output: impl Into<String>) {
140 self.step_outputs.insert(step_id.into(), output.into());
141 }
142}
143
144pub struct WorkflowExecutor {
160 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
163 interaction_gate: Arc<dyn InteractionGate>,
165 provider: Option<Arc<dyn LlmProvider>>,
170}
171
172impl WorkflowExecutor {
173 pub fn new(
175 tools: Arc<HashMap<String, Arc<dyn Tool>>>,
176 interaction_gate: Arc<dyn InteractionGate>,
177 ) -> Self {
178 Self {
180 tools: Arc::new(tokio::sync::RwLock::new((*tools).clone())),
181 interaction_gate,
182 provider: None,
183 }
184 }
185
186 pub fn new_empty(interaction_gate: Arc<dyn InteractionGate>) -> Self {
189 Self {
190 tools: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
191 interaction_gate,
192 provider: None,
193 }
194 }
195
196 pub fn with_provider(
199 tools: Arc<HashMap<String, Arc<dyn Tool>>>,
200 interaction_gate: Arc<dyn InteractionGate>,
201 provider: Arc<dyn LlmProvider>,
202 ) -> Self {
203 Self {
204 tools: Arc::new(tokio::sync::RwLock::new((*tools).clone())),
205 interaction_gate,
206 provider: Some(provider),
207 }
208 }
209
210 pub fn set_provider(&mut self, provider: Arc<dyn LlmProvider>) {
214 self.provider = Some(provider);
215 }
216
217 pub async fn register_tool(&self, tool: Arc<dyn Tool>) {
219 self.tools.write().await.insert(tool.name().to_string(), tool);
220 }
221
222 pub fn tools_handle(&self) -> Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>> {
225 self.tools.clone()
226 }
227
228 pub async fn execute(
234 &self,
235 dag: &WorkflowDag,
236 config: &WorkflowConfig,
237 ) -> Result<WorkflowResult> {
238 let start_time = std::time::Instant::now();
239 let mut step_results: HashMap<String, StepResult> = HashMap::new();
240 let mut context = WorkflowContext::from_config(config);
241 let mut completed_levels = 0;
242
243 for (id, _) in &dag.nodes {
245 step_results.insert(id.clone(), StepResult {
246 step_id: id.clone(),
247 status: StepStatus::Pending,
248 output: None,
249 error: None,
250 retries_used: 0,
251 execution_time_ms: None,
252 });
253 }
254
255 for level_ids in &dag.levels {
257 let _level_start = std::time::Instant::now();
258
259 let mut skip_level = false;
261 if !config.continue_on_failure {
262 for step_id in level_ids {
263 if let Some(node) = dag.get_node(step_id) {
264 for dep_id in &node.depends_on {
265 if let Some(dep_result) = step_results.get(dep_id) {
266 if dep_result.status == StepStatus::Failed || dep_result.status == StepStatus::Skipped {
267 skip_level = true;
268 break;
269 }
270 }
271 }
272 }
273 if skip_level {
274 break;
275 }
276 }
277 }
278
279 if skip_level {
280 for step_id in level_ids {
282 if let Some(result) = step_results.get_mut(step_id) {
283 result.status = StepStatus::Skipped;
284 result.error = Some("Dependency failed — step skipped".to_string());
285 }
286 }
287 continue;
288 }
289
290 let mut level_futures = Vec::new();
292
293 for step_id in level_ids {
294 let node = dag.get_node(step_id).unwrap();
295 let step_config = node.step.clone();
296 let retry_policy = config.effective_retry_policy(step_id);
297 let timeout_secs = config.effective_timeout(step_id);
298 let tools = self.tools.clone();
299 let interaction_gate = self.interaction_gate.clone();
300 let context_snapshot = context.clone();
301 let provider = self.provider.clone();
302
303 level_futures.push(tokio::spawn(async move {
304 execute_step(
305 step_config,
306 retry_policy,
307 timeout_secs,
308 tools,
309 interaction_gate,
310 context_snapshot,
311 provider,
312 ).await
313 }));
314 }
315
316 for future in level_futures {
318 let result = future.await.map_err(|e| {
319 OneAIError::Workflow(format!("Task join error: {}", e))
320 })?;
321
322 match result {
323 Ok(step_result) => {
324 step_results.insert(step_result.step_id.clone(), step_result.clone());
325 if step_result.status == StepStatus::Completed {
326 if let Some(output) = &step_result.output {
327 context.set_step_output(&step_result.step_id, output);
328 }
329 }
330 }
331 Err(e) => {
332 tracing::error!("Step execution error: {}", e);
334 }
335 }
336 }
337
338 completed_levels += 1;
339 }
340
341 let total_time_ms = start_time.elapsed().as_millis() as u64;
342
343 let success = step_results.values()
344 .all(|r| r.status == StepStatus::Completed || r.status == StepStatus::Skipped);
345
346 Ok(WorkflowResult {
347 name: dag.name.clone(),
348 step_results,
349 variables: context.variables,
350 success,
351 total_time_ms,
352 completed_levels,
353 })
354 }
355}
356
357async fn execute_step(
368 step: crate::config::StepConfig,
369 retry_policy: RetryPolicy,
370 timeout_secs: Option<u64>,
371 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
372 interaction_gate: Arc<dyn InteractionGate>,
373 context: WorkflowContext,
374 provider: Option<Arc<dyn LlmProvider>>,
375) -> Result<StepResult> {
376 let start_time = std::time::Instant::now();
377 let step_id = step.id.clone();
378 let mut retries_used = 0;
379 let mut last_error: Option<String> = None;
380
381 let interpolated_prompt = step.prompt.as_ref()
385 .map(|p| interpolate_template(p, &context));
386 let interpolated_tool_args = step.tool_args.as_ref()
387 .map(|a| {
388 let json_str = serde_json::to_string(a).unwrap_or_default();
390 let interpolated = interpolate_template(&json_str, &context);
391 serde_json::from_str(&interpolated).unwrap_or_else(|_| a.clone())
392 });
393
394 if step.requires_approval {
396 let approval_request = oneai_core::ApprovalRequest {
397 tool_name: step.id.clone(),
398 args: step.tool_args.clone().unwrap_or(serde_json::json!({})),
399 risk_level: oneai_core::RiskLevel::High,
400 permission_level: Some(oneai_core::PermissionLevel::Full),
401 justification: format!("Workflow step '{}' requires human approval", step.id),
402 };
403
404 let response = interaction_gate
405 .request(oneai_core::InteractionRequest::ToolApproval {
406 approval: approval_request,
407 })
408 .await?;
409 match response {
410 oneai_core::InteractionResponse::Abort { reason } => {
411 return Ok(StepResult {
412 step_id,
413 status: StepStatus::Failed,
414 output: None,
415 error: Some(format!("Approval denied: {}", reason)),
416 retries_used: 0,
417 execution_time_ms: Some(start_time.elapsed().as_millis() as u64),
418 });
419 }
420 oneai_core::InteractionResponse::Revise { feedback } => {
421 return Ok(StepResult {
423 step_id,
424 status: StepStatus::Failed,
425 output: None,
426 error: Some(format!("Approval denied: {}", feedback)),
427 retries_used: 0,
428 execution_time_ms: Some(start_time.elapsed().as_millis() as u64),
429 });
430 }
431 oneai_core::InteractionResponse::ProceedWith {
432 modification: oneai_core::InteractionModification::ReplaceToolArgs(_args),
433 } => {
434 }
437 oneai_core::InteractionResponse::ProceedWith { .. }
438 | oneai_core::InteractionResponse::Proceed
439 | oneai_core::InteractionResponse::Choose { .. } => {
440 }
442 _ => {}
444 }
445 }
446
447 for attempt in 0..=retry_policy.max_retries {
449 let exec_result: std::result::Result<oneai_core::ToolOutput, OneAIError> = if let Some(tool_name) = &step.tool {
450 let tools_map = tools.read().await;
452 let tool = tools_map.get(tool_name);
453 if let Some(tool) = tool {
454 let args = match &interpolated_tool_args {
456 Some(a) => a.clone(),
457 None => step.tool_args.clone().unwrap_or(serde_json::json!({})),
458 }; let timeout = timeout_secs.unwrap_or(60);
459
460 let timeout_result = tokio::time::timeout(
461 std::time::Duration::from_secs(timeout),
462 tool.execute(args.clone()),
463 ).await;
464
465 match timeout_result {
466 Ok(inner_result) => inner_result, Err(_) => Err(OneAIError::Timeout(format!(
468 "Step '{}' timed out after {} seconds", step_id, timeout
469 ))),
470 }
471 } else {
472 Err(OneAIError::Workflow(format!(
473 "Tool '{}' not found for step '{}'",
474 tool_name, step_id
475 )))
476 }
477 } else if let Some(provider) = &provider {
478 let prompt_text = match &interpolated_prompt {
480 Some(p) => p.clone(),
481 None => step.prompt.clone().unwrap_or_default(),
482 };
483 if prompt_text.is_empty() {
484 Ok(oneai_core::ToolOutput {
485 success: true,
486 content: String::new(),
487 error: None,
488 })
489 } else {
490 let mut conversation = Conversation::new();
492 conversation.add_message(Message::system(
493 "You are executing a step in a deterministic workflow. Respond concisely."
494 ));
495 conversation.add_message(Message::user(prompt_text));
496
497 let request = InferenceRequest {
498 conversation,
499 tools: vec![], max_tokens: Some(2048),
501 temperature: Some(0.3),
502 top_p: None,
503 stop_sequences: vec![],
504 constrained_output: None,
505 thinking_budget: None,
506 metadata: HashMap::new(),
507 };
508
509 let response = provider.infer(request).await;
510 match response {
511 Ok(inference_response) => {
512 Ok(oneai_core::ToolOutput {
513 success: true,
514 content: inference_response.message.text_content(),
515 error: None,
516 })
517 }
518 Err(e) => Err(OneAIError::Provider(format!(
519 "LLM inference failed for step '{}': {}", step_id, e
520 ))),
521 }
522 }
523 } else {
524 let prompt_text = match &interpolated_prompt {
526 Some(p) => p.clone(),
527 None => step.prompt.clone().unwrap_or_default(),
528 };
529 Ok(oneai_core::ToolOutput {
530 success: true,
531 content: prompt_text,
532 error: None,
533 })
534 };
535
536 match exec_result {
537 Ok(output) => {
538 if output.success {
539 return Ok(StepResult {
540 step_id,
541 status: StepStatus::Completed,
542 output: Some(output.content),
543 error: None,
544 retries_used,
545 execution_time_ms: Some(start_time.elapsed().as_millis() as u64),
546 });
547 } else {
548 last_error = Some(output.error.unwrap_or_else(|| "Tool execution failed".to_string()));
549 }
550 }
551 Err(e) => {
552 last_error = Some(format!("Step error: {}", e));
553 }
554 }
555
556 retries_used = attempt;
557
558 if attempt < retry_policy.max_retries {
559 tokio::time::sleep(
561 std::time::Duration::from_secs(retry_policy.retry_delay_secs)
562 ).await;
563 }
564 }
565
566 Ok(StepResult {
568 step_id,
569 status: StepStatus::Failed,
570 output: None,
571 error: last_error,
572 retries_used,
573 execution_time_ms: Some(start_time.elapsed().as_millis() as u64),
574 })
575}
576
577pub fn interpolate_template(template: &str, context: &WorkflowContext) -> String {
596 let mut result = template.to_string();
597
598 for (step_id, output) in &context.step_outputs {
601 let long_pattern = "{{".to_string() + step_id + "_output}}";
604 result = result.replace(&long_pattern, output);
605 }
606 for (step_id, output) in &context.step_outputs {
607 let short_pattern = "{{".to_string() + step_id + "}}";
610 result = result.replace(&short_pattern, output);
611 }
612
613 for (key, value) in &context.variables {
615 result = result.replace(&format!("{{{{{}}}}}", key), value);
616 }
617
618 result
619}
620
621#[cfg(test)]
622mod interpolation_tests {
623 use super::*;
624
625 #[test]
626 fn test_interpolate_step_outputs() {
627 let mut context = WorkflowContext::from_config(&WorkflowConfig::new("test", vec![]));
628 context.set_step_output("step1", "result of step1");
629 context.set_step_output("step2", "result of step2");
630
631 let result = interpolate_template("{{step1}} then {{step2}}", &context);
633 assert_eq!(result, "result of step1 then result of step2");
634
635 let result = interpolate_template("{{step1_output}} and {{step2_output}}", &context);
637 assert_eq!(result, "result of step1 and result of step2");
638 }
639
640 #[test]
641 fn test_interpolate_variables() {
642 let mut context = WorkflowContext::from_config(&WorkflowConfig::new("test", vec![]));
643 context.set_variable("focus_area", "security");
644 context.set_variable("language", "Rust");
645
646 let result = interpolate_template("Focus on {{focus_area}} in {{language}}", &context);
647 assert_eq!(result, "Focus on security in Rust");
648 }
649
650 #[test]
651 fn test_interpolate_mixed() {
652 let mut context = WorkflowContext::from_config(&WorkflowConfig::new("test", vec![]));
653 context.set_step_output("read_diff", "auth.rs changes");
654 context.set_variable("focus", "security");
655
656 let result = interpolate_template("Review {{read_diff_output}} with focus on {{focus}}", &context);
657 assert_eq!(result, "Review auth.rs changes with focus on security");
658 }
659
660 #[test]
661 fn test_interpolate_no_match() {
662 let context = WorkflowContext::from_config(&WorkflowConfig::new("test", vec![]));
663 let result = interpolate_template("No variables here {{unknown}}", &context);
664 assert_eq!(result, "No variables here {{unknown}}");
665 }
666}