fix(agui): migrate adapter to v2 streamEvents and bridge HITL confirmations (#2437) - #2473
fix(agui): migrate adapter to v2 streamEvents and bridge HITL confirmations (#2437)#2473wzq-xzwj wants to merge 1 commit into
Conversation
…ations (agentscope-ai#2437) The AG-UI adapter consumed the deprecated v1 `Flux<Event>` stream, which only surfaces coarse REASONING/SUMMARY/TOOL_RESULT events and cannot carry human-in-the-loop signals. As a result, tool-confirmation events (RequireUserConfirmEvent) were silently dropped: over AG-UI, HITL was completely non-functional -- a paused run either hung or ran to a bare completion, and the client was never told a confirmation was required, nor could it resume an approved call. This change: * Adds `EventStreamingAgent`, a small capability interface in `io.agentscope.core.agent` declaring `Flux<AgentEvent> streamEvents(List<Msg>, RuntimeContext)`. Both `ReActAgent` and `HarnessAgent` implement it, so the adapter can consume the fine-grained v2 stream uniformly via `instanceof EventStreamingAgent`, independent of the concrete agent type, and still fall back to the deprecated v1 path for other `Agent` implementations. * Migrates `AguiAgentAdapter` to consume the v2 `Flux<AgentEvent>` stream and map each typed event (text/thinking/tool-call/tool-result blocks) to the corresponding AG-UI events. * Bridges HITL in both directions: - Outbound: when a run pauses on RequireUserConfirmEvent, RUN_FINISHED now carries a RunFinishedInterruptOutcome with one Interrupt per pending tool call (reason "tool_confirmation"), so the client learns a confirmation is required instead of seeing a bare completion. - Inbound: confirmation results supplied via `forwardedProps["agentscope_confirm_results"]` are parsed into `List<ConfirmResult>` and attached to the resumed message under `Msg.METADATA_CONFIRM_RESULTS`, letting a paused ReActAgent apply them and continue. The reconstructed ToolUseBlock also carries a raw-args JSON string in `content`, because the tool executor validates `ToolUseBlock.getContent()` (not the parsed input map) and applying a ConfirmResult fully replaces the stored block. Adds AguiAgentAdapterV2Test (11 tests) covering the v2 mapping path and both HITL directions. Verified end-to-end against a local Harness + MCP setup: an echo tool call pauses with an interrupt outcome and is not executed, and a follow-up resume with confirmation results executes it and returns the real result. Closes agentscope-ai#2437
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Thanks @jujn for the pointer to #2306 — I hadn't seen it when I opened this. #2306 already covers the root cause of #2437 (the v1 Closing this in favor of #2306. I'll follow up on #2437 to confirm it's resolved once #2306 lands. One small thing I ran into during end-to-end testing that might be worth a quick check against #2306's resume path: when a Thanks again for the coordination! |
oss-maintainer
left a comment
There was a problem hiding this comment.
Review — Approved ✅
Summary
Migrates the AG-UI adapter from the deprecated v1 Flux<Event> stream to the v2 Flux<AgentEvent> stream, and adds bidirectional HITL (human-in-the-loop) bridging for tool confirmation.
Changes Reviewed
1. New EventStreamingAgent interface (agentscope-core)
- Clean capability interface — right pattern for opt-in v2 streaming
- Properly documented as replacement for deprecated
StreamableAgent.stream() ReActAgentandHarnessAgentboth implement it; adapter branches oninstanceof
2. Adapter migration to v2 stream (AguiAgentAdapter)
streamAgentEvents()now checksinstanceof EventStreamingAgentfirst, falls back to v1 for other agents — good backward compatibilityEventConversionStatetracks active messages, tool calls, and reasoning state for proper event ordering- Handles edge cases: orphan deltas without start events, tool results without explicit starts
3. HITL outbound bridging
RequireUserConfirmEvent→ closes dangling text/reasoning messages →RunFinishedInterruptOutcomewithInterruptper pending tool call- Reason is
"tool_confirmation"— clear and actionable for clients
4. HITL inbound bridging
- Parses
forwardedProps["agentscope_confirm_results"]intoList<ConfirmResult> - Attaches to last message under
Msg.METADATA_CONFIRM_RESULTS - Correctly handles the
ToolUseBlock.getContent()requirement (raw-args JSON string) for tool executor validation - Synthesizes a minimal user message when no carrier message exists
5. Test coverage (AguiAgentAdapterV2Test, 11 tests)
- Text/thinking/tool-call event mapping
- Reasoning gated by config
- Tool call args suppression
- HITL outbound: interrupt outcome on
RequireUserConfirmEvent - HITL inbound: confirm results attached to resumed message
- Edge cases: orphan deltas, no forwarded results
6. CI: All green — build ✅, codecov ✅, CLA ✅, license ✅
Minor Observations (non-blocking)
- The
ToolUseBlock.getContent()carrying raw-args JSON is a workaround for the tool executor validation. A future cleanup could relax that validation to accept the parsed input map directly, but that's out of scope here. - The
EventConversionStateclass is getting substantial state-tracking logic. Consider extracting it to its own file if it grows further.
Verdict
Well-structured architectural migration with comprehensive HITL support. The fallback to v1 ensures no breaking changes for custom agent implementations. LGTM.
Summary
Fixes #2437 — the AG-UI adapter dropped human-in-the-loop (HITL) tool-confirmation events, so HITL was completely non-functional over the AG-UI protocol.
The adapter consumed the deprecated v1
Flux<Event>stream, which only surfaces coarseREASONING/SUMMARY/TOOL_RESULTevents and cannot carry HITL signals (RequireUserConfirmEvent/RequestStopEvent). Consequently a paused run either hung or ran to a bare completion; the client was never told a confirmation was required, and an approved call could not be resumed.What this PR does
New capability interface
EventStreamingAgent(io.agentscope.core.agent)Flux<AgentEvent> streamEvents(List<Msg>, RuntimeContext), the modern replacement for the deprecated v1StreamableAgent.stream(...).ReActAgentandHarnessAgent(which wraps aReActAgentdelegate) implement it, so the adapter can branch oninstanceof EventStreamingAgentand consume the v2 stream uniformly regardless of the concrete agent type — while still falling back to the deprecated v1 path for otherAgentimplementations.Adapter migrated to the v2
Flux<AgentEvent>streamBidirectional HITL bridging
RequireUserConfirmEvent,RUN_FINISHEDnow carries aRunFinishedInterruptOutcomewith oneInterruptper pending tool call (reason"tool_confirmation"), instead of a bare completion.forwardedProps["agentscope_confirm_results"]are parsed intoList<ConfirmResult>and attached to the resumed message underMsg.METADATA_CONFIRM_RESULTS, letting a pausedReActAgentapply them and continue. The reconstructedToolUseBlockalso carries a raw-args JSON string incontent, because the tool executor validatesToolUseBlock.getContent()(not the parsed input map) and applying aConfirmResultfully replaces the stored block.Testing
AguiAgentAdapterV2Test(11 tests) covering the v2 event-mapping path and both HITL directions. The fullagentscope-extensions-aguisuite passes (339 tests).echotool underPermissionMode.DEFAULT:RUN_FINISHEDwithoutcome.type=interrupt(reason: "tool_confirmation"), and the tool is not executed.forwardedProps.agentscope_confirm_resultsexecutes the tool and returns the real result (TOOL_CALL_RESULTcontent"echo: hello-hitl"), followed by a normalRUN_FINISHED.Wire example
Outbound interrupt (RUN_FINISHED):
{"type":"RUN_FINISHED","outcome":{"type":"interrupt","interrupts":[ {"id":"call_...","reason":"tool_confirmation", "message":"Tool call 'echo' requires confirmation before it can run.", "toolCallId":"call_..."}]}}Inbound resume (
forwardedProps):{"agentscope_confirm_results":[ {"toolCallId":"call_...","confirmed":true,"toolName":"echo", "input":{"message":"hello-hitl"}}]}