ToolsNode.Stream leaks successful sibling streams when another tool fails
Bug
When ToolsNode.Stream executes multiple tools and one tool returns a stream while another returns a regular error, the method returns the error without closing the successful sibling stream. A producer using an unbuffered schema.Pipe can therefore remain blocked after ToolsNode.Stream has returned.
Verified on main at 9d983b36a5112a1c233056b1a099825298fafb8f.
Reproduction
- Create two
StreamableToolimplementations. - The first returns a reader backed by
schema.Pipe[string](0)and starts a producer blocked inSend. - The second returns a sentinel error from
StreamableRun. - Call
ToolsNode.Streamwith both tool calls. - The call returns the second tool error, but the first producer does not observe reader closure and remains blocked.
A deterministic regression test confirms that the producer still has not exited one second after the API returns.
Root cause
parallelRunToolCall waits for both endpoints and stores the successful reader in the corresponding toolCallTask. During the subsequent error scan, ToolsNode.Stream returns immediately for a non-interrupt error. The successful readers are only transferred to MergeStreamReaders on the all-success path, so this early return abandons resources that the caller cannot access or close.
This violates the stream ownership contract: once ToolsNode accepts a returned reader, every return path must either transfer ownership to its caller or close it.
Scope and intended fix
The issue is limited to ToolsNode.Stream early-error cleanup. I plan to add focused cleanup for both standard and enhanced successful streams, preserve the existing interrupt/rerun behavior, and add regression coverage for mixed success/error execution. I expect to send the focused fix within 24 hours.
Source: cloudwego/eino