#1256·eino

ToolsNode.Stream leaks successful sibling streams when another tool fails

Author: CorgiBoyGCreated Sep 8, 2026Updated Sep 8, 2026

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

  1. Create two StreamableTool implementations.
  2. The first returns a reader backed by schema.Pipe[string](0) and starts a producer blocked in Send.
  3. The second returns a sentinel error from StreamableRun.
  4. Call ToolsNode.Stream with both tool calls.
  5. 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.