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

  1. Wrap single Tool objects in a list: tools=[my_tool]
  2. Pass only tool-name strings OR Tool/Toolset objects, never a mix of strings and objects
  3. If tools come from serialized config, reconstruct Tool objects via Tool.from_dict before passing
  4. 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

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


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/00ca7229f4481203. Report an issue: GitHub.