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 438d1a32f..a91a62b07 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
@@ -102,7 +102,7 @@ default io.agentscope.core.state.AgentState getAgentState() {
* Returns the agent's live {@link Toolkit}, or {@code null} if this agent type does not
* maintain one.
*
- *
This is the runtime toolkit — the same instance the agent uses when listing
+ *
This is the runtime toolkit - the same instance the agent uses when listing
* available tools for the model and dispatching tool calls. Middleware that needs to register
* tools dynamically (e.g., skill loaders) must use this accessor rather than any toolkit
* reference captured at build time, because agents may deep-copy the toolkit during
@@ -111,4 +111,12 @@ default io.agentscope.core.state.AgentState getAgentState() {
default Toolkit getToolkit() {
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 c1ff7385a..dafc3570e 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
@@ -566,6 +566,7 @@ public AgentState getAgentState() {
* on one instance this reflects the latest call — middlewares/tools that need their own call's
* context should read it from the per-subscription {@link RuntimeContext} they are handed.
*/
+ @Override
public RuntimeContext getRuntimeContext() {
return null;
}
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 3e7e0c10a..46c564cbe 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
@@ -61,6 +61,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 ctx per-call runtime context (session, user, attributes)
* @param input agent input (messages)
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 000000000..e03b8128c
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/ReActAgentAgentImplRuntimeContextTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.assertSame;
+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.MiddlewareBase;
+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 MiddlewareBase {
+ private final boolean shortCircuit;
+ private final AtomicReference seen = new AtomicReference<>();
+
+ private CapturingMiddleware(boolean shortCircuit) {
+ this.shortCircuit = shortCircuit;
+ }
+
+ @Override
+ public Flux onAgent(
+ Agent agent,
+ RuntimeContext ctx,
+ AgentInput input,
+ Function> next) {
+ seen.set(ctx);
+ return shortCircuit ? Flux.empty() : next.apply(input);
+ }
+
+ @Override
+ public Flux onReasoning(
+ Agent agent,
+ RuntimeContext ctx,
+ ReasoningInput input,
+ Function> next) {
+ return next.apply(input);
+ }
+
+ @Override
+ public Flux onModelCall(
+ Agent agent,
+ RuntimeContext ctx,
+ ModelCallInput input,
+ Function> next) {
+ return next.apply(input);
+ }
+
+ @Override
+ public Flux onActing(
+ Agent agent,
+ RuntimeContext ctx,
+ ActingInput input,
+ Function> next) {
+ return next.apply(input);
+ }
+
+ @Override
+ public Mono onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
+ return Mono.just(currentPrompt);
+ }
+ }
+
+ private static ReActAgent buildAgent(MiddlewareBase middleware) {
+ return ReActAgent.builder()
+ .name("asst")
+ .sysPrompt("hello-system")
+ .model(new FixedTextModel())
+ .toolkit(new Toolkit())
+ .middlewares(List.of(middleware))
+ .build();
+ }
+
+ @Test
+ void streamEventsRunsCoreLifecycleWithRuntimeContext() {
+ CapturingMiddleware middleware = new CapturingMiddleware(false);
+ ReActAgent agent = buildAgent(middleware);
+ RuntimeContext runtimeContext =
+ RuntimeContext.builder().sessionId("runtime-context-session").build();
+
+ List events =
+ agent.streamEvents(List.of(), runtimeContext).collectList().block();
+
+ assertNotNull(events);
+ assertTrue(events.get(events.size() - 1) instanceof AgentEndEvent);
+ assertSame(runtimeContext, middleware.seen.get());
+ assertNull(agent.getRuntimeContext());
+ }
+
+ @Test
+ void streamEventsClearsRuntimeContextWhenShortCircuited() {
+ CapturingMiddleware middleware = new CapturingMiddleware(true);
+ ReActAgent agent = buildAgent(middleware);
+ RuntimeContext runtimeContext =
+ RuntimeContext.builder().sessionId("runtime-context-session").build();
+
+ List events =
+ agent.streamEvents(List.of(), runtimeContext).collectList().block();
+
+ assertNotNull(events);
+ assertTrue(events.isEmpty(), events.toString());
+ assertSame(runtimeContext, middleware.seen.get());
+ assertNull(agent.getRuntimeContext());
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/AgentTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/AgentTest.java
new file mode 100644
index 000000000..4540132fa
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/agent/AgentTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.agent;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.agentscope.core.message.Msg;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+class AgentTest {
+
+ @Test
+ void getRuntimeContext_defaultsToNull() {
+ Agent agent = new BareAgent();
+
+ assertNull(agent.getRuntimeContext());
+ }
+
+ private static final class BareAgent implements Agent {
+
+ @Override
+ public String getAgentId() {
+ return "bare-agent";
+ }
+
+ @Override
+ public String getName() {
+ return "bare-agent";
+ }
+
+ @Override
+ public void interrupt() {}
+
+ @Override
+ public void interrupt(Msg msg) {}
+
+ @Override
+ public Mono call(List msgs) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono call(List msgs, Class> structuredModel) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono call(List msgs, JsonNode schema) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Flux stream(List msgs, StreamOptions options) {
+ return Flux.empty();
+ }
+
+ @Override
+ public Flux stream(List msgs, StreamOptions options, Class> structuredModel) {
+ return Flux.empty();
+ }
+
+ @Override
+ public Flux stream(List msgs, StreamOptions options, JsonNode schema) {
+ return Flux.empty();
+ }
+
+ @Override
+ public Mono observe(Msg msg) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono observe(List msgs) {
+ return Mono.empty();
+ }
+ }
+}
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 000000000..fac475e72
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/skill/DynamicSkillMiddlewareRuntimeContextTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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, RuntimeContext.empty(), "BASE").block();
+
+ assertNotNull(prompt);
+ assertTrue(prompt.startsWith("BASE"));
+ assertNotNull(middleware.seen.get());
+ }
+
+ @Test
+ void onSystemPromptUsesSuppliedRuntimeContext() {
+ RuntimeContext runtimeContext =
+ RuntimeContext.builder().sessionId("dynamic-skill-session").build();
+ Agent agent = mock(Agent.class);
+ CapturingDynamicSkillMiddleware middleware =
+ new CapturingDynamicSkillMiddleware(List.of(skillRepo()));
+
+ String prompt = middleware.onSystemPrompt(agent, runtimeContext, "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/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
index 97667c37d..1af5de7d0 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
@@ -392,6 +392,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/SubagentsMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java
index b75689ca5..a42ae630e 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.
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 000000000..643790a33
--- /dev/null
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/MiddlewareRuntimeContextCoverageTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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,
+ runtimeContext,
+ 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, runtimeContext, "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,
+ runtimeContext("compaction-session"),
+ 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,
+ runtimeContext("flush-session"),
+ new AgentInput(List.of()),
+ in -> Flux.empty())
+ .blockLast();
+
+ assertNotNull(agent);
+ }
+
+ @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,
+ runtimeContext("maint-session"),
+ new AgentInput(List.of()),
+ in -> Flux.empty())
+ .blockLast();
+
+ assertNotNull(agent);
+ }
+
+ @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,
+ runtimeContext,
+ 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,
+ runtimeContext("eviction-session"),
+ new ActingInput(List.of()),
+ in -> Flux.empty())
+ .blockLast();
+
+ assertNotNull(agent);
+ }
+
+ 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 cc84b5e50..ec47e4f74 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,22 @@ 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);
+ RuntimeContext runtimeContext =
+ RuntimeContext.builder().sessionId("workspace-session").build();
+ when(agent.getRuntimeContext()).thenReturn(runtimeContext);
+
+ String prompt = mw.onSystemPrompt(agent, runtimeContext, "BASE\n").block();
+
+ assertNotNull(prompt);
+ assertTrue(prompt.contains("Project (the user's source tree"));
+ }
}