agentscope-ai / agentscope-ai/agentscope-java
[Bug]: ThinkingAccumulator drops earlier streamed reasoning_details during replay
- 主要語言
- Java
- 星號
- 5.6k
- 分支
- 1.3k
- 平均合併
- 4 天 12 小時
- 30 天內合併 PR
- 77
描述
## 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.
貢獻指南
評估
這個 Issue 還沒有評估資料。