-
Notifications
You must be signed in to change notification settings - Fork 463
Description
Sub-agent events are not visible in AG-UI frontend when using SubAgentTool with forwardEvents(true)
Summary
When a supervisor agent delegates tasks to sub-agents via SubAgentTool with SubAgentConfig.forwardEvents(true), the sub-agent's reasoning process (thinking, tool calls, tool results) is completely invisible to the AG-UI frontend. The user sees the sub-agent tool call start, then a long pause (black box), and finally the complete result all at once — with no intermediate streaming.
Environment
- agentscope-java version: 1.0.10
- AG-UI adapter:
agentscope-agui-spring-boot-starter:1.0.10
Steps to Reproduce
- Create a supervisor agent with a sub-agent registered via
SubAgentConfig.forwardEvents(true):
SubAgentConfig config = SubAgentConfig.builder()
.forwardEvents(true)
.streamOptions(
StreamOptions.builder()
.eventTypes(EventType.ALL)
.includeReasoningChunk(true)
.includeActingChunk(true)
.build()
)
.build();
toolkit.registration()
.subAgent(mySubAgentProvider, config)
.apply();- Connect the supervisor agent to AG-UI via
AguiAgentAdapter - Send a message that triggers sub-agent invocation
- Observe the AG-UI SSE event stream
Expected Behavior
The frontend should receive real-time streaming events showing the sub-agent's:
- Reasoning/thinking process (incremental text chunks)
- Tool call invocations (tool name, arguments)
- Tool call results
This would provide transparency into the sub-agent's execution, similar to how the parent agent's events are streamed.
Actual Behavior
The frontend receives:
TOOL_CALL_START(sub-agent tool call begins)- Nothing for the entire duration of sub-agent execution (black box)
TOOL_CALL_END+TOOL_CALL_RESULT(final result appears all at once)
The sub-agent's intermediate reasoning and tool usage are completely hidden from the user.
Root Cause Analysis
I traced this through two specific gaps in the event pipeline:
Gap 1: AguiAgentAdapter.run() hardcodes StreamOptions without includeActingChunk
In AguiAgentAdapter.java line 98-99:
StreamOptions options =
StreamOptions.builder().eventTypes(EventType.ALL).incremental(true).build();Since includeActingChunk defaults to false, the parent agent's event stream silently drops all ActingChunkEvents. These are exactly the events that carry forwarded sub-agent data (emitted by SubAgentTool.forwardEvent() via ToolEmitter.emit()).
Gap 2: AguiAgentAdapter.convertEvent() has no handling for forwarded sub-agent events
Even if ActingChunkEvents were included in the stream, convertEvent() only handles two event types:
if (type == EventType.REASONING) {
// ... handles text, thinking, tool use blocks
} else if (type == EventType.TOOL_RESULT && event.isLast()) {
// ... handles tool results, but ONLY when isLast=true
}
// No handling for ACTING events or forwarded sub-agent metadataThe sub-agent events forwarded by SubAgentTool.forwardEvent() are wrapped in ToolResultBlock metadata:
// SubAgentTool.forwardEvent()
Map<String, Object> metadata = new HashMap<>();
metadata.put("subagent_event", event);
metadata.put("subagent_name", agent.getName());
metadata.put("subagent_id", agent.getAgentId());
metadata.put("subagent_session_id", sessionId);
emitter.emit(new ToolResultBlock(null, null,
List.of(TextBlock.builder().text(json).build()), metadata));But AguiAgentAdapter never inspects this metadata or converts it to AG-UI events.
Event Flow Diagram
Sub-agent generates events (REASONING chunks, TOOL_RESULT, etc.)
↓
SubAgentTool.forwardEvent() wraps as ToolResultBlock with metadata
↓
ToolEmitter.emit() → triggers ActingChunkEvent on parent agent
↓
AguiAgentAdapter.run() streams parent agent with includeActingChunk=false
↓
✗ ActingChunkEvent is DROPPED — never reaches convertEvent()
↓
Even if it did reach convertEvent(), no logic exists to extract
sub-agent metadata and convert to AG-UI events
↓
Frontend sees nothing until sub-agent completes
Workaround
We implemented an application-level workaround using a custom Hook + Sinks.Many<AguiEvent> merge pattern:
1. Register a per-request event sink
// In the chat service, before agent execution:
val subAgentSink = Sinks.many()
.multicast()
.onBackpressureBuffer<AguiEvent>()
// Store sink keyed by agent identity for hook access
AiChatContext.registerSubAgentSink(agent, SubAgentSinkInfo(
subAgentSink, threadId, runId
))
// Merge custom events with main AG-UI stream
val mergedEvents = Flux.merge(
aguiResult.events().doOnComplete { subAgentSink.tryEmitComplete() },
subAgentSink.asFlux()
)2. Custom Hook intercepts ActingChunkEvent and emits AG-UI events
@Component
class SubAgentEventForwardingHook : Hook {
override fun <T : HookEvent> onEvent(event: T): Mono<T> {
if (event !is ActingChunkEvent) return Mono.just(event)
val metadata = event.chunk.metadata ?: return Mono.just(event)
if (!metadata.containsKey("subagent_event")) return Mono.just(event)
val sinkInfo = AiChatContext.getSubAgentSink(event.agent)
?: return Mono.just(event)
val subEvent = metadata["subagent_event"] as? Event
?: return Mono.just(event)
val agentName = metadata["subagent_name"] as? String ?: "SubAgent"
// Convert to AG-UI event and push to sink
val aguiEvent = convertToAguiEvent(subEvent, agentName, sinkInfo)
aguiEvent?.let { sinkInfo.sink.tryEmitNext(it) }
return Mono.just(event)
}
}This works but requires significant boilerplate at the application level. Ideally, the framework should handle this natively.
Important: StreamOptions tuning required
When configuring SubAgentConfig.streamOptions, you must set includeReasoningResult(false) to avoid duplicate content:
StreamOptions.builder()
.eventTypes(EventType.ALL)
.includeReasoningChunk(true)
.includeReasoningResult(false) // ← Critical: prevents duplicate output
.includeActingChunk(true)
.build()With includeReasoningResult(true), both incremental chunks AND accumulated results are forwarded, causing every sentence to appear twice on the frontend.
Suggested Fix
Option A: Enhance AguiAgentAdapter to natively support sub-agent event forwarding
- Add
includeActingChunk(true)to theStreamOptionsinAguiAgentAdapter.run():
StreamOptions options = StreamOptions.builder()
.eventTypes(EventType.ALL)
.incremental(true)
.includeActingChunk(true) // Enable acting chunk events
.build();- Add sub-agent event handling in
convertEvent():
// In convertEvent(), handle forwarded sub-agent events
if (msg.getContent() != null) {
for (ContentBlock block : msg.getContent()) {
if (block instanceof ToolResultBlock toolResult
&& toolResult.getMetadata() != null
&& toolResult.getMetadata().containsKey("subagent_event")) {
Event subEvent = (Event) toolResult.getMetadata().get("subagent_event");
String agentName = (String) toolResult.getMetadata().get("subagent_name");
// Convert sub-agent event to appropriate AG-UI events
events.addAll(convertSubAgentEvent(subEvent, agentName, state));
}
}
}Option B: Allow AguiAdapterConfig to accept custom StreamOptions
Let users override the StreamOptions used in AguiAgentAdapter.run():
public class AguiAdapterConfig {
// ... existing fields ...
private StreamOptions streamOptions; // New: custom stream options
}Option C: Provide a built-in SubAgentEventConverter
Create a dedicated component that integrates with AguiAgentAdapter to handle sub-agent event conversion, without requiring application-level hooks.
Related Issues
- [Bug]:agentscope agent as tools模式下流式场景报错:Cannot add messages without tool results when pending tool calls exist. #918 — Streaming bug with agents as tools
- [Feature]: Message and Status Delivery when using agent as a tool #818 — Message delivery when using agent as a tool
Impact
This issue affects any AG-UI application using the supervisor + sub-agent pattern. Without the workaround, sub-agent execution appears as a "black box" to end users, degrading the user experience significantly — especially for sub-agents that perform complex, multi-step reasoning (e.g., permission analysis, data retrieval).
Metadata
Metadata
Assignees
Labels
Type
Projects
Status