Problem
When a streaming tool call response is interrupted (user manually interrupts the agent), the partially accumulated tool call arguments (an incomplete JSON string) are saved into memory. On the next request to the model, this invalid JSON is sent as the arguments field of the tool call in the conversation history, causing DashScope (百炼) API to reject the request with a 400 error.
Root Cause Chain
-
Streaming accumulation: ToolCallsAccumulator.ToolCallBuilder appends each chunk's argument fragment to rawContent StringBuilder during streaming.
-
Interrupt triggers build(): When interrupted, ReActAgent.reasoning() catches InterruptedException and calls context.buildFinalMessage(), which triggers ToolCallBuilder.build().
-
build() preserves invalid JSON: In build(), when JSON parsing of rawContent fails (as expected for incomplete JSON), input stays empty but content is still set to the raw broken string:
.content(rawContentStr.isEmpty() ? "{}" : rawContentStr)
For example, content might be {"query": "hello wor — a clearly invalid JSON fragment.
-
Saved to memory: For user interruptions (non-SYSTEM source), the message with the broken ToolUseBlock is always saved to memory via memory.addMessage(msg).
-
PendingToolRecoveryHook doesn't fix arguments: The recovery hook correctly generates an error ToolResultBlock to close the tool call cycle, but does NOT repair the broken arguments in the original ToolUseBlock.
-
Converter sends invalid JSON as-is: Both DashScopeToolsHelper.convertToolCalls() and OpenAIMessageConverter prioritize the content field over input:
if (toolUse.getContent() != null && !toolUse.getContent().isEmpty()) {
argsJson = toolUse.getContent(); // broken JSON sent directly
}
-
API rejects: DashScope API validates that tool call arguments must be valid JSON → 400 error.
Affected Models
Any model accessed via DashScope API (qwen3.5, qwen3.6, etc.) that validates historical tool call arguments.
Proposed Fix
-
Primary (root cause): In ToolCallBuilder.build(), validate that rawContentStr is valid JSON before using it as content. If invalid, fall back to "{}".
-
Secondary (defense in depth): In DashScopeToolsHelper.convertToolCalls() and OpenAIMessageConverter, validate content is valid JSON before using it as arguments. If invalid, fall back to serializing input or "{}".
Problem
When a streaming tool call response is interrupted (user manually interrupts the agent), the partially accumulated tool call arguments (an incomplete JSON string) are saved into memory. On the next request to the model, this invalid JSON is sent as the
argumentsfield of the tool call in the conversation history, causing DashScope (百炼) API to reject the request with a 400 error.Root Cause Chain
Streaming accumulation:
ToolCallsAccumulator.ToolCallBuilderappends each chunk's argument fragment torawContentStringBuilder during streaming.Interrupt triggers
build(): When interrupted,ReActAgent.reasoning()catchesInterruptedExceptionand callscontext.buildFinalMessage(), which triggersToolCallBuilder.build().build()preserves invalid JSON: Inbuild(), when JSON parsing ofrawContentfails (as expected for incomplete JSON),inputstays empty butcontentis still set to the raw broken string:For example,
contentmight be{"query": "hello wor— a clearly invalid JSON fragment.Saved to memory: For user interruptions (non-SYSTEM source), the message with the broken
ToolUseBlockis always saved to memory viamemory.addMessage(msg).PendingToolRecoveryHookdoesn't fix arguments: The recovery hook correctly generates an errorToolResultBlockto close the tool call cycle, but does NOT repair the brokenargumentsin the originalToolUseBlock.Converter sends invalid JSON as-is: Both
DashScopeToolsHelper.convertToolCalls()andOpenAIMessageConverterprioritize thecontentfield overinput:API rejects: DashScope API validates that tool call
argumentsmust be valid JSON → 400 error.Affected Models
Any model accessed via DashScope API (qwen3.5, qwen3.6, etc.) that validates historical tool call arguments.
Proposed Fix
Primary (root cause): In
ToolCallBuilder.build(), validate thatrawContentStris valid JSON before using it ascontent. If invalid, fall back to"{}".Secondary (defense in depth): In
DashScopeToolsHelper.convertToolCalls()andOpenAIMessageConverter, validatecontentis valid JSON before using it asarguments. If invalid, fall back to serializinginputor"{}".