From 8d9e18d40b60426113adec32cfb6c7a6a816bdd2 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 12 Aug 2026 17:27:23 -0700 Subject: [PATCH] Inspired by Perplexity Computer: Model Council mode for Mixture of Agents Adds a 'council' synthesis style to MoA (per preset via synthesis_style, one-shot via the new /council command on CLI + gateway). Reference models answer independently; the aggregator chairs the deliberation and produces a user-facing report of consensus, per-model disagreements (with the differing assumptions behind them), unique contributions, and a recommendation with an explicit confidence level. Inspired by Perplexity's Model Council rollout to Perplexity Computer (changelog 08/04/26): pick a board of 2-8 models, run them independently, synthesize where they agree/disagree and what each uniquely surfaces. --- agent/conversation_loop.py | 3 + agent/moa_loop.py | 102 +++++++++--- cli.py | 25 +++ gateway/run.py | 29 ++++ hermes_cli/commands.py | 4 +- hermes_cli/moa_cmd.py | 3 + hermes_cli/moa_config.py | 57 ++++++- tests/agent/test_moa_council_style.py | 147 ++++++++++++++++++ website/docs/reference/slash-commands.md | 2 + .../user-guide/features/mixture-of-agents.md | 59 +++++++ 10 files changed, 407 insertions(+), 24 deletions(-) create mode 100644 tests/agent/test_moa_council_style.py diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 95d48421c12fc..aeb7bf10ca7b9 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -2001,6 +2001,9 @@ def run_conversation( degraded_reference_policy=str( moa_config.get("degraded_reference_policy") or "loud" ), + synthesis_style=str( + moa_config.get("synthesis_style") or "guidance" + ), agent=agent, ) if _moa_context: diff --git a/agent/moa_loop.py b/agent/moa_loop.py index 0e15492a8fca1..fda8a0654abe9 100644 --- a/agent/moa_loop.py +++ b/agent/moa_loop.py @@ -1217,6 +1217,7 @@ def aggregate_moa_context( reference_max_tokens: int | None = None, reference_timeout: float | None = None, degraded_reference_policy: str = "loud", + synthesis_style: str = "guidance", agent: Any = None, ) -> str: """Run configured reference models and synthesize their advice. @@ -1240,6 +1241,14 @@ def aggregate_moa_context( ``agent``, when passed, lets the reference fan-out be aborted early on a user interrupt — see ``_run_references_parallel``'s docstring. + + ``synthesis_style`` selects how advisor output is synthesized (see + ``hermes_cli.moa_config.coerce_synthesis_style``): ``"guidance"`` (the + default) produces private context for the acting model; ``"council"`` + (inspired by Perplexity Computer's Model Council, Aug 2026) makes the + aggregator act as a council *chair*, producing a user-facing deliberation + report that surfaces agreement, disagreement, per-model unique + contributions, and a recommendation with confidence. """ reference_models = [slot for slot in reference_models if slot.get("enabled", True)] reference_outputs: list[tuple[str, str, Any]] = [] @@ -1297,15 +1306,36 @@ def aggregate_moa_context( f"{notice}" ) - synth_prompt = ( - "You are the aggregator in a Mixture of Agents process. Synthesize the " - "reference responses into concise, actionable guidance for the main " - "Hermes agent. Focus on next steps, tool-use strategy, risks, and any " - "disagreements. Do not answer the user directly unless that is all that " - "is needed; produce context the main agent should use in its normal loop.\n\n" - f"Original user prompt:\n{user_prompt}\n\n" - f"Reference responses:\n{joined}" - ) + council = str(synthesis_style or "guidance").strip().lower() == "council" + if council: + synth_prompt = ( + "You are the CHAIR of a model council. Several independent frontier " + "models were each asked the same question and answered without seeing " + "each other's responses. Chair the deliberation: produce a council " + "report with these sections —\n" + "1. Consensus: where the models agree (consensus supports acting with " + "confidence).\n" + "2. Disagreements: where they diverge, attributing each position to " + "its model by label, and identifying the differing starting " + "assumptions behind each divergence.\n" + "3. Unique contributions: what each model uniquely surfaced that the " + "others missed.\n" + "4. Chair's recommendation: your synthesized position, with an " + "explicit confidence level and what evidence would change it.\n" + "Be concrete and attribute positions to models by name.\n\n" + f"Original user prompt:\n{user_prompt}\n\n" + f"Council member responses:\n{joined}" + ) + else: + synth_prompt = ( + "You are the aggregator in a Mixture of Agents process. Synthesize the " + "reference responses into concise, actionable guidance for the main " + "Hermes agent. Focus on next steps, tool-use strategy, risks, and any " + "disagreements. Do not answer the user directly unless that is all that " + "is needed; produce context the main agent should use in its normal loop.\n\n" + f"Original user prompt:\n{user_prompt}\n\n" + f"Reference responses:\n{joined}" + ) agg_label = _slot_label(aggregator) agg_runtime = _slot_runtime(aggregator) @@ -1351,6 +1381,18 @@ def aggregate_moa_context( if not synthesis: synthesis = joined + if council: + return ( + "[Model Council report — a board of independent models deliberated " + "on the user's question and the chair synthesized their positions. " + "Present this deliberation to the user: preserve the " + "consensus/disagreement structure and the per-model attributions, " + "and add your own judgement where useful.]\n" + f"Chair: {agg_label}\n" + f"Council members: {', '.join(_slot_label(slot) for slot in reference_models)}\n\n" + f"{synthesis.strip()}" + ) + return ( "[Mixture of Agents context — use this as private guidance for the " "normal Hermes agent loop. You may call tools, continue reasoning, or " @@ -2227,15 +2269,39 @@ class MoAChatCompletions: elif joined or degraded: if degraded: joined = f"{joined}\n\n{degraded}" if joined else degraded - guidance = ( - "[Mixture of Agents reference context]\n" - f"Preset: {self.preset_name}\n" - f"Aggregator/acting model: {_slot_label(aggregator)}\n" - f"References: {', '.join(label for label, _, _ in _agg_refs)}\n\n" - "Use the reference responses below as private context. You are the aggregator and acting model: " - "answer the user directly or call tools as needed.\n\n" - f"{joined}" - ) + if str(preset.get("synthesis_style") or "guidance").strip().lower() == "council": + # Council style (inspired by Perplexity Computer's Model + # Council, Aug 2026): the acting model chairs a deliberation + # instead of consuming private advice. Its user-facing answer + # must surface consensus, disagreements (with per-model + # attribution and the differing assumptions behind them), + # unique contributions, and a recommendation with confidence. + guidance = ( + "[Model Council deliberation]\n" + f"Preset: {self.preset_name}\n" + f"Chair (you): {_slot_label(aggregator)}\n" + f"Council members: {', '.join(label for label, _, _ in _agg_refs)}\n\n" + "You are the chair of a model council. The council member " + "responses below are independent answers to the current " + "state of the task. Chair the deliberation in your reply: " + "surface where the members agree (consensus), where they " + "disagree (attribute positions to members by name and name " + "the differing assumptions), what each uniquely surfaced, " + "and close with your own recommendation and an explicit " + "confidence level. You may still call tools when the task " + "requires acting rather than deliberating.\n\n" + f"{joined}" + ) + else: + guidance = ( + "[Mixture of Agents reference context]\n" + f"Preset: {self.preset_name}\n" + f"Aggregator/acting model: {_slot_label(aggregator)}\n" + f"References: {', '.join(label for label, _, _ in _agg_refs)}\n\n" + "Use the reference responses below as private context. You are the aggregator and acting model: " + "answer the user directly or call tools as needed.\n\n" + f"{joined}" + ) _attach_reference_guidance(agg_messages, guidance) prepared_request = { diff --git a/cli.py b/cli.py index 2e822be3746ac..8505bfaba7908 100644 --- a/cli.py +++ b/cli.py @@ -10678,6 +10678,31 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self._pending_moa_disable_after_turn = True self._pending_agent_seed = payload _cprint(f" MoA one-shot queued with preset {preset}; previous model will be restored after this turn.") + elif canonical == "council": + # /council — one-shot Model Council (inspired by + # Perplexity Computer's Model Council, Aug 2026): the default MoA + # preset's reference models answer the question independently, the + # preset's aggregator chairs the deliberation, and the council + # report (consensus / disagreements / unique contributions / + # recommendation with confidence) is handed to the current session + # model to present. The session model is never switched. + from hermes_cli.moa_config import build_moa_turn_prompt + + parts = cmd_original.split(None, 1) + payload = parts[1].strip() if len(parts) > 1 else "" + if not payload: + _cprint(" Usage: /council (runs the default MoA preset's models as an independent council and reports consensus vs disagreement)") + return True + try: + moa_cfg = self.config.get("moa") if isinstance(self.config, dict) else {} + encoded = build_moa_turn_prompt( + payload, moa_cfg, synthesis_style="council" + ) + except Exception as exc: + _cprint(f" Council setup failed: {exc}") + return True + self._pending_input.put(encoded) + _cprint(" ⚖ Council convened — reference models are deliberating.") elif canonical == "subgoal": self._handle_subgoal_command(cmd_original) elif canonical == "skin": diff --git a/gateway/run.py b/gateway/run.py index 4957b56f63286..0391ca6679a11 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -16132,6 +16132,35 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if canonical == "refine": return await self._handle_refine_command(event) + if canonical == "council": + # /council — one-shot Model Council (inspired by + # Perplexity Computer's Model Council, Aug 2026). Encodes the + # question as a hidden MoA one-shot marker with the council + # synthesis style forced on; run_conversation decodes it, the + # default preset's reference models answer independently, and + # the chair's consensus/disagreement report reaches the session + # model as context. No model switch, no agent eviction. + from hermes_cli.moa_config import build_moa_turn_prompt + from hermes_cli.config import load_config + + council_payload = event.get_command_args().strip() + if not council_payload: + return ( + "Usage: /council (runs the default MoA preset's " + "models as an independent council and reports consensus vs " + "disagreement)" + ) + try: + cfg = load_config() + event.text = build_moa_turn_prompt( + council_payload, + cfg.get("moa") if isinstance(cfg, dict) else {}, + synthesis_style="council", + ) + except Exception: + return "Failed to prepare council turn." + # Fall through to _handle_message_with_agent with the rewritten text. + if canonical == "moa": # /moa is one-shot sugar only: run a single prompt through the # default MoA preset, then restore the prior model. To *switch* to a diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 1f9e70e430891..e9bb08b23cfd1 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -170,6 +170,8 @@ COMMAND_REGISTRY: list[CommandDef] = [ args_hint="[focus instructions]"), CommandDef("moa", "Run one prompt through the default Mixture of Agents preset, then restore your model", "Session", args_hint="", busy_policy="reject", busy_handler="moa"), + CommandDef("council", "Convene a model council: reference models answer independently, a chair synthesizes consensus and disagreements", "Session", + args_hint="", busy_policy="reject"), CommandDef("subgoal", "Add or manage extra criteria on the active goal", "Session", args_hint="[text | remove N | clear]", busy_policy="dispatch"), CommandDef("status", "Show session, model, token, and context info", "Session", @@ -1277,7 +1279,7 @@ _SLACK_PRIORITY_ALIASES = ("btw", "bg") # native slash. # - pause: global emergency stop; reached via /hermes pause [off] on # Slack. Added at the 50-cap — a native slot would clamp /platform. -_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "debug", "egress", "init", "version", "diff", "update", "heartbeat", "refine", "pause"}) +_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "council", "debug", "egress", "init", "version", "diff", "update", "heartbeat", "refine", "pause"}) def _sanitize_slack_name(raw: str) -> str: diff --git a/hermes_cli/moa_cmd.py b/hermes_cli/moa_cmd.py index 8fcd8174e6d75..0a6d00c61ad55 100644 --- a/hermes_cli/moa_cmd.py +++ b/hermes_cli/moa_cmd.py @@ -89,6 +89,9 @@ def _print_config(config: dict[str, Any]) -> None: print(f" {idx}. {_format_slot(slot)}") agg = preset["aggregator"] print(f" Aggregator: {_format_slot(agg)}") + style = str(preset.get("synthesis_style") or "guidance") + if style != "guidance": + print(f" Synthesis style: {style}") def cmd_moa(args) -> None: diff --git a/hermes_cli/moa_config.py b/hermes_cli/moa_config.py index 24d36e4a19d87..fe62433581cad 100644 --- a/hermes_cli/moa_config.py +++ b/hermes_cli/moa_config.py @@ -71,6 +71,26 @@ def _coerce_degraded_reference_policy(value: Any) -> str: return policy if policy in {"loud", "silent"} else "loud" +def coerce_synthesis_style(value: Any) -> str: + """Normalize the MoA synthesis style; unknown values degrade to 'guidance'. + + - ``'guidance'`` (default): the classic MoA shape — advisor responses are + synthesized into PRIVATE context for the acting model, which then + answers the user normally. Advisor perspectives never surface as a + deliverable. + - ``'council'``: multi-model deliberation shape (inspired by Perplexity + Computer's Model Council, Aug 2026): each reference model weighs in + independently and the acting model becomes the council *chair*, + producing a USER-FACING report that explicitly surfaces where the + models agree, where they disagree (and why — differing starting + assumptions), what each uniquely contributes, and a recommended + position with confidence. Built for ambiguous judgment calls where + disagreement between frontier models is itself the signal. + """ + style = str(value or "guidance").strip().lower() + return style if style in {"guidance", "council"} else "guidance" + + def _coerce_int(value: Any, default: int) -> int: if value is None or value == "": return default @@ -306,6 +326,7 @@ def _default_preset() -> dict[str, Any]: "max_tokens": 4096, "reference_max_tokens": None, "fanout": "user_turn", + "synthesis_style": "guidance", "enabled": True, } @@ -366,6 +387,11 @@ def _normalize_preset(raw: Any) -> dict[str, Any]: # last advisor run. Also accepts the mapping form # {mode: every_n, n: N}, normalized to the canonical string. "fanout": _coerce_fanout(raw.get("fanout")), + # How advisor output is synthesized for the acting model. "guidance" + # (default) keeps advisor perspectives as private context; "council" + # turns the acting model into a council chair producing a user-facing + # agree/disagree report (see coerce_synthesis_style). + "synthesis_style": coerce_synthesis_style(raw.get("synthesis_style")), } @@ -415,6 +441,7 @@ def normalize_moa_config(raw: Any) -> dict[str, Any]: "max_tokens": active["max_tokens"], "reference_max_tokens": active.get("reference_max_tokens"), "fanout": active.get("fanout", "user_turn"), + "synthesis_style": active.get("synthesis_style", "guidance"), "enabled": active["enabled"], # MoA-level (not per-preset) toggles ride at the top level alongside # save_traces. privacy_filter: '' (off, default) | 'display' | 'full' @@ -475,11 +502,24 @@ def set_active_moa_preset(config: Any, name: str | None) -> dict[str, Any]: return cfg -def encode_moa_turn(prompt: str, config: Any = None, preset: str | None = None) -> str: - """Encode a /moa one-shot turn for frontends that can only send text.""" +def encode_moa_turn( + prompt: str, + config: Any = None, + preset: str | None = None, + synthesis_style: str | None = None, +) -> str: + """Encode a /moa one-shot turn for frontends that can only send text. + + ``synthesis_style``, when given, overrides the resolved preset's style for + this one turn (used by ``/council`` to force a council deliberation over + the default preset without touching config). + """ + turn_config = resolve_moa_preset(config or {}, preset) + if synthesis_style is not None: + turn_config["synthesis_style"] = coerce_synthesis_style(synthesis_style) payload = { "prompt": str(prompt or ""), - "config": resolve_moa_preset(config or {}, preset), + "config": turn_config, } encoded = base64.urlsafe_b64encode( json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8") @@ -500,9 +540,16 @@ def decode_moa_turn(message: Any) -> tuple[str, dict[str, Any] | None]: return prompt, _normalize_preset(payload.get("config") or {}) -def build_moa_turn_prompt(user_prompt: str, config: Any = None, preset: str | None = None) -> str: +def build_moa_turn_prompt( + user_prompt: str, + config: Any = None, + preset: str | None = None, + synthesis_style: str | None = None, +) -> str: """Build the hidden one-shot payload used by TUI/gateway routing.""" - return encode_moa_turn(user_prompt, config, preset=preset) + return encode_moa_turn( + user_prompt, config, preset=preset, synthesis_style=synthesis_style + ) def moa_usage() -> str: diff --git a/tests/agent/test_moa_council_style.py b/tests/agent/test_moa_council_style.py new file mode 100644 index 0000000000000..aaefbeac83998 --- /dev/null +++ b/tests/agent/test_moa_council_style.py @@ -0,0 +1,147 @@ +"""Tests for the MoA 'council' synthesis style (Model Council). + +Inspired by Perplexity Computer's Model Council (Aug 2026): reference models +answer independently, and the aggregator acts as a council chair producing a +user-facing consensus/disagreement report instead of private guidance. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from hermes_cli.moa_config import ( + build_moa_turn_prompt, + coerce_synthesis_style, + decode_moa_turn, + normalize_moa_config, +) + + +# ── coerce_synthesis_style normalization ──────────────────────────────────── + + +def test_coerce_synthesis_style_defaults_and_tolerance(): + assert coerce_synthesis_style(None) == "guidance" + assert coerce_synthesis_style("") == "guidance" + assert coerce_synthesis_style("guidance") == "guidance" + assert coerce_synthesis_style("council") == "council" + assert coerce_synthesis_style("COUNCIL ") == "council" + # Unknown / bad types degrade to the default (tolerant-read contract). + assert coerce_synthesis_style("debate") == "guidance" + assert coerce_synthesis_style(42) == "guidance" + + +def test_normalize_preset_carries_synthesis_style(): + cfg = normalize_moa_config( + {"presets": {"p": { + "reference_models": [{"provider": "openrouter", "model": "openai/gpt-5.5"}], + "aggregator": {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"}, + "synthesis_style": "council", + }}} + ) + assert cfg["presets"]["p"]["synthesis_style"] == "council" + # Flattened compatibility view mirrors the default preset. + assert cfg["synthesis_style"] == "council" + + +def test_normalize_preset_synthesis_style_defaults_to_guidance(): + cfg = normalize_moa_config({}) + assert cfg["presets"]["default"]["synthesis_style"] == "guidance" + assert cfg["synthesis_style"] == "guidance" + + +# ── one-shot marker round-trip (/council) ─────────────────────────────────── + + +def test_council_one_shot_marker_round_trip(): + encoded = build_moa_turn_prompt( + "should we launch in Q4 or Q1?", {}, synthesis_style="council" + ) + prompt, config = decode_moa_turn(encoded) + assert prompt == "should we launch in Q4 or Q1?" + assert config is not None + assert config["synthesis_style"] == "council" + + +def test_moa_one_shot_marker_keeps_guidance_default(): + encoded = build_moa_turn_prompt("hello", {}) + _prompt, config = decode_moa_turn(encoded) + assert config is not None + assert config["synthesis_style"] == "guidance" + + +# ── aggregate_moa_context council path ────────────────────────────────────── + + +def _response(content: str = "ok"): + message = SimpleNamespace(content=content, tool_calls=[]) + choice = SimpleNamespace(message=message, finish_reason="stop") + return SimpleNamespace(choices=[choice], usage=None, model="fake") + + +@pytest.fixture +def hermes_home(tmp_path, monkeypatch): + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + return home + + +def _run_aggregate(monkeypatch, synthesis_style): + from agent.moa_loop import aggregate_moa_context + + calls: list[dict] = [] + + def fake_call_llm(**kwargs): + calls.append(kwargs) + return _response( + "advice" if kwargs.get("task") == "moa_reference" else "synthesis" + ) + + monkeypatch.setattr("agent.moa_loop.call_llm", fake_call_llm) + + result = aggregate_moa_context( + user_prompt="buy or lease?", + api_messages=[{"role": "user", "content": "buy or lease?"}], + reference_models=[{"provider": "openrouter", "model": "openai/gpt-5.5"}], + aggregator={"provider": "openrouter", "model": "anthropic/claude-opus-4.8"}, + synthesis_style=synthesis_style, + ) + return result, calls + + +def _flatten(content) -> str: + if isinstance(content, str): + return content + return "".join( + part.get("text", "") for part in content if isinstance(part, dict) + ) + + +def test_council_style_uses_chair_prompt_and_report_framing(hermes_home, monkeypatch): + result, calls = _run_aggregate(monkeypatch, "council") + + agg_calls = [c for c in calls if c.get("task") == "moa_aggregator"] + assert len(agg_calls) == 1 + synth_prompt = _flatten(agg_calls[0]["messages"][-1]["content"]) + assert "CHAIR of a model council" in synth_prompt + assert "Council member responses" in synth_prompt + + # The returned context is a user-facing council report, not private + # guidance. + assert result.startswith("[Model Council report") + assert "Chair: openrouter:anthropic/claude-opus-4.8" in result + assert "Council members: openrouter:openai/gpt-5.5" in result + + +def test_guidance_style_unchanged(hermes_home, monkeypatch): + """Default style must be byte-compatible with the classic MoA framing.""" + result, calls = _run_aggregate(monkeypatch, "guidance") + + agg_calls = [c for c in calls if c.get("task") == "moa_aggregator"] + synth_prompt = _flatten(agg_calls[0]["messages"][-1]["content"]) + assert "aggregator in a Mixture of Agents process" in synth_prompt + assert result.startswith("[Mixture of Agents context") + assert "CHAIR" not in synth_prompt diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index bbf996ac840f6..21575d1563843 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -56,6 +56,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in | `/heartbeat every ` (alias: `/hb`) | Set a recurring prompt that re-enters **this session** as a normal user turn whenever it's idle and the interval has elapsed (min 60s; missed ticks coalesce). Subcommands: `/heartbeat status`, `/heartbeat pause`, `/heartbeat resume`, `/heartbeat clear`. Session-scoped and in-process — use `hermes cron` for durable isolated schedules. See [Session Heartbeats](/user-guide/features/heartbeat). | | `/refine [focus]` | Run the background memory/skill self-improvement review **now** instead of waiting for the automatic post-turn trigger. Optional focus text steers the review (e.g. `/refine save the deploy workflow as a skill`). Runs in a background fork against a conversation snapshot — the live session and prompt cache are untouched; results are reported when done. | | `/moa ` | Run a single prompt through the default [Mixture of Agents](/user-guide/features/mixture-of-agents) preset, then restore your current model. One-shot — does not change your session model. | +| `/council ` | Convene a **model council**: the default MoA preset's reference models answer the question independently, and the aggregator chairs the deliberation — reporting where the models agree, where they disagree (with per-model attribution and the differing assumptions behind each divergence), what each uniquely surfaced, and a recommendation with an explicit confidence level. Built for ambiguous judgment calls where model disagreement is itself the signal. One-shot — does not change your session model. See [Council mode](/user-guide/features/mixture-of-agents#council-mode). | | `/resume [name]` | Resume a previously-named session | | `/sessions` (TUI alias: `/switch`) | Classic CLI: browse and resume previous sessions in an interactive picker. TUI: open the live session switcher for currently open TUI sessions. Use `/sessions new` in the TUI to start another live session immediately. | | `/egress [status]` | Show Docker egress proxy status — enabled/configured/running state, credential source, token mappings, uncovered providers, and next remediation step. Works in CLI, TUI, Desktop chat, and messaging gateway. | @@ -256,6 +257,7 @@ The messaging gateway supports the following built-in commands inside Telegram, | `/heartbeat every ` (alias: `/hb`) | Set a recurring prompt that re-enters this session when idle. Subcommands: `status`, `pause`, `resume`, `clear`. On Slack use `/hermes heartbeat …`. | | `/refine [focus]` | Run the memory/skill self-improvement review now, optionally with focus instructions. On Slack use `/hermes refine …`. | | `/moa ` | Run one prompt through the default [Mixture of Agents](/user-guide/features/mixture-of-agents) preset, then restore the session model. | +| `/council ` | One-shot [model council](/user-guide/features/mixture-of-agents#council-mode): reference models answer independently; the chair reports consensus, disagreements, and a recommendation with confidence. | | `/branch [name]` (alias: `/fork`) | Branch the current session (explore a different path). | | `/agents` (alias: `/tasks`) | Show active agents and running tasks. | | `/sessions` | Browse and resume previous sessions. | diff --git a/website/docs/user-guide/features/mixture-of-agents.md b/website/docs/user-guide/features/mixture-of-agents.md index 55c6d23791f0f..cf7a653685b91 100644 --- a/website/docs/user-guide/features/mixture-of-agents.md +++ b/website/docs/user-guide/features/mixture-of-agents.md @@ -230,6 +230,65 @@ moa: Omit `reasoning_effort` to use the provider/Hermes default for that slot. +## Council mode + +*Inspired by Perplexity Computer's Model Council (Aug 2026).* + +By default, MoA advisor perspectives are **private context** — the acting model +consumes them silently and answers normally. Council mode flips that: the +reference models become an explicit **board of independent models**, and the +acting model becomes the council **chair**, producing a user-facing +deliberation report that surfaces: + +1. **Consensus** — where the models agree (consensus supports acting with + confidence). +2. **Disagreements** — where they diverge, attributed per model, with the + differing starting assumptions behind each divergence. +3. **Unique contributions** — what each model surfaced that the others missed. +4. **Chair's recommendation** — a synthesized position with an explicit + confidence level and what evidence would change it. + +Council mode is built for ambiguous questions: judgment calls, trade-offs, +risk assessments — situations where there is no single right answer and +disagreement between frontier models is itself the signal. + +### One-shot: `/council` + +``` +/council should we launch this feature in Q4 or Q1? +``` + +Runs the default MoA preset's reference models independently against your +question, chairs the deliberation, and reports back — then your session +continues on your normal model. Works on the CLI, TUI, and every messaging +platform (on Slack: `/hermes council …`). + +### Persistent: `synthesis_style: council` + +Set it per preset to make every turn of a MoA session deliberate as a council: + +```yaml +moa: + presets: + board: + reference_models: + - provider: openrouter + model: openai/gpt-5.5 + - provider: openrouter + model: google/gemini-3.1-pro + aggregator: + provider: openrouter + model: anthropic/claude-opus-4.8 + synthesis_style: council # default: guidance +``` + +`guidance` (the default) is the classic MoA shape — advisor output stays +private. Unknown values degrade to `guidance`. + +Tips (they sharpen council output noticeably): ask for decisions rather than +summaries ("what would you bet on and why"), include numbers/timelines/budget, +and ask each model to state the assumptions behind its conclusion. + ## Terminal preset management ```bash