Skip to content

fix(core): preserve AgentEventEmitter context for subagent streaming - #2468

Closed
wlmwang wants to merge 3 commits into
agentscope-ai:mainfrom
wlmwang:main
Closed

fix(core): preserve AgentEventEmitter context for subagent streaming#2468
wlmwang wants to merge 3 commits into
agentscope-ai:mainfrom
wlmwang:main

Conversation

@wlmwang

@wlmwang wlmwang commented Jul 28, 2026

Copy link
Copy Markdown

AgentScope-Java Version

2.0.1-SNAPSHOT

Description

Background

streamEvents() did not consistently forward local synchronous subagent events to the parent agent event stream.

For a parent HarnessAgent.streamEvents(...) call, ReActAgent#buildAgentStream installs an AgentEventEmitter in the Reactor Context so tools such as agent_spawn can forward child agent events into the parent Flux<AgentEvent>.

However, during the acting phase, CallExecution#runToolBatch bridges tool execution through an internal Flux.create(...) and manually subscribes to executeToolCalls(...). In that path, the Reactor Context available to tool execution may no longer contain AgentEventEmitter.CONTEXT_KEY.

As a result, AgentSpawnTool#execLocalSync cannot enter its streamEvents() forwarding path:

AgentEventEmitter.fromContext(ctxView)

and falls back to non-forwarding local execution. The subagent still runs and produces its own internal events, but those events are not tagged with the subagent source path and are not emitted into the parent agent's event stream.
This breaks real-time subagent streaming for local synchronous subagents spawned from a parent streamEvents() run.

Changes

  • Preserved the parent AgentEventEmitter when dispatching approved tool calls from CallExecution#runToolBatch.
  • Added a context fallback so tool execution receives AgentEventEmitter.CONTEXT_KEY when:
    • the current Reactor Context does not already contain an AgentEventEmitter
    • the deprecated SubagentEventBus path is not active
    • the current call scope already has eventSink or externalEventEmitter
  • Kept the deprecated stream() / SubagentEventBus behavior unchanged by avoiding forced AgentEventEmitter injection when SubagentEventBus.CONTEXT_KEY is present.
  • Ensured AgentSpawnTool#execLocalSync can enter the streamEvents() forwarding path and emit child events into the parent stream with source tagging.

Expected Behavior

  • A parent HarnessAgent.streamEvents(...) call can spawn a local synchronous subagent via agent_spawn.
  • The spawned subagent's AgentEvents are forwarded into the parent Flux<AgentEvent>.
  • Forwarded subagent events include the source path produced by AgentSpawnTool.
  • Clients consuming the parent stream can render subagent output incrementally instead of waiting for the final agent_spawn tool result.
  • Existing deprecated stream() / SubagentEventBus behavior remains unchanged.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@CLAassistant

CLAassistant commented Jul 28, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #2468

fix(core): preserve AgentEventEmitter context for subagent streaming

Summary

This PR fixes a context propagation issue where AgentEventEmitter is lost during tool execution in CallExecution#runToolBatch, preventing subagent events from being forwarded to the parent stream.

Analysis

The fix correctly identifies the root cause: when executeToolCalls(...) is bridged through Flux.create(...) and manually subscribed, the Reactor Context available to tool execution may not contain AgentEventEmitter.CONTEXT_KEY. The solution adds a conditional context injection that:

  1. Only injects when SubagentEventBus.CONTEXT_KEY is not present (preserving deprecated path)
  2. Only injects when AgentEventEmitter.CONTEXT_KEY is not already present (no override)
  3. Only injects when eventSink != null or externalEventEmitter != null (scoped to active streaming)

Observations

  • ✅ Logic is sound — the guard conditions prevent unintended side effects on existing code paths
  • ✅ Backward compatible — deprecated SubagentEventBus path is explicitly preserved
  • ✅ Well-documented PR body with clear problem → cause → fix narrative
  • ⚠️ CLA not signedlicense/cla check is pending. @wlmwang please sign the CLA so this can proceed.
  • ⚠️ Build checks are still running (pending) — will verify once complete.

Verdict

Approve (pending CLA + CI green). The fix is targeted and well-scoped.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/src/main/java/io/agentscope/core/ReActAgent.java 91.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@wlmwang

wlmwang commented Jul 29, 2026

Copy link
Copy Markdown
Author

BUG Reproduction

event.getSource() is empty even when the subagent is activated.

agent.streamEvents(new UserMessage(userMessage), RuntimeContext.builder().sessionId(FIXED_SESSION_ID).userId(USER_ID).build())
    .doOnNext(event -> {
        String src = event.getSource();
        String prefix = (src != null) ? "[" + src + "] " : "";
        if (prefix != null && !prefix.isBlank()) {
            log.info("AgentScope event type={} source={}", event.getType(), prefix);
        }
    
        if (event.getType() == AgentEventType.TEXT_BLOCK_DELTA) {
            System.out.print(prefix + ((TextBlockDeltaEvent) event).getDelta());
        } else if (event.getType() == AgentEventType.TOOL_CALL_START) {
            System.out.println(prefix + "[tool] " + ((ToolCallStartEvent) event).getToolCallName());
        } else if (event.getType() == AgentEventType.AGENT_START) {
            if (src != null) System.out.println("── 子 agent 启动: " + src);
        } else if (event.getType() == AgentEventType.AGENT_END) {
            if (src != null) System.out.println("── 子 agent 结束: " + src);
        }
    }).blockLast();

@wlmwang wlmwang closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants