Topology #7 — independent_analysis → iterate(pairwise_elimination) → synthesis.
Members analyse the input in parallel, then a tournament bracket runs pairwise elimination rounds (judged by the chair, or by a custom 2-arity function) until one member remains. The chair then synthesizes a final answer from the winner's output.
Usage
council =
CouncilEx.Councils.Tournament.new(
as: MyApp.Tournament,
members: [
{:m1, MyApp.Members.M1, [provider: :openai, model: "gpt-4o-mini"]},
{:m2, MyApp.Members.M2, [provider: :openai, model: "gpt-4o-mini"]},
{:m3, MyApp.Members.M3, [provider: :openai, model: "gpt-4o-mini"]},
{:m4, MyApp.Members.M4, [provider: :openai, model: "gpt-4o-mini"]}
],
chair: {MyApp.Members.Synth, [provider: :openai, model: "gpt-4o"]}
)
{:ok, result} = CouncilEx.run(council, %{topic: "..."})Options
:as— generated module name (default: anonymous unique name).:members(required) —[{id, module, opts}].:chair(required) —{module, opts}tuple. Used both as bracket judge (default) and final synthesizer.:judge— optional custom judge: a 2-arity functionfn output_a, output_b -> :a | :b end. Skips the chair for bracket judgments. Useful for deterministic / non-LLM brackets.:max_iterations— bracket depth cap (default16, supports up to 65536-member brackets).
Internal — judge dispatch
When a 2-arity :judge fn is provided, it is stored in :persistent_term
keyed by the generated council module. The generated module exposes a
__pairwise_judge__/2 function that looks it up; this captured function
reference is what flows through the wrap: :pairwise_elimination round's
wrapped_opts[:judge] (anonymous function values cannot be embedded as
AST literals, so the :persistent_term indirection is required).
Dynamic form
new_dynamic/1 returns a %CouncilEx.DynamicCouncil{} with the same
topology. The optional :judge value MUST be a remote function capture
— &MyMod.my_judge/2 — not an inline fn, because dynamic councils
are designed to be JSON-serializable (closures aren't). Inline fn
works at runtime but won't survive to_json/from_json round-trips.
Summary
Functions
Build the Tournament topology as a generated council module.
Options
:as— module name for the generated council (default: anonymous unique).:members(required) —[{id, module, opts}]member tuples.:chair(required) —{module, opts}chair tuple. Used both as bracket judge (default) and final synthesizer.:judge— optional 2-arity(output_a, output_b -> :a | :b)function that overrides the chair for bracket judgments. Useful for deterministic / non-LLM brackets.:max_iterations— bracket depth cap (default16).
See the moduledoc for a complete example. Use new_dynamic/1 for the
data-only %CouncilEx.DynamicCouncil{} form.
@spec new_dynamic(keyword()) :: CouncilEx.DynamicCouncil.t()
Build the same Tournament topology as new/1 but as a data-only
%CouncilEx.DynamicCouncil{}.
Members and chair accept the same shapes as Specialist.new_dynamic/1.
The optional :judge MUST be a remote function capture
(&MyMod.fun/2); inline fn literals will work at runtime but break
JSON round-trips. Omit :judge to let the chair perform bracket
judgments (the standard Vote-style chair-as-judge path).