Skip to content

Mixture of Agents (MoA)

MoA is a Hermes virtual provider (added upstream after v0.7). A named MoA preset bundles:

  • reference_models — a list of {provider, model} slots (proposers). They run first, without tools, on a trimmed advisory view of the conversation.
  • aggregator — a single {provider, model} slot. It is the acting model: it writes the assistant response and emits tool calls, using the reference outputs as private context.

Config lives in config.yaml under moa: (hermes_cli/moa_config.py, resolve_moa_preset). Verified against NousResearch/hermes-agent@main.

How MoA surfaces to telemetry (the two-path problem)

Section titled “How MoA surfaces to telemetry (the two-path problem)”

There are two execution paths, and they behave differently:

Path Trigger What runs What fires post_api_request
Preset-as-model /model <preset> --provider moa MoAClient facade: references + aggregator via auxiliary call_llm; the aggregator response is returned as the main model response Once, with provider="moa", model="<preset>", response_model=aggregator id, usage=aggregator only
/moa <prompt> one-shot decode_moa_turn in conversation_loop.py:827 references + aggregator via auxiliary call_llm, output injected as context; then the real main model runs normally fires for the real main model, not MoA

The hard constraint (verified): reference-model and one-shot-aggregator calls go through agent/auxiliary_client.py::call_llm, which contains no hook dispatch (no invoke_hook). So:

  • Reference-model tokens are never captured — a hook-dispatch gap (call_llm invokes no hook at all), not a missing-kwarg gap. Contrast with the old subagent per-cron-job gap: that one was a missing-kwarg problem and is now solved via subagent_edges (schema v11, issue #49) — see § Subagent Architecture. This one is structural: with no hook firing, there’s no kwarg to add.
  • In the preset-as-model path, the aggregator is captured (it becomes the main response), but the hook reports provider="moa" and model="<preset>" — neither is a real, priceable identifier.

post_api_request detects provider == "moa" and resolves the preset (model kwarg = preset name) via moa.resolve_preset() → hermes_cli.moa_config.resolve_moa_preset (reads Hermes’ live config; load_config() honors HERMES_HOME, so it respects test isolation). Then:

  1. Re-attribute provider. The call is recorded under the aggregator’s real provider (e.g. openrouter), not moa. Without this the provider-aware pricing guard rejects the aggregator’s true rate and falls back to a noisy provider_assumed estimate.
  2. Re-attribute model. effective_model = response_model or <aggregator model> or model — never the bare preset name.
  3. Tag the row. llm_calls.moa_preset = "<preset>" and runs.moa_calls++ (schema v10) so /stats and the dashboard can flag that the recorded cost is a lower bound (references untracked).

If the preset can’t be resolved (no hermes_cli, unknown preset), the call falls back to the raw hook values (provider="moa") but is still recorded and still tagged with the preset — never dropped, never crashes (the resolver swallows every error).

  • Reference cost is not estimated. No token counts are available for reference calls, and the display callbacks (moa.reference/moa.aggregating) route through tool_progress_callback, not a plugin hook. A rough opt-in estimate (aggregator input tokens × each reference’s price) is a possible future phase, explicitly flagged estimated=1; deliberately deferred to keep the “real vs estimated” contract honest.
  • Budgets: the aggregator cost counts as real spend (correct). Because it is now attributed to the aggregator’s real provider, it is no longer provider_assumed, so it enforces normally instead of degrading.

hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.