vex_adversarial/
consensus.rs

1//! Consensus protocols for multi-agent agreement
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// A vote from an agent
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Vote {
9    /// Agent that cast this vote
10    pub agent_id: Uuid,
11    /// Whether they agree (true) or disagree (false)
12    pub agrees: bool,
13    /// Confidence in the vote (0.0 - 1.0)
14    pub confidence: f64,
15    /// Optional reasoning
16    pub reasoning: Option<String>,
17}
18
19impl Vote {
20    /// Create a new vote
21    /// Uses SHA-256 hash of agent_id for UUID generation (collision resistant)
22    pub fn new(agent_id: &str, agrees: bool, confidence: f64) -> Self {
23        use sha2::{Digest, Sha256};
24
25        // Hash the agent_id to get deterministic but collision-resistant bytes
26        let mut hasher = Sha256::new();
27        hasher.update(agent_id.as_bytes());
28        let hash = hasher.finalize();
29
30        // Take first 16 bytes of hash for UUID
31        let mut bytes = [0u8; 16];
32        bytes.copy_from_slice(&hash[..16]);
33
34        Self {
35            agent_id: Uuid::from_bytes(bytes),
36            agrees,
37            confidence,
38            reasoning: None,
39        }
40    }
41}
42
43/// Type of consensus protocol
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ConsensusProtocol {
46    /// Simple majority (> 50%)
47    Majority,
48    /// Super majority (> 66%)
49    SuperMajority,
50    /// Unanimous agreement
51    Unanimous,
52    /// Weighted by confidence scores
53    WeightedConfidence,
54}
55
56/// Result of a consensus vote
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Consensus {
59    /// The protocol used
60    pub protocol: ConsensusProtocol,
61    /// All votes cast
62    pub votes: Vec<Vote>,
63    /// Whether consensus was reached
64    pub reached: bool,
65    /// The consensus decision (if reached)
66    pub decision: Option<bool>,
67    /// Overall confidence
68    pub confidence: f64,
69}
70
71impl Consensus {
72    /// Create a new consensus with the given protocol
73    pub fn new(protocol: ConsensusProtocol) -> Self {
74        Self {
75            protocol,
76            votes: Vec::new(),
77            reached: false,
78            decision: None,
79            confidence: 0.0,
80        }
81    }
82
83    /// Add a vote
84    pub fn add_vote(&mut self, vote: Vote) {
85        self.votes.push(vote);
86    }
87
88    /// Evaluate the votes and determine consensus
89    pub fn evaluate(&mut self) {
90        if self.votes.is_empty() {
91            return;
92        }
93
94        let total = self.votes.len() as f64;
95        let agrees: f64 = self.votes.iter().filter(|v| v.agrees).count() as f64;
96        if total == 0.0 {
97            self.reached = false;
98            self.decision = None;
99            return;
100        }
101
102        let agree_ratio = agrees / total;
103
104        let (reached, decision) = match self.protocol {
105            ConsensusProtocol::Majority => (agree_ratio != 0.5, Some(agree_ratio > 0.5)),
106            ConsensusProtocol::SuperMajority => {
107                if agree_ratio > 0.66 {
108                    (true, Some(true))
109                } else if agree_ratio < 0.34 {
110                    (true, Some(false))
111                } else {
112                    (false, None)
113                }
114            }
115            ConsensusProtocol::Unanimous => {
116                if agree_ratio == 1.0 {
117                    (true, Some(true))
118                } else if agree_ratio == 0.0 {
119                    (true, Some(false))
120                } else {
121                    (false, None)
122                }
123            }
124            ConsensusProtocol::WeightedConfidence => {
125                let weighted_agree: f64 = self
126                    .votes
127                    .iter()
128                    .filter(|v| v.agrees)
129                    .map(|v| v.confidence)
130                    .sum();
131                let weighted_disagree: f64 = self
132                    .votes
133                    .iter()
134                    .filter(|v| !v.agrees)
135                    .map(|v| v.confidence)
136                    .sum();
137                let total_confidence = weighted_agree + weighted_disagree;
138
139                if total_confidence > 0.0 {
140                    let weighted_ratio = weighted_agree / total_confidence;
141                    (true, Some(weighted_ratio > 0.5))
142                } else {
143                    (false, None)
144                }
145            }
146        };
147
148        self.reached = reached;
149        self.decision = decision;
150        if total == 0.0 {
151            self.confidence = 0.0;
152        } else {
153            self.confidence = self.votes.iter().map(|v| v.confidence).sum::<f64>() / total;
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_majority_consensus() {
164        let mut consensus = Consensus::new(ConsensusProtocol::Majority);
165
166        consensus.add_vote(Vote {
167            agent_id: Uuid::new_v4(),
168            agrees: true,
169            confidence: 0.9,
170            reasoning: None,
171        });
172        consensus.add_vote(Vote {
173            agent_id: Uuid::new_v4(),
174            agrees: true,
175            confidence: 0.8,
176            reasoning: None,
177        });
178        consensus.add_vote(Vote {
179            agent_id: Uuid::new_v4(),
180            agrees: false,
181            confidence: 0.7,
182            reasoning: None,
183        });
184
185        consensus.evaluate();
186
187        assert!(consensus.reached);
188        assert_eq!(consensus.decision, Some(true));
189    }
190
191    #[test]
192    fn test_unanimous_fails() {
193        let mut consensus = Consensus::new(ConsensusProtocol::Unanimous);
194
195        consensus.add_vote(Vote {
196            agent_id: Uuid::new_v4(),
197            agrees: true,
198            confidence: 0.9,
199            reasoning: None,
200        });
201        consensus.add_vote(Vote {
202            agent_id: Uuid::new_v4(),
203            agrees: false,
204            confidence: 0.8,
205            reasoning: None,
206        });
207
208        consensus.evaluate();
209
210        assert!(!consensus.reached);
211        assert_eq!(consensus.decision, None);
212    }
213}