Skip to content

Toolset.add() silently drops tools from lazy Toolsets before parent warm-up #12009

Description

@Success6666

Describe the bug

Toolset.add() silently drops all tools when a not-yet-warmed parent adds a Toolset subclass that materializes its tools in warm_up().

The method only warms the object being added when the parent is already warmed up. It then immediately flattens a child Toolset with list(tool). For a lazy child, that list is empty. Because only the flattened tools are retained, a later parent.warm_up() cannot recover them.

This contradicts the comment in Toolset.add() that the source is warmed before flattening, and affects custom Toolset implementations that use the documented dynamic-loading pattern.

Error message

No exception is raised. The tools disappear silently:

ADD_REPRO {'lazy_warmups': 0, 'parent_tools': []}

Expected behavior

A child Toolset should be warmed before it is flattened, regardless of whether the parent has already been warmed. Existing behavior for adding a plain Tool to a cold parent should remain unchanged: that tool should still be warmed when the parent is warmed.

Additional context

The relevant branch is in haystack/tools/toolset.py::Toolset.add():

if self._is_warmed_up and hasattr(tool, "warm_up"):
    tool.warm_up()

new_tools = [tool] if isinstance(tool, Tool) else list(tool)

A focused fix could warm every child Toolset before flattening while retaining the current parent-state condition for plain Tool objects. Regression coverage should include a cold parent, idempotent child warm-up, retained tools, and duplicate-name validation after lazy loading.

I am happy to prepare a focused PR with regression tests if maintainers confirm that this issue is open for external contributions.

To Reproduce

Run the following against current main:

from haystack.tools import Tool, Toolset


def make_tool(name: str) -> Tool:
    return Tool(
        name=name,
        description=f"{name} tool",
        parameters={"type": "object", "properties": {}},
        function=lambda: f"{name} result",
    )


class LazyToolset(Toolset):
    def __init__(self) -> None:
        super().__init__([])
        self.warmups = 0

    def warm_up(self) -> None:
        if self._is_warmed_up:
            return
        self.warmups += 1
        self.tools.append(make_tool("lazy"))
        self._is_warmed_up = True


lazy = LazyToolset()
parent = Toolset()
parent.add(lazy)

print({"lazy_warmups": lazy.warmups, "parent_tools": [tool.name for tool in parent]})

Actual output:

{'lazy_warmups': 0, 'parent_tools': []}

FAQ Check

System:

  • OS: Windows 11
  • Python: 3.13 (Hatch test environment)
  • Haystack version: main at 4c83c80fb55ef76c67c115e2fabab6732674f3cb

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions