diff --git a/tests/tools/test_delegate_composite_toolsets.py b/tests/tools/test_delegate_composite_toolsets.py new file mode 100644 index 0000000000..8546023994 --- /dev/null +++ b/tests/tools/test_delegate_composite_toolsets.py @@ -0,0 +1,46 @@ +"""Tests for composite toolset expansion in delegate_task intersection.""" + +import unittest +from unittest.mock import patch + +from tools.delegate_tool import _expand_parent_toolsets + + +class TestExpandParentToolsets(unittest.TestCase): + """Verify _expand_parent_toolsets recognises individual toolsets within composites.""" + + def test_composite_hermes_cli_expands_web(self): + """hermes-cli includes web_search/web_extract → 'web' should be in expansion.""" + expanded = _expand_parent_toolsets({"hermes-cli"}) + self.assertIn("web", expanded) + self.assertIn("terminal", expanded) + self.assertIn("browser", expanded) + # Original composite is preserved + self.assertIn("hermes-cli", expanded) + + def test_individual_toolset_unchanged(self): + """When parent already uses individual toolsets, expansion keeps them.""" + expanded = _expand_parent_toolsets({"web", "terminal"}) + self.assertIn("web", expanded) + self.assertIn("terminal", expanded) + + def test_empty_parent_toolsets(self): + expanded = _expand_parent_toolsets(set()) + self.assertEqual(expanded, set()) + + def test_unknown_toolset_passthrough(self): + """Unknown toolset names pass through without error.""" + expanded = _expand_parent_toolsets({"nonexistent-toolset-xyz"}) + self.assertIn("nonexistent-toolset-xyz", expanded) + + def test_intersection_with_expanded_composite(self): + """End-to-end: requesting ['web'] from parent with ['hermes-cli'] yields ['web'].""" + parent_toolsets = {"hermes-cli"} + expanded = _expand_parent_toolsets(parent_toolsets) + toolsets = ["web"] + child_toolsets = [t for t in toolsets if t in expanded] + self.assertEqual(child_toolsets, ["web"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 7b4595cb71..5a1ec534f8 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -462,6 +462,37 @@ def _is_mcp_toolset_name(name: str) -> bool: return bool(target and str(target).startswith("mcp-")) +def _expand_parent_toolsets(parent_toolsets: set) -> set: + """Expand composite toolsets so individual toolset names are recognized. + + When a parent uses a composite toolset like ``hermes-cli`` (which bundles + all core tools), the child may request individual toolsets such as ``web`` + or ``terminal``. A simple name-based intersection would reject them + because ``"web" != "hermes-cli"``. + + This helper collects the tool names from each parent toolset, then adds + the names of any individual toolsets whose tools are a *subset* of the + parent's available tools. The original parent toolset names are preserved. + """ + parent_tool_names: set = set() + for ts_name in parent_toolsets: + ts_def = TOOLSETS.get(ts_name) + if ts_def: + parent_tool_names.update(ts_def.get("tools", [])) + + if not parent_tool_names: + return set(parent_toolsets) + + expanded = set(parent_toolsets) + for ts_name, ts_def in TOOLSETS.items(): + if ts_name in expanded: + continue + ts_tools = ts_def.get("tools", []) + if ts_tools and set(ts_tools).issubset(parent_tool_names): + expanded.add(ts_name) + return expanded + + def _preserve_parent_mcp_toolsets( child_toolsets: List[str], parent_toolsets: set[str] ) -> List[str]: @@ -907,8 +938,11 @@ def _build_child_agent( parent_toolsets = set(DEFAULT_TOOLSETS) if toolsets: - # Intersect with parent — subagent must not gain tools the parent lacks - child_toolsets = [t for t in toolsets if t in parent_toolsets] + # Intersect with parent — subagent must not gain tools the parent lacks. + # Expand composite toolsets (e.g. hermes-cli) so that individual + # toolset names (e.g. web, terminal) are recognised during intersection. + expanded_parent = _expand_parent_toolsets(parent_toolsets) + child_toolsets = [t for t in toolsets if t in expanded_parent] if _get_inherit_mcp_toolsets(): child_toolsets = _preserve_parent_mcp_toolsets( child_toolsets, parent_toolsets