Multi-Agent Consensus Pattern
The Multi-Agent Consensus Pattern enables multiple AI agents to reach agreement on decisions or shared state through distributed consensus algorithms.
Pattern Overview
This pattern allows multiple agents to coordinate and agree on decisions even in the presence of failures or network partitions, ensuring system-wide consistency and reliability.
Structure
use std::collections::HashMap;
// Consensus proposal
#[derive(Debug, Clone)]
pub struct Proposal {
pub id: String,
pub value: String,
pub proposer_id: String,
pub round: u64,
}
// Agent vote
#[derive(Debug, Clone)]
pub struct Vote {
pub agent_id: String,
pub proposal_id: String,
pub decision: bool, // true = accept, false = reject
pub reasoning: String,
}
// Consensus result
#[derive(Debug, Clone)]
pub struct ConsensusResult {
pub proposal: Proposal,
pub votes: Vec<Vote>,
pub consensus_reached: bool,
pub final_decision: bool,
}
// Consensus agent trait
pub trait ConsensusAgent {
fn get_id(&self) -> &str;
fn propose(&self, value: String, round: u64) -> Proposal;
fn evaluate_proposal(&self, proposal: &Proposal) -> Vote;
fn can_vote(&self, proposal: &Proposal) -> bool;
fn get_weight(&self) -> f64; // Voting weight
}
// Consensus coordinator
pub struct ConsensusCoordinator {
agents: Vec<Box<dyn ConsensusAgent>>,
active_proposals: HashMap<String, Proposal>,
votes: HashMap<String, Vec<Vote>>,
consensus_threshold: f64,
}
impl ConsensusCoordinator {
pub fn new(consensus_threshold: f64) -> Self {
Self {
agents: Vec::new(),
active_proposals: HashMap::new(),
votes: HashMap::new(),
consensus_threshold,
}
}
pub fn add_agent(&mut self, agent: Box<dyn ConsensusAgent>) {
self.agents.push(agent);
}
pub fn submit_proposal(&mut self, proposal: Proposal) -> String {
let proposal_id = proposal.id.clone();
self.active_proposals.insert(proposal_id.clone(), proposal);
self.votes.insert(proposal_id.clone(), Vec::new());
proposal_id
}
pub fn collect_votes(&mut self, proposal_id: &str) -> Option<ConsensusResult> {
if let Some(proposal) = self.active_proposals.get(proposal_id) {
let mut votes = Vec::new();
for agent in &self.agents {
if agent.can_vote(proposal) {
let vote = agent.evaluate_proposal(proposal);
votes.push(vote);
}
}
self.votes.insert(proposal_id.to_string(), votes.clone());
// Calculate consensus
let (consensus_reached, final_decision) = self.calculate_consensus(&votes);
Some(ConsensusResult {
proposal: proposal.clone(),
votes,
consensus_reached,
final_decision,
})
} else {
None
}
}
fn calculate_consensus(&self, votes: &[Vote]) -> (bool, bool) {
let total_weight: f64 = self.agents.iter().map(|a| a.get_weight()).sum();
let mut accept_weight = 0.0;
let mut reject_weight = 0.0;
for vote in votes {
if let Some(agent) = self.agents.iter().find(|a| a.get_id() == vote.agent_id) {
let weight = agent.get_weight();
if vote.decision {
accept_weight += weight;
} else {
reject_weight += weight;
}
}
}
let accept_ratio = accept_weight / total_weight;
let reject_ratio = reject_weight / total_weight;
if accept_ratio >= self.consensus_threshold {
(true, true)
} else if reject_ratio >= self.consensus_threshold {
(true, false)
} else {
(false, false)
}
}
}
// Example consensus agents
pub struct DataValidatorAgent {
id: String,
expertise_domain: String,
}
impl DataValidatorAgent {
pub fn new(id: &str, domain: &str) -> Self {
Self {
id: id.to_string(),
expertise_domain: domain.to_string(),
}
}
}
impl ConsensusAgent for DataValidatorAgent {
fn get_id(&self) -> &str {
&self.id
}
fn propose(&self, value: String, round: u64) -> Proposal {
Proposal {
id: format!("{}_{}", self.id, round),
value,
proposer_id: self.id.clone(),
round,
}
}
fn evaluate_proposal(&self, proposal: &Proposal) -> Vote {
// Simple validation logic
let is_valid = proposal.value.len() > 5 && proposal.value.contains(&self.expertise_domain);
Vote {
agent_id: self.id.clone(),
proposal_id: proposal.id.clone(),
decision: is_valid,
reasoning: if is_valid {
"Proposal meets validation criteria".to_string()
} else {
"Proposal lacks required domain expertise".to_string()
},
}
}
fn can_vote(&self, proposal: &Proposal) -> bool {
proposal.value.contains(&self.expertise_domain) || self.expertise_domain == "general"
}
fn get_weight(&self) -> f64 {
1.0 // Equal weight for all validators
}
}
pub struct QualityAssuranceAgent {
id: String,
quality_threshold: f64,
}
impl QualityAssuranceAgent {
pub fn new(id: &str, threshold: f64) -> Self {
Self {
id: id.to_string(),
quality_threshold: threshold,
}
}
}
impl ConsensusAgent for QualityAssuranceAgent {
fn get_id(&self) -> &str {
&self.id
}
fn propose(&self, value: String, round: u64) -> Proposal {
Proposal {
id: format!("qa_{}_{}", self.id, round),
value,
proposer_id: self.id.clone(),
round,
}
}
fn evaluate_proposal(&self, proposal: &Proposal) -> Vote {
// Quality assessment based on length and content
let quality_score = proposal.value.len() as f64 * 0.1;
let meets_quality = quality_score >= self.quality_threshold;
Vote {
agent_id: self.id.clone(),
proposal_id: proposal.id.clone(),
decision: meets_quality,
reasoning: format!("Quality score: {:.2}, threshold: {:.2}",
quality_score, self.quality_threshold),
}
}
fn can_vote(&self, _proposal: &Proposal) -> bool {
true // QA can vote on all proposals
}
fn get_weight(&self) -> f64 {
1.5 // Higher weight for quality assurance
}
}Usage Example
let mut coordinator = ConsensusCoordinator::new(0.6); // 60% consensus threshold
// Add consensus agents
coordinator.add_agent(Box::new(DataValidatorAgent::new("validator_1", "finance")));
coordinator.add_agent(Box::new(DataValidatorAgent::new("validator_2", "general")));
coordinator.add_agent(Box::new(QualityAssuranceAgent::new("qa_1", 5.0)));
// Submit proposal for consensus
let proposal = Proposal {
id: "proposal_001".to_string(),
value: "Implement new finance risk assessment algorithm".to_string(),
proposer_id: "system".to_string(),
round: 1,
};
let proposal_id = coordinator.submit_proposal(proposal);
// Collect votes and determine consensus
if let Some(result) = coordinator.collect_votes(&proposal_id) {
println!("Consensus reached: {}", result.consensus_reached);
println!("Final decision: {}", result.final_decision);
for vote in result.votes {
println!("Agent {}: {} - {}", vote.agent_id, vote.decision, vote.reasoning);
}
}Benefits
- Distributed Decision Making: No single point of failure
- Democratic Process: All agents participate in decisions
- Fault Tolerance: Continues working despite agent failures
- Transparency: All votes and reasoning are recorded
Use Cases
- Distributed system configuration changes
- Multi-agent task allocation decisions
- Collaborative data validation
- Consensus-based model updates