Skip to content

fix: leave empty-string tool results in context instead of offloading them - #12585

Closed
TheDeveloperDoctor wants to merge 1 commit into
deepset-ai:mainfrom
TheDeveloperDoctor:fix/empty-string-tool-result-not-offloaded
Closed

fix: leave empty-string tool results in context instead of offloading them#12585
TheDeveloperDoctor wants to merge 1 commit into
deepset-ai:mainfrom
TheDeveloperDoctor:fix/empty-string-tool-result-not-offloaded

Conversation

@TheDeveloperDoctor

@TheDeveloperDoctor TheDeveloperDoctor commented Sep 3, 2026

Copy link
Copy Markdown

Related Issues

Proposed Changes:

ToolResultOffloadHook._maybe_offload documents that a tool result is left in context "when the result is empty", but the emptiness guard ran after a str result had already been wrapped into a single-element list:

content_blocks = [TextContent(text=result.result)] if isinstance(result.result, str) else list(result.result)
if not content_blocks:
    return message

ToolCallResultContentT is str | Sequence[TextContent | ImageContent | FileContent], so an empty result has two shapes — and only one of them reached the guard:

result.result content_blocks outcome
[] [] left in context ✅
"" [TextContent("")] offloaded ❌

The empty-string case was written to the store as a zero-byte entry and replaced in the conversation by a pointer ending in a dangling Preview: — no ellipsis is emitted, because len("") > preview_chars is false and the slice itself is empty:

Tool result offloaded to text (0 characters) at '<store>/0_a_1.txt'. Preview:

That entry is never read again, so the run pays a store write and hands the model a pointer to nothing.

This is reachable from a normal Agent run, not only from a hand-built message: a tool returning "" passes through _result_to_string, which returns strings as-is, into ChatMessage.from_tool, which stores the value verbatim — so result.result keeps its str shape all the way to the hook.

The fix tests the raw result before the wrapping, so both empty shapes take the same path:

if not result.result:
    return message

The if not content_blocks check is dropped as unreachable: past the new guard the result is truthy, and both arms of the union are truthy only when they yield at least one block.

No API change, and no behavior change for non-empty results.

How did you test it?

Extended the existing test_empty_result_is_not_offloaded into a parametrized test over "" and [], asserting both that the result is unchanged in state and that nothing was written to the store.

Checked against the pre-fix code so the new case is a real regression test — it reproduces the reported symptom exactly:

FAILED test/hooks/tool_result_offloading/test_hooks.py::TestToolResultOffloadHookBehavior::test_empty_result_is_not_offloaded[empty_string]
E   assert "Tool result ...t'. Preview: " == ''
E     + Tool result offloaded to text (0 characters) at '.../1_a_1.txt'. Preview:
1 failed, 1 passed

With the change, test/hooks/tool_result_offloading/ passes in full (37 passed), as do ruff format --check, ruff check, and mypy on the package.

Notes for the reviewer

One adjacent shape is deliberately left alone: a non-empty list whose blocks are all empty text ([TextContent(text="")]) still offloads, and still produces the dangling Preview: . Covering it would need all(isinstance(b, TextContent) and not b.text for b in content_blocks), which reads further from the docstring's "the result is empty" than the raw-value check does. Happy to widen it if you would rather the symptom be gone in every shape.

Checklist

What changed:
- ToolResultOffloadHook._maybe_offload now tests the raw tool result for
  emptiness before a string result is wrapped in a TextContent block, so ""
  is left in context exactly like [] already was
- Dropped the now-unreachable `if not content_blocks` guard: past the new
  check the result is truthy, and both arms of
  `str | Sequence[TextContent | ImageContent | FileContent]` are truthy only
  when they yield at least one block
- Parametrized test_empty_result_is_not_offloaded over "" and []
- Added a release note

Why:
_maybe_offload documents that a message is left as-is "when the result is
empty", but the guard ran after a str result had already been wrapped into
[TextContent("")], so only the empty-sequence shape ever reached it. An
empty-string result was therefore written to the store as a zero-byte entry
and replaced in the conversation by a pointer ending in a dangling
"Preview: " (no ellipsis is emitted because len("") is not greater than
preview_chars). The entry is never read again, so the run paid a store write
and handed the model a pointer to nothing. Checking the raw result restores
the documented behavior for both shapes an empty result can take.

Fixes deepset-ai#12583

Author: harisahmed510.00@gmail.com
@TheDeveloperDoctor
TheDeveloperDoctor requested a review from a team as a code owner September 3, 2026 11:35
@TheDeveloperDoctor
TheDeveloperDoctor requested review from anakin87 and removed request for a team September 3, 2026 11:35
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

@TheDeveloperDoctor is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Hi @TheDeveloperDoctor, thanks for your interest in contributing to Haystack! 🙏

⚠️ Issue #12583 is already being addressed by open pull request(s) #12584. Before opening a PR for an issue, please check whether a PR is already linked to it, and consider contributing to the existing PR instead. We may close duplicate PRs to keep the review queue manageable.

This is an automated message to help us keep the review queue healthy.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@TheDeveloperDoctor

Copy link
Copy Markdown
Author

Closing as a duplicate of #12584, which was opened first and carries the same patch (the test diff is byte-identical). I've left my verification notes and one adjacent-shape finding over there: #12584 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ToolResultOffloadHook offloads empty-string results but leaves empty-list results in context

2 participants