Summary
The AG-UI adapter (AguiAgentAdapter) consumes the deprecated v1 Flux<io.agentscope.core.agent.Event> stream via agent.stream(...). Because the v1 Event/EventType vocabulary only contains REASONING / SUMMARY / TOOL_RESULT, the fine-grained v2 events — in particular RequireUserConfirmEvent and RequestStopEvent — structurally can never reach the adapter.
As a result, human-in-the-loop (HITL) tool confirmation is completely dropped over AG-UI:
- When a tool call enters
ToolCallState.ASKING (e.g. any non-read-only tool under PermissionMode.DEFAULT, which is the default), ReActAgent emits RequireUserConfirmEvent + RequestStopEvent and pauses.
- Neither event is converted to any AG-UI event, so the client sees
TOOL_CALL_START/ARGS/END and then a bare RUN_FINISHED (or a RUN_ERROR on a subsequent run whose paused state was persisted) — the tool call silently never executes.
- There is also no inbound path:
AguiRequestProcessor / RunAgentInput / AguiMessageConverter never read confirmation results back, so even a client that wanted to approve/deny has no channel to send ConfirmResult (Msg metadata key agentscope_confirm_results).
This is especially visible with MCP tools: the MCP server receives initialize and tools/list but never receives tools/call, because the agent halts at the ASKING gate before ToolExecutor runs.
Environment
- agentscope-java:
2.0.0
- Module:
agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui
- Transport: AG-UI over SSE (
agentscope-agui-spring-boot-starter, MVC SseEmitter)
Root cause (verified against main)
1. The adapter is on the deprecated v1 event stream.
AguiAgentAdapter.run(RunAgentInput) calls:
// AguiAgentAdapter.java
Flux<Event> agentEvents = agent.stream(msgs, options, runtimeContext); // io.agentscope.core.agent.Event (v1)
...
agentEvents.concatMapIterable(event -> convertEvent(event, state));
and convertEvent(Event, ...) only branches on EventType.REASONING | SUMMARY | TOOL_RESULT. The entire StreamableAgent surface is already marked for removal:
// StreamableAgent.java
* @deprecated since 2.0.0, for removal in a future minor release. Every stream(...)
* method returns the coarse-grained Event type (a v1 carry-over). Use
* ReActAgent#streamEvents(...) instead — it returns Flux<io.agentscope.core.event.AgentEvent>,
* the fine-grained event hierarchy ...
@Deprecated(since = "2.0.0", forRemoval = true)
public interface StreamableAgent { ... }
The v1 Event.java deprecation javadoc even names AG-UI explicitly as a module still on the old wire format:
* @deprecated since 2.0.0. This is the v1 coarse-grained event type ... the wire format
* consumed by the harness, AGUI, A2A, chat-completions-web, and Kotlin extension modules.
* New code should use io.agentscope.core.event.AgentEvent via ReActAgent#streamEvents(...).
2. The two event hierarchies are disjoint and there is no bridge.
io.agentscope.core.agent.Event (v1, a flat class wrapping Msg + coarse EventType) and io.agentscope.core.event.AgentEvent (v2 abstract base with 28 typed subtypes and its own AgentEventType) are in different packages, unrelated by inheritance. There is no in-core converter from AgentEvent down to v1 Event. So RequireUserConfirmEvent cannot be observed from the stream the adapter currently subscribes to — a "small patch inside the v1 adapter" is not possible.
3. The AG-UI layer already defines the target types but never constructs them.
AguiEvent.java defines Interrupt, RunFinishedOutcome, RunFinishedSuccessOutcome, and RunFinishedInterruptOutcome(List<Interrupt>) — the Interrupt javadoc reads "requiring user input to resolve before the agent can continue" — but no production code ever instantiates them. finishRun(...) always emits the bare new AguiEvent.RunFinished(threadId, runId) (outcome = null). The interrupt types are exercised only in AguiEventTest (serialization).
Reproduction
- Register any
HarnessAgent/ReActAgent with default permissions (PermissionMode.DEFAULT) behind the AG-UI Spring Boot starter.
- Configure any non-read-only tool (e.g. an MCP
echo tool via tools.json).
POST /agui/run asking the model to call that tool.
Observed: SSE stream shows RUN_STARTED → TEXT_* → TOOL_CALL_START/ARGS/END → RUN_FINISHED, with no TOOL_CALL_RESULT and no confirmation event. Server-side, the agent stops at PRE_ACTING with RequestStopEvent(reason=permission asking) and ToolExecutor never runs; an MCP server never receives tools/call.
Expected: the confirmation request should surface to the client (as an AG-UI Interrupt / RunFinishedInterruptOutcome, or another agreed representation), and the client should be able to resume the run by sending approval/denial that is translated back into ConfirmResult (Msg metadata agentscope_confirm_results).
Workaround today: set PermissionMode.BYPASS (or add explicit allow rules) on the agent's PermissionContextState, which skips the ASK gate entirely. This unblocks tool execution but discards the HITL capability that AG-UI otherwise seems designed to support.
Reference for the intended v2 pattern
The canonical dispatch + resume flow already exists in tests:
agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java — subscribes to streamEvents(...), dispatches via instanceof over RequireUserConfirmEvent / RequestStopEvent (GenerateReason.PERMISSION_ASKING), extracts pending ToolUseBlocks via getToolCalls(), and resumes with new ConfirmResult(approved, toolCall).
agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java — subscribes to streamEvents(...) and dispatches by AgentEventType.
Suggested direction
Migrate AguiAgentAdapter from the deprecated agent.stream(...) (Flux<Event>) to ReActAgent#streamEvents(...) (Flux<AgentEvent>), rewrite convertEvent to dispatch over the typed v2 events (TEXT_BLOCK_*, THINKING_BLOCK_*, TOOL_CALL_*, TOOL_RESULT_*), and add HITL handling that maps RequireUserConfirmEvent → AguiEvent.Interrupt (+ RunFinishedInterruptOutcome) outbound, plus an inbound path that reads confirmation results from the incoming RunAgentInput and forwards them as ConfirmResult (Msg.METADATA_CONFIRM_RESULTS).
Note: streamEvents is declared on ReActAgent, not on the Agent interface, so the adapter would need to target ReActAgent (or a narrower event-streaming type) — worth deciding as part of the fix.
I'm happy to work on a PR for this.
Note: HITL confirmation over AG-UI is currently non-functional; the adapter being on the forRemoval v1 stream is the underlying cause. This is a defensive/UX correctness issue rather than a data-loss bug.
Summary
The AG-UI adapter (
AguiAgentAdapter) consumes the deprecated v1Flux<io.agentscope.core.agent.Event>stream viaagent.stream(...). Because the v1Event/EventTypevocabulary only containsREASONING/SUMMARY/TOOL_RESULT, the fine-grained v2 events — in particularRequireUserConfirmEventandRequestStopEvent— structurally can never reach the adapter.As a result, human-in-the-loop (HITL) tool confirmation is completely dropped over AG-UI:
ToolCallState.ASKING(e.g. any non-read-only tool underPermissionMode.DEFAULT, which is the default),ReActAgentemitsRequireUserConfirmEvent+RequestStopEventand pauses.TOOL_CALL_START/ARGS/ENDand then a bareRUN_FINISHED(or aRUN_ERRORon a subsequent run whose paused state was persisted) — the tool call silently never executes.AguiRequestProcessor/RunAgentInput/AguiMessageConverternever read confirmation results back, so even a client that wanted to approve/deny has no channel to sendConfirmResult(Msgmetadata keyagentscope_confirm_results).This is especially visible with MCP tools: the MCP server receives
initializeandtools/listbut never receivestools/call, because the agent halts at the ASKING gate beforeToolExecutorruns.Environment
2.0.0agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-aguiagentscope-agui-spring-boot-starter, MVCSseEmitter)Root cause (verified against
main)1. The adapter is on the deprecated v1 event stream.
AguiAgentAdapter.run(RunAgentInput)calls:and
convertEvent(Event, ...)only branches onEventType.REASONING | SUMMARY | TOOL_RESULT. The entireStreamableAgentsurface is already marked for removal:The v1
Event.javadeprecation javadoc even names AG-UI explicitly as a module still on the old wire format:2. The two event hierarchies are disjoint and there is no bridge.
io.agentscope.core.agent.Event(v1, a flat class wrappingMsg+ coarseEventType) andio.agentscope.core.event.AgentEvent(v2 abstract base with 28 typed subtypes and its ownAgentEventType) are in different packages, unrelated by inheritance. There is no in-core converter fromAgentEventdown to v1Event. SoRequireUserConfirmEventcannot be observed from the stream the adapter currently subscribes to — a "small patch inside the v1 adapter" is not possible.3. The AG-UI layer already defines the target types but never constructs them.
AguiEvent.javadefinesInterrupt,RunFinishedOutcome,RunFinishedSuccessOutcome, andRunFinishedInterruptOutcome(List<Interrupt>)— theInterruptjavadoc reads "requiring user input to resolve before the agent can continue" — but no production code ever instantiates them.finishRun(...)always emits the barenew AguiEvent.RunFinished(threadId, runId)(outcome = null). The interrupt types are exercised only inAguiEventTest(serialization).Reproduction
HarnessAgent/ReActAgentwith default permissions (PermissionMode.DEFAULT) behind the AG-UI Spring Boot starter.echotool viatools.json).POST /agui/runasking the model to call that tool.Observed: SSE stream shows
RUN_STARTED → TEXT_* → TOOL_CALL_START/ARGS/END → RUN_FINISHED, with noTOOL_CALL_RESULTand no confirmation event. Server-side, the agent stops atPRE_ACTINGwithRequestStopEvent(reason=permission asking)andToolExecutornever runs; an MCP server never receivestools/call.Expected: the confirmation request should surface to the client (as an AG-UI
Interrupt/RunFinishedInterruptOutcome, or another agreed representation), and the client should be able to resume the run by sending approval/denial that is translated back intoConfirmResult(Msgmetadataagentscope_confirm_results).Workaround today: set
PermissionMode.BYPASS(or add explicit allow rules) on the agent'sPermissionContextState, which skips the ASK gate entirely. This unblocks tool execution but discards the HITL capability that AG-UI otherwise seems designed to support.Reference for the intended v2 pattern
The canonical dispatch + resume flow already exists in tests:
agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java— subscribes tostreamEvents(...), dispatches viainstanceofoverRequireUserConfirmEvent/RequestStopEvent(GenerateReason.PERMISSION_ASKING), extracts pendingToolUseBlocks viagetToolCalls(), and resumes withnew ConfirmResult(approved, toolCall).agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java— subscribes tostreamEvents(...)and dispatches byAgentEventType.Suggested direction
Migrate
AguiAgentAdapterfrom the deprecatedagent.stream(...)(Flux<Event>) toReActAgent#streamEvents(...)(Flux<AgentEvent>), rewriteconvertEventto dispatch over the typed v2 events (TEXT_BLOCK_*,THINKING_BLOCK_*,TOOL_CALL_*,TOOL_RESULT_*), and add HITL handling that mapsRequireUserConfirmEvent→AguiEvent.Interrupt(+RunFinishedInterruptOutcome) outbound, plus an inbound path that reads confirmation results from the incomingRunAgentInputand forwards them asConfirmResult(Msg.METADATA_CONFIRM_RESULTS).Note:
streamEventsis declared onReActAgent, not on theAgentinterface, so the adapter would need to targetReActAgent(or a narrower event-streaming type) — worth deciding as part of the fix.I'm happy to work on a PR for this.
Note: HITL confirmation over AG-UI is currently non-functional; the adapter being on the
forRemovalv1 stream is the underlying cause. This is a defensive/UX correctness issue rather than a data-loss bug.