diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 5751f3a9a..b796ed0f2 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -1129,15 +1129,32 @@ class Agent(BaseAgent): return agent_tools.tools() def get_platform_tools(self, apps: list[PlatformAppOrAction]) -> list[BaseTool]: + platform_tools: list[BaseTool] = [] try: from crewai_tools import ( CrewaiPlatformTools, ) - return CrewaiPlatformTools(apps=apps) + platform_tools = CrewaiPlatformTools(apps=apps) except Exception as e: self._logger.log("error", f"Error getting platform tools: {e!s}") - return [] + + if apps and not platform_tools: + # A non-empty `apps` that resolves to zero tools leaves the agent + # unable to do what those apps were for — surface it loudly (the + # underlying resolver only logs, and Logger.log is verbose-gated) so + # it isn't discovered as a mysterious runtime failure. + app_names = ", ".join(str(app) for app in apps) + warnings.warn( + f"Agent '{self.role}' declares apps [{app_names}] but none " + "resolved to any tools; the agent will run without them and may " + "be unable to complete its goal. Check that these apps exist and " + "are connected, and that CREWAI_PLATFORM_INTEGRATION_TOKEN is set.", + UserWarning, + stacklevel=2, + ) + + return platform_tools def get_mcp_tools(self, mcps: list[str | MCPServerConfig]) -> list[BaseTool]: """Convert MCP server references/configs to CrewAI tools. diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py index 2b126016c..8c7ba4e87 100644 --- a/lib/crewai/tests/agents/test_agent.py +++ b/lib/crewai/tests/agents/test_agent.py @@ -2595,6 +2595,59 @@ def test_agent_without_apps_no_platform_tools(): assert tools == [] +def test_get_platform_tools_warns_when_apps_resolve_to_zero_tools(monkeypatch): + """A non-empty `apps` that resolves to no tools must warn loudly.""" + import crewai_tools + + monkeypatch.setattr(crewai_tools, "CrewaiPlatformTools", lambda apps: []) + agent = Agent( + role="Empty Apps Agent", + goal="Use apps", + backstory="b", + apps=["gmail", "slack"], + ) + + with pytest.warns(UserWarning, match="resolved to any tools"): + result = agent.get_platform_tools(agent.apps) + + assert result == [] + + +def test_get_platform_tools_quiet_when_tools_resolve(monkeypatch): + """No warning when the apps resolve to at least one tool.""" + from crewai.tools import tool + + @tool + def platform_tool() -> str: + """A resolved platform tool.""" + return "ok" + + import crewai_tools + + monkeypatch.setattr(crewai_tools, "CrewaiPlatformTools", lambda apps: [platform_tool]) + agent = Agent(role="A", goal="g", backstory="b", apps=["gmail"]) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = agent.get_platform_tools(agent.apps) + + assert result == [platform_tool] + assert not any("resolved to any tools" in str(w.message) for w in caught) + + +def test_crew_prepare_tools_warns_on_empty_apps(monkeypatch): + """The warning also fires through the crew tool-injection path.""" + import crewai_tools + + monkeypatch.setattr(crewai_tools, "CrewaiPlatformTools", lambda apps: []) + agent = Agent(role="A", goal="g", backstory="b", apps=["gmail"]) + task = Task(description="d", expected_output="o", agent=agent) + crew = Crew(agents=[agent], tasks=[task]) + + with pytest.warns(UserWarning, match="resolved to any tools"): + crew._prepare_tools(agent, task, []) + + def test_agent_mcps_accepts_slug_with_specific_tool(): """Agent(mcps=["notion#get_page"]) must pass validation (_SLUG_RE).""" agent = Agent(