Council Pattern
Multi-agent deliberation framework for collaborative decision-making
Table of Contents
- Overview
- Quick Start
- Turn-Taking Strategies
- Termination Conditions
- Garrison Integration
- Configuration
- Examples
- Best Practices
- API Reference
Overview
The Council pattern enables multiple Paladin agents to engage in structured deliberation and collaborative decision-making. Unlike parallel execution (Phalanx) or sequential processing (Formation), Council creates a conversational dynamic where agents take turns, build on each other's contributions, and work toward consensus or comprehensive analysis.
Key Concepts
Council: A group of Paladin agents (participants) engaging in structured discussion around a topic.
Moderator: Optional specialized agent controlling discussion flow and termination decisions.
Turn-Taking: Strategy determining which participant speaks next (RoundRobin, ModeratorDirected).
Termination Condition: Rule determining when deliberation concludes (MaxRounds, Consensus, ModeratorDecision, Keyword).
Conversation History: Accumulated context allowing agents to reference and build on previous contributions.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Council │
├─────────────────────────────────────────────────────────┤
│ │
│ Topic: "Should we implement feature X?" │
│ │
│ Round 1: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TechnicalExp │→ │ BusinessExp │→ │ SecurityExp │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Round 2: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TechnicalExp │→ │ BusinessExp │→ │ SecurityExp │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ [Continues until termination condition met] │
│ │
│ Final Output: Synthesized recommendations │
└─────────────────────────────────────────────────────────┘
When to Use Council
✅ Ideal Use Cases:
- Expert panel discussions: Gather diverse perspectives on complex decisions
- Consensus building: Work toward agreement among stakeholders
- Comprehensive analysis: Ensure all angles considered through dialogue
- Deliberative decision-making: Structured debate with turn-taking
- Collaborative problem-solving: Build on each other's ideas iteratively
❌ Not Ideal For:
- Simple sequential processing → Use Formation
- Independent parallel analysis → Use Phalanx
- Quick routing decisions → Use Grove
- Complex conditional workflows → Use Campaign
Quick Start
Basic Council Example
use paladin::core::platform::container::battalion::council::{ CouncilBuilder, CouncilConfig, TurnStrategy, TerminationCondition }; use paladin::application::services::battalion::council_service::CouncilExecutionService; use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Create participants let technical_expert = create_paladin( "TechnicalExpert", "You are a technical expert focusing on implementation feasibility." ); let business_expert = create_paladin( "BusinessExpert", "You are a business strategist focusing on ROI and market impact." ); let security_expert = create_paladin( "SecurityExpert", "You are a security expert focusing on risks and compliance." ); // Build council let council = CouncilBuilder::new() .name("Expert Panel Council") .add_participant(technical_expert) .add_participant(business_expert) .add_participant(security_expert) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::MaxRounds(3)) .build()?; // Execute council discussion let service = CouncilExecutionService::new( Arc::new(paladin_port), Some(Arc::new(garrison_port)) // Optional: store conversation history ); let topic = "Should we implement two-factor authentication for all users?"; let result = service.convene(&council, topic).await?; println!("Discussion Transcript:\n{}", result.conversation_history); println!("\nFinal Recommendation:\n{}", result.final_output); Ok(()) }
Output Example
Round 1:
--------
TechnicalExpert: Implementing 2FA is technically feasible. We can use TOTP
with existing libraries like `authenticator`. Main effort is UI/UX for enrollment
and recovery flows. Estimate: 2 sprint cycles.
BusinessExpert: From a business perspective, 2FA adds friction but increases trust.
Our enterprise customers require it per SOC 2 compliance. Churn risk for consumer
users is moderate, can be mitigated with optional rollout. ROI positive within 6 months.
SecurityExpert: 2FA significantly reduces account takeover risk (98% reduction per
Microsoft data). Essential for PII protection. Recommend mandatory for admin accounts,
optional for users. Need backup codes and recovery process for support.
Round 2:
--------
TechnicalExpert: Agreed on phased rollout. Suggest SMS fallback for users without
smartphones, though less secure. Need to handle edge cases like lost devices.
BusinessExpert: Phased rollout aligns with Q3 enterprise push. Can market as security
upgrade. Estimate $50K implementation, $200K annual revenue uplift from enterprise.
SecurityExpert: SMS is vulnerable to SIM swapping. Recommend authenticator app as
primary, with backup codes. Must document recovery procedures for customer support.
Round 3:
--------
[All participants refine recommendations based on discussion...]
Final Recommendation:
--------------------
Implement 2FA with phased rollout: (1) Admin accounts mandatory Q2, (2) Enterprise
customers Q3, (3) All users optional Q4. Use authenticator apps with backup codes.
Skip SMS due to security concerns. Budget approved: $50K dev + $30K support training.
Expected impact: 98% reduction in account takeovers, $200K annual revenue increase.
Turn-Taking Strategies
Turn-taking strategies determine who speaks next in the council discussion.
1. RoundRobin
Description: Participants speak in order, cycling through the list repeatedly.
Behavior:
- Fair: Each participant gets equal speaking opportunities
- Predictable: Order known in advance
- Balanced: No participant dominates discussion
Use When:
- Equal expertise importance
- Balanced participation desired
- Simple discussion structure
Example:
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .add_participant(expert1) .add_participant(expert2) .add_participant(expert3) .turn_strategy(TurnStrategy::RoundRobin) .build()?; // Turn order: Expert1 → Expert2 → Expert3 → Expert1 → Expert2 → ... }
Diagram:
Round 1: [Expert1] → [Expert2] → [Expert3]
Round 2: [Expert1] → [Expert2] → [Expert3]
Round 3: [Expert1] → [Expert2] → [Expert3]
2. ModeratorDirected
Description: A moderator agent controls the discussion flow, selecting who speaks next.
Behavior:
- Strategic: Moderator calls on relevant experts based on context
- Flexible: Can skip participants if not relevant
- Guided: Moderator ensures productive discussion
Use When:
- Complex topics requiring expert guidance
- Some experts more relevant than others
- Need to avoid tangents
- Senior oversight required
Example:
#![allow(unused)] fn main() { let moderator = create_paladin( "Moderator", "You moderate the council. Call on experts strategically and decide when to conclude." ); let council = CouncilBuilder::new() .moderator(moderator) .add_participant(frontend_expert) .add_participant(backend_expert) .add_participant(devops_expert) .turn_strategy(TurnStrategy::ModeratorDirected) .build()?; }
Moderator System Prompt Example:
#![allow(unused)] fn main() { let moderator_prompt = r#" You are the Chief Architect moderating a technical council. Your responsibilities: 1. FACILITATE: Call on relevant experts based on topic 2. MANAGE: Ensure focused, productive discussion 3. SYNTHESIZE: Identify key themes and consensus points 4. DECIDE: Determine when sufficient deliberation achieved Example commands: - "I call on [ExpertName] to address [topic]" - "Let's hear from [ExpertName] on [aspect]" - "We have consensus - discussion complete" Keep discussion focused and drive toward actionable recommendations. "#; }
Diagram:
┌──────────────┐
│ Moderator │
└──────┬───────┘
│ (calls on)
┌───────────┼───────────┐
▼ ▼ ▼
[Expert1] [Expert2] [Expert3]
│ │ │
└───────────┴───────────┘
│
(responds to)
┌──────▼───────┐
│ Moderator │
└──────────────┘
Termination Conditions
Termination conditions determine when the council discussion concludes.
1. MaxRounds
Description: Discussion ends after a fixed number of rounds.
Use When:
- Time-boxed discussions
- Budget constraints (LLM API costs)
- Simple topics not requiring extended debate
Configuration:
#![allow(unused)] fn main() { .termination_condition(TerminationCondition::MaxRounds(5)) }
Behavior:
- Deterministic: Always stops after N rounds
- Predictable cost: Known number of LLM calls
- May end prematurely if consensus not reached
Example:
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .add_participant(expert1) .add_participant(expert2) .add_participant(expert3) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::MaxRounds(3)) // 3 rounds .build()?; // 3 participants × 3 rounds = 9 total turns }
2. Consensus
Description: Discussion continues until participants reach consensus (detected via keyword or sentiment analysis).
Use When:
- Consensus critical to outcome
- Quality more important than speed
- Sufficient budget for extended discussion
Configuration:
#![allow(unused)] fn main() { .termination_condition(TerminationCondition::Consensus { required_agreement_keywords: vec![ "I agree".to_string(), "consensus reached".to_string(), "we all support".to_string(), ], min_participants: 2, // At least 2 participants must express agreement }) }
Detection Logic:
- Check if recent participant outputs contain agreement keywords
- Count how many participants expressed agreement
- If
min_participantsthreshold met → terminate
Example:
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .add_participant(expert1) .add_participant(expert2) .add_participant(expert3) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::Consensus { required_agreement_keywords: vec!["I agree".into(), "consensus".into()], min_participants: 2, }) .max_rounds(10) // Safety limit .build()?; }
Behavior:
- Dynamic: Stops when agreement detected
- Quality-focused: Ensures alignment
- Risk: May run to max_rounds if no consensus
3. ModeratorDecision
Description: Moderator decides when sufficient deliberation has occurred.
Use When:
- ModeratorDirected turn strategy
- Need expert judgment on completeness
- Complex topics requiring flexible stopping point
Configuration:
#![allow(unused)] fn main() { .termination_condition(TerminationCondition::ModeratorDecision) }
Moderator Signal: The moderator indicates completion by including a termination phrase:
"The discussion is complete."
"We have sufficient input to proceed."
"I conclude this council session."
Detection Keywords (configurable):
#![allow(unused)] fn main() { pub const DEFAULT_MODERATOR_TERMINATION_KEYWORDS: &[&str] = &[ "discussion complete", "conclude", "sufficient input", "end discussion", ]; }
Example:
#![allow(unused)] fn main() { let moderator = create_paladin("ChiefArchitect", moderator_prompt); let council = CouncilBuilder::new() .moderator(moderator) .add_participant(expert1) .add_participant(expert2) .turn_strategy(TurnStrategy::ModeratorDirected) .termination_condition(TerminationCondition::ModeratorDecision) .max_rounds(20) // Safety limit .build()?; }
4. Keyword
Description: Discussion ends when any participant uses a specific keyword.
Use When:
- Explicit approval workflows (e.g., "APPROVED")
- Go/no-go decisions
- Trigger-based termination
Configuration:
#![allow(unused)] fn main() { .termination_condition(TerminationCondition::Keyword("APPROVED".to_string())) }
Example - Code Review Approval:
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .add_participant(senior_dev) .add_participant(security_reviewer) .add_participant(qa_lead) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::Keyword("APPROVED".into())) .build()?; // Discussion continues until any participant says "APPROVED" }
Use Case - Budget Approval:
CFO: "After reviewing the proposal, I approve the $500K budget. APPROVED."
→ Discussion terminates immediately
Garrison Integration
Council supports conversation history storage via Garrison (memory system), enabling:
✅ Context Persistence: Store full discussion transcript ✅ Retrieval: Reference past council decisions ✅ Analysis: Track consensus patterns over time ✅ Auditing: Complete audit trail of deliberations
Enabling Garrison
#![allow(unused)] fn main() { use paladin::infrastructure::adapters::garrison::in_memory_garrison::InMemoryGarrison; // Create Garrison let garrison = Arc::new(InMemoryGarrison::new()); // Create Council service with Garrison let service = CouncilExecutionService::new( Arc::new(paladin_port), Some(garrison.clone()) // Enable history storage ); // Execute council let result = service.convene(&council, topic).await?; // Access stored conversation let history = garrison.retrieve(&council.id()).await?; println!("Full transcript: {}", history); }
Storage Format
{
"council_id": "council-uuid-123",
"topic": "Should we implement feature X?",
"participants": ["TechnicalExpert", "BusinessExpert", "SecurityExpert"],
"rounds": [
{
"round": 1,
"turns": [
{
"speaker": "TechnicalExpert",
"content": "Technical perspective: ...",
"timestamp": "2026-02-04T10:30:00Z"
},
...
]
}
],
"termination_reason": "MaxRounds",
"final_output": "Synthesized recommendation: ..."
}
Configuration
CouncilConfig
#![allow(unused)] fn main() { pub struct CouncilConfig { /// Turn-taking strategy (RoundRobin or ModeratorDirected) pub turn_strategy: TurnStrategy, /// Termination condition pub termination_condition: TerminationCondition, /// Maximum rounds (safety limit) pub max_rounds: u32, /// Whether to store conversation history in Garrison pub store_history: bool, /// Timeout per participant turn (seconds) pub turn_timeout: Duration, } impl Default for CouncilConfig { fn default() -> Self { Self { turn_strategy: TurnStrategy::RoundRobin, termination_condition: TerminationCondition::MaxRounds(5), max_rounds: 10, store_history: true, turn_timeout: Duration::from_secs(120), } } } }
Builder Pattern
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .name("Expert Panel") .add_participant(expert1) .add_participant(expert2) .add_participant(expert3) .moderator(moderator) // Optional .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::MaxRounds(5)) .max_rounds(10) .store_history(true) .build()?; }
Examples
Example 1: Security Review Panel
#![allow(unused)] fn main() { let security_expert = create_paladin("SecurityExpert", "Focus on security risks and controls"); let legal_expert = create_paladin("LegalExpert", "Focus on compliance and legal requirements"); let technical_expert = create_paladin("TechnicalExpert", "Focus on implementation feasibility"); let council = CouncilBuilder::new() .name("Security Review Council") .add_participant(security_expert) .add_participant(legal_expert) .add_participant(technical_expert) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::MaxRounds(3)) .build()?; let topic = "Evaluate the security implications of storing customer payment data"; let result = service.convene(&council, topic).await?; }
Example 2: Moderated Architecture Review
#![allow(unused)] fn main() { let moderator = create_paladin("ChiefArchitect", MODERATOR_PROMPT); let council = CouncilBuilder::new() .name("Architecture Review") .moderator(moderator) .add_participant(frontend_lead) .add_participant(backend_lead) .add_participant(devops_lead) .turn_strategy(TurnStrategy::ModeratorDirected) .termination_condition(TerminationCondition::ModeratorDecision) .max_rounds(15) .build()?; let topic = "Should we adopt GraphQL or stick with REST?"; let result = service.convene(&council, topic).await?; }
Example 3: Consensus-Based Decision
#![allow(unused)] fn main() { let council = CouncilBuilder::new() .name("Product Launch Council") .add_participant(product_manager) .add_participant(engineering_lead) .add_participant(marketing_lead) .turn_strategy(TurnStrategy::RoundRobin) .termination_condition(TerminationCondition::Consensus { required_agreement_keywords: vec!["I agree".into(), "consensus".into()], min_participants: 2, }) .max_rounds(8) .build()?; let topic = "Are we ready to launch the new feature to production?"; let result = service.convene(&council, topic).await?; }
Best Practices
1. Participant Selection
✅ Do:
- Choose 3-7 participants (optimal for discussion)
- Ensure diverse perspectives
- Define clear expertise areas in system prompts
- Use descriptive names (TechnicalExpert vs Expert1)
❌ Don't:
- Use too many participants (>10 = chaotic)
- Include redundant perspectives
- Use generic system prompts
- Forget to specify participant roles
2. System Prompts
✅ Do:
#![allow(unused)] fn main() { let prompt = r#" You are a security expert in a council discussion. Your role: - Identify security risks and vulnerabilities - Recommend security controls - Build on points made by other council members - Keep responses concise (2-3 paragraphs) Discussion format: 1. Acknowledge relevant points from previous speakers 2. Contribute your security perspective 3. Ask clarifying questions if needed "#; }
❌ Don't:
#![allow(unused)] fn main() { let prompt = "You are an expert."; // Too vague }
3. Turn Strategy Selection
| Scenario | Recommended Strategy | Reason |
|---|---|---|
| Equal expertise importance | RoundRobin | Fair, balanced |
| Complex topics | ModeratorDirected | Expert guidance |
| Time-sensitive | RoundRobin + MaxRounds | Predictable |
| Critical decisions | ModeratorDirected + ModeratorDecision | Quality focus |
4. Termination Condition Selection
| Goal | Recommended Condition | Configuration |
|---|---|---|
| Time-boxed | MaxRounds | 3-5 rounds typical |
| Consensus required | Consensus | min_participants = ⌈N/2⌉ |
| Expert-guided | ModeratorDecision | With moderator |
| Approval workflow | Keyword | "APPROVED" or "GO" |
5. Cost Optimization
Council discussions can be expensive (multiple LLM calls per round).
Cost Calculation:
Total Calls = Participants × Rounds
Cost = Total Calls × LLM_Cost_Per_Call
Example: 3 participants × 5 rounds = 15 calls
With GPT-4: 15 × $0.03 = $0.45 per council
With GPT-4o-mini: 15 × $0.005 = $0.075 per council
Optimization Strategies:
- Use MaxRounds termination for cost ceiling
- Choose lower-cost models for non-critical discussions
- Limit participants to essential perspectives
- Cache common participant responses
- Consider Phalanx for independent analysis
6. Conversation Quality
Improve discussion quality:
- Clear topics: "Should we implement X?" not "Tell me about X"
- Specific context: Provide background information in topic
- Response length: Guide participants to 2-3 paragraphs
- Build-on prompts: Encourage referencing previous speakers
- Summarization: Have final turn synthesize discussion
Example high-quality topic:
#![allow(unused)] fn main() { let topic = r#" Should we implement two-factor authentication for all users? Context: - 100K active users (70% consumer, 30% enterprise) - Recent industry trend toward mandatory 2FA - Enterprise customers requesting this feature - Current: Email/password only Consider: - Technical implementation complexity - User experience and friction - Security improvement quantification - Cost vs benefit analysis "#; }
API Reference
Core Types
#![allow(unused)] fn main() { // Council configuration pub struct Council { pub id: String, pub name: String, pub participants: Vec<Paladin>, pub moderator: Option<Paladin>, pub config: CouncilConfig, } // Turn-taking strategies pub enum TurnStrategy { RoundRobin, ModeratorDirected, } // Termination conditions pub enum TerminationCondition { MaxRounds(u32), Consensus { required_agreement_keywords: Vec<String>, min_participants: usize, }, ModeratorDecision, Keyword(String), } // Council result pub struct CouncilResult { pub final_output: String, pub conversation_history: String, pub rounds_completed: u32, pub termination_reason: String, } }
Services
#![allow(unused)] fn main() { // Council execution service pub struct CouncilExecutionService { paladin_port: Arc<dyn PaladinPort>, garrison_port: Option<Arc<dyn GarrisonPort>>, } impl CouncilExecutionService { pub fn new( paladin_port: Arc<dyn PaladinPort>, garrison_port: Option<Arc<dyn GarrisonPort>>, ) -> Self; pub async fn convene( &self, council: &Council, topic: &str, ) -> Result<CouncilResult, CouncilError>; } }
Builder
#![allow(unused)] fn main() { pub struct CouncilBuilder { // ... } impl CouncilBuilder { pub fn new() -> Self; pub fn name(self, name: impl Into<String>) -> Self; pub fn add_participant(self, paladin: Paladin) -> Self; pub fn moderator(self, paladin: Paladin) -> Self; pub fn turn_strategy(self, strategy: TurnStrategy) -> Self; pub fn termination_condition(self, condition: TerminationCondition) -> Self; pub fn max_rounds(self, rounds: u32) -> Self; pub fn store_history(self, store: bool) -> Self; pub fn build(self) -> Result<Council, CouncilError>; } }
See Also
- Battalion Overview - All orchestration patterns
- Grove Pattern - Intelligent agent routing
- Commander - Strategy selection
- Configuration Examples - YAML configs
- Code Examples - Rust examples
Next Steps:
- Try the Quick Start example
- Explore YAML configurations
- See practical examples
- Review API documentation