agentscope-ai / agentscope-ai/agentscope-java

[Bug]: ThinkingAccumulator drops earlier streamed reasoning_details during replay

Open
#3,127 1 comment 0 reactions 0 assignees View on GitHub
area/core/agent bug
Dominant language
Java
Stars
5.6k
Forks
1.3k
Avg merge
4d 12h
Merged PRs (30d)
77

Description

## Describe the bug

On current main, streamed OpenAI-compatible `reasoning_details` survive parsing individually but earlier chunks are lost when `ReasoningContext` assembles the final message. `ThinkingAccumulator.add()` merges metadata with `putAll`; every chunk uses the same `reasoningDetails` key, so the last list replaces all previous lists.

This is distinct from #2913/#2914 (thinking/tool metadata overwriting inside the converter) and #2909/#2910 (typed metadata after JSON persistence). This reproduction uses the latest main after those fixes, without tool calls or persistence.

## To Reproduce

Add the following test to `agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/StreamingReasoningDetailsReplayTest.java`:

```java
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.extensions.model.openai.formatter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import io.agentscope.core.agent.accumulator.ReasoningContext;
import io.agentscope.core.util.JsonUtils;
import io.agentscope.extensions.model.openai.dto.OpenAIMessage;
import io.agentscope.extensions.model.openai.dto.OpenAIReasoningDetail;
import io.agentscope.extensions.model.openai.dto.OpenAIResponse;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Test;

class StreamingReasoningDetailsReplayTest {

@Test
void preservesDistinctReasoningBlocksAcrossStreamingChunks() {
OpenAIResponseParser parser = new OpenAIResponseParser();
ReasoningContext context = new ReasoningContext("assistant");
for (int index = 0; index < 2; index++) {
String json = """
{"id":"reply-1","object":"chat.completion.chunk","choices":[
{"index":0,"delta":{"reasoning_details":[
{"type":"reasoning.encrypted","id":"rs_%d","index":%d,
"format":"anthropic-claude-v1","data":"opaque_%d"}
]}}
]}
""".formatted(index, index, index);
OpenAIResponse response =
JsonUtils.getJsonCodec().fromJson(json, OpenAIResponse.class);
context.processChunk(parser.parseResponse(response, Instant.now()));
}

OpenAIMessage replay =
new OpenAIMessageConverter(m -> "", b -> "")
.convertToMessage(context.buildFinalMessage(), false);
assertNotNull(replay.getReasoningDetails());
assertEquals(
List.of("rs_0", "rs_1"),
replay.getReasoningDetails().stream().map(OpenAIReasoningDetail::getId).toList());
assertEquals(
List.of("opaque_0", "opaque_1"),
replay.getReasoningDetails().stream().map(OpenAIReasoningDetail::getData).toList());
}
}

```

From the repository root:

```text
mvn -B -pl agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai -am -Dtest=StreamingReasoningDetailsReplayTest -Dsurefire.failIfNoSpecifiedTests=false -Dspotless.skip=true test
```

This runs the actual JSON DTO parser -> OpenAIResponseParser -> ReasoningContext -> OpenAIMessageConverter pipeline, using synthetic local chunks and no model API key.

## Expected behavior

Both distinct reasoning records, in received order, are present in the follow-up message: `[rs_0, rs_1]`, with their opaque payloads unchanged.

## Error messages

```text
expected: <[rs_0, rs_1]> but was: <[rs_1]>
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
```

## Environment

- AgentScope Java: `2.0.3-SNAPSHOT`, main `7399ed61b2ea22b25e2ead4dec9d59df6a77785c`
- Java: Temurin 17.0.15
- Maven: 3.9.12
- OS: Windows 11

## Additional context

The observed impact is loss of earlier reasoning metadata in the replay payload. No live provider rejection is claimed. OpenRouter documents that the full reasoning-details sequence must be preserved for replay: https://openrouter.ai/docs/guides/best-practices/reasoning-tokens#preserving-reasoning

I also checked #2335, #2418 and #2233, which touch the same accumulator for native Gemini/Anthropic signatures. Their current patches do not accumulate the OpenAI `reasoningDetails` lists. The proposed focused fix is to accumulate this list-valued metadata key in stream order, while preserving existing last-value behavior for other metadata and clearing the list on reset. The test should also cover snapshots and reset to prevent metadata leaking between rounds.

I intend to submit a small linked fix with regression tests. AI assistance was used for source review and reproduction; the failure above was executed locally against the stated commit.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.