Skip to content

fix(graph): keep streamed tool-call argument fragments that are substrings of accumulated args#4772

Open
logicwu0 wants to merge 1 commit into
alibaba:mainfrom
logicwu0:fix/4066-toolcall-merge-tests
Open

fix(graph): keep streamed tool-call argument fragments that are substrings of accumulated args#4772
logicwu0 wants to merge 1 commit into
alibaba:mainfrom
logicwu0:fix/4066-toolcall-merge-tests

Conversation

@logicwu0

@logicwu0 logicwu0 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Closes #4066

Problem

When a model streams a tool call, the argument JSON arrives split across chunks and NodeExecutor.mergeToolArguments() reassembles it. It short-circuited on String.contains():

if (currentArguments.startsWith(previousArguments) || currentArguments.contains(previousArguments)) return currentArguments;
if (previousArguments.startsWith(currentArguments) || previousArguments.contains(currentArguments)) return previousArguments;

An incremental delta can legitimately be a substring of the already-accumulated arguments, and the contains() branch then drops it. Concrete case: streaming {"count": 100} digit-by-digit as {"count": / 1 / 0 / 0 / }. When merging the accumulated {"count": 10 with the final 0, previousArguments.contains("0") is true, so the fragment is dropped and the arguments become {"count": 10}.

This is the same class of failure as #4066 (toolInput cannot be null or empty / lost tool arguments): the merge already handles the name/args split (commit 5c2a0fb), but the contains() heuristic silently corrupts arguments containing repeated characters.

Fix

Only a prefix (startsWith) indicates a cumulative-snapshot chunk (some providers resend the full arguments each chunk) that should replace the accumulator. A mere substring does not. Removing the two contains() checks makes incremental fragments always append, while cumulative-snapshot providers are still handled by startsWith.

Tests

Extends StreamFunctionCallTest (which already covers the split-chunk merge for #4406):

  • testStreamingRepeatedDigitFragmentsShouldNotBeDropped — streams {"count": 100} digit-by-digit; asserts {"count": 100} (fails on pre-fix code: produced {"count": 10}).
  • testStreamingCumulativeSnapshotFragmentsShouldNotBeConcatenated — a provider resending the full arguments each chunk still collapses to a single {"city":"Beijing"} (guards the retained startsWith path).

All spring-ai-alibaba-graph-core executor tests pass; spotless:apply run.

Note on #4066

The original name/arguments split reported in #4066 is already handled by the current mergeToolCalls implementation; this PR fixes the remaining argument-corruption edge case and adds regression coverage, so #4066 can be closed.

…e substrings of accumulated args

mergeToolArguments() short-circuited on String.contains(): when an incremental
argument delta happened to be a substring of the already-accumulated arguments
(e.g. streaming {"count": 100} digit-by-digit, where the final "0" is contained
in the accumulated "...10"), the fragment was silently dropped, corrupting the
tool-call arguments.

Only a prefix (startsWith) indicates a cumulative-snapshot chunk that should
replace the accumulator; a mere substring does not. Drop the contains() checks
so incremental fragments are always appended. Cumulative-snapshot providers are
still handled via startsWith.

Adds regression tests for the repeated-digit fragment case and a cumulative-
snapshot case alongside the existing split-chunk test.

Closes alibaba#4066
@logicwu0 logicwu0 force-pushed the fix/4066-toolcall-merge-tests branch from df2af6c to 5c3ae9f Compare July 5, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/graph SAA Grpah module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 模型流式模式下工具调用(Function Calling)失败:toolInput cannot be null or empty

1 participant