Implement comprehensive A2A protocol improvements

- Add tool deduplication logic in agent_tools.py
- Re-implement recursion depth limits and validation in base_agent_tools.py
- Add proper type hinting for _agent_tools attributes
- Add consistent tool retrieval method across delegation tools
- Enhance logging for debugging recursive agent invocations

Addresses all remaining PR review comments for full A2A protocol support.

Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
Devin AI
2025-05-25 04:37:28 +00:00
parent 96735c027a
commit 231dd027ba
4 changed files with 58 additions and 13 deletions

View File

@@ -1,3 +1,5 @@
from typing import List, Set
from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.tools.base_tool import BaseTool from crewai.tools.base_tool import BaseTool
from crewai.utilities import I18N from crewai.utilities import I18N
@@ -22,21 +24,33 @@ class AgentTools:
i18n=self.i18n, i18n=self.i18n,
description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore
) )
delegate_tool._agent_tools = self._get_all_agent_tools() delegate_tool._agent_tools: List[BaseTool] = self._get_all_agent_tools()
ask_tool = AskQuestionTool( ask_tool = AskQuestionTool(
agents=self.agents, agents=self.agents,
i18n=self.i18n, i18n=self.i18n,
description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore
) )
ask_tool._agent_tools = self._get_all_agent_tools() ask_tool._agent_tools: List[BaseTool] = self._get_all_agent_tools()
return [delegate_tool, ask_tool] return [delegate_tool, ask_tool]
def _get_all_agent_tools(self) -> list[BaseTool]: def _get_all_agent_tools(self) -> list[BaseTool]:
"""Get all tools from all agents for recursive invocation""" """
all_tools = [] Get all tools from all agents for recursive invocation.
Returns:
list[BaseTool]: A deduplicated list of all tools from all agents.
"""
seen_tools: Set[int] = set()
unique_tools: List[BaseTool] = []
for agent in self.agents: for agent in self.agents:
if agent.tools: if agent.tools:
all_tools.extend(agent.tools) for tool in agent.tools:
return all_tools tool_id = id(tool)
if tool_id not in seen_tools:
seen_tools.add(tool_id)
unique_tools.append(tool)
return unique_tools

View File

@@ -1,8 +1,9 @@
from typing import Optional from typing import List, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool
from crewai.tools.base_tool import BaseTool
class AskQuestionToolSchema(BaseModel): class AskQuestionToolSchema(BaseModel):
@@ -16,6 +17,7 @@ class AskQuestionTool(BaseAgentTool):
name: str = "Ask question to coworker" name: str = "Ask question to coworker"
args_schema: type[BaseModel] = AskQuestionToolSchema args_schema: type[BaseModel] = AskQuestionToolSchema
_agent_tools: Optional[List[BaseTool]] = None
def _run( def _run(
self, self,
@@ -25,5 +27,5 @@ class AskQuestionTool(BaseAgentTool):
**kwargs, **kwargs,
) -> str: ) -> str:
coworker = self._get_coworker(coworker, **kwargs) coworker = self._get_coworker(coworker, **kwargs)
tools = getattr(self, '_agent_tools', None) or kwargs.get('tools') tools = self._get_tools(**kwargs)
return self._execute(coworker, question, context, tools) return self._execute(coworker, question, context, tools)

View File

@@ -18,6 +18,7 @@ class BaseAgentTool(BaseTool):
i18n: I18N = Field( i18n: I18N = Field(
default_factory=I18N, description="Internationalization settings" default_factory=I18N, description="Internationalization settings"
) )
MAX_RECURSION_DEPTH: int = 5
def sanitize_agent_name(self, name: str) -> str: def sanitize_agent_name(self, name: str) -> str:
""" """
@@ -46,12 +47,22 @@ class BaseAgentTool(BaseTool):
coworker = coworker[1:-1].split(",")[0] coworker = coworker[1:-1].split(",")[0]
return coworker return coworker
def _get_tools(self, **kwargs) -> Optional[List[BaseTool]]:
"""
Get tools from instance or kwargs.
Returns:
Optional[List[BaseTool]]: The tools to use for recursive invocation.
"""
return getattr(self, "_agent_tools", None) or kwargs.get("tools")
def _execute( def _execute(
self, self,
agent_name: Optional[str], agent_name: Optional[str],
task: str, task: str,
context: Optional[str] = None, context: Optional[str] = None,
tools: Optional[List[Any]] = None tools: Optional[List[BaseTool]] = None,
recursion_depth: int = 0,
) -> str: ) -> str:
""" """
Execute delegation to an agent with case-insensitive and whitespace-tolerant matching. Execute delegation to an agent with case-insensitive and whitespace-tolerant matching.
@@ -61,11 +72,24 @@ class BaseAgentTool(BaseTool):
task: The specific question or task to delegate task: The specific question or task to delegate
context: Optional additional context for the task execution context: Optional additional context for the task execution
tools: Optional tools to pass to the delegated agent for recursive invocation tools: Optional tools to pass to the delegated agent for recursive invocation
recursion_depth: Current recursion depth to prevent infinite loops
Returns: Returns:
str: The execution result from the delegated agent or an error message str: The execution result from the delegated agent or an error message
if the agent cannot be found if the agent cannot be found
""" """
if tools is not None and not all(isinstance(tool, BaseTool) for tool in tools):
return self.i18n.errors("agent_tool_execution_error").format(
agent_role="unknown",
error="Invalid tools provided: all tools must inherit from BaseTool",
)
if recursion_depth >= self.MAX_RECURSION_DEPTH:
return self.i18n.errors("agent_tool_execution_error").format(
agent_role="unknown",
error=f"Maximum recursion depth ({self.MAX_RECURSION_DEPTH}) exceeded",
)
try: try:
if agent_name is None: if agent_name is None:
agent_name = "" agent_name = ""
@@ -110,6 +134,7 @@ class BaseAgentTool(BaseTool):
agent = agent[0] agent = agent[0]
try: try:
logger.debug(f"Executing task with {len(tools) if tools else 0} tools at recursion depth {recursion_depth}")
task_with_assigned_agent = Task( task_with_assigned_agent = Task(
description=task, description=task,
agent=agent, agent=agent,
@@ -118,7 +143,9 @@ class BaseAgentTool(BaseTool):
tools=tools, tools=tools,
) )
logger.debug(f"Created task for agent '{self.sanitize_agent_name(agent.role)}': {task}") logger.debug(f"Created task for agent '{self.sanitize_agent_name(agent.role)}': {task}")
return agent.execute_task(task_with_assigned_agent, context, tools) return agent.execute_task(
task_with_assigned_agent, context, tools, recursion_depth + 1
)
except Exception as e: except Exception as e:
# Handle task creation or execution errors # Handle task creation or execution errors
return self.i18n.errors("agent_tool_execution_error").format( return self.i18n.errors("agent_tool_execution_error").format(

View File

@@ -1,8 +1,9 @@
from typing import Optional from typing import List, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool
from crewai.tools.base_tool import BaseTool
class DelegateWorkToolSchema(BaseModel): class DelegateWorkToolSchema(BaseModel):
@@ -18,6 +19,7 @@ class DelegateWorkTool(BaseAgentTool):
name: str = "Delegate work to coworker" name: str = "Delegate work to coworker"
args_schema: type[BaseModel] = DelegateWorkToolSchema args_schema: type[BaseModel] = DelegateWorkToolSchema
_agent_tools: Optional[List[BaseTool]] = None
def _run( def _run(
self, self,
@@ -27,5 +29,5 @@ class DelegateWorkTool(BaseAgentTool):
**kwargs, **kwargs,
) -> str: ) -> str:
coworker = self._get_coworker(coworker, **kwargs) coworker = self._get_coworker(coworker, **kwargs)
tools = getattr(self, '_agent_tools', None) or kwargs.get('tools') tools = self._get_tools(**kwargs)
return self._execute(coworker, task, context, tools) return self._execute(coworker, task, context, tools)