From 0e1d02929e8331fbba83131460fd7ee3b0df2182 Mon Sep 17 00:00:00 2001 From: guslegend <1670547022@qq.com> Date: Mon, 8 Jun 2026 20:44:57 +0800 Subject: [PATCH 1/4] fix(middleware): expose runtime context in onAgent --- .../java/io/agentscope/core/ReActAgent.java | 141 +++++++++++------- .../java/io/agentscope/core/agent/Agent.java | 8 + .../io/agentscope/core/agent/AgentBase.java | 1 + .../core/middleware/MiddlewareBase.java | 3 + .../core/skill/DynamicSkillMiddleware.java | 9 +- .../ReActAgentMiddlewareIntegrationTest.java | 55 +++++++ .../harness/agent/HarnessAgent.java | 1 + .../middleware/AtPathExpansionMiddleware.java | 9 +- .../middleware/CompactionMiddleware.java | 7 +- .../DynamicSubagentsMiddleware.java | 9 +- .../middleware/HarnessSkillMiddleware.java | 7 +- .../middleware/MemoryFlushMiddleware.java | 7 +- .../MemoryMaintenanceMiddleware.java | 7 +- .../agent/middleware/SubagentsMiddleware.java | 9 +- .../ToolResultEvictionMiddleware.java | 7 +- .../WorkspaceContextMiddleware.java | 9 +- 16 files changed, 189 insertions(+), 100 deletions(-) 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 79842d60d1..67f414be00 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -353,6 +353,12 @@ private static ReactConfig assembleReactConfig(Builder b) { // ==================== RuntimeContext ==================== + @Override + public RuntimeContext getRuntimeContext() { + RuntimeContext current = super.getRuntimeContext(); + return current != null ? current : pendingRuntimeContext; + } + @Override protected void beforeAgentExecution(List msgs) { RuntimeContext ctx = this.pendingRuntimeContext; @@ -506,32 +512,42 @@ public Flux stream( * @return event stream covering the full agent invocation lifecycle */ public Flux streamEvents(List msgs) { - String replyId = UUID.randomUUID().toString().replace("-", ""); - Function> core = - input -> - Flux.create( - sink -> { - activeEventSink.set(sink); - sink.next( - new AgentStartEvent(null, replyId, getName())); - reactor.util.context.Context subscriberCtx = - reactor.util.context.Context.of( - sink.contextView()); - call(input.msgs()) - .doFinally( - signal -> { - sink.next( - new AgentEndEvent(replyId)); - activeEventSink.set(null); - sink.complete(); - }) - .contextWrite(subscriberCtx) - .subscribe(finalMsg -> {}, sink::error); - }, - FluxSink.OverflowStrategy.BUFFER) - .doOnError(e -> activeEventSink.set(null)); - return MiddlewareChain.build(middlewares, this, MiddlewareBase::onAgent, core) - .apply(new AgentInput(msgs == null ? List.of() : msgs)); + return Flux.defer( + () -> { + RuntimeContext outerContext = ensurePendingRuntimeContext(); + String replyId = UUID.randomUUID().toString().replace("-", ""); + Function> core = + input -> + Flux.create( + sink -> { + activeEventSink.set(sink); + sink.next( + new AgentStartEvent( + null, replyId, getName())); + reactor.util.context.Context subscriberCtx = + reactor.util.context.Context.of( + sink.contextView()); + call(input.msgs()) + .doFinally( + signal -> { + sink.next( + new AgentEndEvent( + replyId)); + activeEventSink.set( + null); + sink.complete(); + }) + .contextWrite(subscriberCtx) + .subscribe( + finalMsg -> {}, + sink::error); + }, + FluxSink.OverflowStrategy.BUFFER) + .doOnError(e -> activeEventSink.set(null)); + return MiddlewareChain.build(middlewares, this, MiddlewareBase::onAgent, core) + .apply(new AgentInput(msgs == null ? List.of() : msgs)) + .doFinally(signal -> clearPendingRuntimeContextIfUnbound(outerContext)); + }); } /** @@ -810,31 +826,56 @@ private void maybePatchPendingToolCalls(List msgs) { * @return event stream covering the full agent invocation lifecycle */ Flux agentImpl(List msgs) { - String replyId = UUID.randomUUID().toString().replace("-", ""); + return Flux.defer( + () -> { + RuntimeContext outerContext = ensurePendingRuntimeContext(); + String replyId = UUID.randomUUID().toString().replace("-", ""); + + Function> core = + input -> + Flux.create( + sink -> { + activeEventSink.set(sink); + sink.next( + new AgentStartEvent( + null, replyId, getName())); + + doCall(input.msgs()) + .doFinally( + signal -> { + sink.next( + new AgentEndEvent( + replyId)); + activeEventSink.set( + null); + sink.complete(); + }) + .subscribe( + finalMsg -> {}, + sink::error); + }, + FluxSink.OverflowStrategy.BUFFER) + .doOnError(e -> activeEventSink.set(null)); + + return MiddlewareChain.build(middlewares, this, MiddlewareBase::onAgent, core) + .apply(new AgentInput(msgs)) + .doFinally(signal -> clearPendingRuntimeContextIfUnbound(outerContext)); + }); + } + + private RuntimeContext ensurePendingRuntimeContext() { + RuntimeContext ctx = pendingRuntimeContext; + if (ctx == null) { + ctx = RuntimeContext.empty(); + pendingRuntimeContext = ctx; + } + return ctx; + } - Function> core = - input -> - Flux.create( - sink -> { - activeEventSink.set(sink); - sink.next( - new AgentStartEvent(null, replyId, getName())); - - doCall(input.msgs()) - .doFinally( - signal -> { - sink.next( - new AgentEndEvent(replyId)); - activeEventSink.set(null); - sink.complete(); - }) - .subscribe(finalMsg -> {}, sink::error); - }, - FluxSink.OverflowStrategy.BUFFER) - .doOnError(e -> activeEventSink.set(null)); - - return MiddlewareChain.build(middlewares, this, MiddlewareBase::onAgent, core) - .apply(new AgentInput(msgs)); + private void clearPendingRuntimeContextIfUnbound(RuntimeContext expected) { + if (super.getRuntimeContext() == null && pendingRuntimeContext == expected) { + pendingRuntimeContext = null; + } } private void publishEvent(AgentEvent event) { diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/Agent.java b/agentscope-core/src/main/java/io/agentscope/core/agent/Agent.java index 6aa19cc05f..c397d724ad 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/agent/Agent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/agent/Agent.java @@ -96,4 +96,12 @@ default String getDescription() { default io.agentscope.core.state.AgentState getAgentState() { return null; } + + /** + * Returns the current per-call {@link RuntimeContext} when the agent is executing, or + * {@code null} if this agent type does not expose one or no invocation is active. + */ + default RuntimeContext getRuntimeContext() { + return null; + } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java index cbc0d6dfa5..c168ca8fdd 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java +++ b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java @@ -530,6 +530,7 @@ public AgentState getAgentState() { * Current per-call {@link RuntimeContext} when bound (e.g. by {@code ReActAgent} during a * {@code call}). */ + @Override public RuntimeContext getRuntimeContext() { return currentRuntimeContext.get(); } diff --git a/agentscope-core/src/main/java/io/agentscope/core/middleware/MiddlewareBase.java b/agentscope-core/src/main/java/io/agentscope/core/middleware/MiddlewareBase.java index f3d7fd87f8..7a4fb6b4e9 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/middleware/MiddlewareBase.java +++ b/agentscope-core/src/main/java/io/agentscope/core/middleware/MiddlewareBase.java @@ -60,6 +60,9 @@ public interface MiddlewareBase { /** * Intercept the entire agent invocation. * + *

During an active invocation, per-call metadata is available through + * {@link Agent#getRuntimeContext()} for agent implementations that bind a runtime context. + * * @param agent the agent instance * @param input agent input (messages) * @param next calls the next middleware or the core agent logic diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/DynamicSkillMiddleware.java b/agentscope-core/src/main/java/io/agentscope/core/skill/DynamicSkillMiddleware.java index ecfced14de..e95a999e8c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/DynamicSkillMiddleware.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/DynamicSkillMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.core.skill; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.middleware.MiddlewareBase; import io.agentscope.core.skill.repository.AgentSkillRepository; @@ -80,10 +79,10 @@ public SkillBox getCurrentSkillBox() { @Override public Mono onSystemPrompt(Agent agent, String currentPrompt) { - RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + if (rc == null) { + rc = RuntimeContext.empty(); + } reloadSkills(rc); if (currentSkillBox == null) { return Mono.just(currentPrompt); diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java index f615292000..efdcd85d62 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java @@ -17,6 +17,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import io.agentscope.core.ReActAgent; @@ -39,6 +41,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -73,6 +76,14 @@ protected Flux doStream( } } + private static final class MiddlewareMarker { + private final String value; + + private MiddlewareMarker(String value) { + this.value = value; + } + } + /** Records entry/exit at every middleware hook to a shared trace list. */ private static final class RecordingMiddleware implements Middleware { private final String tag; @@ -122,6 +133,38 @@ public Mono onSystemPrompt(Agent agent, String currentPrompt) { } } + private static final class RuntimeContextMiddleware implements Middleware { + private final AtomicReference seen = new AtomicReference<>(); + + @Override + public Flux onAgent( + Agent agent, AgentInput input, Function> next) { + RuntimeContext rc = agent.getRuntimeContext(); + assertNotNull(rc); + rc.put(MiddlewareMarker.class, new MiddlewareMarker("from-on-agent")); + seen.set(rc); + return next.apply(input); + } + + @Override + public Flux onReasoning( + Agent agent, + ReasoningInput input, + Function> next) { + RuntimeContext rc = agent.getRuntimeContext(); + assertNotNull(rc); + assertSame(seen.get(), rc); + MiddlewareMarker marker = rc.get(MiddlewareMarker.class); + assertNotNull(marker); + assertEquals("from-on-agent", marker.value); + return next.apply(input); + } + + RuntimeContext seenContext() { + return seen.get(); + } + } + private static ReActAgent buildAgent(ChatModelBase model, List middlewares) { return ReActAgent.builder() .name("asst") @@ -183,4 +226,16 @@ void middlewareSeesEveryHookCategoryOnPlainTextReply() { modelCallEnters, "reasoning and modelCall enter counts must match"); } + + @Test + void onAgentHookCanAccessLiveRuntimeContext() { + RuntimeContextMiddleware middleware = new RuntimeContextMiddleware(); + ReActAgent agent = buildAgent(new FixedTextModel("ok"), List.of(middleware)); + + List events = agent.streamEvents(List.of()).collectList().block(); + + assertNotNull(events); + assertNotNull(middleware.seenContext()); + assertNull(agent.getRuntimeContext()); + } } diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index 68b79641c2..bdf3390c84 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -319,6 +319,7 @@ public int getMaxIters() { return delegate.getMaxIters(); } + @Override public RuntimeContext getRuntimeContext() { return delegate.getRuntimeContext(); } diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddleware.java index 1add585ee2..530e597e4f 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; @@ -101,10 +100,10 @@ public Flux onAgent( return next.apply(input); } - RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + if (rc == null) { + rc = RuntimeContext.empty(); + } List rewritten = new ArrayList<>(input.msgs().size()); boolean changed = false; diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/CompactionMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/CompactionMiddleware.java index a9114f7053..ac7a75e155 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/CompactionMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/CompactionMiddleware.java @@ -17,7 +17,6 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; @@ -72,10 +71,8 @@ public Flux onReasoning( if (!(agent instanceof ReActAgent reActAgent)) { return next.apply(input); } - final RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext runtimeContext = agent != null ? agent.getRuntimeContext() : null; + final RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); return Flux.defer( () -> { diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java index 36c84a6a5b..5a91bbfc45 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; @@ -113,10 +112,10 @@ public List getTools() { @Override public Flux onReasoning( Agent agent, ReasoningInput input, Function> next) { - RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + if (rc == null) { + rc = RuntimeContext.empty(); + } List merged = reloadEntries(rc); if (agentManager != null) { agentManager.replaceAgents(merged); diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java index 89e329a5e2..48bc962ee4 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.middleware.MiddlewareBase; import io.agentscope.core.skill.AgentSkill; @@ -193,10 +192,8 @@ public Mono onSystemPrompt(Agent agent, String currentPrompt) { // --------------------------------------------------------------------- private RuntimeContext resolveContext(Agent agent) { - if (agent instanceof AgentBase ab && ab.getRuntimeContext() != null) { - return ab.getRuntimeContext(); - } - return RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + return rc != null ? rc : RuntimeContext.empty(); } /** diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryFlushMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryFlushMiddleware.java index ba1e461af5..a4d7260cc1 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryFlushMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryFlushMiddleware.java @@ -17,7 +17,6 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; @@ -58,10 +57,8 @@ public MemoryFlushMiddleware(WorkspaceManager workspaceManager, Model model) { @Override public Flux onAgent( Agent agent, AgentInput input, Function> next) { - final RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext runtimeContext = agent != null ? agent.getRuntimeContext() : null; + final RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); return next.apply(input).doOnComplete(() -> doFlush(agent, rc).subscribe()); } diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryMaintenanceMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryMaintenanceMiddleware.java index 1b87a9ec69..fac09c1417 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryMaintenanceMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/MemoryMaintenanceMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.middleware.AgentInput; @@ -88,10 +87,8 @@ public MemoryMaintenanceMiddleware( @Override public Flux onAgent( Agent agent, AgentInput input, Function> next) { - final RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext runtimeContext = agent != null ? agent.getRuntimeContext() : null; + final RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); return next.apply(input).doOnComplete(() -> maybeRunMaintenance(rc)); } diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java index 6e5d9fa560..39dd250d1e 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java @@ -17,7 +17,6 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; @@ -294,10 +293,10 @@ public Flux onReasoning( if (currentEntries.isEmpty()) { return next.apply(input); } - RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + if (rc == null) { + rc = RuntimeContext.empty(); + } String sessionId = rc != null ? rc.getSessionId() : null; // ---- Phase B-3 push delivery ------------------------------------------------------- diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/ToolResultEvictionMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/ToolResultEvictionMiddleware.java index eacc2a930e..345930ebeb 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/ToolResultEvictionMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/ToolResultEvictionMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.ContentBlock; @@ -72,10 +71,8 @@ public ToolResultEvictionMiddleware( @Override public Flux onActing( Agent agent, ActingInput input, Function> next) { - final RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext runtimeContext = agent != null ? agent.getRuntimeContext() : null; + final RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); AgentState state = agent.getAgentState(); final int sizeBefore = state != null ? state.contextMutable().size() : -1; return next.apply(input).doOnComplete(() -> evictAddedToolResults(agent, rc, sizeBefore)); diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddleware.java index 25d0759f1a..88ad2d6878 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddleware.java @@ -16,7 +16,6 @@ package io.agentscope.harness.agent.middleware; import io.agentscope.core.agent.Agent; -import io.agentscope.core.agent.AgentBase; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.middleware.MiddlewareBase; import io.agentscope.harness.agent.filesystem.AbstractFilesystem; @@ -119,10 +118,10 @@ public void setAdditionalContextFiles(List files) { @Override public Mono onSystemPrompt(Agent agent, String currentPrompt) { - RuntimeContext rc = - agent instanceof AgentBase ab && ab.getRuntimeContext() != null - ? ab.getRuntimeContext() - : RuntimeContext.empty(); + RuntimeContext rc = agent != null ? agent.getRuntimeContext() : null; + if (rc == null) { + rc = RuntimeContext.empty(); + } String section = buildWorkspaceSection(rc); if (section.isEmpty()) { return Mono.just(currentPrompt); From 537e2532bab07015144ed34cb7969d1056115abe Mon Sep 17 00:00:00 2001 From: guslegend <1670547022@qq.com> Date: Mon, 8 Jun 2026 21:44:44 +0800 Subject: [PATCH 2/4] test(middleware): cover runtime context branches --- ...ReActAgentAgentImplRuntimeContextTest.java | 142 +++++++++++ .../ReActAgentMiddlewareIntegrationTest.java | 13 ++ ...amicSkillMiddlewareRuntimeContextTest.java | 87 +++++++ .../AtPathExpansionMiddlewareTest.java | 26 ++- .../MiddlewareRuntimeContextCoverageTest.java | 220 ++++++++++++++++++ ...kspaceContextMiddlewarePathBoundsTest.java | 21 ++ 6 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 agentscope-core/src/test/java/io/agentscope/core/ReActAgentAgentImplRuntimeContextTest.java create mode 100644 agentscope-core/src/test/java/io/agentscope/core/skill/DynamicSkillMiddlewareRuntimeContextTest.java create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/MiddlewareRuntimeContextCoverageTest.java diff --git a/agentscope-core/src/test/java/io/agentscope/core/ReActAgentAgentImplRuntimeContextTest.java b/agentscope-core/src/test/java/io/agentscope/core/ReActAgentAgentImplRuntimeContextTest.java new file mode 100644 index 0000000000..5bbfa9d781 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/ReActAgentAgentImplRuntimeContextTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.agent.Agent; +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.event.AgentEndEvent; +import io.agentscope.core.event.AgentEvent; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.middleware.ActingInput; +import io.agentscope.core.middleware.AgentInput; +import io.agentscope.core.middleware.Middleware; +import io.agentscope.core.middleware.ModelCallInput; +import io.agentscope.core.middleware.ReasoningInput; +import io.agentscope.core.model.ChatModelBase; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.ToolSchema; +import io.agentscope.core.tool.Toolkit; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +class ReActAgentAgentImplRuntimeContextTest { + + private static final class FixedTextModel extends ChatModelBase { + @Override + public String getModelName() { + return "fixed"; + } + + @Override + protected Flux doStream( + List messages, List tools, GenerateOptions options) { + return Flux.just( + ChatResponse.builder() + .content(List.of(TextBlock.builder().text("ok").build())) + .build()); + } + } + + private static final class CapturingMiddleware implements Middleware { + private final boolean shortCircuit; + private final AtomicReference seen = new AtomicReference<>(); + + private CapturingMiddleware(boolean shortCircuit) { + this.shortCircuit = shortCircuit; + } + + @Override + public Flux onAgent( + Agent agent, AgentInput input, Function> next) { + seen.set(agent.getRuntimeContext()); + return shortCircuit ? Flux.empty() : next.apply(input); + } + + @Override + public Flux onReasoning( + Agent agent, + ReasoningInput input, + Function> next) { + return next.apply(input); + } + + @Override + public Flux onModelCall( + Agent agent, + ModelCallInput input, + Function> next) { + return next.apply(input); + } + + @Override + public Flux onActing( + Agent agent, ActingInput input, Function> next) { + return next.apply(input); + } + + @Override + public Mono onSystemPrompt(Agent agent, String currentPrompt) { + return Mono.just(currentPrompt); + } + } + + private static ReActAgent buildAgent(Middleware middleware) { + return ReActAgent.builder() + .name("asst") + .sysPrompt("hello-system") + .model(new FixedTextModel()) + .toolkit(new Toolkit()) + .middlewares(List.of(middleware)) + .build(); + } + + @Test + void agentImplRunsCoreLifecycleWithPendingRuntimeContext() { + CapturingMiddleware middleware = new CapturingMiddleware(false); + ReActAgent agent = buildAgent(middleware); + + List events = agent.agentImpl(List.of()).collectList().block(); + + assertNotNull(events); + assertTrue(events.get(events.size() - 1) instanceof AgentEndEvent); + assertNotNull(middleware.seen.get()); + assertNull(agent.getRuntimeContext()); + } + + @Test + void agentImplClearsPendingRuntimeContextWhenShortCircuited() { + CapturingMiddleware middleware = new CapturingMiddleware(true); + ReActAgent agent = buildAgent(middleware); + + List events = agent.agentImpl(List.of()).collectList().block(); + + assertNotNull(events); + assertTrue(events.isEmpty(), events.toString()); + assertNotNull(middleware.seen.get()); + assertNull(agent.getRuntimeContext()); + } +} diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java index efdcd85d62..eb40eb0292 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentMiddlewareIntegrationTest.java @@ -238,4 +238,17 @@ void onAgentHookCanAccessLiveRuntimeContext() { assertNotNull(middleware.seenContext()); assertNull(agent.getRuntimeContext()); } + + @Test + void onAgentHookSeesCallerSuppliedRuntimeContext() { + RuntimeContextMiddleware middleware = new RuntimeContextMiddleware(); + ReActAgent agent = buildAgent(new FixedTextModel("ok"), List.of(middleware)); + RuntimeContext supplied = RuntimeContext.builder().sessionId("supplied-session").build(); + + List events = agent.streamEvents(List.of(), supplied).collectList().block(); + + assertNotNull(events); + assertSame(supplied, middleware.seenContext()); + assertNull(agent.getRuntimeContext()); + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/DynamicSkillMiddlewareRuntimeContextTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/DynamicSkillMiddlewareRuntimeContextTest.java new file mode 100644 index 0000000000..4480b3a5c8 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/DynamicSkillMiddlewareRuntimeContextTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.skill; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.agentscope.core.agent.Agent; +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.skill.repository.AgentSkillRepository; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class DynamicSkillMiddlewareRuntimeContextTest { + + private static final class CapturingDynamicSkillMiddleware extends DynamicSkillMiddleware { + private final AtomicReference seen = new AtomicReference<>(); + + private CapturingDynamicSkillMiddleware(List repositories) { + super(repositories, null); + } + + @Override + protected List filterVisible(List raw, RuntimeContext ctx) { + seen.set(ctx); + return raw; + } + } + + @Test + void onSystemPromptFallsBackToEmptyContextWhenAgentIsNull() { + CapturingDynamicSkillMiddleware middleware = + new CapturingDynamicSkillMiddleware(List.of(skillRepo())); + + String prompt = middleware.onSystemPrompt(null, "BASE").block(); + + assertNotNull(prompt); + assertTrue(prompt.startsWith("BASE")); + assertNotNull(middleware.seen.get()); + } + + @Test + void onSystemPromptUsesAgentRuntimeContextWhenAvailable() { + RuntimeContext runtimeContext = + RuntimeContext.builder().sessionId("dynamic-skill-session").build(); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext); + CapturingDynamicSkillMiddleware middleware = + new CapturingDynamicSkillMiddleware(List.of(skillRepo())); + + String prompt = middleware.onSystemPrompt(agent, "BASE").block(); + + assertNotNull(prompt); + assertSame(runtimeContext, middleware.seen.get()); + } + + private static AgentSkillRepository skillRepo() { + AgentSkillRepository repo = mock(AgentSkillRepository.class); + when(repo.getAllSkills()) + .thenReturn( + List.of( + new AgentSkill( + "sample", + "Sample skill", + "You can use the sample skill.", + Map.of()))); + return repo; + } +} diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddlewareTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddlewareTest.java index d3ed70823c..4e19f2cdf2 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddlewareTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/AtPathExpansionMiddlewareTest.java @@ -18,8 +18,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import io.agentscope.core.agent.Agent; +import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.event.AgentEvent; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; @@ -142,6 +145,23 @@ void deduplicatesRepeatedReferences(@TempDir Path project, @TempDir Path workspa assertEquals(-1, secondBlock, "duplicate references should produce one block, not two"); } + @Test + void expandsPathWhenAgentAlreadyHasRuntimeContext( + @TempDir Path project, @TempDir Path workspace) throws IOException { + Files.writeString(project.resolve("README.md"), "WITH_CONTEXT", StandardCharsets.UTF_8); + + WorkspaceManager wm = workspaceManagerFor(project, workspace); + AtPathExpansionMiddleware mw = new AtPathExpansionMiddleware(wm); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()) + .thenReturn(RuntimeContext.builder().sessionId("ctx-session").build()); + Msg user = userMsg("Open @./README.md"); + + List result = runOnAgent(mw, agent, user); + + assertTrue(result.get(0).getTextContent().contains("WITH_CONTEXT")); + } + // ----------------------------------------------------------------- // helpers // ----------------------------------------------------------------- @@ -178,10 +198,14 @@ private static Msg userMsg(String text) { * the rewritten {@link AgentInput}, then returns its messages. */ private static List runOnAgent(AtPathExpansionMiddleware mw, Msg... msgs) { + return runOnAgent(mw, null, msgs); + } + + private static List runOnAgent(AtPathExpansionMiddleware mw, Agent agent, Msg... msgs) { AgentInput input = new AgentInput(List.of(msgs)); List captured = new ArrayList<>(); mw.onAgent( - (Agent) null, + agent, input, in -> { captured.addAll(in.msgs()); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/MiddlewareRuntimeContextCoverageTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/MiddlewareRuntimeContextCoverageTest.java new file mode 100644 index 0000000000..b147f13889 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/MiddlewareRuntimeContextCoverageTest.java @@ -0,0 +1,220 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.middleware; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.Agent; +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.event.AgentEvent; +import io.agentscope.core.middleware.ActingInput; +import io.agentscope.core.middleware.AgentInput; +import io.agentscope.core.middleware.ReasoningInput; +import io.agentscope.core.model.Model; +import io.agentscope.core.skill.AgentSkill; +import io.agentscope.core.skill.repository.AgentSkillRepository; +import io.agentscope.core.state.AgentState; +import io.agentscope.harness.agent.filesystem.AbstractFilesystem; +import io.agentscope.harness.agent.memory.compaction.CompactionConfig; +import io.agentscope.harness.agent.memory.compaction.ToolResultEvictionConfig; +import io.agentscope.harness.agent.skill.curator.SkillVisibilityFilter; +import io.agentscope.harness.agent.subagent.SubagentFactory; +import io.agentscope.harness.agent.subagent.task.TaskRepository; +import io.agentscope.harness.agent.subagent.task.TaskStatus; +import io.agentscope.harness.agent.workspace.WorkspaceManager; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +class MiddlewareRuntimeContextCoverageTest { + + @Test + void dynamicSubagentsMiddlewarePassesRuntimeContextToTaskSummary() { + RuntimeContext runtimeContext = runtimeContext("dyn-sub-session"); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext); + TaskRepository taskRepository = mock(TaskRepository.class); + when(taskRepository.listTasks( + same(runtimeContext), eq("dyn-sub-session"), eq((TaskStatus) null))) + .thenReturn(List.of()); + DynamicSubagentsMiddleware middleware = + new DynamicSubagentsMiddleware( + List.of(), null, null, null, null, new Object(), taskRepository); + + middleware + .onReasoning( + agent, + new ReasoningInput(List.of(), List.of(), null), + in -> Flux.empty()) + .blockLast(); + + verify(taskRepository) + .listTasks(same(runtimeContext), eq("dyn-sub-session"), eq((TaskStatus) null)); + } + + @Test + void harnessSkillMiddlewarePassesRuntimeContextToVisibilityFilter() { + RuntimeContext runtimeContext = runtimeContext("harness-skill-session"); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext); + AgentSkillRepository repo = mock(AgentSkillRepository.class); + when(repo.getSource()).thenReturn("repo"); + when(repo.getAllSkills()) + .thenReturn( + List.of( + new AgentSkill( + "sample", + "Sample skill", + "Use the sample skill.", + Map.of()))); + AtomicReference seen = new AtomicReference<>(); + SkillVisibilityFilter filter = + (all, ctx) -> { + seen.set(ctx); + return all; + }; + HarnessSkillMiddleware middleware = + new HarnessSkillMiddleware(List.of(repo), null, null, filter); + + String prompt = middleware.onSystemPrompt(agent, "BASE").block(); + + assertNotNull(prompt); + assertSame(runtimeContext, seen.get()); + } + + @Test + void compactionMiddlewareAllowsReasoningWithAgentRuntimeContext() { + ReActAgent agent = mock(ReActAgent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext("compaction-session")); + CompactionMiddleware middleware = + new CompactionMiddleware( + mock(WorkspaceManager.class), + mock(Model.class), + CompactionConfig.builder().triggerMessages(10).triggerTokens(10).build()); + + ReasoningInput input = new ReasoningInput(List.of(), List.of(), null); + AtomicReference forwarded = new AtomicReference<>(); + middleware + .onReasoning( + agent, + input, + in -> { + forwarded.set(in); + return Flux.empty(); + }) + .blockLast(); + + assertSame(input, forwarded.get()); + } + + @Test + void memoryFlushMiddlewareCompletesWhenAgentHasRuntimeContext() { + ReActAgent agent = mock(ReActAgent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext("flush-session")); + when(agent.getAgentState()) + .thenReturn(AgentState.builder().sessionId("flush-session").build()); + MemoryFlushMiddleware middleware = + new MemoryFlushMiddleware(mock(WorkspaceManager.class), mock(Model.class)); + + middleware + .onAgent(agent, new AgentInput(List.of()), in -> Flux.empty()) + .blockLast(); + + verify(agent).getRuntimeContext(); + } + + @Test + void memoryMaintenanceMiddlewareCompletesWhenAgentHasRuntimeContext() { + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext("maint-session")); + WorkspaceManager workspaceManager = mock(WorkspaceManager.class); + when(workspaceManager.getFilesystem()).thenReturn(null); + MemoryMaintenanceMiddleware middleware = + new MemoryMaintenanceMiddleware(workspaceManager, null, 1, 1, Duration.ZERO); + + middleware + .onAgent(agent, new AgentInput(List.of()), in -> Flux.empty()) + .blockLast(); + + verify(agent).getRuntimeContext(); + } + + @Test + void subagentsMiddlewarePassesRuntimeContextToTaskRepository() { + RuntimeContext runtimeContext = runtimeContext("subagents-session"); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext); + TaskRepository taskRepository = mock(TaskRepository.class); + when(taskRepository.findPendingDeliveries(same(runtimeContext), eq("subagents-session"))) + .thenReturn(List.of()); + when(taskRepository.listTasks( + same(runtimeContext), eq("subagents-session"), eq((TaskStatus) null))) + .thenReturn(List.of()); + SubagentsMiddleware middleware = + new SubagentsMiddleware( + List.of(new SubagentEntry("worker", "Worker", mock(SubagentFactory.class))), + taskRepository, + (WorkspaceManager) null); + AtomicReference forwarded = new AtomicReference<>(); + + middleware + .onReasoning( + agent, + new ReasoningInput(List.of(), List.of(), null), + in -> { + forwarded.set(in); + return Flux.empty(); + }) + .blockLast(); + + assertNotNull(forwarded.get()); + assertTrue(!forwarded.get().messages().isEmpty()); + verify(taskRepository).findPendingDeliveries(same(runtimeContext), eq("subagents-session")); + verify(taskRepository) + .listTasks(same(runtimeContext), eq("subagents-session"), eq((TaskStatus) null)); + } + + @Test + void toolResultEvictionMiddlewareCompletesWhenAgentHasRuntimeContext() { + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()).thenReturn(runtimeContext("eviction-session")); + when(agent.getAgentState()).thenReturn(null); + ToolResultEvictionMiddleware middleware = + new ToolResultEvictionMiddleware( + mock(AbstractFilesystem.class), ToolResultEvictionConfig.defaults()); + + middleware + .onActing(agent, new ActingInput(List.of()), in -> Flux.empty()) + .blockLast(); + + verify(agent).getRuntimeContext(); + } + + private static RuntimeContext runtimeContext(String sessionId) { + return RuntimeContext.builder().sessionId(sessionId).build(); + } +} diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddlewarePathBoundsTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddlewarePathBoundsTest.java index e9f80643ab..96ba240825 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddlewarePathBoundsTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/WorkspaceContextMiddlewarePathBoundsTest.java @@ -18,7 +18,11 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import io.agentscope.core.agent.Agent; +import io.agentscope.core.agent.RuntimeContext; import io.agentscope.harness.agent.filesystem.AbstractFilesystem; import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec; import io.agentscope.harness.agent.workspace.LocalFsMode; @@ -110,4 +114,21 @@ void localOverlay_unrestrictedMode_describesEscapeHatch( assertTrue( prompt.contains("UNRESTRICTED"), () -> "UNRESTRICTED mode not surfaced: " + prompt); } + + @Test + void localOverlay_withAgentRuntimeContext_stillBuildsPrompt( + @TempDir Path project, @TempDir Path workspace) { + AbstractFilesystem fs = + new LocalFilesystemSpec().project(project).toFilesystem(workspace, null); + WorkspaceManager wm = track(new WorkspaceManager(workspace, fs)); + WorkspaceContextMiddleware mw = new WorkspaceContextMiddleware(wm); + Agent agent = mock(Agent.class); + when(agent.getRuntimeContext()) + .thenReturn(RuntimeContext.builder().sessionId("workspace-session").build()); + + String prompt = mw.onSystemPrompt(agent, "BASE\n").block(); + + assertNotNull(prompt); + assertTrue(prompt.contains("Project (the user's source tree")); + } } From 71108d19843e259ea8f751ad95c0a746d4e7aae5 Mon Sep 17 00:00:00 2001 From: guslegend <1670547022@qq.com> Date: Wed, 10 Jun 2026 06:10:33 +0800 Subject: [PATCH 3/4] chore: retrigger ci From fb2afdb515fb11b155be1439861ee03e4bd81713 Mon Sep 17 00:00:00 2001 From: guslegend <1670547022@qq.com> Date: Wed, 10 Jun 2026 06:25:50 +0800 Subject: [PATCH 4/4] style(harness): remove redundant runtime context null check --- .../harness/agent/middleware/SubagentsMiddleware.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java index b75689ca59..a42ae630e7 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java @@ -330,7 +330,7 @@ public Flux onReasoning( return next.apply(input); } RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty(); - String sessionId = rc != null ? rc.getSessionId() : null; + String sessionId = rc.getSessionId(); // ---- Phase B-3 push delivery ------------------------------------------------------- // Drain newly-terminal tasks first so the SYSTEM summary built afterwards can omit them.