1use std::collections::HashMap;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum ConsensusStrategy {
22 Majority,
24 Weighted,
26 Unanimous,
28 HumanInTheLoop,
30}
31
32#[derive(Debug, Clone)]
34pub struct ConsensusRequest {
35 pub prompt: String,
37 pub agents: Vec<String>,
39 pub strategy: ConsensusStrategy,
41 pub timeout: Duration,
43 pub weights: Option<HashMap<String, f32>>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AgentResponse {
50 pub agent: String,
52 pub content: String,
54 pub weight: f32,
56 pub timed_out: bool,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ConsensusResult {
63 pub decision: Option<String>,
65 pub responses: Vec<AgentResponse>,
67 pub strategy_used: ConsensusStrategy,
69 pub consensus_reached: bool,
71}
72
73pub fn resolve(strategy: ConsensusStrategy, responses: &[AgentResponse]) -> ConsensusResult {
79 match strategy {
80 ConsensusStrategy::Majority => resolve_majority(responses),
81 ConsensusStrategy::Weighted => resolve_weighted(responses),
82 ConsensusStrategy::Unanimous => resolve_unanimous(responses),
83 ConsensusStrategy::HumanInTheLoop => resolve_human_in_the_loop(responses),
84 }
85}
86
87pub fn resolve_majority(responses: &[AgentResponse]) -> ConsensusResult {
92 let active: Vec<&AgentResponse> = responses.iter().filter(|r| !r.timed_out).collect();
93
94 if active.is_empty() {
95 return ConsensusResult {
96 decision: None,
97 responses: responses.to_vec(),
98 strategy_used: ConsensusStrategy::Majority,
99 consensus_reached: false,
100 };
101 }
102
103 let mut counts: HashMap<&str, usize> = HashMap::new();
104 for r in &active {
105 *counts.entry(r.content.as_str()).or_insert(0) += 1;
106 }
107
108 let (winner, max_count) = counts
109 .iter()
110 .max_by_key(|&(_, &count)| count)
111 .map(|(&content, &count)| (content.to_string(), count))
112 .unwrap();
113
114 let threshold = active.len() / 2 + 1;
115 let consensus_reached = max_count >= threshold;
116
117 ConsensusResult {
118 decision: if consensus_reached {
119 Some(winner)
120 } else {
121 None
122 },
123 responses: responses.to_vec(),
124 strategy_used: ConsensusStrategy::Majority,
125 consensus_reached,
126 }
127}
128
129pub fn resolve_weighted(responses: &[AgentResponse]) -> ConsensusResult {
131 let active: Vec<&AgentResponse> = responses.iter().filter(|r| !r.timed_out).collect();
132
133 if active.is_empty() {
134 return ConsensusResult {
135 decision: None,
136 responses: responses.to_vec(),
137 strategy_used: ConsensusStrategy::Weighted,
138 consensus_reached: false,
139 };
140 }
141
142 let mut weight_sums: HashMap<&str, f32> = HashMap::new();
143 for r in &active {
144 *weight_sums.entry(r.content.as_str()).or_insert(0.0) += r.weight;
145 }
146
147 let (winner, _) = weight_sums
148 .iter()
149 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
150 .map(|(&content, &weight)| (content.to_string(), weight))
151 .unwrap();
152
153 ConsensusResult {
154 decision: Some(winner),
155 responses: responses.to_vec(),
156 strategy_used: ConsensusStrategy::Weighted,
157 consensus_reached: true,
158 }
159}
160
161pub fn resolve_unanimous(responses: &[AgentResponse]) -> ConsensusResult {
163 let active: Vec<&AgentResponse> = responses.iter().filter(|r| !r.timed_out).collect();
164
165 if active.is_empty() {
166 return ConsensusResult {
167 decision: None,
168 responses: responses.to_vec(),
169 strategy_used: ConsensusStrategy::Unanimous,
170 consensus_reached: false,
171 };
172 }
173
174 let first = &active[0].content;
175 let all_agree = active.iter().all(|r| r.content == *first);
176
177 ConsensusResult {
178 decision: if all_agree {
179 Some(first.clone())
180 } else {
181 None
182 },
183 responses: responses.to_vec(),
184 strategy_used: ConsensusStrategy::Unanimous,
185 consensus_reached: all_agree,
186 }
187}
188
189pub fn resolve_human_in_the_loop(responses: &[AgentResponse]) -> ConsensusResult {
191 ConsensusResult {
192 decision: None,
193 responses: responses.to_vec(),
194 strategy_used: ConsensusStrategy::HumanInTheLoop,
195 consensus_reached: false,
196 }
197}
198
199#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn make_response(agent: &str, content: &str) -> AgentResponse {
208 AgentResponse {
209 agent: agent.to_string(),
210 content: content.to_string(),
211 weight: 1.0,
212 timed_out: false,
213 }
214 }
215
216 fn make_weighted_response(agent: &str, content: &str, weight: f32) -> AgentResponse {
217 AgentResponse {
218 agent: agent.to_string(),
219 content: content.to_string(),
220 weight,
221 timed_out: false,
222 }
223 }
224
225 fn make_timed_out(agent: &str) -> AgentResponse {
226 AgentResponse {
227 agent: agent.to_string(),
228 content: String::new(),
229 weight: 1.0,
230 timed_out: true,
231 }
232 }
233
234 #[test]
235 fn majority_clear_winner() {
236 let responses = vec![
237 make_response("a1", "yes"),
238 make_response("a2", "yes"),
239 make_response("a3", "no"),
240 ];
241 let result = resolve_majority(&responses);
242
243 assert!(result.consensus_reached);
244 assert_eq!(result.decision, Some("yes".to_string()));
245 assert_eq!(result.strategy_used, ConsensusStrategy::Majority);
246 }
247
248 #[test]
249 fn majority_no_consensus() {
250 let responses = vec![
252 make_response("a1", "yes"),
253 make_response("a2", "no"),
254 make_response("a3", "yes"),
255 make_response("a4", "no"),
256 ];
257 let result = resolve_majority(&responses);
258
259 assert!(!result.consensus_reached);
260 assert!(result.decision.is_none());
261 }
262
263 #[test]
264 fn majority_with_timeouts() {
265 let responses = vec![
267 make_response("a1", "yes"),
268 make_response("a2", "yes"),
269 make_timed_out("a3"),
270 ];
271 let result = resolve_majority(&responses);
272
273 assert!(result.consensus_reached);
274 assert_eq!(result.decision, Some("yes".to_string()));
275 }
276
277 #[test]
278 fn unanimous_all_agree() {
279 let responses = vec![
280 make_response("a1", "42"),
281 make_response("a2", "42"),
282 make_response("a3", "42"),
283 ];
284 let result = resolve_unanimous(&responses);
285
286 assert!(result.consensus_reached);
287 assert_eq!(result.decision, Some("42".to_string()));
288 }
289
290 #[test]
291 fn unanimous_disagreement() {
292 let responses = vec![
293 make_response("a1", "42"),
294 make_response("a2", "43"),
295 ];
296 let result = resolve_unanimous(&responses);
297
298 assert!(!result.consensus_reached);
299 assert!(result.decision.is_none());
300 }
301
302 #[test]
303 fn weighted_higher_weight_wins() {
304 let responses = vec![
305 make_weighted_response("expert", "A", 5.0),
306 make_weighted_response("junior1", "B", 1.0),
307 make_weighted_response("junior2", "B", 1.0),
308 ];
309 let result = resolve_weighted(&responses);
310
311 assert!(result.consensus_reached);
312 assert_eq!(result.decision, Some("A".to_string()));
313 }
314
315 #[test]
316 fn human_in_the_loop_no_decision() {
317 let responses = vec![
318 make_response("a1", "option A"),
319 make_response("a2", "option B"),
320 ];
321 let result = resolve_human_in_the_loop(&responses);
322
323 assert!(!result.consensus_reached);
324 assert!(result.decision.is_none());
325 assert_eq!(result.responses.len(), 2);
326 }
327
328 #[test]
329 fn all_timed_out() {
330 let responses = vec![
331 make_timed_out("a1"),
332 make_timed_out("a2"),
333 ];
334
335 let majority = resolve_majority(&responses);
336 assert!(!majority.consensus_reached);
337 assert!(majority.decision.is_none());
338
339 let unanimous = resolve_unanimous(&responses);
340 assert!(!unanimous.consensus_reached);
341
342 let weighted = resolve_weighted(&responses);
343 assert!(!weighted.consensus_reached);
344 }
345
346 #[test]
347 fn serde_round_trip() {
348 let result = ConsensusResult {
349 decision: Some("answer".to_string()),
350 responses: vec![make_response("a1", "answer")],
351 strategy_used: ConsensusStrategy::Majority,
352 consensus_reached: true,
353 };
354
355 let json = serde_json::to_string_pretty(&result).unwrap();
356 let parsed: ConsensusResult = serde_json::from_str(&json).unwrap();
357
358 assert_eq!(parsed.decision, Some("answer".to_string()));
359 assert!(parsed.consensus_reached);
360 assert_eq!(parsed.strategy_used, ConsensusStrategy::Majority);
361 assert_eq!(parsed.responses.len(), 1);
362 assert_eq!(parsed.responses[0].agent, "a1");
363 }
364
365 #[test]
366 fn resolve_dispatch() {
367 let responses = vec![
368 make_response("a1", "yes"),
369 make_response("a2", "yes"),
370 make_response("a3", "no"),
371 ];
372
373 let r1 = resolve(ConsensusStrategy::Majority, &responses);
374 assert_eq!(r1.strategy_used, ConsensusStrategy::Majority);
375 assert!(r1.consensus_reached);
376
377 let r2 = resolve(ConsensusStrategy::Unanimous, &responses);
378 assert_eq!(r2.strategy_used, ConsensusStrategy::Unanimous);
379 assert!(!r2.consensus_reached);
380
381 let r3 = resolve(ConsensusStrategy::HumanInTheLoop, &responses);
382 assert_eq!(r3.strategy_used, ConsensusStrategy::HumanInTheLoop);
383 assert!(!r3.consensus_reached);
384 }
385}