deepset-ai / deepset-ai/haystack

Agent tool scheduler picks write-write winner by batch order, not call order, contradicting its own docstring and #11690

Open
#12,621 3 comments 0 reactions 1 assignee View on GitHub

@anakin87 is already working on this.

Since Sep 6, 2026.

Dominant language
Python
Stars
26.6k
Forks
3.2k
Avg merge
1d 3h
Merged PRs (30d)
194

Description

Describe the bug

_schedule_tool_calls (haystack/components/agents/tool_calling.py) groups a step's tool calls into batches by read-after-write dependency on State keys, and each batch's outputs get merged into State right after that batch finishes. The function's own docstring says a plain write-write overlap with no shared reads keeps outputs merged in call order afterward, and #11690 says the LLM's call order stops mattering for exactly that case. Neither holds once one of the two writers gets pushed into a later batch by an unrelated dependency: the write-write winner ends up decided by which batch a call lands in, not by which call the LLM listed first.

To Reproduce

Three tools: tool_a (call 0) reads dep and writes shared, tool_b (call 1) has no state reads and also writes shared, tool_c (call 2) writes dep. _schedule_tool_calls batches this as [[1, 2], [0]], since tool_a has to wait on tool_c for dep. tool_b's write to shared merges in the first batch, then tool_a's overwrites it in the second, so shared ends up as tool_a's value even though tool_b was requested after it and neither call reads the other's output.

from haystack.components.agents.state.state import State
from haystack.components.agents.tool_calling import _run_tool
from haystack.dataclasses import ChatMessage, ToolCall
from haystack.tools import Tool

def tool_a_fn(dep): return {"out": "A-value"}
def tool_b_fn(): return {"out": "B-value"}
def tool_c_fn(): return {"out": "dep-value"}

tool_a = Tool(name="tool_a", description="reads dep, writes shared",
              parameters={"type": "object", "properties": {"dep": {"type": "string"}}, "required": ["dep"]},
              function=tool_a_fn, inputs_from_state={"dep": "dep"}, outputs_to_state={"shared": {"source": "out"}})
tool_b = Tool(name="tool_b", description="writes shared, no reads",
              parameters={"type": "object", "properties": {}},
              function=tool_b_fn, outputs_to_state={"shared": {"source": "out"}})
tool_c = Tool(name="tool_c", description="writes dep",
              parameters={"type": "object", "properties": {}},
              function=tool_c_fn, outputs_to_state={"dep": {"source": "out"}})

message = ChatMessage.from_assistant(tool_calls=[
    ToolCall(tool_name="tool_a", arguments={}),
    ToolCall(tool_name="tool_b", arguments={}),
    ToolCall(tool_name="tool_c", arguments={}),
])
state = State(schema={"dep": {"type": str}, "shared": {"type": str}})
_run_tool(messages=[message], state=state, tools=[tool_a, tool_b, tool_c], raise_on_failure=True)
print(state.get("shared"))  # "A-value", should be "B-value"

Expected behavior

state["shared"] should come out "B-value", since tool_b was requested after tool_a and neither call reads the other's output.

System

  • Haystack: main, 82da3adc2
  • Deterministic, no LLM or network calls involved

I can't open a PR for this myself right now, a CLA question on my end is still unresolved. Happy to send one once that clears if it's still useful by then. The fix probably means replacing the per-batch merge with a single call-order pass once every batch has resolved, but that changes what a later-batch reader sees from an earlier-indexed writer landing in the same step, so it reads more like a design call than a one-liner.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.