deepset-ai/haystack · error · TypeError
tools must be a list of Tool and/or Toolset objects, a Tools
Error message
tools must be a list of Tool and/or Toolset objects, a Toolset, or a list of tool names (strings).
What it means
Agent._select_tools raises this TypeError when the `tools` argument passed to run()/select-tools has a shape that is neither a Toolset, a list of Tool/Toolset objects, nor a list of tool-name strings. The library validates the input against a Union type at runtime because Python cannot enforce it statically.
Source
Thrown at haystack/components/agents/agent.py:822
or if any provided tool name is not valid.
:raises TypeError: If tools is not a list of Tool objects, a Toolset, or a list of tool names (strings).
"""
# Toolsets are spawned per run (see _spawn_tools / _select_tools_by_name) so concurrent runs
# sharing the same configured Toolset don't corrupt each other's run-scoped state.
if tools is None:
return _spawn_tools(tools=self.tools)
if isinstance(tools, list) and all(isinstance(t, str) for t in tools):
return _select_tools_by_name(self.tools, cast(list[str], tools))
if isinstance(tools, (Toolset, list)):
selected = cast(ToolsType, tools) # mypy can't narrow the Union type from the isinstance checks
# Per-run tools are not covered by the Agent's own warm_up(), so warm them up here.
# warm_up() is expected to be idempotent, so re-warming on every run is cheap.
warm_up_tools(tools=selected)
return _spawn_tools(tools=selected)
raise TypeError(
"tools must be a list of Tool and/or Toolset objects, a Toolset, or a list of tool names (strings)."
)
def run(
self,
messages: list[ChatMessage],
streaming_callback: StreamingCallbackT | None = None,
*,
generation_kwargs: dict[str, Any] | None = None,
tools: ToolsType | list[str] | None = None,
hook_context: dict[str, Any] | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Process messages and execute tools until an exit condition is met.
:param messages: List of Haystack ChatMessage objects to process.
:param streaming_callback: A callback that will be invoked when a response is streamed from the LLM.View on GitHub (pinned to e318778c9b)
Solutions
- Wrap single Tool objects in a list: tools=[my_tool]
- Pass only tool-name strings OR Tool/Toolset objects, never a mix of strings and objects
- If tools come from serialized config, reconstruct Tool objects via Tool.from_dict before passing
- Check the API docs for ToolsType and conform to one of its variants
Example fix
// before agent.run(messages, tools=my_tool) // after agent.run(messages, tools=[my_tool])
Defensive patterns
Strategy: validation
Validate before calling
def valid_tools(tools):
from haystack.tools import Tool, Toolset
if isinstance(tools, Toolset):
return True
if isinstance(tools, list):
return all(isinstance(t, (Tool, Toolset)) or isinstance(t, str) for t in tools) and \
not (any(isinstance(t, str) for t in tools) and any(isinstance(t, (Tool, Toolset)) for t in tools))
return False Type guard
def is_tools_type(tools) -> bool:
from haystack.tools import Tool, Toolset
if isinstance(tools, Toolset):
return True
return isinstance(tools, list) and (
all(isinstance(t, (Tool, Toolset)) for t in tools)
or all(isinstance(t, str) for t in tools)
) Try / catch
try:
agent.run(messages, tools=tools)
except TypeError as e:
if "tools must be" in str(e):
tools = [tools] if isinstance(tools, Tool) else list(tools)
else:
raise Prevention
- Wrap single Tool objects in a list
- Never mix name strings with Tool/Toolset objects in one list
- Reconstruct Tool objects from serialized config instead of passing raw dicts
When it happens
Trigger: Passing tools as a single Tool (not in a list and not a Toolset), a list containing strings mixed with Tool objects, a dict of tools, None other than allowed sentinel, or any other non-conforming type to Agent tool selection.
Common situations: Config mistakes where tools are loaded from YAML/JSON as plain dicts, forgetting to wrap a single Tool in a list, or passing tool names mixed with Tool instances.
Understand the failure class
Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.
Related errors
- Tool execution requires at least one tool.
- Tool '{tool.name}': failed to merge outputs into state. {e}
- No tools were configured for the Agent at initialization.
- StateSchema: Key '{param}' is missing a 'type' entry.
- StateSchema: 'type' for key '{param}' must be a Python type,
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/00ca7229f4481203.
Report an issue: GitHub.