Bug Description
When using AguiAgentAdapter with enableReasoning(true) and a DeepSeek model (e.g. deepseek-v4-pro), the REASONING_MESSAGE_END event is emitted twice for the same messageId within a single model call turn.
Steps to Reproduce
- Create a
ReActAgent with a DeepSeek model and tools
- Configure AguiAdapterConfig.builder().enableReasoning(true).build()
- Send a request that triggers a tool call (e.g. "calculate 5+3")
- Observe the AG-UI event stream
Expected Behavior
Each REASONING_MESSAGE_START should have exactly one matching REASONING_MESSAGE_END for the same messageId.
Actual Behavior
REASONING_MESSAGE_END is emitted twice for the same messageId. Example event sequence:
REASONING_MESSAGE_START (messageId=A)
REASONING_MESSAGE_CONTENT × N
TEXT_MESSAGE_START → CONTENT × N → TEXT_MESSAGE_END
REASONING_MESSAGE_END (messageId=A) ← 1st END (from ThinkingBlock isLast=true, line 204-209)
TOOL_CALL_START → ARGS × N
REASONING_MESSAGE_END (messageId=A) ← 2nd END (duplicate!)
TOOL_CALL_END
TOOL_CALL_RESULT
Root Cause Analysis
In AguiAgentAdapter.convertEvent(), there are two code paths that can emit REASONING_MESSAGE_END:
-
Line 204-209: When ThinkingBlock.isLast=true, it emits END and calls state.endReasoningMessage(messageId), which sets currentReasoningMessageId = null.
-
Line 225-231: When encountering a ToolUseBlock, it checks state.hasActiveReasoningMessage() and emits END if active.
However, the duplicate is likely caused by DeepSeek emitting an additional ThinkingBlock (possibly empty or with isLast=true) after the tool call args stream ends. This triggers the first code path again, emitting a second REASONING_MESSAGE_END for the same messageId.
The endReasoningMessage() method adds to endedReasoningMessages set and clears currentReasoningMessageId, but neither code path checks whether the message has already been ended before emitting the event.
Suggested Fix
Add idempotency check before emitting REASONING_MESSAGE_END:
Line 204-209 — check if already ended:
if (!state.hasEndedReasoningMessage(messageId)) {
events.add(new AguiEvent.ReasoningMessageEnd(state.threadId, state.runId, messageId));
state.endReasoningMessage(messageId);
}
Line 225-231 — same check (though hasActiveReasoningMessage partially covers this, adding explicit check is safer):
if (state.hasActiveReasoningMessage() && !state.hasEndedReasoningMessage(activeReasoningMessageId)) {
events.add(new AguiEvent.ReasoningMessageEnd(state.threadId, state.runId, activeReasoningMessageId));
state.endReasoningMessage(activeReasoningMessageId);
}
Environment
- agentscope-java version: latest (from main branch)
- Model:
deepseek-v4-pro via OpenAI-compatible API
- Java: 21
- Spring Boot: 3.2.5
Bug Description
When using AguiAgentAdapter with enableReasoning(true) and a DeepSeek model (e.g.
deepseek-v4-pro), theREASONING_MESSAGE_ENDevent is emitted twice for the samemessageIdwithin a single model call turn.Steps to Reproduce
ReActAgentwith a DeepSeek model and toolsExpected Behavior
Each
REASONING_MESSAGE_STARTshould have exactly one matchingREASONING_MESSAGE_ENDfor the samemessageId.Actual Behavior
REASONING_MESSAGE_ENDis emitted twice for the samemessageId. Example event sequence:Root Cause Analysis
In AguiAgentAdapter.convertEvent(), there are two code paths that can emit
REASONING_MESSAGE_END:Line 204-209: When
ThinkingBlock.isLast=true, it emits END and calls state.endReasoningMessage(messageId), which setscurrentReasoningMessageId = null.Line 225-231: When encountering a
ToolUseBlock, it checks state.hasActiveReasoningMessage() and emits END if active.However, the duplicate is likely caused by DeepSeek emitting an additional
ThinkingBlock(possibly empty or with isLast=true) after the tool call args stream ends. This triggers the first code path again, emitting a secondREASONING_MESSAGE_ENDfor the samemessageId.The endReasoningMessage() method adds to
endedReasoningMessagesset and clearscurrentReasoningMessageId, but neither code path checks whether the message has already been ended before emitting the event.Suggested Fix
Add idempotency check before emitting
REASONING_MESSAGE_END:Line 204-209 — check if already ended:
Line 225-231 — same check (though hasActiveReasoningMessage partially covers this, adding explicit check is safer):
Environment
deepseek-v4-provia OpenAI-compatible API