Skip to content

Commit 9beab1b

Browse files
author
yjq
committed
fix(agui): Fix duplicate TextMessageEnd emission (#562)
1 parent 57c98db commit 9beab1b

File tree

2 files changed

+81
-15
lines changed

2 files changed

+81
-15
lines changed

agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,12 @@ private List<AguiEvent> convertEvent(Event event, EventConversionState state) {
150150
state.threadId, state.runId, messageId, text));
151151
} else {
152152
// End message if this is the last event
153-
events.add(
154-
new AguiEvent.TextMessageEnd(
155-
state.threadId, state.runId, messageId));
156-
state.endMessage(messageId);
153+
if (!state.hasEndedMessage(messageId)) {
154+
events.add(
155+
new AguiEvent.TextMessageEnd(
156+
state.threadId, state.runId, messageId));
157+
state.endMessage(messageId);
158+
}
157159
}
158160
}
159161
} else if (block instanceof ToolUseBlock toolUse) {

agentscope-extensions/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/adapter/AguiAgentAdapterTest.java

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@
1515
*/
1616
package io.agentscope.core.agui.adapter;
1717

18-
import static org.junit.jupiter.api.Assertions.assertEquals;
19-
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
20-
import static org.junit.jupiter.api.Assertions.assertNotNull;
21-
import static org.junit.jupiter.api.Assertions.assertTrue;
22-
import static org.mockito.ArgumentMatchers.any;
23-
import static org.mockito.ArgumentMatchers.anyList;
24-
import static org.mockito.Mockito.mock;
25-
import static org.mockito.Mockito.when;
26-
2718
import io.agentscope.core.agent.Agent;
2819
import io.agentscope.core.agent.Event;
2920
import io.agentscope.core.agent.EventType;
@@ -36,13 +27,23 @@
3627
import io.agentscope.core.message.TextBlock;
3728
import io.agentscope.core.message.ToolResultBlock;
3829
import io.agentscope.core.message.ToolUseBlock;
39-
import java.util.List;
40-
import java.util.Map;
4130
import org.junit.jupiter.api.BeforeEach;
4231
import org.junit.jupiter.api.Test;
4332
import reactor.core.publisher.Flux;
4433
import reactor.test.StepVerifier;
4534

35+
import java.util.List;
36+
import java.util.Map;
37+
38+
import static org.junit.jupiter.api.Assertions.assertEquals;
39+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
40+
import static org.junit.jupiter.api.Assertions.assertNotNull;
41+
import static org.junit.jupiter.api.Assertions.assertTrue;
42+
import static org.mockito.ArgumentMatchers.any;
43+
import static org.mockito.ArgumentMatchers.anyList;
44+
import static org.mockito.Mockito.mock;
45+
import static org.mockito.Mockito.when;
46+
4647
/**
4748
* Unit tests for AguiAgentAdapter.
4849
*/
@@ -508,4 +509,67 @@ void testReactiveStreamCompletion() {
508509
.expectNextMatches(e -> e instanceof AguiEvent.RunFinished)
509510
.verifyComplete();
510511
}
512+
513+
@Test
514+
void testTextMessageEndNotDuplicatedWhenLastEventAfterToolCall() {
515+
// Test that when a text message is interrupted by a tool call and then the last event
516+
// contains text blocks with the same message ID, only one TextMessageEnd is emitted
517+
String msgId = "msg-text";
518+
Msg firstMsg =
519+
Msg.builder()
520+
.id(msgId)
521+
.role(MsgRole.ASSISTANT)
522+
.content(
523+
List.of(
524+
TextBlock.builder().text("first part").build()))
525+
.build();
526+
527+
Msg toolCall1 =
528+
Msg.builder()
529+
.id("msg-tc")
530+
.role(MsgRole.ASSISTANT)
531+
.content(
532+
ToolUseBlock.builder()
533+
.id("tc-1")
534+
.name("tool")
535+
.input(Map.of())
536+
.build())
537+
.build();
538+
Msg lastMsg =
539+
Msg.builder()
540+
.id(msgId)
541+
.role(MsgRole.ASSISTANT)
542+
.content(
543+
List.of(
544+
TextBlock.builder().text("last part").build()))
545+
.build();
546+
547+
Event firstEvent = new Event(EventType.REASONING, firstMsg, false);
548+
Event toolCallEvent = new Event(EventType.REASONING, toolCall1, false);
549+
Event lastEvent = new Event(EventType.REASONING, lastMsg, true);
550+
when(mockAgent.stream(anyList(), any(StreamOptions.class)))
551+
.thenReturn(Flux.just(firstEvent, toolCallEvent, lastEvent));
552+
553+
RunAgentInput input =
554+
RunAgentInput.builder()
555+
.threadId("thread-1")
556+
.runId("run-1")
557+
.messages(List.of(AguiMessage.userMessage("msg-1", "Test")))
558+
.build();
559+
560+
List<AguiEvent> events = adapter.run(input).collectList().block();
561+
562+
assertNotNull(events);
563+
564+
// Should have exactly one TextMessageEnd for the same message ID
565+
long textEndCount =
566+
events.stream()
567+
.filter(e -> e instanceof AguiEvent.TextMessageEnd)
568+
.filter(e -> {
569+
AguiEvent.TextMessageEnd end = (AguiEvent.TextMessageEnd) e;
570+
return msgId.equals(end.messageId());
571+
})
572+
.count();
573+
assertEquals(1, textEndCount, "Should have exactly 1 TextMessageEnd per message ID");
574+
}
511575
}

0 commit comments

Comments
 (0)