fix(delegate): inherit parent fallback_chain in _build_child_agent

_build_child_agent constructed child AIAgents without passing
fallback_model, leaving _fallback_chain=[] for every subagent.
When a subagent hit a rate-limit or credential exhaustion the
runtime fallback check (run_agent.py:7486 / 12267) found an empty
chain and failed immediately — even though the parent agent was
configured with fallback_providers and would have recovered.

The cron scheduler already propagates fallback_model correctly
(scheduler.py:1038). Fix closes the parity gap by reading the
parent's _fallback_chain (the normalised list form accepted by
AIAgent's fallback_model parameter) and threading it through.

Empty chains coerce to None so AIAgent initialises _fallback_chain=[]
as usual rather than iterating an empty list.
This commit is contained in:
nftpoetrist 2026-05-03 11:35:12 +03:00 committed by Teknium
parent cb33c73418
commit 9faaa292b4
2 changed files with 54 additions and 0 deletions

View file

@ -2403,5 +2403,52 @@ class TestSubagentApprovalCallback(unittest.TestCase):
self.assertIsNone(_get_approval_callback())
class TestFallbackModelInheritance(unittest.TestCase):
"""Subagents must inherit the parent's fallback provider chain."""
def test_child_inherits_fallback_chain(self):
"""_build_child_agent passes parent._fallback_chain as fallback_model."""
parent = _make_mock_parent(depth=0)
fallback_entry = {"provider": "openrouter", "model": "gpt-4o-mini", "api_key": "sk-or-x"}
parent._fallback_chain = [fallback_entry]
with patch("run_agent.AIAgent") as MockAgent:
MockAgent.return_value = MagicMock()
_build_child_agent(
task_index=0,
goal="test fallback inheritance",
context=None,
toolsets=None,
model=None,
max_iterations=10,
parent_agent=parent,
task_count=1,
)
_, kwargs = MockAgent.call_args
self.assertEqual(kwargs["fallback_model"], [fallback_entry])
def test_child_gets_no_fallback_when_parent_chain_empty(self):
"""When parent._fallback_chain is empty, fallback_model is None."""
parent = _make_mock_parent(depth=0)
parent._fallback_chain = []
with patch("run_agent.AIAgent") as MockAgent:
MockAgent.return_value = MagicMock()
_build_child_agent(
task_index=0,
goal="test no fallback",
context=None,
toolsets=None,
model=None,
max_iterations=10,
parent_agent=parent,
task_count=1,
)
_, kwargs = MockAgent.call_args
self.assertIsNone(kwargs["fallback_model"])
if __name__ == "__main__":
unittest.main()

View file

@ -1026,6 +1026,12 @@ def _build_child_agent(
except Exception as exc:
logger.debug("Could not load delegation reasoning_effort: %s", exc)
# Inherit the parent's fallback provider chain so subagents can recover
# from rate-limits and credential exhaustion exactly like the top-level
# agent does. _fallback_chain is a list accepted by AIAgent's
# fallback_model parameter (which handles both list and dict forms).
parent_fallback = getattr(parent_agent, "_fallback_chain", None) or None
child = AIAgent(
base_url=effective_base_url,
api_key=effective_api_key,
@ -1038,6 +1044,7 @@ def _build_child_agent(
max_tokens=getattr(parent_agent, "max_tokens", None),
reasoning_config=child_reasoning,
prefill_messages=getattr(parent_agent, "prefill_messages", None),
fallback_model=parent_fallback,
enabled_toolsets=child_toolsets,
quiet_mode=True,
ephemeral_system_prompt=child_prompt,