Skip to main content

argentor_orchestrator/
patterns.rs

1//! Advanced multi-agent collaboration patterns.
2//!
3//! Goes beyond the basic Orchestrator-Workers pattern to support sophisticated
4//! multi-agent workflows: pipelines, map-reduce, adversarial debate, ensemble
5//! voting, supervised review, and swarm convergence.
6
7use crate::types::AgentRole;
8use serde::{Deserialize, Serialize};
9
10// ---------------------------------------------------------------------------
11// Core pattern enum
12// ---------------------------------------------------------------------------
13
14/// A collaboration pattern describing how multiple agents interact to solve a task.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum CollaborationPattern {
18    /// Sequential processing chain -- each stage's output feeds the next stage's input,
19    /// like Unix pipes: Agent A output -> Agent B input -> Agent C input.
20    Pipeline {
21        /// Ordered list of processing stages.
22        stages: Vec<PipelineStage>,
23    },
24
25    /// Split work across N mapper agents, then reduce (aggregate) the partial results
26    /// with a dedicated reducer agent.
27    MapReduce {
28        /// Agent role for the mapping (parallelizable) phase.
29        mapper_role: AgentRole,
30        /// Agent role for the reducing (aggregation) phase.
31        reducer_role: AgentRole,
32        /// Number of chunks the input is split into.
33        chunk_count: usize,
34    },
35
36    /// Two agents argue opposing positions while a judge evaluates and decides,
37    /// improving decision quality through adversarial deliberation.
38    Debate {
39        /// Agent role arguing in favor.
40        proponent: AgentRole,
41        /// Agent role arguing against.
42        opponent: AgentRole,
43        /// Agent role making the final decision.
44        judge: AgentRole,
45        /// Maximum debate rounds before forcing a decision.
46        max_rounds: u32,
47    },
48
49    /// Multiple agents tackle the same task independently and results are aggregated
50    /// via a configurable strategy (voting, best-of-N, concatenation, LLM synthesis).
51    Ensemble {
52        /// Agent roles participating in the ensemble.
53        agents: Vec<AgentRole>,
54        /// How the individual results are combined.
55        aggregation: AggregationStrategy,
56    },
57
58    /// A supervisor agent reviews worker outputs and can accept or reject them
59    /// according to a configurable review policy.
60    Supervisor {
61        /// Agent role responsible for review.
62        supervisor: AgentRole,
63        /// Agent roles producing work to be reviewed.
64        workers: Vec<AgentRole>,
65        /// When and how outputs are reviewed.
66        review_policy: ReviewPolicy,
67    },
68
69    /// Agents iterate collaboratively until consensus or a convergence threshold is met.
70    Swarm {
71        /// Agent roles participating in the swarm.
72        roles: Vec<AgentRole>,
73        /// Maximum number of iterations before stopping.
74        max_iterations: u32,
75        /// Convergence threshold (0.0 -- 1.0); iteration stops when reached.
76        convergence_threshold: f64,
77    },
78}
79
80// ---------------------------------------------------------------------------
81// Supporting types
82// ---------------------------------------------------------------------------
83
84/// A single stage in a [`CollaborationPattern::Pipeline`].
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PipelineStage {
87    /// The agent role responsible for this stage.
88    pub role: AgentRole,
89    /// Human-readable description of what this stage does.
90    pub description: String,
91    /// Optional transformation hint applied to the output before passing it downstream.
92    pub transform: Option<String>,
93}
94
95/// Strategy used by [`CollaborationPattern::Ensemble`] to aggregate results.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(tag = "strategy", rename_all = "snake_case")]
98pub enum AggregationStrategy {
99    /// Pick the result that appears most often among agents.
100    MajorityVote,
101    /// Pick the single best result according to a named metric.
102    BestOfN {
103        /// Metric name used to rank results.
104        metric: String,
105    },
106    /// Concatenate all results in order.
107    Concatenate,
108    /// Use an LLM to synthesize a unified answer from all results.
109    LlmSynthesize,
110}
111
112/// Policy for when a supervisor reviews worker output.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "policy", rename_all = "snake_case")]
115pub enum ReviewPolicy {
116    /// Supervisor reviews every piece of output.
117    AlwaysReview,
118    /// Supervisor randomly reviews a percentage of outputs.
119    SamplePercent(f64),
120    /// Supervisor only reviews outputs flagged as errors.
121    OnError,
122    /// No review — workers are fully trusted.
123    Never,
124}
125
126/// Top-level configuration wrapping a [`CollaborationPattern`] with execution settings.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PatternConfig {
129    /// The collaboration pattern to execute.
130    pub pattern: CollaborationPattern,
131    /// Maximum wall-clock seconds the entire pattern execution may take.
132    pub timeout_secs: u64,
133    /// Maximum number of retries on transient failures.
134    pub max_retries: u32,
135}
136
137/// Outcome of executing a collaboration pattern.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PatternResult {
140    /// Name of the pattern that was executed (e.g. "pipeline", "debate").
141    pub pattern_name: String,
142    /// How many stages / rounds were completed.
143    pub stages_completed: u32,
144    /// Total stages / rounds that were planned.
145    pub total_stages: u32,
146    /// Identifiers (or summaries) of artifacts produced.
147    pub artifacts: Vec<String>,
148    /// Whether agents reached consensus (meaningful for Debate / Swarm).
149    pub consensus_reached: bool,
150    /// The final aggregated output text.
151    pub final_output: String,
152}
153
154// ---------------------------------------------------------------------------
155// Builder
156// ---------------------------------------------------------------------------
157
158/// Ergonomic builder for [`PatternConfig`].
159#[derive(Debug, Clone)]
160pub struct PatternConfigBuilder {
161    pattern: CollaborationPattern,
162    timeout_secs: u64,
163    max_retries: u32,
164}
165
166impl PatternConfig {
167    /// Start building a pipeline pattern (stages will be added via the builder).
168    pub fn pipeline() -> PatternConfigBuilder {
169        PatternConfigBuilder {
170            pattern: CollaborationPattern::Pipeline { stages: Vec::new() },
171            timeout_secs: 300,
172            max_retries: 1,
173        }
174    }
175
176    /// Start building a debate pattern with default roles and 3 rounds.
177    pub fn debate() -> PatternConfigBuilder {
178        PatternConfigBuilder {
179            pattern: CollaborationPattern::Debate {
180                proponent: AgentRole::Coder,
181                opponent: AgentRole::Reviewer,
182                judge: AgentRole::Architect,
183                max_rounds: 3,
184            },
185            timeout_secs: 300,
186            max_retries: 1,
187        }
188    }
189
190    /// Start building an ensemble pattern.
191    pub fn ensemble() -> PatternConfigBuilder {
192        PatternConfigBuilder {
193            pattern: CollaborationPattern::Ensemble {
194                agents: Vec::new(),
195                aggregation: AggregationStrategy::MajorityVote,
196            },
197            timeout_secs: 300,
198            max_retries: 1,
199        }
200    }
201
202    /// Start building a map-reduce pattern.
203    pub fn map_reduce() -> PatternConfigBuilder {
204        PatternConfigBuilder {
205            pattern: CollaborationPattern::MapReduce {
206                mapper_role: AgentRole::Coder,
207                reducer_role: AgentRole::Orchestrator,
208                chunk_count: 4,
209            },
210            timeout_secs: 600,
211            max_retries: 1,
212        }
213    }
214
215    /// Start building a supervisor pattern.
216    pub fn supervisor() -> PatternConfigBuilder {
217        PatternConfigBuilder {
218            pattern: CollaborationPattern::Supervisor {
219                supervisor: AgentRole::Reviewer,
220                workers: Vec::new(),
221                review_policy: ReviewPolicy::AlwaysReview,
222            },
223            timeout_secs: 300,
224            max_retries: 1,
225        }
226    }
227
228    /// Start building a swarm pattern.
229    pub fn swarm() -> PatternConfigBuilder {
230        PatternConfigBuilder {
231            pattern: CollaborationPattern::Swarm {
232                roles: Vec::new(),
233                max_iterations: 10,
234                convergence_threshold: 0.9,
235            },
236            timeout_secs: 600,
237            max_retries: 1,
238        }
239    }
240}
241
242impl PatternConfigBuilder {
243    /// Set the execution timeout in seconds.
244    pub fn with_timeout(mut self, secs: u64) -> Self {
245        self.timeout_secs = secs;
246        self
247    }
248
249    /// Set the maximum number of retries.
250    pub fn with_retries(mut self, n: u32) -> Self {
251        self.max_retries = n;
252        self
253    }
254
255    /// Add a stage to a pipeline pattern. No-op if the pattern is not a pipeline.
256    pub fn add_stage(mut self, stage: PipelineStage) -> Self {
257        if let CollaborationPattern::Pipeline { ref mut stages } = self.pattern {
258            stages.push(stage);
259        }
260        self
261    }
262
263    /// Add an agent to an ensemble pattern. No-op if not an ensemble.
264    pub fn add_agent(mut self, role: AgentRole) -> Self {
265        if let CollaborationPattern::Ensemble { ref mut agents, .. } = self.pattern {
266            agents.push(role);
267        }
268        self
269    }
270
271    /// Set the aggregation strategy for an ensemble. No-op if not an ensemble.
272    pub fn with_aggregation(mut self, strategy: AggregationStrategy) -> Self {
273        if let CollaborationPattern::Ensemble {
274            ref mut aggregation,
275            ..
276        } = self.pattern
277        {
278            *aggregation = strategy;
279        }
280        self
281    }
282
283    /// Set the roles participating in a debate.
284    pub fn with_debate_roles(
285        mut self,
286        proponent: AgentRole,
287        opponent: AgentRole,
288        judge: AgentRole,
289    ) -> Self {
290        if let CollaborationPattern::Debate {
291            proponent: ref mut p,
292            opponent: ref mut o,
293            judge: ref mut j,
294            ..
295        } = self.pattern
296        {
297            *p = proponent;
298            *o = opponent;
299            *j = judge;
300        }
301        self
302    }
303
304    /// Set the maximum number of rounds for a debate.
305    pub fn with_max_rounds(mut self, rounds: u32) -> Self {
306        if let CollaborationPattern::Debate {
307            ref mut max_rounds, ..
308        } = self.pattern
309        {
310            *max_rounds = rounds;
311        }
312        self
313    }
314
315    /// Add a worker to a supervisor pattern.
316    pub fn add_worker(mut self, role: AgentRole) -> Self {
317        if let CollaborationPattern::Supervisor {
318            ref mut workers, ..
319        } = self.pattern
320        {
321            workers.push(role);
322        }
323        self
324    }
325
326    /// Set the review policy for a supervisor pattern.
327    pub fn with_review_policy(mut self, policy: ReviewPolicy) -> Self {
328        if let CollaborationPattern::Supervisor {
329            ref mut review_policy,
330            ..
331        } = self.pattern
332        {
333            *review_policy = policy;
334        }
335        self
336    }
337
338    /// Set the mapper and reducer roles for map-reduce.
339    pub fn with_mapper_reducer(
340        mut self,
341        mapper: AgentRole,
342        reducer: AgentRole,
343        chunks: usize,
344    ) -> Self {
345        if let CollaborationPattern::MapReduce {
346            ref mut mapper_role,
347            ref mut reducer_role,
348            ref mut chunk_count,
349        } = self.pattern
350        {
351            *mapper_role = mapper;
352            *reducer_role = reducer;
353            *chunk_count = chunks;
354        }
355        self
356    }
357
358    /// Add a role to a swarm pattern.
359    pub fn add_swarm_role(mut self, role: AgentRole) -> Self {
360        if let CollaborationPattern::Swarm { ref mut roles, .. } = self.pattern {
361            roles.push(role);
362        }
363        self
364    }
365
366    /// Set convergence parameters for a swarm.
367    pub fn with_convergence(mut self, max_iterations: u32, threshold: f64) -> Self {
368        if let CollaborationPattern::Swarm {
369            max_iterations: ref mut mi,
370            convergence_threshold: ref mut ct,
371            ..
372        } = self.pattern
373        {
374            *mi = max_iterations;
375            *ct = threshold;
376        }
377        self
378    }
379
380    /// Consume the builder and produce a [`PatternConfig`].
381    pub fn build(self) -> PatternConfig {
382        PatternConfig {
383            pattern: self.pattern,
384            timeout_secs: self.timeout_secs,
385            max_retries: self.max_retries,
386        }
387    }
388}
389
390// ---------------------------------------------------------------------------
391// Helper functions
392// ---------------------------------------------------------------------------
393
394/// Returns a human-readable description of a collaboration pattern.
395pub fn describe_pattern(pattern: &CollaborationPattern) -> String {
396    match pattern {
397        CollaborationPattern::Pipeline { stages } => {
398            let names: Vec<String> = stages.iter().map(|s| s.role.to_string()).collect();
399            format!(
400                "Pipeline with {} stages: {}",
401                stages.len(),
402                names.join(" -> ")
403            )
404        }
405        CollaborationPattern::MapReduce {
406            mapper_role,
407            reducer_role,
408            chunk_count,
409        } => {
410            format!("MapReduce: {chunk_count} x {mapper_role} mappers -> {reducer_role} reducer")
411        }
412        CollaborationPattern::Debate {
413            proponent,
414            opponent,
415            judge,
416            max_rounds,
417        } => {
418            format!(
419                "Debate: {proponent} vs {opponent}, judged by {judge} (max {max_rounds} rounds)"
420            )
421        }
422        CollaborationPattern::Ensemble {
423            agents,
424            aggregation,
425        } => {
426            let names: Vec<String> = agents
427                .iter()
428                .map(std::string::ToString::to_string)
429                .collect();
430            let strat = match aggregation {
431                AggregationStrategy::MajorityVote => "majority vote",
432                AggregationStrategy::BestOfN { metric } => {
433                    return format!(
434                        "Ensemble of {} agents [{}], aggregated by best-of-N (metric: {metric})",
435                        agents.len(),
436                        names.join(", ")
437                    );
438                }
439                AggregationStrategy::Concatenate => "concatenation",
440                AggregationStrategy::LlmSynthesize => "LLM synthesis",
441            };
442            format!(
443                "Ensemble of {} agents [{}], aggregated by {strat}",
444                agents.len(),
445                names.join(", ")
446            )
447        }
448        CollaborationPattern::Supervisor {
449            supervisor,
450            workers,
451            review_policy,
452        } => {
453            let worker_names: Vec<String> = workers
454                .iter()
455                .map(std::string::ToString::to_string)
456                .collect();
457            let policy = match review_policy {
458                ReviewPolicy::AlwaysReview => "always review",
459                ReviewPolicy::SamplePercent(p) => {
460                    return format!(
461                        "Supervisor {supervisor} overseeing [{}], reviewing {:.0}% of outputs",
462                        worker_names.join(", "),
463                        p * 100.0
464                    );
465                }
466                ReviewPolicy::OnError => "review on error",
467                ReviewPolicy::Never => "no review",
468            };
469            format!(
470                "Supervisor {supervisor} overseeing [{}], policy: {policy}",
471                worker_names.join(", ")
472            )
473        }
474        CollaborationPattern::Swarm {
475            roles,
476            max_iterations,
477            convergence_threshold,
478        } => {
479            let names: Vec<String> = roles.iter().map(std::string::ToString::to_string).collect();
480            format!(
481                "Swarm of {} agents [{}], max {max_iterations} iterations, convergence >= {convergence_threshold}",
482                roles.len(),
483                names.join(", ")
484            )
485        }
486    }
487}
488
489/// Validates that a collaboration pattern has a sensible configuration.
490///
491/// Returns `Ok(())` if valid, or `Err(reason)` describing the problem.
492pub fn validate_pattern(pattern: &CollaborationPattern) -> Result<(), String> {
493    match pattern {
494        CollaborationPattern::Pipeline { stages } => {
495            if stages.is_empty() {
496                return Err("Pipeline must have at least one stage".into());
497            }
498            Ok(())
499        }
500        CollaborationPattern::MapReduce { chunk_count, .. } => {
501            if *chunk_count == 0 {
502                return Err("MapReduce chunk_count must be > 0".into());
503            }
504            Ok(())
505        }
506        CollaborationPattern::Debate {
507            max_rounds,
508            proponent,
509            opponent,
510            ..
511        } => {
512            if *max_rounds == 0 {
513                return Err("Debate must have at least 1 round".into());
514            }
515            if proponent == opponent {
516                return Err("Debate proponent and opponent must be different roles".into());
517            }
518            Ok(())
519        }
520        CollaborationPattern::Ensemble { agents, .. } => {
521            if agents.len() < 2 {
522                return Err("Ensemble requires at least 2 agents".into());
523            }
524            Ok(())
525        }
526        CollaborationPattern::Supervisor { workers, .. } => {
527            if workers.is_empty() {
528                return Err("Supervisor pattern requires at least one worker".into());
529            }
530            Ok(())
531        }
532        CollaborationPattern::Swarm {
533            roles,
534            max_iterations,
535            convergence_threshold,
536        } => {
537            if roles.len() < 2 {
538                return Err("Swarm requires at least 2 agents".into());
539            }
540            if *max_iterations == 0 {
541                return Err("Swarm must have at least 1 iteration".into());
542            }
543            if *convergence_threshold <= 0.0 || *convergence_threshold > 1.0 {
544                return Err("Swarm convergence_threshold must be in (0.0, 1.0]".into());
545            }
546            Ok(())
547        }
548    }
549}
550
551/// Estimates the total token usage for a pattern given a per-agent token budget.
552///
553/// This is a rough heuristic:
554/// - Pipeline: `stages * tokens_per_agent`
555/// - MapReduce: `(chunk_count + 1) * tokens_per_agent`
556/// - Debate: `(2 * max_rounds + 1) * tokens_per_agent`  (proponent + opponent per round, plus judge)
557/// - Ensemble: `agents.len() * tokens_per_agent`  (plus aggregation cost if LLM)
558/// - Supervisor: `(workers.len() + review_overhead) * tokens_per_agent`
559/// - Swarm: `roles.len() * max_iterations * tokens_per_agent`
560pub fn estimate_cost(pattern: &CollaborationPattern, tokens_per_agent: u64) -> u64 {
561    match pattern {
562        CollaborationPattern::Pipeline { stages } => stages.len() as u64 * tokens_per_agent,
563        CollaborationPattern::MapReduce { chunk_count, .. } => {
564            (*chunk_count as u64 + 1) * tokens_per_agent
565        }
566        CollaborationPattern::Debate { max_rounds, .. } => {
567            // Each round: proponent + opponent; final: judge
568            (2 * *max_rounds as u64 + 1) * tokens_per_agent
569        }
570        CollaborationPattern::Ensemble {
571            agents,
572            aggregation,
573        } => {
574            let base = agents.len() as u64 * tokens_per_agent;
575            match aggregation {
576                AggregationStrategy::LlmSynthesize => base + tokens_per_agent,
577                _ => base,
578            }
579        }
580        CollaborationPattern::Supervisor {
581            workers,
582            review_policy,
583            ..
584        } => {
585            let worker_cost = workers.len() as u64 * tokens_per_agent;
586            let review_cost = match review_policy {
587                ReviewPolicy::AlwaysReview => workers.len() as u64 * tokens_per_agent,
588                ReviewPolicy::SamplePercent(p) => {
589                    (workers.len() as f64 * p * tokens_per_agent as f64) as u64
590                }
591                ReviewPolicy::OnError => tokens_per_agent, // assume ~1 review
592                ReviewPolicy::Never => 0,
593            };
594            worker_cost + review_cost
595        }
596        CollaborationPattern::Swarm {
597            roles,
598            max_iterations,
599            ..
600        } => roles.len() as u64 * *max_iterations as u64 * tokens_per_agent,
601    }
602}
603
604// ---------------------------------------------------------------------------
605// Tests
606// ---------------------------------------------------------------------------
607
608#[cfg(test)]
609#[allow(clippy::unwrap_used, clippy::expect_used)]
610mod tests {
611    use super::*;
612
613    // -- Pipeline --
614
615    #[test]
616    fn test_pipeline_describe() {
617        let pattern = CollaborationPattern::Pipeline {
618            stages: vec![
619                PipelineStage {
620                    role: AgentRole::Spec,
621                    description: "Generate spec".into(),
622                    transform: None,
623                },
624                PipelineStage {
625                    role: AgentRole::Coder,
626                    description: "Implement".into(),
627                    transform: Some("extract_code".into()),
628                },
629            ],
630        };
631        let desc = describe_pattern(&pattern);
632        assert!(desc.contains("Pipeline"));
633        assert!(desc.contains("2 stages"));
634        assert!(desc.contains("spec -> coder"));
635    }
636
637    #[test]
638    fn test_pipeline_validate_empty() {
639        let pattern = CollaborationPattern::Pipeline { stages: Vec::new() };
640        assert!(validate_pattern(&pattern).is_err());
641    }
642
643    #[test]
644    fn test_pipeline_validate_ok() {
645        let pattern = CollaborationPattern::Pipeline {
646            stages: vec![PipelineStage {
647                role: AgentRole::Coder,
648                description: "Code it".into(),
649                transform: None,
650            }],
651        };
652        assert!(validate_pattern(&pattern).is_ok());
653    }
654
655    #[test]
656    fn test_pipeline_estimate_cost() {
657        let pattern = CollaborationPattern::Pipeline {
658            stages: vec![
659                PipelineStage {
660                    role: AgentRole::Spec,
661                    description: "Spec".into(),
662                    transform: None,
663                },
664                PipelineStage {
665                    role: AgentRole::Coder,
666                    description: "Code".into(),
667                    transform: None,
668                },
669                PipelineStage {
670                    role: AgentRole::Tester,
671                    description: "Test".into(),
672                    transform: None,
673                },
674            ],
675        };
676        assert_eq!(estimate_cost(&pattern, 1000), 3000);
677    }
678
679    // -- MapReduce --
680
681    #[test]
682    fn test_map_reduce_validate_zero_chunks() {
683        let pattern = CollaborationPattern::MapReduce {
684            mapper_role: AgentRole::Coder,
685            reducer_role: AgentRole::Orchestrator,
686            chunk_count: 0,
687        };
688        assert!(validate_pattern(&pattern).is_err());
689    }
690
691    #[test]
692    fn test_map_reduce_estimate() {
693        let pattern = CollaborationPattern::MapReduce {
694            mapper_role: AgentRole::Coder,
695            reducer_role: AgentRole::Orchestrator,
696            chunk_count: 4,
697        };
698        // 4 mappers + 1 reducer = 5
699        assert_eq!(estimate_cost(&pattern, 500), 2500);
700    }
701
702    // -- Debate --
703
704    #[test]
705    fn test_debate_validate_same_roles() {
706        let pattern = CollaborationPattern::Debate {
707            proponent: AgentRole::Coder,
708            opponent: AgentRole::Coder,
709            judge: AgentRole::Architect,
710            max_rounds: 3,
711        };
712        assert!(validate_pattern(&pattern).is_err());
713    }
714
715    #[test]
716    fn test_debate_validate_zero_rounds() {
717        let pattern = CollaborationPattern::Debate {
718            proponent: AgentRole::Coder,
719            opponent: AgentRole::Reviewer,
720            judge: AgentRole::Architect,
721            max_rounds: 0,
722        };
723        assert!(validate_pattern(&pattern).is_err());
724    }
725
726    #[test]
727    fn test_debate_estimate() {
728        let pattern = CollaborationPattern::Debate {
729            proponent: AgentRole::Coder,
730            opponent: AgentRole::Reviewer,
731            judge: AgentRole::Architect,
732            max_rounds: 3,
733        };
734        // 2 * 3 + 1 = 7
735        assert_eq!(estimate_cost(&pattern, 1000), 7000);
736    }
737
738    // -- Ensemble --
739
740    #[test]
741    fn test_ensemble_validate_too_few() {
742        let pattern = CollaborationPattern::Ensemble {
743            agents: vec![AgentRole::Coder],
744            aggregation: AggregationStrategy::MajorityVote,
745        };
746        assert!(validate_pattern(&pattern).is_err());
747    }
748
749    #[test]
750    fn test_ensemble_llm_synthesize_extra_cost() {
751        let pattern = CollaborationPattern::Ensemble {
752            agents: vec![AgentRole::Coder, AgentRole::Reviewer, AgentRole::Architect],
753            aggregation: AggregationStrategy::LlmSynthesize,
754        };
755        // 3 agents + 1 synthesis = 4
756        assert_eq!(estimate_cost(&pattern, 1000), 4000);
757    }
758
759    // -- Supervisor --
760
761    #[test]
762    fn test_supervisor_validate_no_workers() {
763        let pattern = CollaborationPattern::Supervisor {
764            supervisor: AgentRole::Reviewer,
765            workers: Vec::new(),
766            review_policy: ReviewPolicy::AlwaysReview,
767        };
768        assert!(validate_pattern(&pattern).is_err());
769    }
770
771    #[test]
772    fn test_supervisor_estimate_always_review() {
773        let pattern = CollaborationPattern::Supervisor {
774            supervisor: AgentRole::Reviewer,
775            workers: vec![AgentRole::Coder, AgentRole::Tester],
776            review_policy: ReviewPolicy::AlwaysReview,
777        };
778        // workers: 2 * 1000 = 2000, reviews: 2 * 1000 = 2000 => 4000
779        assert_eq!(estimate_cost(&pattern, 1000), 4000);
780    }
781
782    // -- Swarm --
783
784    #[test]
785    fn test_swarm_validate_threshold_out_of_range() {
786        let pattern = CollaborationPattern::Swarm {
787            roles: vec![AgentRole::Coder, AgentRole::Reviewer],
788            max_iterations: 5,
789            convergence_threshold: 1.5,
790        };
791        assert!(validate_pattern(&pattern).is_err());
792    }
793
794    #[test]
795    fn test_swarm_validate_zero_threshold() {
796        let pattern = CollaborationPattern::Swarm {
797            roles: vec![AgentRole::Coder, AgentRole::Reviewer],
798            max_iterations: 5,
799            convergence_threshold: 0.0,
800        };
801        assert!(validate_pattern(&pattern).is_err());
802    }
803
804    #[test]
805    fn test_swarm_estimate() {
806        let pattern = CollaborationPattern::Swarm {
807            roles: vec![AgentRole::Coder, AgentRole::Reviewer, AgentRole::Tester],
808            max_iterations: 10,
809            convergence_threshold: 0.9,
810        };
811        // 3 roles * 10 iterations * 500 = 15000
812        assert_eq!(estimate_cost(&pattern, 500), 15000);
813    }
814
815    // -- Builder --
816
817    #[test]
818    fn test_builder_pipeline() {
819        let config = PatternConfig::pipeline()
820            .add_stage(PipelineStage {
821                role: AgentRole::Spec,
822                description: "Requirements".into(),
823                transform: None,
824            })
825            .add_stage(PipelineStage {
826                role: AgentRole::Coder,
827                description: "Implementation".into(),
828                transform: Some("extract_rust".into()),
829            })
830            .with_timeout(120)
831            .with_retries(3)
832            .build();
833
834        assert_eq!(config.timeout_secs, 120);
835        assert_eq!(config.max_retries, 3);
836        if let CollaborationPattern::Pipeline { stages } = &config.pattern {
837            assert_eq!(stages.len(), 2);
838            assert_eq!(stages[0].role, AgentRole::Spec);
839            assert_eq!(stages[1].transform.as_deref(), Some("extract_rust"));
840        } else {
841            panic!("Expected Pipeline pattern");
842        }
843    }
844
845    #[test]
846    fn test_builder_debate() {
847        let config = PatternConfig::debate()
848            .with_debate_roles(
849                AgentRole::Architect,
850                AgentRole::SecurityAuditor,
851                AgentRole::Reviewer,
852            )
853            .with_max_rounds(5)
854            .with_timeout(600)
855            .build();
856
857        if let CollaborationPattern::Debate {
858            proponent,
859            opponent,
860            judge,
861            max_rounds,
862        } = &config.pattern
863        {
864            assert_eq!(*proponent, AgentRole::Architect);
865            assert_eq!(*opponent, AgentRole::SecurityAuditor);
866            assert_eq!(*judge, AgentRole::Reviewer);
867            assert_eq!(*max_rounds, 5);
868        } else {
869            panic!("Expected Debate pattern");
870        }
871    }
872
873    #[test]
874    fn test_builder_ensemble() {
875        let config = PatternConfig::ensemble()
876            .add_agent(AgentRole::Coder)
877            .add_agent(AgentRole::Reviewer)
878            .add_agent(AgentRole::Architect)
879            .with_aggregation(AggregationStrategy::BestOfN {
880                metric: "accuracy".into(),
881            })
882            .build();
883
884        if let CollaborationPattern::Ensemble {
885            agents,
886            aggregation,
887        } = &config.pattern
888        {
889            assert_eq!(agents.len(), 3);
890            if let AggregationStrategy::BestOfN { metric } = aggregation {
891                assert_eq!(metric, "accuracy");
892            } else {
893                panic!("Expected BestOfN strategy");
894            }
895        } else {
896            panic!("Expected Ensemble pattern");
897        }
898    }
899
900    // -- Serialization --
901
902    #[test]
903    fn test_pattern_config_roundtrip() {
904        let config = PatternConfig::debate()
905            .with_debate_roles(AgentRole::Coder, AgentRole::Reviewer, AgentRole::Architect)
906            .with_max_rounds(3)
907            .with_timeout(180)
908            .with_retries(2)
909            .build();
910
911        let json = serde_json::to_string(&config).unwrap();
912        let parsed: PatternConfig = serde_json::from_str(&json).unwrap();
913
914        assert_eq!(parsed.timeout_secs, 180);
915        assert_eq!(parsed.max_retries, 2);
916        if let CollaborationPattern::Debate { max_rounds, .. } = &parsed.pattern {
917            assert_eq!(*max_rounds, 3);
918        } else {
919            panic!("Expected Debate after deserialization");
920        }
921    }
922
923    #[test]
924    fn test_pattern_result_construction() {
925        let result = PatternResult {
926            pattern_name: "debate".into(),
927            stages_completed: 6,
928            total_stages: 7,
929            artifacts: vec!["spec.md".into(), "review.md".into()],
930            consensus_reached: true,
931            final_output: "Use approach A".into(),
932        };
933        assert!(result.consensus_reached);
934        assert_eq!(result.artifacts.len(), 2);
935        assert_eq!(result.stages_completed, 6);
936    }
937
938    #[test]
939    fn test_describe_all_patterns() {
940        // Verify describe_pattern does not panic for any variant.
941        let patterns = vec![
942            CollaborationPattern::Pipeline {
943                stages: vec![PipelineStage {
944                    role: AgentRole::Coder,
945                    description: "code".into(),
946                    transform: None,
947                }],
948            },
949            CollaborationPattern::MapReduce {
950                mapper_role: AgentRole::Coder,
951                reducer_role: AgentRole::Orchestrator,
952                chunk_count: 3,
953            },
954            CollaborationPattern::Debate {
955                proponent: AgentRole::Coder,
956                opponent: AgentRole::Reviewer,
957                judge: AgentRole::Architect,
958                max_rounds: 2,
959            },
960            CollaborationPattern::Ensemble {
961                agents: vec![AgentRole::Coder, AgentRole::Reviewer],
962                aggregation: AggregationStrategy::Concatenate,
963            },
964            CollaborationPattern::Supervisor {
965                supervisor: AgentRole::Reviewer,
966                workers: vec![AgentRole::Coder],
967                review_policy: ReviewPolicy::SamplePercent(0.5),
968            },
969            CollaborationPattern::Swarm {
970                roles: vec![AgentRole::Coder, AgentRole::Tester],
971                max_iterations: 5,
972                convergence_threshold: 0.8,
973            },
974        ];
975
976        for p in &patterns {
977            let desc = describe_pattern(p);
978            assert!(!desc.is_empty(), "Description should not be empty");
979        }
980    }
981}