diff --git a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java index 003bbfeb15..a51a808015 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -148,6 +148,7 @@ import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import reactor.util.context.Context; /** * ReAct (Reasoning and Acting) Agent implementation. @@ -2635,9 +2636,39 @@ private Flux runToolBatch( Disposable toolCallsDisposable = executeToolCalls(approved) .contextWrite( - ctx -> - ctx.putAll( - parentCtx)) + ctx -> { + Context merged = + ctx.putAll( + parentCtx); + if (!merged.hasKey( + SubagentEventBus + .CONTEXT_KEY) + && !merged + .hasKey( + AgentEventEmitter + .CONTEXT_KEY)) { + if (eventSink + != null) { + merged = + merged + .put( + AgentEventEmitter + .CONTEXT_KEY, + (AgentEventEmitter) + eventSink + ::next); + } else if (externalEventEmitter + != null) { + merged = + merged + .put( + AgentEventEmitter + .CONTEXT_KEY, + externalEventEmitter); + } + } + return merged; + }) .subscribe( results -> { List< diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java index 60b588a272..e136369c24 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java @@ -22,6 +22,7 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.event.AgentEndEvent; import io.agentscope.core.event.AgentEvent; +import io.agentscope.core.event.AgentEventEmitter; import io.agentscope.core.event.AgentStartEvent; import io.agentscope.core.event.ModelCallEndEvent; import io.agentscope.core.event.ToolCallEndEvent; @@ -184,6 +185,21 @@ public Flux onActing( } } + private static final class StripToolEventContextMiddleware implements MiddlewareBase { + @Override + public Flux onActing( + Agent agent, + RuntimeContext ctx, + ActingInput input, + Function> next) { + return next.apply(input) + .contextWrite( + context -> + context.delete(SubagentEventBus.CONTEXT_KEY) + .delete(AgentEventEmitter.CONTEXT_KEY)); + } + } + @Test void twoToolReactLoopProducesOrderedEventsAndFinalText() { ScriptedModel model = @@ -292,4 +308,39 @@ void toolReturningErrorBlockEmitsErrorResultEndState() { .orElseThrow(); assertEquals(ToolResultState.ERROR, end.getState()); } + + @Test + void streamEventsRestoresEmitterWhenActingContextLosesEventKeys() { + ScriptedModel model = + new ScriptedModel( + List.of( + () -> Flux.just(toolUseResponse("c1", "search", "alpha")), + () -> Flux.just(textResponse("done")))); + Toolkit tk = new Toolkit(); + tk.registerAgentTool(new AlwaysAllowTool("search")); + + ReActAgent agent = + ReActAgent.builder() + .name("asst") + .sysPrompt("you are helpful") + .model(model) + .toolkit(tk) + .middleware(new StripToolEventContextMiddleware()) + .build(); + + List events = + agent.streamEvents( + List.of( + Msg.builder() + .role(MsgRole.USER) + .textContent("find alpha") + .build())) + .collectList() + .block(); + + assertNotNull(events); + assertEquals(1L, events.stream().filter(ToolResultEndEvent.class::isInstance).count()); + assertTrue(events.get(0) instanceof AgentStartEvent); + assertTrue(events.get(events.size() - 1) instanceof AgentEndEvent); + } }