fix(delegation): honor delegation.fallback_providers for child agents (#65038) - #65052
fix(delegation): honor delegation.fallback_providers for child agents (#65038)#65052ayushnangia wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused fallback-routing fix. The premise holds on current main: _build_child_agent() loads delegation config at tools/delegate_tool.py:1100, but unconditionally derives the child fallback from parent_agent._fallback_chain at tools/delegate_tool.py:1280 and passes it at line 1332. Reusing hermes_cli/fallback_config.py:51-72 is the correct normalization path, and agent/agent_init.py:1166-1178 confirms that an explicit empty list disables the child chain.
Problems
- The new public setting needs user-documentation updates.
website/docs/user-guide/features/fallback-providers.md:384describes inherited child fallback, while line 431 still says delegation has no automatic fallback;website/docs/user-guide/configuration.md:1990-2011omits the new key and its precedence semantics. - The added tests mock both
_load_configandAIAgent, so they do not cover the real profile-aware config/default merge used on this path.
Suggested changes
- Document absent/null inheritance, configured-chain override,
[]disablement, and invalid-only fallback behavior in the two user guides. - Add one temporary-
HERMES_HOMEconfig-load regression alongside the focused constructor tests.
This is an automated hermes-sweeper review.
| @@ -2211,6 +2211,9 @@ def _ensure_hermes_home_managed(home: Path): | |||
| # "codex_responses", or "anthropic_messages". Empty = auto-detect | |||
| # from URL (e.g. /anthropic suffix → anthropic_messages). Set this | |||
| # explicitly for non-standard endpoints the heuristic can't detect. | |||
| "fallback_providers": None, # fallback chain for delegated children, same entry | |||
There was a problem hiding this comment.
This introduces a user-facing config key, but the user guides still omit it and the fallback routing table currently says delegation has no automatic fallback. Please update website/docs/user-guide/features/fallback-providers.md and website/docs/user-guide/configuration.md with the override, inherit, and [] semantics.
There was a problem hiding this comment.
Done in aecc4c3 — added a "Delegation Fallback Chain" section to fallback-providers.md (semantics table: unset/null = inherit, list = own chain, [] = disabled, invalid-entries = warn + inherit), corrected the "Where Fallback Works" row and the routing table's "no automatic fallback" line, and documented the key in configuration.md's delegation block (yaml example + prose, cross-linked).
|
|
||
| def _child_fallback(self, delegation_cfg, parent_chain): | ||
| parent = _make_mock_parent(depth=0) | ||
| parent._fallback_chain = parent_chain |
There was a problem hiding this comment.
These cases patch both _load_config and AIAgent, so they do not exercise the profile-aware config/default merge that _build_child_agent() uses. Please add one temporary-HERMES_HOME regression that loads delegation.fallback_providers through the real config loader.
There was a problem hiding this comment.
Done in aecc4c3 — test_fallback_providers_flow_through_real_config_loader writes delegation.fallback_providers into the hermetic $HERMES_HOME/config.yaml and drives _build_child_agent() with only run_agent.AIAgent mocked, so _load_config() \u2192 load_config_readonly() runs the real DEFAULT_CONFIG deep-merge (where the shipped default is None) against the user file. 162/162 in the file under scripts/run_tests.sh.
|
Reporter confirmation — still reproduces on current I re-verified against upstream
Repo-wide check — it is not read anywhere else either: grep -rn "delegation.*fallback_providers\|fallback_providers.*delegation" --include=*.py . | grep -v ./tests/
# (no hits)Meanwhile Minimal E2E repro against a temp # delegation.fallback_providers: [{provider: anthropic, model: CHILD-FALLBACK-MODEL}]
# fallback_providers: [{provider: openai-codex, model: PARENT-FALLBACK-MODEL}]
# EXPECT child chain <- CHILD-FALLBACK-MODEL
# ACTUAL child chain <- parent_agent._fallback_chain (delegation key never read)Not asking for special treatment on my own issue — just recording that the premise has not gone stale, since the sweeper marked this |
aecc4c3 to
1807463
Compare
|
Thanks for the re-verification @mlahatte. Rebased onto current Validation on the rebased head (
One clarification on the schema point: And yes — I'd gladly take you up on validating a merged build against your split-credential setup (delegation on |
|
@mlahatte taking you up on your offer, ahead of the merge: the rebased branch is directly testable against your split-credential setup (delegation seat on git fetch https://github.com/ayushnangia/hermes-agent fix/delegation-fallback-providers && git checkout FETCH_HEAD
# your config: delegation.fallback_providers with the worker chainExpected: a delegated child's failure walks your configured chain instead of the parent's metered one; |
1807463 to
3b916dc
Compare
SummaryOne PR addresses #65038. #65052 changes delegated-child fallback resolution so a configured Related pull requests
Suggested consolidationKeep #65052 open with a salvage path: retain the focused child-fallback resolution, public documentation, and regression coverage while the PR remains the sole implementation addressing #65038. This follows the contributor keep_open review, whose requested documentation and real-loader test are addressed by the visible diff; there are no competing PRs to close as duplicates. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I65038(["issue #65038 (open)"])
P65052["PR #65052 (open)"]
P65052 -->|best fix| I65038
class I65038 open
class P65052 open
class P65052 best
class P65052 target
click I65038 "https://github.com/NousResearch/hermes-agent/issues/65038"
click P65052 "https://github.com/NousResearch/hermes-agent/pull/65052"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 16 kB of PR diffs, 9 kB of issue/PR text, 6 kB of discussion (5 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
|
Reporter-verified run, as offered above. Environment. Isolated clone of this branch (head 1. Existing suites — 159/159 passed2. Split-credential E2E through the real config loaderWrote a
3/3. That first row is exactly the wrong-provider / wrong-spend path from #65038, and this diff closes it. 3. Operator note on
|
|
Thank you — this is a more rigorous verification than I could have asked for: the real-config-loader E2E on the exact split-credential shape from #65038, plus the production-operator comparison. Your malformed-entries finding is worth highlighting for whoever merges: your local patch's For the record the thread now carries: premise confirmed by the sweeper review, reporter re-verification on |
FILE-LIST / credit#80421 reimplements this design against current Your approach (honor |
|
Cross-link for reviewers navigating the delegation-fallback family, since triage just closed #80438 into it: the precedence is #65052 (config plumbing — |
Summary
Honor a
delegation.fallback_providerschain for delegated children, so a worker explicitly routed viadelegation.*no longer silently escalates onto fallback models intended only for the head agent (#65038).Changes
tools/delegate_tool.py:_build_child_agent()— resolve the child chain asdelegation.fallback_providers> parent inheritance. Key absent/null → inherit the parent's resolved_fallback_chainexactly as before (Feature request: per-delegation fallback provider chain #7481 behavior preserved);[]→ explicitly disable child fallback; malformed value →logger.warning+ inherit (mirrors thedelegation.reasoning_effortfallback pattern a few lines up). Normalization reuseshermes_cli.fallback_config.get_fallback_chain(), the same chokepoint the top-level chain goes through — no parallel parserhermes_cli/config.py:delegation.fallback_providers: Noneadded toDEFAULT_CONFIG(null sentinel = inherit, so the deep-merged default cannot flip existing users off inheritance; no_config_versionbump needed for an added key)cli-config.yaml.example: commented example in thedelegation:blocktests/tools/test_delegate.py:TestDelegationFallbackChain— 5 tests: override beats inheritance, absent key inherits, null (the shipped default) inherits,[]disables, malformed inherits. The override and[]tests fail on unfixed codetests/tools/test_async_delegation.py: follow-up — deterministic drain of the two blocker completions intest_dispatch_rejected_at_capacity. This is a latent race, not fallout of this change: the blockers enqueue their completion events asynchronously afterev.set(), and any event landing after the fixture's teardown drain leaks into the next test's_drain_one(), which then reads a stalecompletedevent instead of its own error event. Fails 8/8 consecutive bare-pytest runs on clean main; masked underscripts/run_tests.shby hermetic-env timing until any config-load change (like theDEFAULT_CONFIGkey above) re-times itRoot cause
_build_child_agent()reads the parent's resolved chain and passes it unconditionally:The
delegationconfig section is consulted on this path for provider, model, base_url, api_mode, and reasoning_effort — but not for a fallback chain;delegation.fallback_providerswas never a key (git log -Sshows the inheritance landed for #7481's "workers should inherit the parent chain", closedimplemented_on_main). This issue is the complementary control: with a worker pinned to a distinct provider/model, a failure walks the head agent's chain — wrong models, wrong provider, wrong spend, silently.The same leak was already recognized and fixed for OpenRouter provider filters in this exact function ("parent-level filters would silently force the child back onto the parent's provider" — cleared when
delegation.provideris set). This change applies the identical reasoning to the fallback chain, but opt-in via config rather than implicit, since unconditional inheritance is deliberate, relied-upon behavior (#7481, and #49477's pinned-base_url coverage asserts it — the key-unset path here keeps those semantics byte-identical)._build_child_agent()is the singleAIAgent()construction site for delegation (sync and async paths both flow through it), so the chain resolution covers all delegated children.Validation
delegation.fallback_providersnull(shipped default)[{provider: d, model: worker-fb}][]fallback_modelwas[{provider-b, head-fallback-model}]with the worker chain configured; now receives the configured chainscripts/run_tests.sh tests/tools/test_delegate.py tests/tools/test_async_delegation.py tests/hermes_cli/test_config.py— 3 files, 339 passed, 0 failedscripts/run_tests.sh tests/hermes_cli/test_fallback_cmd.py— 32 passedtests/tools/test_async_delegation.py— 3× consecutive bare-pytest runs green (was 8/8 red on main)Fixes #65038. Refs #7481, #49477, #65035 (sibling defect in the same file: the
base_urlcredential path dropsrequest_overrides— separate fix).