Ensemble · Patterns

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.

Key takeaways
  • 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_materials stamps a merged candidate independent=false with an influence edge to every input, so integration work is honest about what it saw.
  • judge_panel is 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.

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.

pipeline
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.

integrate
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.

map_reduce
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.

debate
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.

judge_panel
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.

The differentiator is citation validation, not the LLM call. Any framework can ask a model "which is better." Only a system with an evidence log can require the model to point at recorded facts and then check that the facts are real. That is what separates evidence-grounded judgment from a plausible-sounding vibe, and it is why the panel lives here and not in a general orchestrator.

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.

Read the manual Star on GitHub