Skip to content

[Feature]: Sub-agent events are not visible in AG-UI frontend when using SubAgentTool with forwardEvents(true) #1046

@fcfang123

Description

@fcfang123

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

  1. 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();
  1. Connect the supervisor agent to AG-UI via AguiAgentAdapter
  2. Send a message that triggers sub-agent invocation
  3. 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:

  1. TOOL_CALL_START (sub-agent tool call begins)
  2. Nothing for the entire duration of sub-agent execution (black box)
  3. 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 metadata

The 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

  1. Add includeActingChunk(true) to the StreamOptions in AguiAgentAdapter.run():
StreamOptions options = StreamOptions.builder()
    .eventTypes(EventType.ALL)
    .incremental(true)
    .includeActingChunk(true)  // Enable acting chunk events
    .build();
  1. 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

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

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions