1use std::collections::HashMap;
26use std::sync::Arc;
27
28use crate::coordination::consensus::Consensus;
29use crate::coordination::shared_memory::{MemoryKey, SharedMemory};
30use crate::lifecycle::AgentHandle;
31use crate::workflow_dsl::{WorkflowDefinition, WorkflowStepDef};
32
33use serde_json::Value;
34
35#[derive(Debug, Clone)]
37pub struct StepOutput {
38 pub index: usize,
40 pub variant: String,
43 pub summary: String,
45 pub duration_ms: u128,
47 pub success: bool,
49 pub error: Option<String>,
51}
52
53impl StepOutput {
54 pub fn new(
56 variant: impl Into<String>,
57 summary: impl Into<String>,
58 error: Option<String>,
59 success: bool,
60 ) -> Self {
61 Self {
62 index: 0,
63 variant: variant.into(),
64 summary: summary.into(),
65 duration_ms: 0,
66 success,
67 error,
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct WorkflowResult {
75 pub name: String,
77 pub step_outputs: Vec<StepOutput>,
80 pub total_duration_ms: u128,
82 pub success: bool,
84}
85
86pub struct WorkflowEngine {
93 agents: HashMap<String, AgentHandle>,
94 shared_memory: Arc<SharedMemory>,
95 consensus: Arc<Consensus>,
96}
97
98impl WorkflowEngine {
99 pub fn new(agents: HashMap<String, AgentHandle>) -> Self {
102 Self {
103 agents,
104 shared_memory: Arc::new(SharedMemory::new()),
105 consensus: Arc::new(Consensus::new()),
106 }
107 }
108
109 pub fn with_memory(mut self, memory: Arc<SharedMemory>) -> Self {
112 self.shared_memory = memory;
113 self
114 }
115
116 pub fn with_consensus(mut self, consensus: Arc<Consensus>) -> Self {
118 self.consensus = consensus;
119 self
120 }
121
122 pub fn shared_memory(&self) -> &SharedMemory {
124 &self.shared_memory
125 }
126
127 pub fn consensus(&self) -> &Consensus {
129 &self.consensus
130 }
131
132 pub async fn execute(&self, workflow: &WorkflowDefinition) -> WorkflowResult {
135 let start = std::time::Instant::now();
136 let mut step_outputs = Vec::with_capacity(workflow.steps.len());
137 let mut success = true;
138 let mut last_output: Option<String> = None;
139
140 for (i, step) in workflow.steps.iter().enumerate() {
141 let step_start = std::time::Instant::now();
142 let (mut output, new_last) = self.run_step(step, last_output.as_deref()).await;
143 output.index = i;
144 output.duration_ms = step_start.elapsed().as_millis();
145 let ok = output.success;
146 step_outputs.push(output);
147 if new_last.is_some() {
148 last_output = new_last;
149 }
150 if !ok {
151 success = false;
152 break;
153 }
154 }
155
156 WorkflowResult {
157 name: workflow.name.clone(),
158 step_outputs,
159 total_duration_ms: start.elapsed().as_millis(),
160 success,
161 }
162 }
163
164 async fn run_step(
173 &self,
174 step: &WorkflowStepDef,
175 previous: Option<&str>,
176 ) -> (StepOutput, Option<String>) {
177 match step {
178 WorkflowStepDef::Run {
179 agent,
180 task,
181 output,
182 } => {
183 let task = substitute_previous(task, previous);
184 let Some(handle) = self.agents.get(agent) else {
185 return (
186 StepOutput::new(
187 "Run",
188 format!("Unknown agent '{agent}'"),
189 Some(format!("agent not found: {agent}")),
190 false,
191 ),
192 None,
193 );
194 };
195 match handle.run(task).await {
196 Ok((response, _)) => {
197 let text = response.content;
198 if let Some(key) = output {
199 let mk = MemoryKey::new("workflow", key);
200 let _ = self.shared_memory.write(
201 &mk,
202 serde_json::json!(&text),
203 "engine",
204 None,
205 );
206 }
207 (
208 StepOutput::new(
209 "Run",
210 format!(
211 "agent '{agent}' responded ({} chars)",
212 text.chars().count()
213 ),
214 None,
215 true,
216 ),
217 Some(text),
218 )
219 }
220 Err(e) => (
221 StepOutput::new(
222 "Run",
223 format!("agent '{agent}' failed"),
224 Some(e.to_string()),
225 false,
226 ),
227 None,
228 ),
229 }
230 }
231
232 WorkflowStepDef::Parallel {
233 agents,
234 task,
235 concurrency: _,
236 } => {
237 let mut handles = Vec::new();
239 for name in agents {
240 match self.agents.get(name) {
241 Some(h) => handles.push(h.clone()),
242 None => {
243 return (
244 StepOutput::new(
245 "Parallel",
246 format!("Unknown agent '{name}'"),
247 Some(format!("agent not found: {name}")),
248 false,
249 ),
250 None,
251 );
252 }
253 }
254 }
255 let task = substitute_previous(task, previous);
256
257 let mut join_handles = Vec::with_capacity(handles.len());
262 for h in handles {
263 let task = task.clone();
264 join_handles.push(tokio::spawn(async move { h.run(task).await }));
265 }
266
267 let mut responses: Vec<String> = Vec::new();
268 let mut failures: Vec<String> = Vec::new();
269 for jh in join_handles {
270 match jh.await {
271 Ok(Ok((r, _))) => responses.push(r.content),
272 Ok(Err(e)) => failures.push(e.to_string()),
273 Err(e) => failures.push(format!("join error: {e}")),
274 }
275 }
276
277 let ok = failures.is_empty();
278 let err = if failures.is_empty() {
279 None
280 } else {
281 Some(failures.join("; "))
282 };
283 let summary_text = responses.last().cloned();
286 let summary = format!(
287 "Parallel: {}/{} agents succeeded",
288 responses.len(),
289 agents.len()
290 );
291 (StepOutput::new("Parallel", summary, err, ok), summary_text)
292 }
293
294 WorkflowStepDef::Chain { steps } => {
295 let mut last: Option<String> = previous.map(str::to_string);
299 let mut chain_ok = true;
300 let mut chain_err: Option<String> = None;
301 let mut last_child_summary = String::new();
302 let mut ran = 0usize;
303 let total = steps.len();
304
305 for child in steps {
306 let (out, new_last) = Box::pin(self.run_step(child, last.as_deref())).await;
307 ran += 1;
308 last = new_last;
309 last_child_summary = out.summary;
310 if !out.success {
311 chain_ok = false;
312 chain_err = out.error;
313 break;
314 }
315 }
316
317 let summary = if chain_ok {
318 format!("chain of {total} steps ok — last: {last_child_summary}")
319 } else {
320 format!("chain failed at step {ran}/{total}: {last_child_summary}")
321 };
322
323 (StepOutput::new("Chain", summary, chain_err, chain_ok), last)
324 }
325
326 WorkflowStepDef::ForEach {
327 items_key,
328 namespace,
329 agent,
330 task_template,
331 concurrency: _,
332 } => {
333 let ns = namespace.as_deref().unwrap_or("workflow");
334 let mk = MemoryKey::new(ns, items_key);
335
336 let items = match self.shared_memory.read(&mk) {
337 Some(v) => match v.as_array() {
338 Some(a) => a.clone(),
339 None => {
340 return (
341 StepOutput::new(
342 "ForEach",
343 format!("ForEach: value at {ns}/{items_key} is not an array"),
344 Some(format!("items at {ns}/{items_key} is not a JSON array")),
345 false,
346 ),
347 None,
348 );
349 }
350 },
351 None => {
352 return (
353 StepOutput::new(
354 "ForEach",
355 format!("ForEach: key {ns}/{items_key} not found"),
356 Some(format!(
357 "items key {ns}/{items_key} not found in SharedMemory"
358 )),
359 false,
360 ),
361 None,
362 );
363 }
364 };
365
366 let Some(handle) = self.agents.get(agent) else {
367 return (
368 StepOutput::new(
369 "ForEach",
370 format!("ForEach: unknown agent '{agent}'"),
371 Some(format!("agent not found: {agent}")),
372 false,
373 ),
374 None,
375 );
376 };
377
378 let total = items.len();
379 let mut succeeded = 0usize;
380 let mut last_err: Option<String> = None;
381 let mut last_text: Option<String> = None;
382
383 for (i, item) in items.iter().enumerate() {
387 let item_str = match item {
388 Value::String(s) => s.clone(),
389 other => other.to_string(),
390 };
391 let task = task_template.replace("{item}", &item_str);
392 match handle.run(task).await {
393 Ok((r, _)) => {
394 succeeded += 1;
395 last_text = Some(r.content);
396 }
397 Err(e) => {
398 last_err = Some(format!("item {i} ({item_str}): {e}"));
399 break;
400 }
401 }
402 }
403
404 let ok = last_err.is_none();
405 let summary = format!("ForEach: {succeeded}/{total} items succeeded on '{agent}'");
406 (StepOutput::new("ForEach", summary, last_err, ok), last_text)
407 }
408
409 WorkflowStepDef::Vote {
410 agents,
411 question,
412 threshold,
413 } => {
414 let sid = format!("wf-vote-{}-{}", std::process::id(), question.len());
415 let thr = threshold.unwrap_or(0.5);
416 self.consensus.start(&sid, agents.clone(), thr);
417
418 let mut missing: Vec<String> = Vec::new();
419 let mut first_decision: Option<String> = None;
420
421 for voter in agents {
422 let Some(handle) = self.agents.get(voter) else {
423 missing.push(voter.clone());
424 continue;
425 };
426 let task = question.clone();
427 match handle.run(task).await {
428 Ok((r, _)) => {
429 let value = r.content.trim().to_string();
430 if let Ok(vr) = self.consensus.vote(&sid, voter, value)
431 && vr.decided
432 && first_decision.is_none()
433 {
434 first_decision = vr.decision.clone();
435 }
436 }
437 Err(e) => {
438 tracing::debug!(
442 voter = %voter,
443 error = %e,
444 "Vote: agent failed to respond"
445 );
446 }
447 }
448 }
449
450 let final_result = self.consensus.status(&sid);
451 let (decided, summary) = match final_result {
452 Some(vr) if vr.decided => (
453 true,
454 format!(
455 "Vote: decided = {:?} ({} of {} votes)",
456 vr.decision, vr.votes_received, vr.total_voters
457 ),
458 ),
459 Some(vr) => (
460 false,
461 format!(
462 "Vote: no consensus ({}/{} votes cast)",
463 vr.votes_received, vr.total_voters
464 ),
465 ),
466 None => (false, "Vote: session not found".to_string()),
467 };
468
469 let err = if !missing.is_empty() {
470 Some(format!("missing voters: {}", missing.join(", ")))
471 } else if !decided {
472 Some("no consensus reached".to_string())
473 } else {
474 None
475 };
476
477 let ok = missing.is_empty() && decided;
478 (StepOutput::new("Vote", summary, err, ok), first_decision)
479 }
480
481 WorkflowStepDef::SetState {
482 key,
483 value,
484 namespace,
485 } => {
486 let n = namespace.as_deref().unwrap_or("workflow");
487 let mk = MemoryKey::new(n, key);
488 match self.shared_memory.write(&mk, value.clone(), "engine", None) {
489 Ok(_) => (
490 StepOutput::new("SetState", format!("{n}/{key} = {value}"), None, true),
491 None,
492 ),
493 Err(e) => (
494 StepOutput::new(
495 "SetState",
496 format!("{n}/{key} failed"),
497 Some(e.to_string()),
498 false,
499 ),
500 None,
501 ),
502 }
503 }
504 }
505 }
506}
507
508fn substitute_previous(text: &str, previous: Option<&str>) -> String {
513 match previous {
514 Some(p) => text.replace("{previous}", p),
515 None => text.to_string(),
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[tokio::test]
524 async fn set_state_workflow() {
525 let yaml = "---\nname: test\nsteps:\n - type: set_state\n key: m\n value: 42\n";
526 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
527 let engine = WorkflowEngine::new(HashMap::new());
528 let result = engine.execute(&wf).await;
529 assert!(result.success);
530 assert_eq!(result.step_outputs.len(), 1);
531 assert_eq!(result.step_outputs[0].variant, "SetState");
532 }
533
534 #[tokio::test]
535 async fn unknown_agent_fails_run() {
536 let yaml = "---\nname: t\nsteps:\n - type: run\n agent: ghost\n task: h\n";
537 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
538 let engine = WorkflowEngine::new(HashMap::new());
539 let result = engine.execute(&wf).await;
540 assert!(!result.success);
541 }
542
543 #[tokio::test]
544 async fn short_circuits_on_failure() {
545 let yaml = "---\nname: t\nsteps:\n - type: run\n agent: ghost\n task: x\n - type: set_state\n key: n\n value: 1\n";
546 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
547 let engine = WorkflowEngine::new(HashMap::new());
548 let result = engine.execute(&wf).await;
549 assert!(!result.success);
550 assert_eq!(result.step_outputs.len(), 1);
551 }
552
553 #[tokio::test]
554 async fn chain_runs_nested_steps_and_threads_previous() {
555 let yaml = "---\nname: c\nsteps:\n - type: chain\n steps:\n - type: set_state\n key: a\n value: 1\n - type: set_state\n key: b\n value: 2\n";
559 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
560 let engine = WorkflowEngine::new(HashMap::new());
561 let result = engine.execute(&wf).await;
562 assert!(
563 result.success,
564 "chain should succeed: {:?}",
565 result.step_outputs
566 );
567 assert_eq!(result.step_outputs.len(), 1, "chain emits one summary row");
568 assert_eq!(result.step_outputs[0].variant, "Chain");
569 let mk_a = MemoryKey::new("workflow", "a");
570 let mk_b = MemoryKey::new("workflow", "b");
571 assert_eq!(
572 engine.shared_memory().read(&mk_a),
573 Some(serde_json::json!(1))
574 );
575 assert_eq!(
576 engine.shared_memory().read(&mk_b),
577 Some(serde_json::json!(2))
578 );
579 }
580
581 #[tokio::test]
582 async fn chain_propagates_failure_and_short_circuits() {
583 let yaml = "---\nname: c\nsteps:\n - type: chain\n steps:\n - type: set_state\n key: pre\n value: 1\n - type: run\n agent: ghost\n task: x\n - type: set_state\n key: post\n value: 2\n";
586 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
587 let engine = WorkflowEngine::new(HashMap::new());
588 let result = engine.execute(&wf).await;
589 assert!(!result.success, "chain should fail");
590 let mk_pre = MemoryKey::new("workflow", "pre");
591 let mk_post = MemoryKey::new("workflow", "post");
592 assert_eq!(
593 engine.shared_memory().read(&mk_pre),
594 Some(serde_json::json!(1))
595 );
596 assert_eq!(engine.shared_memory().read(&mk_post), None);
597 }
598
599 #[tokio::test]
600 async fn foreach_rejects_missing_items_key() {
601 let yaml = "---\nname: fe\nsteps:\n - type: for_each\n items_key: nope\n agent: a\n task_template: \"x {item}\"\n";
602 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
603 let engine = WorkflowEngine::new(HashMap::new());
604 let result = engine.execute(&wf).await;
605 assert!(!result.success);
606 assert!(
607 result.step_outputs[0]
608 .error
609 .as_deref()
610 .unwrap_or("")
611 .contains("not found")
612 );
613 }
614
615 #[tokio::test]
616 async fn foreach_rejects_non_array_items() {
617 let engine = WorkflowEngine::new(HashMap::new());
618 let mk = MemoryKey::new("workflow", "items");
619 engine
620 .shared_memory()
621 .write(&mk, serde_json::json!("not an array"), "test", None)
622 .unwrap();
623 let yaml = "---\nname: fe\nsteps:\n - type: for_each\n items_key: items\n agent: a\n task_template: \"x {item}\"\n";
624 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
625 let result = engine.execute(&wf).await;
626 assert!(!result.success);
627 assert!(
628 result.step_outputs[0]
629 .error
630 .as_deref()
631 .unwrap_or("")
632 .contains("not a JSON array")
633 );
634 }
635
636 #[tokio::test]
637 async fn foreach_rejects_unknown_agent() {
638 let engine = WorkflowEngine::new(HashMap::new());
639 let mk = MemoryKey::new("workflow", "items");
640 engine
641 .shared_memory()
642 .write(&mk, serde_json::json!(["a", "b"]), "test", None)
643 .unwrap();
644 let yaml = "---\nname: fe\nsteps:\n - type: for_each\n items_key: items\n agent: ghost\n task_template: \"x {item}\"\n";
645 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
646 let result = engine.execute(&wf).await;
647 assert!(!result.success);
648 assert!(
649 result.step_outputs[0]
650 .error
651 .as_deref()
652 .unwrap_or("")
653 .contains("agent not found")
654 );
655 }
656
657 #[tokio::test]
658 async fn vote_with_unknown_voter_fails() {
659 let yaml = "---\nname: v\nsteps:\n - type: vote\n agents: [ghost]\n question: yes or no\n threshold: 0.5\n";
662 let wf = WorkflowDefinition::from_yaml_str(yaml).unwrap();
663 let engine = WorkflowEngine::new(HashMap::new());
664 let result = engine.execute(&wf).await;
665 assert!(!result.success);
666 assert_eq!(result.step_outputs[0].variant, "Vote");
667 assert!(
668 result.step_outputs[0]
669 .error
670 .as_deref()
671 .unwrap_or("")
672 .contains("missing voters")
673 );
674 }
675}