Orchestration Patterns Beyond Ensemble: arena, pipeline, integrate, debate, judge_panel
The ensemble is one shape. There are more.
h5i-orchestra ships a library of orchestration patterns, and every one of them
is written in the same public eDSL you would use yourself. They are not engine features.
They are readable, forkable functions, so the built-ins double as worked examples.
- Each pattern is an ordinary eDSL function, not a privileged engine primitive. You can read it, fork it, or write your own next to it.
- Independence and influence are recorded facts.
with_materialsstamps a merged candidateindependent=falsewith an influence edge to every input, so integration work is honest about what it saw. judge_panelis LLM judgment over the run's recorded evidence, and every citation is validated against real run state. That is the difference between grounded judgment and a vibe.ask::<T>makes typed data turns. It is how judges, routers, and moderators are built without any of them producing code.
The ensemble answers one question: run N agents
on a task, seal, review, verify, decide. It is the right default and it is what
h5i team run drives. But once orchestration is a program rather than a fixed
protocol, the useful shapes multiply. A design that hands a plan to two implementers is a
pipeline. Three blind attempts you want ranked is an arena. Many workers whose output one agent
has to merge is an integration. A question you want argued before you decide is a debate.
h5i-orchestra ships patterns::{ensemble, integrate, pipeline, arena,
map_reduce, debate, judge_panel}, and all of them live in the public API. There is no
hidden machinery: a pattern is a function that composes agent.work,
c.freeze, c.verify, c.judge, and the rest. The rest of
this post is a tour, each with the shape you write and the moment you reach for it.
arena: rank blind attempts
An arena runs N agents on the same task with no cross-influence, seals the round, verifies every candidate with one command, and ranks them. It is the ensemble with the mutual-review round removed: when you do not want peers reshaping each other's work, you want an arena.
let outcome = patterns::arena(&c, "implement the parser") .agents(roster) .verify(["cargo", "test"]) // one command, applied to all .judge(policy::tests_then_smallest_diff()) .run().await?; // outcome.artifacts : the candidates // outcome.rows : Vec<TeamCompareRow>, the side-by-side ranking // outcome.verdict : the recorded winner
Each attempt is dispatched as work().expect_independent(), so the run refuses to
count a candidate that came back stamped as influenced. The verifier runs one command against
every candidate, which keeps the comparison apples to apples: a candidate cannot be waved
through with a weaker command than its rivals. What you get back is the artifacts, the
TeamCompareRow ranking the dashboard reads, and the recorded verdict.
pipeline: role-specialized stages
A pipeline chains agents in sequence, each playing a role: an architect designs, an implementer builds, a reviewer checks. Stage one works independently and the round seals. Every later stage receives the previous stage's artifact as material, so its output is stamped non-independent by construction. That is correct: the implementer's work was shaped by the architect's plan, and the record says so.
let artifacts = patterns::pipeline(&c, vec![ (architect, "design the module layout".into()), (implementer, "implement the design".into()), (reviewer, "review and tighten it".into()), ]).await?; // one artifact per stage, in order; stage N+1 saw stage N via with_materials
integrate and map_reduce: the merge seat
When many agents produce diverging branches, someone has to fuse them. integrate is
that seat. It seals the round, then hands one integrator agent the granted diffs of every part
through with_materials, and the integrator merges them in its own env. The merged
candidate is stamped independent=false with an influence edge to each input, plus a
materials_granted audit event, so the provenance of the combined result is exact.
An optional neutral verify runs at the end.
let outcome = patterns::integrate(&c, "merge the three feature branches") .parts(&parts) // the artifacts to fuse .integrator(mira) // one agent does the merge .verify(["cargo", "test"]) .run().await?; // outcome.merged : one artifact, independent=false, edges to every input
The integrator does not have to burn agent tokens on conflicts a machine can settle. The
sensible escalation ladder inside its turn is: try a mechanical git merge first,
fall back to h5i resolve per conflicted file, and hand only the genuinely semantic
collisions to the agent's judgment. You spend the expensive resource on the conflicts that
actually need it.
map_reduce is the same seat with a fan-out in front. You map a work
list across agents (assignments to the same agent run sequentially, since there is one resident
session per agent), then the reduce slot takes an integrator agent that merges the
parts. The reduce step is exactly the conflict-resolution seat, so a fan-out and a merge are one
call.
let outcome = patterns::map_reduce(&c) .map(claude, "port the core module") .map(codex, "port the CLI module") .reduce(mira, "merge both ports, resolve conflicts, make it build") .run().await?; // outcome.parts : each mapped candidate // outcome.merged : the reduced artifact (None if no reducer was set)
debate: argue before you decide
A debate is pure argument, no code. Each side speaks in turn for a number of rounds, seeing the
transcript so far, and an optional moderator concludes. There are no artifacts and no freeze:
it is built entirely on ask turns, which is why the sides return prose and the
moderator returns a structured verdict.
let outcome = patterns::debate(&c, "rewrite the scheduler, or patch it?") .sides([advocate, skeptic]) .moderator(lead) .rounds(3) .run().await?; // outcome.transcript : Vec<(agent_id, argument)> in speaking order // outcome.conclusion : Option<DebateConclusion { winner, rationale }>
judge_panel: judgment over recorded evidence
This is the pattern that shows what h5i can do that a general framework cannot, so it is worth slowing down on. A judge panel scores the sealed candidates, but it does not score them on a hunch. It scores them over the run's recorded evidence: the submission ids, the verification ids, the diffs that are already in the event log. And every citation a judge makes is validated against real run state.
let outcome = patterns::judge_panel(&c, "pick the cleanest correct solution") .judges([reviewer, maintainer, verifier]) .run().await?; // each judge returns Ballot { artifact_id, score (0-10), rationale, cited_ids } // cited_ids must reference real submission / verification ids in this run
Under the hood, each judge is an agent answering an ask for a set of ballots. A
Ballot carries the candidate's artifact_id, a score, a rationale, and
cited_ids, the evidence ids the rationale is grounded in. Before a ballot is
counted, the panel checks every cited id against the run's actual submissions and verifications.
A judge that cites an id that does not exist gets its ballot rejected and re-asked, with the
offending citation named, up to a bounded number of attempts. A judge that scores a candidate
that is not on the ballot is caught the same way.
One more thing keeps judge_panel honest as a citizen of the eDSL: its aggregation is
a VerdictPolicy. The mean-score rule is expressed with policy::from_fn
and the verdict is recorded through the same c.judge path as any other verdict. The
panel's job is eliciting evidence-cited ballots, not owning a bespoke verdict format, so
VerdictPolicy stays the single way a verdict is decided and recorded. A caller could
score the same ballots with a median, a quorum, or a veto rule instead.
The building blocks, and your own patterns
Two primitives power most of the above, and they are yours to use directly. ask::<T>
is a typed data turn: the agent's reply must deserialize as T, so a judge or a router
is an ordinary function that returns a struct rather than a diff. Replies come back over the
team agent reply spool, and an unparseable answer is re-asked with the parse error, a
bounded number of times. work().expect_independent() is a runtime check that a first
attempt really came back stamped independent, so arena and ensemble cannot accidentally count a
contaminated candidate as a blind one.
Because the patterns are plain functions over these primitives, composing a new one is not a
framework extension, it is just more code. map_reduce already calls
integrate for its reduce step. A tournament bracket, a self-repair loop, a
staged-escalation review: each is a score you write next to the built-ins, using the same
Conductor. The library is a starting set, not a wall.
FAQ
Are these patterns special-cased in the engine? No. Every one is a public function in the patterns module, built from the same Conductor and Agent methods you call. They are examples with a nice signature, not privileged primitives.
How is judge_panel different from asking a model to grade? The judges score over the run's recorded evidence, and every citation is validated against real submission and verification ids, with a bounded re-ask when a judge invents one. Grounding the judgment in checked facts is the whole point.
Why does with_materials mark work as non-independent? Because it is. Once an integrator or a later pipeline stage has seen a teammate's diff, its output was influenced by that diff. The stamp and the influence edges record exactly what shaped the result, which is what makes the provenance defensible.
Do patterns like arena need the review round? No. Arena runs blind attempts and ranks them with no mutual review, which is the difference from ensemble. Use arena when you do not want peers reshaping each other's candidates.
Can I mix patterns in one run? Yes. They share a Conductor and a run, so you can fan out with map_reduce, then hand the parts to integrate, then decide with a judge_panel. It is all one program.
Compose your own pattern
Start from a built-in, fork it, or write a new score against the same Conductor. Every turn journaled, every verdict recorded.