Description
Problem Statement
Currently, the Agent's process_tools
method expects a flat list of tool objects but doesn't handle nested structures gracefully. When initialized with tools from multiple sources (built-in tools, MCP-fetched tools, custom tools), developers need to manually flatten these collections:
built_in_tools = [calculator, memory] # a list
mcp_tools = mcp_client.list_tools_sync() # another list
custom_tool = "custom_tool.py" # an individual item
When passing these collections directly:
agent = Agent(tools=[built_in_tools, mcp_tools, custom_tool])
The Agent treats each list itself as a tool object rather than processing the items within the lists, leading to errors like:
tool=<[<function calculator at 0x7f7265a29800>, ...]> | unrecognized tool specification
This forces developers to manually flatten collections using one of these verbose and error-prone patterns, all while remembering which of the items is a list of tools, and which is an individual tool:
# List concatenation
tools = built_in_tools + mcp_tools + [custom_tool]
# Unpacking
tools = [*built_in_tools, *mcp_tools, custom_tool]
# Manual list manipulation
all_tools = []
all_tools.extend(built_in_tools)
all_tools.extend(mcp_tools)
all_tools.append(custom_tool)
tools = all_tools
Proposed Solution
Enhance the process_tools
method in the ToolRegistry
class to automatically flatten nested tool collections. This would allow developers to simply pass in tools however they have them without worrying about the structure.
Implementation Details
Add a recursive flattening function to the ToolRegistry
class:
def _flatten_tools(self, tools: Any) -> List[Any]:
"""Recursively flatten tool collections into a single list.
Args:
tools: A single tool, a list of tools, or nested lists of tools.
Returns:
A flattened list of tools.
"""
# Handle None or empty case
if tools is None:
return []
# Handle single tool case (non-iterable)
if not isinstance(tools, (list, tuple)):
# Check against both list and tuple:
# 1. For defensive programming - handles both sequence types
# 2. To support unpacking syntax which can produce tuples
# 3. To properly process cases where tools might be provided as a tuple
return [tools]
# Recursively handle nested lists/tuples
flattened = []
for item in tools:
if isinstance(item, (list, tuple)):
flattened.extend(self._flatten_tools(item))
else:
flattened.append(item)
return flattened
Then, modify the process_tools
method to use this flattening function:
def process_tools(self, tools: List[Any]) -> List[str]:
"""Process tools list that can contain tool names, paths, imported modules, or functions.
Args:
tools: List of tool specifications.
Can be:
- ...
- Lists of any of the above (NEW)
Returns:
List of tool names that were processed.
"""
# NEW: Flatten nested lists of tools first
flattened_tools = self._flatten_tools(tools)
tool_names = []
for tool in flattened_tools:
# Rest of the existing process_tools method remains unchanged...
# ...
Benefits
- Reduces cognitive load: Developers no longer need to know implementation details about tool registration
- Prevents errors: Eliminates a common source of bugs from improperly flattened lists
- Consistency: Encourages consistent patterns across the codebase
- Backward compatible: All existing code that uses flat lists will continue to work
- Intuitive API: Follows the principle of "be liberal in what you accept" common in well-designed libraries
Impact
This change has minimal performance impact since tool registration typically happens once during initialization. It maintains backward compatibility while making the API more intuitive and resilient.
Use Case
This feature directly addresses three scenarios developers may face:
1: Combining multiple tool sources - When developers need to combine built-in tools, MCP service tools, and custom tools in a single agent. Currently, they must remember which variables are lists and which are individual tools:
agent = Agent(
tools=built_in_tools + mcp_tools + [custom_tool] # Error-prone manual flattening
)
2: Dynamic tool loading - When tools are loaded dynamically from different sources based on configuration, the developer often ends up with a mix of lists and individual tools:
tools = []
tools.extend(get_built_in_tools())
tools.extend(mcp_client.list_tools_sync())
tools.append(config.custom_tool_path)
agent = Agent(tools=tools)
3: Simplified API usage - For new developers learning the framework, the current behavior is counterintuitive and requires detailed knowledge of internal implementation. With this improvement, all these work identically:
# All these would work correctly and identically:
agent = Agent(
tools=[built_in_tools, mcp_tools, custom_tool]
)
agent = Agent(
tools=built_in_tools + mcp_tools + [custom_tool]
)
agent = Agent(
tools=[*built_in_tools, *mcp_tools, custom_tool]
)
Alternatives Solutions
No response
Additional Context
No response