hermes-agent/hermes_cli/moa_config.py
Teknium c6575df927
feat(moa): expose MoA presets as selectable virtual models (#46081)
* feat(moa): expose MoA presets as selectable virtual models

Reconstructed onto current main (PR #46081's base had diverged with no common
ancestor, marking the PR dirty so CI never dispatched). MoA is now a virtual
provider: each named preset is a selectable model under provider 'moa', and the
preset's aggregator is the acting model that answers and calls tools.

Reference models fan out in parallel via a bounded ThreadPoolExecutor (the same
batch pattern delegate_task uses) — all references dispatched at once, collected
when every one finishes, then handed to the aggregator. Output order is
preserved, failures and the MoA-recursion guard stay isolated per reference.

- Removed the old mixture_of_agents model tool and moa toolset.
- Added moa as a virtual provider in the provider/model inventory.
- /moa is shortcut behavior over model selection (default preset / named preset
  / one-shot prompt).
- Dashboard + Desktop manage named presets; presets appear in model pickers.
- Parallel reference fan-out in agent/moa_loop.py with regression test.

* fix(moa): thread moa_config through _run_agent to _run_agent_inner

The reconstructed gateway MoA wiring declared moa_config on _run_agent (the
profile-scoping wrapper) and used it inside _run_agent_inner, but the wrapper
never forwarded it — _run_agent_inner had no such parameter, so the runtime hit
NameError: name 'moa_config' is not defined on the compression-failure session
sync path. Add moa_config to _run_agent_inner's signature and forward it from
both wrapper call sites (multiplex and non-multiplex). Caught by
tests/gateway/test_compression_failure_session_sync.py on CI shard test(4).

* fix(moa): classify moa as a virtual provider in the catalog

The moa virtual provider has no PROVIDER_REGISTRY/ProviderProfile entry, so
provider_catalog() fell through to the default auth_type="api_key" with no
env vars — tripping two catalog invariants:
  - test_provider_catalog: api_key providers must expose a credential env var
  - test_provider_parity: every hermes-model provider must be desktop-configurable

moa already declares auth_type="virtual" in HERMES_OVERLAYS; consult that
overlay as an auth_type fallback so the catalog reports moa as virtual (no real
credential, no network endpoint). Exempt virtual providers from the desktop
parity union check the same way 'custom' is exempt — derived from the catalog,
not a hardcoded slug, so future virtual providers are covered too.
2026-06-25 13:52:06 -07:00

174 lines
5.9 KiB
Python

"""Mixture-of-Agents configuration and slash-command helpers."""
from __future__ import annotations
import base64
import json
from copy import deepcopy
from typing import Any
MOA_MARKER_PREFIX = "__HERMES_MOA_TURN_V1__"
DEFAULT_MOA_PRESET_NAME = "default"
DEFAULT_MOA_REFERENCE_MODELS: list[dict[str, str]] = [
{"provider": "openai-codex", "model": "gpt-5.5"},
{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
]
DEFAULT_MOA_AGGREGATOR: dict[str, str] = {
"provider": "openrouter",
"model": "anthropic/claude-opus-4.8",
}
def _clean_slot(slot: Any) -> dict[str, str] | None:
if not isinstance(slot, dict):
return None
provider = str(slot.get("provider") or "").strip()
model = str(slot.get("model") or "").strip()
if not provider or not model:
return None
return {"provider": provider, "model": model}
def _default_preset() -> dict[str, Any]:
return {
"reference_models": deepcopy(DEFAULT_MOA_REFERENCE_MODELS),
"aggregator": deepcopy(DEFAULT_MOA_AGGREGATOR),
"reference_temperature": 0.6,
"aggregator_temperature": 0.4,
"max_tokens": 4096,
"enabled": True,
}
def _normalize_preset(raw: Any) -> dict[str, Any]:
if not isinstance(raw, dict):
raw = {}
refs = [_clean_slot(item) for item in raw.get("reference_models") or []]
refs = [item for item in refs if item is not None]
if not refs:
refs = deepcopy(DEFAULT_MOA_REFERENCE_MODELS)
aggregator = _clean_slot(raw.get("aggregator")) or deepcopy(DEFAULT_MOA_AGGREGATOR)
return {
"enabled": bool(raw.get("enabled", True)),
"reference_models": refs,
"aggregator": aggregator,
"reference_temperature": float(raw.get("reference_temperature", 0.6) or 0.6),
"aggregator_temperature": float(raw.get("aggregator_temperature", 0.4) or 0.4),
"max_tokens": int(raw.get("max_tokens", 4096) or 4096),
}
def normalize_moa_config(raw: Any) -> dict[str, Any]:
"""Return validated MoA config with named presets.
Backward compatible with the first PR shape where ``moa`` itself contained
``reference_models`` and ``aggregator`` directly.
"""
if not isinstance(raw, dict):
raw = {}
presets_raw = raw.get("presets")
presets: dict[str, dict[str, Any]] = {}
if isinstance(presets_raw, dict):
for name, preset in presets_raw.items():
clean_name = str(name or "").strip()
if clean_name:
presets[clean_name] = _normalize_preset(preset)
# Legacy flat config becomes the default preset.
if not presets:
presets[DEFAULT_MOA_PRESET_NAME] = _normalize_preset(raw)
default_name = str(raw.get("default_preset") or "").strip()
if not default_name or default_name not in presets:
default_name = next(iter(presets), DEFAULT_MOA_PRESET_NAME)
if default_name not in presets:
presets[default_name] = _default_preset()
active_name = str(raw.get("active_preset") or "").strip()
if active_name not in presets:
active_name = ""
active = presets[default_name]
return {
"default_preset": default_name,
"active_preset": active_name,
"presets": presets,
# Compatibility/flattened view for existing dashboard/desktop callers.
"reference_models": deepcopy(active["reference_models"]),
"aggregator": deepcopy(active["aggregator"]),
"reference_temperature": active["reference_temperature"],
"aggregator_temperature": active["aggregator_temperature"],
"max_tokens": active["max_tokens"],
"enabled": active["enabled"],
}
def list_moa_presets(config: Any) -> list[str]:
cfg = normalize_moa_config(config)
return list(cfg["presets"].keys())
def resolve_moa_preset(config: Any, name: str | None = None) -> dict[str, Any]:
cfg = normalize_moa_config(config)
preset_name = str(name or cfg.get("default_preset") or DEFAULT_MOA_PRESET_NAME).strip()
preset = cfg["presets"].get(preset_name)
if preset is None:
raise KeyError(preset_name)
return deepcopy(preset)
def exact_moa_preset_name(config: Any, text: str) -> str | None:
wanted = str(text or "").strip()
if not wanted:
return None
cfg = normalize_moa_config(config)
return wanted if wanted in cfg["presets"] else None
def set_active_moa_preset(config: Any, name: str | None) -> dict[str, Any]:
cfg = normalize_moa_config(config)
clean = str(name or "").strip()
if clean and clean not in cfg["presets"]:
raise KeyError(clean)
cfg["active_preset"] = clean
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."""
payload = {
"prompt": str(prompt or ""),
"config": resolve_moa_preset(config or {}, preset),
}
encoded = base64.urlsafe_b64encode(
json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
).decode("ascii")
return f"{MOA_MARKER_PREFIX}{encoded}"
def decode_moa_turn(message: Any) -> tuple[str, dict[str, Any] | None]:
"""Decode a hidden /moa one-shot marker."""
if not isinstance(message, str) or not message.startswith(MOA_MARKER_PREFIX):
return message, None
encoded = message[len(MOA_MARKER_PREFIX):].strip()
try:
payload = json.loads(base64.urlsafe_b64decode(encoded.encode("ascii")).decode("utf-8"))
except Exception:
return message, None
prompt = str(payload.get("prompt") or "")
return prompt, _normalize_preset(payload.get("config") or {})
def build_moa_turn_prompt(user_prompt: str, config: Any = None, preset: str | None = None) -> str:
"""Build the hidden one-shot payload used by TUI/gateway routing."""
return encode_moa_turn(user_prompt, config, preset=preset)
def moa_usage() -> str:
return "Usage: /moa [preset-name | prompt] (bare /moa toggles the default preset)"