use crate::role_orchestration::{
error::{RoleError, RoleResult},
roles::{AgentRole, RoleTemplate},
};
#[derive(Debug, Clone, PartialEq)]
pub struct RoleStep {
pub position: usize,
pub role: AgentRole,
pub accepts_from: Vec<AgentRole>,
pub parallelizable: bool,
}
#[derive(Debug, Clone)]
pub struct ExecutionPlan {
pub task_title: String,
pub steps: Vec<RoleStep>,
}
impl ExecutionPlan {
pub fn parallel_groups(&self) -> Vec<Vec<&RoleStep>> {
let mut groups: Vec<Vec<&RoleStep>> = Vec::new();
let mut current: Vec<&RoleStep> = Vec::new();
for step in &self.steps {
if step.parallelizable {
current.push(step);
} else {
if !current.is_empty() {
groups.push(std::mem::take(&mut current));
}
groups.push(vec![step]);
}
}
if !current.is_empty() {
groups.push(current);
}
groups
}
}
pub fn validate_handoff_sequence(
proposed_sequence: &[AgentRole],
templates: &[RoleTemplate],
) -> RoleResult<()> {
let is_parallel = |r: &AgentRole| matches!(r, AgentRole::Reviewer | AgentRole::Tester);
for window in proposed_sequence.windows(2) {
let from = &window[0];
let to = &window[1];
if is_parallel(from) && is_parallel(to) {
continue;
}
let template = templates.iter().find(|t| &t.role == to).ok_or_else(|| {
RoleError::UnauthorizedHandoff {
role: to.to_string(),
from: from.to_string(),
}
})?;
if !template.accepts_from.contains(from) {
return Err(RoleError::UnauthorizedHandoff {
role: to.to_string(),
from: from.to_string(),
});
}
}
Ok(())
}
pub fn build_execution_plan(
task_title: &str,
sequence: Vec<AgentRole>,
templates: &[RoleTemplate],
) -> RoleResult<ExecutionPlan> {
if sequence.is_empty() {
return Err(RoleError::EmptyDecomposition {
role: "none".to_string(),
});
}
let is_parallel = |r: &AgentRole| matches!(r, AgentRole::Reviewer | AgentRole::Tester);
for window in sequence.windows(2) {
let from = &window[0];
let to = &window[1];
if is_parallel(from) && is_parallel(to) {
continue;
}
let template = templates.iter().find(|t| &t.role == to).ok_or_else(|| {
RoleError::UnauthorizedHandoff {
role: to.to_string(),
from: from.to_string(),
}
})?;
if !template.accepts_from.contains(from) {
return Err(RoleError::UnauthorizedHandoff {
role: to.to_string(),
from: from.to_string(),
});
}
}
let steps = sequence
.into_iter()
.enumerate()
.map(|(pos, role)| {
let accepts_from = templates
.iter()
.find(|t| t.role == role)
.map(|t| t.accepts_from.clone())
.unwrap_or_default();
let parallelizable = is_parallel(&role);
RoleStep {
position: pos,
role,
accepts_from,
parallelizable,
}
})
.collect();
Ok(ExecutionPlan {
task_title: task_title.to_string(),
steps,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::role_orchestration::roles::RoleTemplate;
fn templates() -> Vec<RoleTemplate> {
RoleTemplate::standard_pipeline()
}
#[test]
fn test_standard_sequence_passes_validation() {
let seq = vec![
AgentRole::Planner,
AgentRole::Coder,
AgentRole::Reviewer,
AgentRole::Tester,
AgentRole::Fixer,
];
assert!(validate_handoff_sequence(&seq, &templates()).is_ok());
}
#[test]
fn test_unauthorized_handoff_coder_to_planner_is_rejected() {
let seq = vec![AgentRole::Coder, AgentRole::Planner];
let result = validate_handoff_sequence(&seq, &templates());
assert!(result.is_err());
match result.unwrap_err() {
RoleError::UnauthorizedHandoff { role, from } => {
assert_eq!(role, "planner");
assert_eq!(from, "coder");
}
other => panic!("Expected UnauthorizedHandoff, got {:?}", other),
}
}
#[test]
fn test_empty_sequence_returns_error() {
let result = build_execution_plan("task", vec![], &templates());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
RoleError::EmptyDecomposition { .. }
));
}
#[test]
fn test_execution_plan_parallel_groups_groups_reviewer_and_tester() {
let plan = build_execution_plan(
"test task",
vec![AgentRole::Coder, AgentRole::Reviewer, AgentRole::Tester],
&templates(),
)
.unwrap();
let groups = plan.parallel_groups();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].len(), 1);
assert_eq!(groups[0][0].role, AgentRole::Coder);
assert_eq!(groups[1].len(), 2);
}
#[test]
fn test_build_plan_preserves_role_order() {
let seq = vec![
AgentRole::Planner,
AgentRole::Coder,
AgentRole::Reviewer,
AgentRole::Tester,
];
let plan = build_execution_plan("ordered task", seq.clone(), &templates()).unwrap();
assert_eq!(plan.steps.len(), 4);
for (i, step) in plan.steps.iter().enumerate() {
assert_eq!(step.position, i);
assert_eq!(step.role, seq[i]);
}
}
#[test]
fn test_single_role_plan_is_valid() {
let plan = build_execution_plan("solo", vec![AgentRole::Planner], &templates()).unwrap();
assert_eq!(plan.steps.len(), 1);
assert!(!plan.steps[0].parallelizable);
}
#[test]
fn test_validate_handoff_sequence_skips_parallel_sibling_edge() {
let seq = vec![
AgentRole::Coder,
AgentRole::Reviewer,
AgentRole::Tester,
AgentRole::Fixer,
];
assert!(validate_handoff_sequence(&seq, &templates()).is_ok());
}
}