fix(core): restore event emitter for detached tool calls - #2483
Conversation
|
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! |
oss-maintainer
left a comment
There was a problem hiding this comment.
Review — Approved ✅
Fix: Restore AgentEventEmitter in Reactor Context when executeToolCalls(...) creates an internal subscription that loses the original streaming context.
Analysis
The root cause is clear: streamEvents() puts the AgentEventEmitter into Reactor Context at subscription time, but executeToolCalls(...) creates a new internal subscription. The contextWrite(ctx -> ctx.putAll(parentCtx)) merges the parent context, but if the parent context itself lost the emitter key (e.g., through a middleware boundary or context isolation), the child agent spawned in AgentSpawnTool#execLocalSync(...) can't find the parent emitter and falls back to non-streaming execution.
The fix adds a fallback in the contextWrite lambda:
- Merge parent context as before
- If both
SubagentEventBus.CONTEXT_KEYandAgentEventEmitter.CONTEXT_KEYare missing:- Prefer
eventSink(local streaming subscription) → wrap asAgentEventEmitter - Fall back to
externalEventEmitter(parent agent forwarding)
- Prefer
- If either key already exists, preserve it (no override)
This is correct because:
eventSinkis bound to the currentCallExecutionduringstreamEvents()setup, so it's always available when streamingexternalEventEmitterhandles the case where this agent was itself called by a parent agent's forwarding- The guard
!merged.hasKey(...)ensures backward compatibility withstream()(deprecated) and explicit event forwarding paths
Test Quality
The test uses a StripToolEventContextMiddleware to simulate the exact failure mode — stripping AgentEventEmitter.CONTEXT_KEY before tool execution. This is a clever and precise test design that:
- Verifies the fix restores the emitter from
eventSink - Confirms
AGENT_START/AGENT_ENDevents are still emitted - Validates exactly 1
ToolResultEndEvent(no duplicates) - Tests both the "eventSink available" and "externalEventEmitter fallback" paths
Minor Notes
- The lambda cast
(AgentEventEmitter) eventSink::nextworks becauseAgentEventEmitteris a functional interface. Consider adding a static factory method likeAgentEventEmitter.fromSink(FluxSink)for clarity if this pattern appears elsewhere. - Thread safety:
FluxSink.next()is thread-safe per Reactor docs, so concurrent tool calls won't corrupt the event stream.
Verdict: Clean, minimal, well-tested fix for a real Context propagation gap. LGTM.
fix: #2482
AgentScope-Java Version
2.0.1-SNAPSHOT
Description
修改概述
修复
HarnessAgent#streamEvents()中,本地同步子 Agent 通过agent_spawn启动后,子 Agent 的AgentEvent无法转发到父 Agent 事件流的问题。问题原因
streamEvents()创建事件流时会把当前订阅对应的FluxSink<AgentEvent>放入 Reactor Context,并在ReActAgent#doCall(...)中绑定到当前CallExecution.eventSink。acting 阶段执行工具调用时,工具执行链路经过内部
executeToolCalls(...)订阅。该链路无法继续读取原始 Reactor Context 中的AgentEventEmitter.CONTEXT_KEY。因此AgentSpawnTool#execLocalSync(...)调用AgentEventEmitter.fromContext(ctxView)时拿不到父事件 emitter,子 Agent 事件无法推送到父事件流。修复方案
在
executeToolCalls(...)的contextWrite(...)边界中,先通过ctx.putAll(parentCtx)合并调用侧的 Reactor ContextView。由于 acting 阶段工具调用使用内部订阅执行,原始streamEvents()订阅中的 event emitter 没有继续出现在工具执行链路里;因此当合并后的 context 中不存在SubagentEventBus.CONTEXT_KEY和AgentEventEmitter.CONTEXT_KEY时,会从当前CallExecution已绑定的eventSink或externalEventEmitter恢复AgentEventEmitter.CONTEXT_KEY:streamEvents()订阅绑定的eventSink;externalEventEmitter;SubagentEventBus.CONTEXT_KEY和AgentEventEmitter.CONTEXT_KEY,避免改变 deprecatedstream()或显式转发链路的行为。这样
AgentSpawnTool#execLocalSync(...)可以在 acting 工具调用链路中重新获取父级 emitter,并把子 Agent 事件带source转发到父事件流。测试覆盖
ReActAgentNewLoopE2ETest覆盖 acting context 丢失事件 key 时,executeToolCalls(...)会从当前eventSink恢复AgentEventEmitter.CONTEXT_KEY。streamEvents()首尾仍为父 AgentAGENT_START/AGENT_END。HarnessAgentSubagentStreamEventsTest中子 Agent 事件会带source转发到父事件流。@TempDir清理。