# NousResearch/hermes-agent · MoA architecture simplification and routing changes

## Open now

- Nothing open on GitHub.

These items concern rethinking and refining the Mixture-of-Agents (MoA) implementation, including making it one-shot, replacing a virtual provider approach with subagent delegation, and applying MoA switches to the live TUI agent.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #53548. Verify it addresses your case.
- **Recommended:** read PR #53548 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-53548`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #53444: feat: Rethink MoA architecture — virtual provider is overkill, subagent delegation is simpler and more robust

## Prior & competing PRs

- **#53548: feat(moa): make /moa one-shot only; route preset switching through the model picker**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `gateway/run.py`, `hermes_cli/commands.py`, `hermes_cli/moa_config.py`, `tests/cli/test_moa_command.py`, `tests/tui_gateway/test_goal_command.py`, `tui_gateway/server.py`, `website/docs/reference/cli-commands.md`, `website/docs/user-guide/features/mixture-of-agents.md`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -8419,50 +8419,41 @@ def process_command(self, command: str) -> bool:
             elif canonical == "goal":
                 self._handle_goal_command(cmd_original)
             elif 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
    +            # MoA preset for the session, pick it from the model picker (MoA
    +            # presets surface as a virtual "Mixture of Agents" provider).
                 from hermes_cli.moa_config import (
    -                exact_moa_preset_name,
                     moa_usage,
                     normalize_moa_config,
    -                resolve_moa_preset,
                 )
     
                 parts = cmd_original.split(None, 1)
                 payload = parts[1].strip() if len(parts) > 1 else ""
    +            if not payload:
    +                _cprint(f"  {moa_usage()}")
    +                return True
                 moa_cfg = self.config.get("moa") if isinstance(self.config, dict) else {}
                 normalized = normalize_moa_config(moa_cfg)
    -            matched_preset = exact_moa_preset_name(normalized, payload) if payload else normalized["default_preset"]
    -            if matched_preset:
    -                self.requested_provider = "moa"
    -                self.provider = "moa"
    -                self.model = matched_preset
    -                self.api_key = "[redacted:secret-assignment]"
    -                self.base_url = "moa://local"
    -                self.api_mode = "chat_completions"
    -                self.agent = None
    -                _cprint(f"  Model switched to MoA preset: {matched_preset}.")
    -            else:
    -                if not payload:
    -                    _cprint(f"  {moa_usage()}")
    -                    return True
    -                preset = normalized["default_preset"]
    -                self._pending_moa_restore_model = {
    -                    "requested_provider": getattr(self, "requested_provider", None),
    -                    "provider": getattr(self, "provider", None),
    -                    "model": getattr(self, "model", None),
    -                    "api_key": [redacted:secret-assignment] "api_key", None),
    -                    "base_url": getattr(self, "base_url", None),
    -                    "api_mode": getattr(self, "api_mode", None),
    -                }
    -                self.requested_provider = "moa"
    -                self.provider = "moa"
    -                self.model = preset
    -                self.api_key = "[redacted:secret-assignment]"
    -                self.base_url = "moa://local"
    -                self.api_mode = "chat_completions"
    -                self.agent = None
    -                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.")
    +            preset = normalized["default_preset"]
    +            self._pending_moa_restore_model = {
    +                "requested_provider": getattr(self, "requested_provider", None),
    +                "provider": getattr(self, "provider", None),
    +                "model": getattr(self, "model", None),
    +                "api_key": [redacted:secret-assignment] "api_key", None),
    +                "base_url": getattr(self, "base_url", None),
    +                "api_mode": getattr(self, "api_mode", None),
    +            }
    +            self.requested_provider = "moa"
    +            self.provider = "moa"
    +            self.model = preset
    +            self.api_key = "[redacted:secret-assignment]"
    +            self.base_url = "moa://local"
    +            self.api_mode = "chat_completions"
    +            self.agent = None
    +            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 == "subgoal":
                 self._handle_subgoal_command(cmd_original)
             elif canonical == "skin":
    ```
  - `gateway/run.py`

    ```diff
    @@ -8756,33 +8756,24 @@ async def _do_undo():
                 return await self._handle_goal_command(event)
     
             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
    +            # MoA preset for the session, pick it from the model picker (MoA
    +            # presets surface as a virtual "Mixture of Agents" provider).
                 from hermes_cli.moa_config import (
    -                exact_moa_preset_name,
                     moa_usage,
                     normalize_moa_config,
    -                resolve_moa_preset,
                 )
                 from hermes_cli.config import load_config
     
                 moa_payload = event.get_command_args().strip()
    +            if not moa_payload:
    +                return moa_usage()
                 try:
                     cfg = load_config()
                     moa_cfg = normalize_moa_config(cfg.get("moa") if isinstance(cfg, dict) else {})
                 except Exception:
                     moa_cfg = normalize_moa_config({})
    -            matched_preset = exact_moa_preset_name(moa_cfg, moa_payload) if moa_payload else moa_cfg["default_preset"]
    -            if matched_preset:
    -                self._session_model_overrides[_quick_key] = {
    -                    "provider": "moa",
    -                    "model": matched_preset,
    -                    "base_url": "moa://local",
    -                    "api_key": "[redacted:secret-assignment]",
    -                    "api_mode": "chat_completions",
    -                }
    -                self._evict_cached_agent(_quick_key)
    -                return f"Model switched to MoA preset: {matched_preset}."
    -            if not moa_payload:
    -                return moa_usage()
                 preset = moa_cfg["default_preset"]
                 try:
                     event.text = moa_payload
    ```
  - `hermes_cli/commands.py`

    ```diff
    @@ -109,7 +109,7 @@ class CommandDef:
                    args_hint="<prompt>"),
         CommandDef("goal", "Set a standing goal Hermes works on across turns until achieved", "Session",
                    args_hint="[text | draft <text> | show | pause | resume | clear | status | wait <pid> | unwait]"),
    -    CommandDef("moa", "Run one prompt through configured Mixture of Agents models", "Session",
    +    CommandDef("moa", "Run one prompt through the default Mixture of Agents preset, then restore your model", "Session",
                    args_hint="<prompt>"),
         CommandDef("subgoal", "Add or manage extra criteria on the active goal", "Session",
                    args_hint="[text | remove N | clear]"),
    ```
  - `hermes_cli/moa_config.py`

    ```diff
    @@ -199,4 +199,4 @@ def build_moa_turn_prompt(user_prompt: str, config: Any = None, preset: str | No
     
     
     def moa_usage() -> str:
    -    return "Usage: /moa [preset-name | prompt]  (bare /moa toggles the default preset)"
    +    return "Usage: /moa <prompt>  (runs one prompt through the default MoA preset, then restores your model; pick a preset from the model picker to switch for the session)"
    ```
  - `tests/cli/test_moa_command.py`

    ```diff
    @@ -25,28 +25,41 @@ def _make_cli():
         cli._pending_input = queue.Queue()
         cli._pending_agent_seed = None
         cli._pending_moa_config = None
    +    cli._pending_moa_disable_after_turn = False
    +    cli._pending_moa_restore_model = None
         cli._agent_running = False
         cli.agent = None
    +    cli.provider = "openrouter"
    +    cli.requested_provider = "openrouter"
    +    cli.model = "anthropic/claude-opus-4.8"
    +    cli.api_key = "[redacted:secret-assignment]"
    +    cli.base_url = "https://openrouter.ai/api/v1"
    +    cli.api_mode = "chat_completions"
         return cli
     
     
    -def test_moa_bare_switches_to_default_preset_model():
    +def test_moa_bare_shows_usage_no_switch():
    +    # /moa with no prompt is usage-only now; switching to a preset for the
    +    # session is done via the model picker, not /moa.
         cli = _make_cli()
    +    cli._pending_moa_disable_after_turn = False
         with patch("cli._cprint"):
             assert cli.process_command("/moa") is True
    -    assert cli.provider == "moa"
    -    assert cli.requested_provider == "moa"
    -    assert cli.model == "default"
    -    assert cli.agent is None
    +    assert cli.provider != "moa"
    +    assert cli._pending_agent_seed is None
    +    assert cli._pending_moa_disable_after_turn is False
     
     
    -def test_moa_exact_preset_switches_to_named_preset_model():
    +def test_moa_arg_is_always_one_shot_prompt():
    +    # Any argument (even a string that matches a preset name) is treated as a
    +    # one-shot prompt through the DEFAULT preset, then the model is restored.
         cli = _make_cli()
         with patch("cli._cprint"):
             cli.process_command("/moa review")
    +    assert cli._pending_agent_seed == "review"
    +    assert cli._pending_moa_disable_after_turn is True
         assert cli.provider == "moa"
    -    assert cli.model == "review"
    -    assert cli.agent is None
    +    assert cli.model == "default"
     
     
     def test_moa_non_preset_is_one_shot_prompt():
    ```
  - `tests/tui_gateway/test_goal_command.py`

    ```diff
    @@ -211,7 +211,7 @@ def _write_moa_config(home, text):
         cfg_path.write_text(text)
     
     
    -def test_moa_bare_switches_to_default_preset_model(server, session, hermes_home):
    +def test_moa_bare_returns_usage(server, session, hermes_home):
         _write_moa_config(hermes_home, """
     moa:
       default_preset: default
    ```

    ```diff
    @@ -226,13 +226,14 @@ def test_moa_bare_switches_to_default_preset_model(server, session, hermes_home)
     """)
         sid, _, s = session
         r = _call(server, "command.dispatch", name="moa", arg="", session_id=sid)
    -    assert r["result"]["type"] == "exec"
    -    assert "Model switched to MoA preset: default" in r["result"]["output"]
    -    assert s["model_override"]["provider"] == "moa"
    -    assert s["model_override"]["model"] == "default"
    +    # Bare /moa is usage-only now; switching to a preset is via the model picker.
    +    assert "error" in r
    +    assert "model_override" not in s
     
     
    -def test_moa_exact_preset_switches_to_named_preset_model(server, session, hermes_home):
    +def test_moa_arg_is_always_one_shot(server, session, hermes_home):
    +    # Any arg (even a preset name) is a one-shot prompt through the DEFAULT
    +    # preset; /moa never does a sticky switch anymore.
         _write_moa_config(hermes_home, """
     moa:
       default_preset: default
    ```

    ```diff
    @@ -248,9 +249,14 @@ def test_moa_exact_preset_switches_to_named_preset_model(server, session, hermes
     """)
         sid, _, s = session
         r = _call(server, "command.dispatch", name="moa", arg="review", session_id=sid)
    -    assert r["result"]["type"] == "exec"
    +    result = r["result"]
    +    assert result["type"] == "send"
    +    assert result["message"] == "review"
    +    assert "one-shot" in result["notice"]
    +    # Lazy session (no live agent) → MoA preset pinned via model_override for
    +    # the build, and it is the DEFAULT preset, not the "review" arg.
         assert s["model_override"]["provider"] == "moa"
    -    assert s["model_override"]["model"] == "review"
    +    assert s["model_override"]["model"] == "default"
     
     
     def test_moa_non_preset_returns_one_shot_send(server, session, hermes_home):
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -8426,7 +8426,39 @@ def _stream(delta):
                 result = agent.run_conversation(run_message, **run_kwargs)
                 if "moa_one_shot_restore" in session:
                     _restore = session.pop("moa_one_shot_restore", None)
    -                if _restore is None:
    +                # Restore the model the user was on before the /moa one-shot.
    +                # The one-shot did a real in-place agent.switch_model() to MoA
    +                # (#53444), so undoing it must go back through the switch path —
    +                # resetting session["model_override"] alone would leave the live
    +                # agent's client pinned to MoA for the next turn.
    +                if isinstance(_restore, dict):
    +                    _prev_override = _restore.get("override")
    +                    _prev_model = _restore.get("model")
    +                    _prev_provider = _restore.get("provider")
    +                    if _prev_override is None:
    +                        session.pop("model_override", None)
    +                    else:
    +                        session["model_override"] = _prev_override
    +                    if _prev_model:
    +                        _raw = (
    +                            f"{_prev_model} --provider {_prev_provider}"
    +                            if _prev_provider
    +                            else _prev_model
    +                        )
    +                        try:
    +                            _apply_model_switch(
    +                                sid,
    +                                session,
    +                                _raw,
    +                                confirm_expensive_model=False,
    +                                pin_session_override=bool(_prev_override),
    +                            )
    +                        except Exception as _moa_restore_exc:
    +                            logger.warning(
    +                                "MoA one-shot model restore failed: %s",
    +                                _moa_restore_exc,
    +                            )
    +                elif _restore is None:
                         session.pop("model_override", None)
                     else:
                         session["model_override"] = _restore
    ```

    ```diff
    @@ -11205,38 +11237,54 @@ def _(rid, params: dict) -> dict:
     
             return _ok(rid, {"type": "send", "message": build_learn_prompt(arg)})
         if name == "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 MoA preset
    +        # for the rest of the session, pick it from the model picker (MoA
    +        # presets surface as a virtual "Mixture of Agents" provider).
             try:
    -            from hermes_cli.moa_config import (
    -                build_moa_turn_prompt, exact_moa_preset_name, moa_usage, normalize_moa_config
    -            )
    +            from hermes_cli.moa_config import moa_usage, normalize_moa_config
     
    +            if not arg:
    +                return _err(rid, 4004, moa_usage())
    +            if not session:
    +                return _err(rid, 4001, "no active session")
    +            sid = params.get("session_id", "")
                 moa_cfg = normalize_moa_config(_load_cfg().get("moa") or {})
    -            matched = exact_moa_preset_name(moa_cfg, arg) if arg else moa_cfg["default_preset"]
    -            if matched:
    -                if not session:
    -                    return _err(rid, 4001, "no active session")
    +            preset = moa_cfg["default_preset"]
    +            # Record the live model identity so it can be restored after the
    +            # one-shot turn, then swap the agent's client in place (#53444:
    +            # setting session["model_override"] alone never switched the
    +            # already-built agent, so the turn silently ran on the old model).
    +            agent = session.get("agent")
    +            session["moa_one_shot_restore"] = {
    +                "override": session.get("model_override"),
    +                "model": getattr(agent, "model", None) if agent else None,
    +                "provider": getattr(agent, "provider", None) if agent else None,
    +            }
    +            if agent is not None:
    +                # Live agent: swap its client in place so THIS turn runs MoA.
    +                try:
    +                    _apply_model_switch(
    +                        sid,
    +                        session,
    +                        f"{preset} --provider moa",
    +                        confirm_expensive_model=False,
    +                        pin_session_override=True,
    +                    )
    +                except Exception as exc:
    +                    session.pop("moa_one_shot_restore", None)
    +                    return _err(rid, 5030, f"moa unavailable: {exc}")
    +            else:
    +                # No agent built yet (lazy/fresh session): the override is
    +                # consumed by the first build, so the turn runs MoA without an
    +                # in-place switch.
                     session["model_override"] = {
    -                    "model": matched,
                         "provider": "moa",
    +                    "model": preset,
                         "base_url": "moa://local",
                         "api_key": "[redacted:secret-assignment]",
                         "api_mode": "chat_completions",
                     }
    -                session["moa_active_preset"] = matched
    -                return _ok(rid, {"type": "exec", "output": f"Model switched to MoA preset: {matched}."})
    -            if not arg:
    -                return _err(rid, 4004, moa_usage())
    -            if not session:
    -                return _err(rid, 4001, "no active session")
    -            preset = moa_cfg["default_preset"]
    -            session["moa_one_shot_restore"] = session.get("model_override")
    -            session["model_override"] = {
    -                "model": preset,
    -                "provider": "moa",
    -                "base_url": "moa://local",
    -                "api_key": "[redacted:secret-assignment]",
    -                "api_mode": "chat_completions",
    -            }
                 return _ok(
                     rid,
                     {
    ```
  - `website/docs/reference/cli-commands.md`

    ```diff
    @@ -39,7 +39,7 @@ hermes [global-options] <command> [subcommand/options]
     |---------|---------|
     | `hermes chat` | Interactive or one-shot chat with the agent. |
     | `hermes model` | Interactively choose the default provider and model. |
    -| `hermes moa` | Configure named Mixture of Agents presets used by `/moa`. |
    +| `hermes moa` | Configure named Mixture of Agents presets selectable from the model picker. |
     | `hermes fallback` | Manage fallback providers tried when the primary model errors. |
     | `hermes gateway` | Run or manage the messaging gateway service. |
     | `hermes proxy` | Local OpenAI-compatible proxy that attaches OAuth provider credentials. See [Subscription Proxy](../user-guide/features/subscription-proxy.md). |
    ```

    ```diff
    @@ -1122,7 +1122,7 @@ See [Curator](../user-guide/features/curator.md) for behavior and config.
     
     ## `hermes moa`
     
    -Configure named Mixture of Agents presets used by the `/moa` slash command.
    +Configure named Mixture of Agents presets. Presets appear as selectable models under a `Mixture of Agents` provider in every model picker; `/moa <prompt>` runs one prompt through the default preset.
     
     ```bash
     hermes moa list
    ```
  - `website/docs/user-guide/features/mixture-of-agents.md`

    ```diff
    @@ -31,27 +31,21 @@ Configured presets therefore show up wherever you would pick any other model.
     
     ## Slash command shortcut
     
    -`/moa` is convenience sugar over model selection:
    +`/moa` is one-shot convenience sugar. It runs a single prompt through the **default** MoA preset, then restores whatever model you were on:
     
     ```bash
    -/moa
    -```
    -
    -Switches the current session to the default MoA preset.
    -
    -```bash
    -/moa review
    +/moa design and implement a migration plan for this flaky test cluster
     ```
     
    -If `review` exactly matches a preset name, switches the current session to provider `moa`, model `review`.
    +Hermes temporarily switches to the default MoA preset for that one turn, sends the prompt, then restores your previous model afterward. The whole argument is the prompt — `/moa` no longer interprets it as a preset name.
     
     ```bash
    -/moa design and implement a migration plan for this flaky test cluster
    +/moa
     ```
     
    -If the text does not exactly match a preset name, Hermes treats it as a one-shot prompt. It temporarily switches to the default MoA preset for that turn, sends the prompt, then restores the previous model afterward.
    +Bare `/moa` (no prompt) just prints usage.
     
    -Preset matching is exact on purpose. Hermes does not fuzzy-match preset names, so normal prompts cannot accidentally become model switches.
    +To **switch** to a MoA preset for the rest of the session, select it from the model picker — MoA presets appear under a `Mixture of Agents` provider in every model-selection surface (see above). `/moa` is deliberately not a model switch, so a normal prompt can never accidentally change your model.
     
     ## How it works in the agent loop
     
    ```
- **#53502: fix: apply moa switches to live tui agent**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#53548`, `#53444`

## Fix status

- Merged to main in PR #53548 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-27`.
