1use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum AgentRole {
16 Planner,
17 Coder,
18 Reviewer,
19 Tester,
20 Fixer,
21}
22
23impl AgentRole {
24 fn merge_priority(self) -> u8 {
25 match self {
26 AgentRole::Fixer => 0,
27 AgentRole::Reviewer => 1,
28 AgentRole::Tester => 2,
29 AgentRole::Coder => 3,
30 AgentRole::Planner => 4,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct RoleTemplate {
38 pub role: AgentRole,
39 pub required_input_keys: BTreeSet<String>,
40 pub produced_output_keys: BTreeSet<String>,
41 pub allowed_handoffs: BTreeSet<AgentRole>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct RoleHandoff {
47 pub task_id: String,
48 pub from: AgentRole,
49 pub to: AgentRole,
50 pub payload: BTreeMap<String, String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct RoleOutput {
56 pub role: AgentRole,
57 pub step: u32,
58 pub values: BTreeMap<String, String>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct MergeConflict {
64 pub key: String,
65 pub existing_role: AgentRole,
66 pub incoming_role: AgentRole,
67 pub existing_value: String,
68 pub incoming_value: String,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum MergeConflictStrategy {
74 FailOnConflict,
76 PreferRolePriority,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct MergeOutcome {
83 pub values: BTreeMap<String, String>,
84 pub conflicts: Vec<MergeConflict>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum HandoffValidationError {
89 MissingTemplate(AgentRole),
90 ForbiddenRoute { from: AgentRole, to: AgentRole },
91 MissingRequiredKeys { role: AgentRole, keys: Vec<String> },
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum ParallelPlanError {
96 DuplicateRole(AgentRole),
97}
98
99pub fn default_role_templates() -> BTreeMap<AgentRole, RoleTemplate> {
101 let mut map = BTreeMap::new();
102
103 map.insert(
104 AgentRole::Planner,
105 RoleTemplate {
106 role: AgentRole::Planner,
107 required_input_keys: BTreeSet::new(),
108 produced_output_keys: ["task_plan".to_string()].into_iter().collect(),
109 allowed_handoffs: [AgentRole::Coder, AgentRole::Tester].into_iter().collect(),
110 },
111 );
112 map.insert(
113 AgentRole::Coder,
114 RoleTemplate {
115 role: AgentRole::Coder,
116 required_input_keys: ["task_plan".to_string()].into_iter().collect(),
117 produced_output_keys: ["code_patch".to_string()].into_iter().collect(),
118 allowed_handoffs: [AgentRole::Reviewer, AgentRole::Tester]
119 .into_iter()
120 .collect(),
121 },
122 );
123 map.insert(
124 AgentRole::Reviewer,
125 RoleTemplate {
126 role: AgentRole::Reviewer,
127 required_input_keys: ["code_patch".to_string()].into_iter().collect(),
128 produced_output_keys: ["review_notes".to_string()].into_iter().collect(),
129 allowed_handoffs: [AgentRole::Fixer, AgentRole::Tester].into_iter().collect(),
130 },
131 );
132 map.insert(
133 AgentRole::Tester,
134 RoleTemplate {
135 role: AgentRole::Tester,
136 required_input_keys: ["code_patch".to_string()].into_iter().collect(),
137 produced_output_keys: ["test_report".to_string()].into_iter().collect(),
138 allowed_handoffs: [AgentRole::Fixer, AgentRole::Reviewer]
139 .into_iter()
140 .collect(),
141 },
142 );
143 map.insert(
144 AgentRole::Fixer,
145 RoleTemplate {
146 role: AgentRole::Fixer,
147 required_input_keys: ["code_patch".to_string()].into_iter().collect(),
148 produced_output_keys: ["code_patch".to_string(), "fix_notes".to_string()]
149 .into_iter()
150 .collect(),
151 allowed_handoffs: BTreeSet::new(),
152 },
153 );
154
155 map
156}
157
158pub fn validate_handoff(
160 templates: &BTreeMap<AgentRole, RoleTemplate>,
161 handoff: &RoleHandoff,
162) -> Result<(), HandoffValidationError> {
163 let from_template = templates
164 .get(&handoff.from)
165 .ok_or(HandoffValidationError::MissingTemplate(handoff.from))?;
166 let to_template = templates
167 .get(&handoff.to)
168 .ok_or(HandoffValidationError::MissingTemplate(handoff.to))?;
169
170 if !from_template.allowed_handoffs.contains(&handoff.to) {
171 return Err(HandoffValidationError::ForbiddenRoute {
172 from: handoff.from,
173 to: handoff.to,
174 });
175 }
176
177 let mut missing = Vec::new();
178 for key in &to_template.required_input_keys {
179 if !handoff.payload.contains_key(key) {
180 missing.push(key.clone());
181 }
182 }
183 if !missing.is_empty() {
184 return Err(HandoffValidationError::MissingRequiredKeys {
185 role: handoff.to,
186 keys: missing,
187 });
188 }
189
190 Ok(())
191}
192
193pub fn merge_role_outputs(outputs: &[RoleOutput], strategy: MergeConflictStrategy) -> MergeOutcome {
195 let mut ordered = outputs.to_vec();
196 ordered.sort_by_key(|o| (o.step, o.role));
197
198 let mut values = BTreeMap::<String, String>::new();
199 let mut owners = BTreeMap::<String, AgentRole>::new();
200 let mut conflicts = Vec::<MergeConflict>::new();
201
202 for output in ordered {
203 for (key, incoming_value) in output.values {
204 match values.get(&key) {
205 None => {
206 values.insert(key.clone(), incoming_value.clone());
207 owners.insert(key, output.role);
208 }
209 Some(existing_value) if existing_value == &incoming_value => {
210 }
212 Some(existing_value) => {
213 let existing_role = *owners.get(&key).unwrap_or(&output.role);
214 conflicts.push(MergeConflict {
215 key: key.clone(),
216 existing_role,
217 incoming_role: output.role,
218 existing_value: existing_value.clone(),
219 incoming_value: incoming_value.clone(),
220 });
221
222 if matches!(strategy, MergeConflictStrategy::PreferRolePriority)
223 && output.role.merge_priority() < existing_role.merge_priority()
224 {
225 values.insert(key.clone(), incoming_value);
226 owners.insert(key, output.role);
227 }
228 }
229 }
230 }
231 }
232
233 MergeOutcome { values, conflicts }
234}
235
236pub fn validate_parallel_roles(roles: &[AgentRole]) -> Result<(), ParallelPlanError> {
238 let mut seen = BTreeSet::new();
239 for role in roles {
240 if !seen.insert(*role) {
241 return Err(ParallelPlanError::DuplicateRole(*role));
242 }
243 }
244 Ok(())
245}
246
247pub fn deterministic_role_order(roles: &[AgentRole]) -> Vec<AgentRole> {
249 let mut ordered = roles.to_vec();
250 ordered.sort_by_key(|r| (r.merge_priority(), *r));
251 ordered
252}