fix(config): recognize MoA aggregator slot fields under any preset name

`moa.presets.<name>.aggregator.{provider,model,reasoning_effort}` are read by
_clean_slot, and reasoning_effort is the documented per-slot effort knob
(config_defaults.py, auxiliary.moa_* comment). They still printed the
unknown-key notice: always for reasoning_effort, and for provider/model on
any preset other than `default` (the only one the schema walk knows), so the
same field got a different verdict depending on the preset name.

Accept those three sub-fields in is_preset_field_path. As with `default`
today, a slot left without provider or model is dropped by the normalizer and
the default aggregator applies. The `fanout` mapping keys keep the notice on
purpose: `n` only takes effect beside `mode: every_n` (either one alone
normalizes to user_turn), and `fanout every_n:N` sets the cadence in one
write. A test in test_moa_config.py pins the accepted slot fields to the keys
_clean_slot keeps.
This commit is contained in:
kshitijk4poor authored and kshitij committed 2026-10-30 15:41:47 +05:30
1 parent 0217c5f6bb
commit 2f5a5a7615
4 files changed
+41 -4

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ Four owners supply them:
:func:`tools.tool_backend_helpers.selection_config_keys`, built from ``read_selection``'s tables.
* Core-owned unseeded keys outside that table, each listed with its reader (:data:`CORE_RUNTIME_KEYS`).
* MoA preset fields under any preset name (:func:`hermes_cli.moa_config.is_preset_field_path`). Only a
preset's own fields qualify, not arbitrary descendants of ``moa.presets``.
preset's own fields and its aggregator slot fields qualify, not arbitrary descendants of ``moa.presets``.
* ACTIVE plugins: a ``config_keys`` list in ``plugin.yaml`` (or ``extensions.com.nousresearch.hermes``
in a portable ``plugin.json``) names core-section paths the plugin's code reads, e.g.
``memory.retaindb``. Plugins are found and gated by the loader's own discovery
+15 -3
View File
@@ -238,15 +238,27 @@ _FLAT_PRESET_KEYS = (
"fanout", "enabled")
# Aggregator slot fields ``_clean_slot`` reads, accepted under any preset name (the schema walk only
# knows the seeded ``default`` preset's provider/model). As there, a slot left without provider or model
# is dropped by the normalizer and the default aggregator applies. The ``fanout`` mapping keys keep the
# notice: ``n`` only takes effect beside ``mode: every_n`` (either one alone normalizes to ``user_turn``),
# and ``fanout every_n:N`` sets the cadence in one write.
_AGGREGATOR_SLOT_FIELDS = frozenset({"provider", "model", "reasoning_effort"})
def is_preset_field_path(parts: list[str]) -> bool:
"""True for a split ``moa.presets.<name>.<field>`` path whose field ``_normalize_preset`` reads.
"""True for a split ``moa.presets.<name>.<field>`` path whose field ``_normalize_preset`` reads, or
``moa.presets.<name>.aggregator.<slot field>``.
``<name>`` may be anything except blank or whitespace-padded: :func:`normalize_moa_config` strips
names, so a ``" review "`` entry would silently replace the ``review`` preset. Such a path is
not a field path and keeps the unknown-key notice.
"""
return (len(parts) == 4 and parts[:2] == ["moa", "presets"]
and bool(parts[2]) and parts[2] == parts[2].strip() and parts[3] in _FLAT_PRESET_KEYS)
if len(parts) not in (4, 5) or parts[:2] != ["moa", "presets"] or parts[3] not in _FLAT_PRESET_KEYS:
return False
if not parts[2] or parts[2] != parts[2].strip():
return False
return len(parts) == 4 or (parts[3] == "aggregator" and parts[4] in _AGGREGATOR_SLOT_FIELDS)
# When the reference fan-out runs. "user_turn" (default) runs the advisors ONCE per user turn (the original
@@ -86,6 +86,19 @@ def test_moa_preset_fields_set_and_get_without_notice(name, hermes_home, capsys)
assert (preset["fanout"], preset["reference_timeout"]) == ("every_n:5", 900)
@pytest.mark.parametrize("name", ["default", "review"])
def test_moa_aggregator_slot_fields_set_without_notice(name, hermes_home, capsys):
from hermes_cli.config import load_config
from hermes_cli.moa_config import resolve_moa_preset
slot = {"provider": "openrouter", "model": "test/actor"}
_write_config(hermes_home, {"moa": {"presets": {name: {"reference_models": [slot], "aggregator": slot}}}})
for field, value in (("provider", "anthropic"), ("model", "test/judge"), ("reasoning_effort", "high")):
assert not _set_notice(_moa_key(name, "aggregator." + field), value, capsys), field
aggregator = resolve_moa_preset(load_config()["moa"], name)["aggregator"]
assert aggregator == {"provider": "anthropic", "model": "test/judge", "reasoning_effort": "high"}
@pytest.mark.parametrize("key", [
pytest.param("moa.presets.default.fanoutt", id="field-typo"),
pytest.param("moa.presets.review.reference_timeot", id="field-typo-custom"),
@@ -95,6 +108,10 @@ def test_moa_preset_fields_set_and_get_without_notice(name, hermes_home, capsys)
# The normalizer strips names, so this entry would replace the ``review`` preset.
pytest.param("moa.presets. review .fanout", id="space-padded-name"),
pytest.param("moa.presets.review", id="whole-preset"),
pytest.param("moa.presets.review.aggregator.bogus", id="unknown-slot-field"),
pytest.param("moa.presets.review.enabled.x", id="scalar-field-subpath"),
# ``n`` only takes effect beside ``mode: every_n``; alone it normalizes to user_turn.
pytest.param("moa.presets.review.fanout.n", id="fanout-mapping-half"),
])
def test_moa_non_field_paths_still_notice(key, hermes_home, monkeypatch, capsys):
# Only the schema walk and the MoA rule decide these keys; skip the plugin scan they would fall through to.
+8
View File
@@ -154,6 +154,14 @@ def test_validate_moa_payload_agrees_with_clean_slot():
assert cfg["presets"]["p"]["aggregator"] == payload["presets"]["p"]["aggregator"]
def test_config_key_aggregator_fields_are_the_slot_fields_clean_slot_keeps():
"""``config set`` recognizes exactly the aggregator fields the normalizer keeps (no drift)."""
from hermes_cli.moa_config import _AGGREGATOR_SLOT_FIELDS, _clean_slot
slot = {"provider": "p", "model": "m", "reasoning_effort": "high", "extra": "dropped"}
assert set(_clean_slot(slot)) == _AGGREGATOR_SLOT_FIELDS
def test_print_config_marks_aggregator_as_billed_and_warns_on_provider_mismatch(capsys):
"""#112359: the aggregator is the acting model billed for the run; when it sits on a
different provider than the main model, ``hermes moa list``/``configure`` say so."""