diff --git a/hermes_cli/config.py b/hermes_cli/config.py index a818ed4203..5ddf37d082 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -538,6 +538,8 @@ DEFAULT_CONFIG = { "api_key": "", # API key for delegation.base_url (falls back to OPENAI_API_KEY) "max_iterations": 50, # per-subagent iteration cap (each subagent gets its own budget, # independent of the parent's max_iterations) + "reasoning_effort": "", # reasoning effort for subagents: "xhigh", "high", "medium", + # "low", "minimal", "none" (empty = inherit parent's level) }, # Ephemeral prefill messages file — JSON list of {role, content} dicts diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 5c64ff2868..3299b927e5 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -1210,5 +1210,73 @@ class TestDelegateHeartbeat(unittest.TestCase): f"Heartbeat should include last_activity_desc: {touch_calls}") +class TestDelegationReasoningEffort(unittest.TestCase): + """Tests for delegation.reasoning_effort config override.""" + + @patch("tools.delegate_tool._load_config") + @patch("run_agent.AIAgent") + def test_inherits_parent_reasoning_when_no_override(self, MockAgent, mock_cfg): + """With no delegation.reasoning_effort, child inherits parent's config.""" + mock_cfg.return_value = {"max_iterations": 50, "reasoning_effort": ""} + MockAgent.return_value = MagicMock() + parent = _make_mock_parent() + parent.reasoning_config = {"enabled": True, "effort": "xhigh"} + + _build_child_agent( + task_index=0, goal="test", context=None, toolsets=None, + model=None, max_iterations=50, parent_agent=parent, + ) + call_kwargs = MockAgent.call_args[1] + self.assertEqual(call_kwargs["reasoning_config"], {"enabled": True, "effort": "xhigh"}) + + @patch("tools.delegate_tool._load_config") + @patch("run_agent.AIAgent") + def test_override_reasoning_effort_from_config(self, MockAgent, mock_cfg): + """delegation.reasoning_effort overrides the parent's level.""" + mock_cfg.return_value = {"max_iterations": 50, "reasoning_effort": "low"} + MockAgent.return_value = MagicMock() + parent = _make_mock_parent() + parent.reasoning_config = {"enabled": True, "effort": "xhigh"} + + _build_child_agent( + task_index=0, goal="test", context=None, toolsets=None, + model=None, max_iterations=50, parent_agent=parent, + ) + call_kwargs = MockAgent.call_args[1] + self.assertEqual(call_kwargs["reasoning_config"], {"enabled": True, "effort": "low"}) + + @patch("tools.delegate_tool._load_config") + @patch("run_agent.AIAgent") + def test_override_reasoning_effort_none_disables(self, MockAgent, mock_cfg): + """delegation.reasoning_effort: 'none' disables thinking for subagents.""" + mock_cfg.return_value = {"max_iterations": 50, "reasoning_effort": "none"} + MockAgent.return_value = MagicMock() + parent = _make_mock_parent() + parent.reasoning_config = {"enabled": True, "effort": "high"} + + _build_child_agent( + task_index=0, goal="test", context=None, toolsets=None, + model=None, max_iterations=50, parent_agent=parent, + ) + call_kwargs = MockAgent.call_args[1] + self.assertEqual(call_kwargs["reasoning_config"], {"enabled": False}) + + @patch("tools.delegate_tool._load_config") + @patch("run_agent.AIAgent") + def test_invalid_reasoning_effort_falls_back_to_parent(self, MockAgent, mock_cfg): + """Invalid delegation.reasoning_effort falls back to parent's config.""" + mock_cfg.return_value = {"max_iterations": 50, "reasoning_effort": "banana"} + MockAgent.return_value = MagicMock() + parent = _make_mock_parent() + parent.reasoning_config = {"enabled": True, "effort": "medium"} + + _build_child_agent( + task_index=0, goal="test", context=None, toolsets=None, + model=None, max_iterations=50, parent_agent=parent, + ) + call_kwargs = MockAgent.call_args[1] + self.assertEqual(call_kwargs["reasoning_config"], {"enabled": True, "effort": "medium"}) + + if __name__ == "__main__": unittest.main() diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 7ec17264b7..f00701cd94 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -312,6 +312,25 @@ def _build_child_agent( effective_acp_command = override_acp_command or getattr(parent_agent, "acp_command", None) effective_acp_args = list(override_acp_args if override_acp_args is not None else (getattr(parent_agent, "acp_args", []) or [])) + # Resolve reasoning config: delegation override > parent inherit + parent_reasoning = getattr(parent_agent, "reasoning_config", None) + child_reasoning = parent_reasoning + try: + delegation_cfg = _load_config() + delegation_effort = str(delegation_cfg.get("reasoning_effort") or "").strip() + if delegation_effort: + from hermes_constants import parse_reasoning_effort + parsed = parse_reasoning_effort(delegation_effort) + if parsed is not None: + child_reasoning = parsed + else: + logger.warning( + "Unknown delegation.reasoning_effort '%s', inheriting parent level", + delegation_effort, + ) + except Exception as exc: + logger.debug("Could not load delegation reasoning_effort: %s", exc) + child = AIAgent( base_url=effective_base_url, api_key=effective_api_key, @@ -322,7 +341,7 @@ def _build_child_agent( acp_args=effective_acp_args, max_iterations=max_iterations, max_tokens=getattr(parent_agent, "max_tokens", None), - reasoning_config=getattr(parent_agent, "reasoning_config", None), + reasoning_config=child_reasoning, prefill_messages=getattr(parent_agent, "prefill_messages", None), enabled_toolsets=child_toolsets, quiet_mode=True,