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")); + } }