agentscope-ai / agentscope-ai/agentscope-java

[Bug]: ThinkingAccumulator drops earlier streamed reasoning_details during replay

Abierto
#3,127 1 comentario 0 reacciones 0 asignados Ver en GitHub
area/core/agent bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.