fix(core): preserve AgentEventEmitter context for subagent streaming - #2468
fix(core): preserve AgentEventEmitter context for subagent streaming#2468wlmwang wants to merge 3 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
oss-maintainer
left a comment
There was a problem hiding this comment.
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:
- Only injects when
SubagentEventBus.CONTEXT_KEYis not present (preserving deprecated path) - Only injects when
AgentEventEmitter.CONTEXT_KEYis not already present (no override) - Only injects when
eventSink != nullorexternalEventEmitter != null(scoped to active streaming)
Observations
- ✅ Logic is sound — the guard conditions prevent unintended side effects on existing code paths
- ✅ Backward compatible — deprecated
SubagentEventBuspath is explicitly preserved - ✅ Well-documented PR body with clear problem → cause → fix narrative
⚠️ CLA not signed —license/clacheck 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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
sync from main
…eventSink and externalEventEmitter fallback paths.
BUG Reproduction
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(); |
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#buildAgentStreaminstalls anAgentEventEmitterin the Reactor Context so tools such asagent_spawncan forward child agent events into the parentFlux<AgentEvent>.However, during the acting phase,
CallExecution#runToolBatchbridges tool execution through an internalFlux.create(...)and manually subscribes toexecuteToolCalls(...). In that path, the Reactor Context available to tool execution may no longer containAgentEventEmitter.CONTEXT_KEY.As a result,
AgentSpawnTool#execLocalSynccannot enter itsstreamEvents()forwarding path: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
AgentEventEmitterwhen dispatching approved tool calls fromCallExecution#runToolBatch.AgentEventEmitter.CONTEXT_KEYwhen:AgentEventEmitterSubagentEventBuspath is not activeeventSinkorexternalEventEmitterstream()/SubagentEventBusbehavior unchanged by avoiding forcedAgentEventEmitterinjection whenSubagentEventBus.CONTEXT_KEYis present.AgentSpawnTool#execLocalSynccan enter thestreamEvents()forwarding path and emit child events into the parent stream with source tagging.Expected Behavior
HarnessAgent.streamEvents(...)call can spawn a local synchronous subagent viaagent_spawn.AgentEventsare forwarded into the parentFlux<AgentEvent>.AgentSpawnTool.agent_spawntool result.stream()/SubagentEventBusbehavior remains unchanged.