Advanced Orchestration
Beyond the basic Workflow Agents (Sequential, Parallel, Loop), Heartbit provides five advanced orchestration patterns for complex multi-agent coordination. Like the basic workflow agents, these are deterministic — no LLM cost for the orchestration layer itself.
DebateAgent
Section titled “DebateAgent”Multi-round adversarial debate between N debater agents, judged by a separate agent.
use heartbit::{AgentRunner, DebateAgent};
let optimist = AgentRunner::builder(provider.clone()) .name("optimist").system_prompt("Argue the optimistic case.").build()?;let pessimist = AgentRunner::builder(provider.clone()) .name("pessimist").system_prompt("Argue the pessimistic case.").build()?;let judge = AgentRunner::builder(provider.clone()) .name("judge").system_prompt("Synthesize a balanced verdict.").build()?;
let debate = DebateAgent::builder() .debater(optimist) .debater(pessimist) .judge(judge) .max_rounds(3) .should_stop(|text| text.contains("CONSENSUS")) .build()?;
let output = debate.execute("Should we rewrite in Rust?").await?;How it works:
- Each round, all debaters run in parallel via
tokio::JoinSet, receiving the full transcript so far - After all rounds complete (or
should_stoptriggers early exit), the judge synthesizes a final answer - Returns
AgentOutputwith accumulatedTokenUsagefrom all debaters and the judge
Requirements: at least 2 debaters, a judge, max_rounds >= 1.
VotingAgent
Section titled “VotingAgent”Majority voting across N voter agents running in parallel.
use heartbit::{AgentRunner, VotingAgent};
let voter_a = AgentRunner::builder(provider.clone()) .name("voter_a").system_prompt("Classify sentiment. Reply POSITIVE or NEGATIVE.").build()?;let voter_b = AgentRunner::builder(provider.clone()) .name("voter_b").system_prompt("Classify sentiment. Reply POSITIVE or NEGATIVE.").build()?;let voter_c = AgentRunner::builder(provider.clone()) .name("voter_c").system_prompt("Classify sentiment. Reply POSITIVE or NEGATIVE.").build()?;
let voting = VotingAgent::builder() .voter(voter_a) .voter(voter_b) .voter(voter_c) .vote_extractor(|output| { if output.result.contains("POSITIVE") { "POSITIVE".into() } else { "NEGATIVE".into() } }) .tie_breaker(|votes| votes.into_iter().next().unwrap()) .build()?;
let result = voting.execute("I love this product!").await?;// result.winner == "POSITIVE"// result.tally == {"POSITIVE": 3}How it works:
- All voters run in parallel
vote_extractormaps each voter’s output to a vote string- Votes are tallied; the majority wins
- On tie,
tie_breakerdecides (default: first alphabetically)
Returns: VoteResult { winner, tally: HashMap<String, usize>, output: AgentOutput } where output contains the winning voter’s full response.
Requirements: at least 2 voters, a vote_extractor function.
MixtureOfAgentsAgent (MoA)
Section titled “MixtureOfAgentsAgent (MoA)”N proposers generate ideas in parallel, then a synthesizer refines them into a final answer. Can be stacked into multiple layers.
use heartbit::{AgentRunner, MixtureOfAgentsAgent};
let proposer_a = AgentRunner::builder(provider.clone()) .name("proposer_a").system_prompt("Draft a creative approach.").build()?;let proposer_b = AgentRunner::builder(provider.clone()) .name("proposer_b").system_prompt("Draft a technical approach.").build()?;let synthesizer = AgentRunner::builder(provider.clone()) .name("synthesizer").system_prompt("Combine proposals into a cohesive plan.").build()?;
let moa = MixtureOfAgentsAgent::builder() .proposer(proposer_a) .proposer(proposer_b) .synthesizer(synthesizer) .layers(2) .build()?;
let output = moa.execute("Design a notification system").await?;How it works:
- All proposers run in parallel, each producing a proposal
- Proposals are sorted alphabetically by proposer name for deterministic output
- The synthesizer receives all proposals and produces a refined result
- With
layers > 1, each layer’s synthesis feeds the next layer’s proposers
Requirements: at least 2 proposers, a synthesizer, layers >= 1 (default: 1).
DagAgent
Section titled “DagAgent”Directed acyclic graph of agents with parallel dispatch at each tier. Supports conditional edges and text transforms.
use heartbit::{AgentRunner, DagAgent};
let fetcher = AgentRunner::builder(provider.clone()) .name("fetcher").system_prompt("Fetch and summarize data.").build()?;let analyzer = AgentRunner::builder(provider.clone()) .name("analyzer").system_prompt("Analyze the data.").build()?;let formatter = AgentRunner::builder(provider.clone()) .name("formatter").system_prompt("Format as a report.").build()?;let alerter = AgentRunner::builder(provider.clone()) .name("alerter").system_prompt("Generate an alert summary.").build()?;
let dag = DagAgent::builder() .node("fetcher", fetcher) .node("analyzer", analyzer) .node("formatter", formatter) .node("alerter", alerter) .edge("fetcher", "analyzer") .edge("analyzer", "formatter") .conditional_edge("analyzer", "alerter", |output| output.contains("CRITICAL")) .edge_with_transform("fetcher", "formatter", |text| text.to_uppercase()) .build()?;
let output = dag.execute("Process today's metrics").await?;How it works:
- Root nodes (in-degree 0) receive the task input
- BFS-style execution: each tier of independent nodes runs in parallel
conditional_edgegates whether an edge is followed based on the source outputedge_with_transformmodifies text before passing to the target node- Terminal nodes’ outputs are merged (sorted by name)
Validation: The builder validates that there are no cycles (using petgraph), all edge endpoints exist, and there are no duplicate node names.
Requirements: at least one node, valid DAG (no cycles).
BatchExecutor
Section titled “BatchExecutor”Runs the same agent on many different inputs with controlled concurrency.
use heartbit::{AgentRunner, BatchExecutor};
let classifier = AgentRunner::builder(provider.clone()) .name("classifier").system_prompt("Classify the input.").build()?;
let batch = BatchExecutor::builder(classifier) .max_concurrency(4) .build()?;
let inputs = vec![ "Email about a meeting".into(), "Spam advertisement".into(), "Bug report from user".into(),];
let results = batch.execute(inputs).await;// results: Vec<BatchResult> sorted by input indexlet total_usage = batch.aggregate_usage(&results);How it works:
- Inputs are dispatched to the agent with concurrency limited by
tokio::sync::Semaphore - Returns
Vec<BatchResult>sorted by input index, each containingOk(AgentOutput)orErr aggregate_usage()sums token usage across successful results
Requirements: max_concurrency defaults to available_parallelism (0 is rejected at build time).
When to Use
Section titled “When to Use”| Pattern | Use case |
|---|---|
| Debate | Adversarial reasoning, code review, decision analysis |
| Voting | Consensus building, multiple-choice classification |
| MoA | Content generation, research synthesis |
| DAG | Multi-stage pipelines with branching, data processing |
| Batch | Bulk processing, parallel evaluations |
Composability
Section titled “Composability”All advanced orchestration patterns return AgentOutput (or VoteResult containing one), so they can be nested inside Sequential, Parallel, or Loop agents. For example, a SequentialAgent could run a DagAgent as its first stage and a VotingAgent as its second stage.
See Also
Section titled “See Also”- Workflow Agents — Sequential, Parallel, and Loop patterns
- Workflow Agent Examples — practical code examples for all patterns