diff --git a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/AgentEventConverterRegistry.java b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/AgentEventConverterRegistry.java index e720d6d4bf..5b2460de0a 100644 --- a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/AgentEventConverterRegistry.java +++ b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/AgentEventConverterRegistry.java @@ -69,6 +69,7 @@ public AgentEventConverterRegistry( Map, AgentEventConverter> map = new LinkedHashMap<>(); register(map, new AgentLifecycleEventConverter()); register(map, new PermissionConfirmEventConverter()); + register(map, new ExternalExecutionEventConverter()); register(map, new TextBlockEventConverter()); register(map, new ThinkingBlockEventConverter()); register(map, new ToolCallEventConverter()); diff --git a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ExternalExecutionEventConverter.java b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ExternalExecutionEventConverter.java new file mode 100644 index 0000000000..9cdecf61cb --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ExternalExecutionEventConverter.java @@ -0,0 +1,41 @@ +/* + * 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.core.agui.adapter.strategy; + +import io.agentscope.core.event.AgentEvent; +import io.agentscope.core.event.ExternalExecutionResultEvent; +import io.agentscope.core.event.RequireExternalExecutionEvent; +import java.util.Set; + +/** + * Suppresses ReActAgent external-execution handshake events in the default AG-UI stream. + * + *

AG-UI surfaces externally suspended tool calls through {@code RUN_FINISHED.outcome}, which is + * derived from the final {@code AgentResultEvent}. These typed AgentScope events are still useful + * to direct AgentEvent consumers, but emitting them would duplicate the native interrupt contract. + */ +final class ExternalExecutionEventConverter implements AgentEventConverter { + + @Override + public Set> eventTypes() { + return Set.of(RequireExternalExecutionEvent.class, ExternalExecutionResultEvent.class); + } + + @Override + public void convert(AgentEvent event, AguiStreamContext context) { + // Intentionally no-op. + } +} diff --git a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/PermissionConfirmEventConverter.java b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/PermissionConfirmEventConverter.java index ea1d5bef30..f390027903 100644 --- a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/PermissionConfirmEventConverter.java +++ b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/PermissionConfirmEventConverter.java @@ -26,6 +26,7 @@ import io.agentscope.core.agui.event.AguiEvent; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.event.RequireUserConfirmEvent; +import io.agentscope.core.event.UserConfirmResultEvent; import io.agentscope.core.message.ToolUseBlock; import io.agentscope.core.util.JsonUtils; import java.util.LinkedHashMap; @@ -52,6 +53,8 @@ * non-null. This matters because {@code ReActAgent.applyConfirmResults} fully replaces the stored * {@code ToolUseBlock}, and tool-input validation reads {@code content} directly with no fallback to * {@code input}; a null content would fail the resume with {@code argument "content" is null}. + * + *

{@link UserConfirmResultEvent} is intentionally registered here as a no-op. */ final class PermissionConfirmEventConverter implements AgentEventConverter { @@ -74,11 +77,14 @@ final class PermissionConfirmEventConverter implements AgentEventConverter { @Override public Set> eventTypes() { - return Set.of(RequireUserConfirmEvent.class); + return Set.of(RequireUserConfirmEvent.class, UserConfirmResultEvent.class); } @Override public void convert(AgentEvent event, AguiStreamContext context) { + if (event instanceof UserConfirmResultEvent) { + return; + } RequireUserConfirmEvent confirmEvent = (RequireUserConfirmEvent) event; String replyId = confirmEvent.getReplyId(); diff --git a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/adapter/AguiAgentAdapterV2Test.java b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/adapter/AguiAgentAdapterV2Test.java index 4a85966668..9435ec1edc 100644 --- a/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/adapter/AguiAgentAdapterV2Test.java +++ b/agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/adapter/AguiAgentAdapterV2Test.java @@ -45,9 +45,12 @@ import io.agentscope.core.event.AgentEvent; import io.agentscope.core.event.AgentResultEvent; import io.agentscope.core.event.AgentStartEvent; +import io.agentscope.core.event.ConfirmResult; import io.agentscope.core.event.CustomEvent; import io.agentscope.core.event.DataBlockStartEvent; +import io.agentscope.core.event.ExternalExecutionResultEvent; import io.agentscope.core.event.ModelCallEndEvent; +import io.agentscope.core.event.RequireExternalExecutionEvent; import io.agentscope.core.event.RequireUserConfirmEvent; import io.agentscope.core.event.TextBlockDeltaEvent; import io.agentscope.core.event.TextBlockEndEvent; @@ -62,6 +65,7 @@ import io.agentscope.core.event.ToolResultEndEvent; import io.agentscope.core.event.ToolResultStartEvent; import io.agentscope.core.event.ToolResultTextDeltaEvent; +import io.agentscope.core.event.UserConfirmResultEvent; import io.agentscope.core.message.AssistantMessage; import io.agentscope.core.message.ContentBlock; import io.agentscope.core.message.GenerateReason; @@ -1734,6 +1738,27 @@ void testRawEventUsesOfficialEventSourceShapeWithoutDefaultRawEvent() { assertNull(raw.rawEvent()); } + @Test + void testReActHandshakeEventsAreSuppressedInsteadOfRaw() { + ToolUseBlock toolUse = ToolUseBlock.builder().id("tool-1").name("lookup").build(); + ToolResultBlock toolResult = + ToolResultBlock.builder() + .id("tool-1") + .name("lookup") + .output(TextBlock.builder().text("done").build()) + .build(); + + List events = + runReActEvents( + new UserConfirmResultEvent( + "reply-confirm", List.of(new ConfirmResult(true, toolUse))), + new RequireExternalExecutionEvent("reply-external", List.of(toolUse)), + new ExternalExecutionResultEvent( + "reply-external", List.of(toolResult))); + + assertTrue(events.isEmpty()); + } + @Test void testCustomConverterOverridesBuiltInConverter() { AguiAdapterConfig config = @@ -1766,6 +1791,46 @@ public void convert( assertEquals("custom", content.delta()); assertNull(content.timestamp()); } + + @Test + void testCustomConverterCanOverrideSuppressedExternalExecutionEvent() { + AguiAdapterConfig config = + AguiAdapterConfig.builder() + .addEventConverter( + new AgentEventConverter() { + @Override + public Set> eventTypes() { + return Set.of(RequireExternalExecutionEvent.class); + } + + @Override + public void convert( + AgentEvent event, AguiStreamContext context) { + context.emit( + new AguiEvent.Custom( + context.getThreadId(), + context.getRunId(), + "external.required", + Map.of("handled", true))); + } + }) + .build(); + + List events = + runReActEvents( + config, + new RequireExternalExecutionEvent( + "reply-external", + List.of( + ToolUseBlock.builder() + .id("tool-1") + .name("lookup") + .build()))); + + assertEquals(List.of(AguiEventType.CUSTOM), types(events)); + AguiEvent.Custom custom = assertInstanceOf(AguiEvent.Custom.class, events.get(0)); + assertEquals("external.required", custom.name()); + } } private static List runReActEvents(AgentEvent... agentEvents) {