A council that picks itself.
AutoCouncil is opt-in sugar on top of regular councils. From the caller's
perspective it behaves like any other runnable: pass it to CouncilEx.run/3
and you get a Result back. Internally a strategy decides which static
council module or %DynamicCouncil{} actually handles the prompt.
Existing callers that hardcode a council
(CouncilEx.run(MyApp.Councils.SEO, input)) are unaffected — AutoCouncil
is purely additive.
Quick start
auto =
CouncilEx.AutoCouncil.new(
name: "general-routing",
strategy: :rules,
catalog: [
%{id: "seo", council: MyApp.Councils.SEO, match: ~r/seo|sitemap/i, desc: "SEO audit"},
%{id: "code", council: MyApp.Councils.CodeReview, match: ~r/code|PR/i, desc: "Code review"}
]
)
{:ok, result} = CouncilEx.run(auto, %{question: "audit my SEO"})
result.metadata.auto
# => %{strategy: :rules, kind: :static, catalog_id: "seo", reason: "...", ...}Catalog sources
Two equivalent forms — pick whichever fits your workflow:
# Inline (explicit, easy to test, lives with calling code)
AutoCouncil.new(strategy: :rules, catalog: [...])
# Registry-backed (shared, hot-reloadable, UI-editable)
AutoCouncil.new(strategy: :rules, catalog: {:registry, :council})Register entries in config or at runtime:
config :council_ex, :registry,
councils: %{
"seo" => %{council: MyApp.Councils.SEO, match: ~r/seo/i, desc: "SEO audit"}
}
CouncilEx.Registry.register_council("editorial", %{
council: my_dynamic_council,
match: ~r/editorial|copy/i,
desc: "Editorial review"
})Strategies
:rules— first regex/fun match wins. Cheap, deterministic, no LLM cost.:cascade— try a chain of strategies; first success wins.:embedding— stub. Cosine similarity over precomputed vectors.:llm_classify— stub. Cheap LLM picks an id from the catalog.:llm_build— stub. LLM synthesizes a freshDynamicCouncil.{module, opts}— bring your own.
On no match
Configure :on_no_match to control fallback behaviour:
:error(default) —CouncilEx.run/3returns{:error, :no_match}.{:fallback, council_or_id}— use the named council instead.
Inspecting a decision
resolve/2 returns the routing decision without running it. Useful for
UIs, dry-runs, and tests:
{:ok, decision, meta} = AutoCouncil.resolve(auto, "audit my SEO")
decision # => {:static, MyApp.Councils.SEO}
meta # => %{strategy: :rules, kind: :static, ...}Composability
Because AutoCouncil is a council from the runner's perspective, it
composes: an AutoCouncil may resolve to another AutoCouncil, to a
DynamicCouncil whose members are sub-councils, etc.
Summary
Types
Where the strategy reads its catalog from.
Catalog entry. Only :council is required by the resolver.
Resolved decision. Caller can inspect what was picked.
Metadata about the routing decision. Stored in Result.metadata.auto.
Behaviour when no catalog entry matches.
Strategy reference. Atom = built-in. Tuple = custom.
Functions
Build an AutoCouncil. See moduledoc for full options.
Resolve the routing decision for prompt without running it. Useful for
UIs and tests. Emits the same telemetry as a full run.
Types
@type catalog() :: [catalog_entry()] | {:registry, :council}
Where the strategy reads its catalog from.
@type catalog_entry() :: %{ :council => module() | CouncilEx.DynamicCouncil.t(), optional(:id) => String.t(), optional(:desc) => String.t(), optional(:match) => Regex.t() | (binary() -> boolean()), optional(:vector) => [float()], optional(:tags) => [atom()] }
Catalog entry. Only :council is required by the resolver.
@type decision() :: {:static, module()} | {:dynamic, CouncilEx.DynamicCouncil.t()} | {:built, CouncilEx.DynamicCouncil.t()}
Resolved decision. Caller can inspect what was picked.
@type meta() :: %{ strategy: atom(), kind: :static | :dynamic | :built, catalog_id: String.t() | nil, reason: String.t() | nil, score: float() | nil, latency_ms: non_neg_integer() }
Metadata about the routing decision. Stored in Result.metadata.auto.
@type on_no_match() :: :error | {:fallback, module() | String.t() | CouncilEx.DynamicCouncil.t()}
Behaviour when no catalog entry matches.
@type strategy() :: :rules | :cascade | :embedding | :llm_classify | :llm_build | {module(), keyword()}
Strategy reference. Atom = built-in. Tuple = custom.
Functions
Build an AutoCouncil. See moduledoc for full options.
Required
:strategy
Optional
:catalog— inline list or{:registry, :council}. Required by most strategies; some (e.g.:llm_build) can synthesize without one.:on_no_match—:error(default) or{:fallback, council_or_id}.:name— human label. Surfaces in telemetry.:options— strategy-specific keyword opts.
Resolve the routing decision for prompt without running it. Useful for
UIs and tests. Emits the same telemetry as a full run.
Returns {:ok, decision, meta} or {:error, reason}.