CouncilEx.DynamicCouncil (CouncilEx v0.1.0)

Copy Markdown View Source

Data-only council representation. Build at runtime, edit, persist to storage, restore, run — all without defining an Elixir module.

Designed for UI-driven workflow builders (React Flow, etc.) where the council shape is user data, not code.

Builder pattern

alias CouncilEx.DynamicCouncil

DynamicCouncil.new("seo-council-1", name: "SEO audit")
|> DynamicCouncil.set_default_profile("openai_mini")
|> DynamicCouncil.add_member(%{id: "seo", system_prompt: "Audit SEO."})
|> DynamicCouncil.add_member(%{id: "content", system_prompt: "Audit content."})
|> DynamicCouncil.add_round(:independent_analysis)
|> DynamicCouncil.set_chair(%{id: "synth", system_prompt: "Synthesize."})

Persistence

{:ok, json} = DynamicCouncil.to_json(council)
{:ok, council} = DynamicCouncil.from_json(json)

Validation

case DynamicCouncil.validate(council) do
  :ok -> ...
  {:error, errors} -> # JSON-friendly error list
end

Run

{:ok, result} = CouncilEx.run(council, %{question: "..."})

Summary

Functions

Append a tool to the council. Council-level tools are merged into every member's tool list at runtime, exposing a shared toolset (e.g. a retrieval tool every member can call).

Append a member. Accepts a %DynamicMember{}, a map, or a keyword list.

Append a round. Accepts a %DynamicRound{}, a map, a string, or an atom.

Latest known schema version.

Decode a JSON string into a %DynamicCouncil{}.

Same as from_json/1 but raises on decoder failure.

Build a %DynamicCouncil{} from a plain map (e.g. JSON-decoded).

Build a new empty council.

Replace the member with the given id, or add it if not present.

Remove a member by id.

Set the chair (or nil to clear). Accepts struct, map, or keyword list.

Replace the council-level tool list outright.

Set the default profile (registered profile name).

Replace the metadata map.

Set the router (registered router name) or nil to clear.

Convert a council to a {nodes, edges} graph suitable for rendering in React Flow (or any node-graph UI).

Serialize council to a JSON string via Jason.

Same as to_json/2 but raises on encoder failure.

JSON-friendly map representation (string keys).

Lower a %DynamicCouncil{} to a %CouncilEx.Spec{} ready for the runner.

Validate the council. Returns :ok or {:error, [%{path, message, code}]} with JSON-friendly error structures suitable for UI display.

Same as validate/1 but raises on failure.

Types

t()

@type t() :: %CouncilEx.DynamicCouncil{
  chair: CouncilEx.DynamicMember.t() | nil,
  default_profile: String.t() | nil,
  id: String.t(),
  members: [CouncilEx.DynamicMember.t()],
  metadata: map(),
  name: String.t() | nil,
  rounds: [CouncilEx.DynamicRound.t()],
  router: String.t() | nil,
  tools: [CouncilEx.DynamicMember.tool_ref()],
  version: pos_integer()
}

Functions

add_council_tool(c, ref)

@spec add_council_tool(t(), CouncilEx.DynamicMember.tool_ref()) :: t()

Append a tool to the council. Council-level tools are merged into every member's tool list at runtime, exposing a shared toolset (e.g. a retrieval tool every member can call).

Accepts the same tool refs as member-level tools — a registered tool name (string) or a tool module (atom).

DynamicCouncil.add_council_tool(council, "search_docs")
DynamicCouncil.add_council_tool(council, MyApp.Tools.SearchDocs)

add_member(c, m)

@spec add_member(t(), CouncilEx.DynamicMember.t() | map() | keyword()) :: t()

Append a member. Accepts a %DynamicMember{}, a map, or a keyword list.

add_round(c, r)

@spec add_round(
  t(),
  CouncilEx.DynamicRound.t() | map() | keyword() | String.t() | atom()
) :: t()

Append a round. Accepts a %DynamicRound{}, a map, a string, or an atom.

current_version()

@spec current_version() :: pos_integer()

Latest known schema version.

from_json(json)

@spec from_json(String.t()) :: {:ok, t()} | {:error, term()}

Decode a JSON string into a %DynamicCouncil{}.

from_json!(json)

@spec from_json!(String.t()) :: t()

Same as from_json/1 but raises on decoder failure.

from_map(m)

@spec from_map(map()) :: t()

Build a %DynamicCouncil{} from a plain map (e.g. JSON-decoded).

new(id, opts \\ [])

@spec new(
  String.t(),
  keyword()
) :: t()

Build a new empty council.

DynamicCouncil.new("council-uuid-...", name: "My SEO Council")

put_member(c, m)

@spec put_member(t(), CouncilEx.DynamicMember.t() | map() | keyword()) :: t()

Replace the member with the given id, or add it if not present.

remove_member(c, id)

@spec remove_member(t(), String.t()) :: t()

Remove a member by id.

set_chair(c, m)

@spec set_chair(t(), CouncilEx.DynamicMember.t() | map() | keyword() | nil) :: t()

Set the chair (or nil to clear). Accepts struct, map, or keyword list.

set_council_tools(c, refs)

@spec set_council_tools(t(), [CouncilEx.DynamicMember.tool_ref()]) :: t()

Replace the council-level tool list outright.

set_default_profile(c, name)

@spec set_default_profile(t(), String.t() | nil) :: t()

Set the default profile (registered profile name).

set_metadata(c, md)

@spec set_metadata(t(), map()) :: t()

Replace the metadata map.

set_router(c, name)

@spec set_router(t(), String.t() | nil) :: t()

Set the router (registered router name) or nil to clear.

to_flow_graph(c)

@spec to_flow_graph(t()) :: %{nodes: [map()], edges: [map()]}

Convert a council to a {nodes, edges} graph suitable for rendering in React Flow (or any node-graph UI).

Nodes:

  • one per member (type: "member")
  • one per round (type: "round") — execution sequence
  • one for the chair if present (type: "chair")

Edges:

  • council root → each round (sequential)
  • each round → each member that participates (currently every member; router-aware filtering deferred until router data is part of the dynamic spec)
  • last round → chair (if present)

Returned shapes are plain maps with string keys — JSON-friendly.

to_json(c, opts \\ [])

@spec to_json(
  t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

Serialize council to a JSON string via Jason.

to_json!(c, opts \\ [])

@spec to_json!(
  t(),
  keyword()
) :: String.t()

Same as to_json/2 but raises on encoder failure.

to_map(c)

@spec to_map(t()) :: map()

JSON-friendly map representation (string keys).

to_spec(c)

@spec to_spec(t()) :: CouncilEx.Spec.t()

Lower a %DynamicCouncil{} to a %CouncilEx.Spec{} ready for the runner.

Resolves profile / tool / schema / router refs via the registry. The returned spec has members containing %DynamicMember{} structs in the module slot — RoundExec dispatches on them via CouncilEx.MemberSpec.

validate(c)

@spec validate(t()) :: :ok | {:error, [map()]}

Validate the council. Returns :ok or {:error, [%{path, message, code}]} with JSON-friendly error structures suitable for UI display.

validate!(c)

@spec validate!(t()) :: :ok

Same as validate/1 but raises on failure.