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_llminvokes 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 viasubagent_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"andmodel="<preset>"— neither is a real, priceable identifier.
What the plugin does (moa.py, schema v10)
Section titled “What the plugin does (moa.py, schema v10)”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:
- Re-attribute provider. The call is recorded under the aggregator’s
real provider (e.g.
openrouter), notmoa. Without this the provider-aware pricing guard rejects the aggregator’s true rate and falls back to a noisyprovider_assumedestimate. - Re-attribute model.
effective_model = response_model or <aggregator model> or model— never the bare preset name. - Tag the row.
llm_calls.moa_preset = "<preset>"andruns.moa_calls++(schema v10) so/statsand 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).
What is NOT done (and why)
Section titled “What is NOT done (and why)”- Reference cost is not estimated. No token counts are available for
reference calls, and the display callbacks (
moa.reference/moa.aggregating) route throughtool_progress_callback, not a plugin hook. A rough opt-in estimate (aggregator input tokens × each reference’s price) is a possible future phase, explicitly flaggedestimated=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.