Skip to content

Interrupted streaming tool call leaves invalid JSON arguments in memory, causing 400 errors on subsequent requests #1147

@flystar32

Description

@flystar32

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

  1. Streaming accumulation: ToolCallsAccumulator.ToolCallBuilder appends each chunk's argument fragment to rawContent StringBuilder during streaming.

  2. Interrupt triggers build(): When interrupted, ReActAgent.reasoning() catches InterruptedException and calls context.buildFinalMessage(), which triggers ToolCallBuilder.build().

  3. 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.

  4. Saved to memory: For user interruptions (non-SYSTEM source), the message with the broken ToolUseBlock is always saved to memory via memory.addMessage(msg).

  5. 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.

  6. 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
    }
  7. 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

  1. Primary (root cause): In ToolCallBuilder.build(), validate that rawContentStr is valid JSON before using it as content. If invalid, fall back to "{}".

  2. 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 "{}".

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

Status

In progress

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions