From 18a7c29a769340f1e3f3a46cdd2afb3f39c9b498 Mon Sep 17 00:00:00 2001 From: wangdy Date: Sat, 23 May 2026 09:51:27 +0800 Subject: [PATCH] fix: preserve typed RuntimeContext attributes in HarnessAgent --- .../agentscope/core/agent/RuntimeContext.java | 21 +++++++++ .../core/agent/RuntimeContextTest.java | 15 +++++++ .../harness/agent/HarnessAgent.java | 26 ++++++----- .../harness/agent/HarnessAgentTest.java | 45 +++++++++++++++++++ 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/RuntimeContext.java b/agentscope-core/src/main/java/io/agentscope/core/agent/RuntimeContext.java index 1d754815f..a7f8b0855 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/agent/RuntimeContext.java +++ b/agentscope-core/src/main/java/io/agentscope/core/agent/RuntimeContext.java @@ -291,6 +291,27 @@ public Builder put(Class type, T value) { return this; } + /** + * Copies singleton typed attributes from an existing {@link RuntimeContext}. + * + *

Only default-keyed typed values exposed via {@link RuntimeContext#get(Class)} are + * copied. Keyed typed entries are intentionally not included because the builder currently + * models singleton typed registrations only. + */ + public Builder typedFrom(RuntimeContext source) { + if (source == null || source.typedAttributes == null) { + return this; + } + for (Map.Entry, ConcurrentMap> e : + source.typedAttributes.entrySet()) { + Object value = e.getValue().get(TYPED_DEFAULT_KEY); + if (value != null) { + this.typedSingletons.put(e.getKey(), value); + } + } + return this; + } + /** * Nests a {@link ToolExecutionContext} (e.g. agent builder-level tool DI) that will be * visible at lower priority than runtime attributes in {@link #asToolExecutionContext()}. diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/RuntimeContextTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/RuntimeContextTest.java index 89e338173..3341bdae3 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/RuntimeContextTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/RuntimeContextTest.java @@ -89,6 +89,21 @@ void typedAccess() { assertNull(ctx.get(PojoA.class)); } + @Test + @DisplayName("builder typedFrom copies singleton typed attributes") + void builderTypedFromCopiesSingletonTypedAttributes() { + PojoA a = new PojoA("a"); + PojoB keyed = new PojoB(7); + RuntimeContext source = RuntimeContext.builder().put(PojoA.class, a).build(); + source.put("keyed", PojoB.class, keyed); + + RuntimeContext copy = RuntimeContext.builder().put("extra", 42).typedFrom(source).build(); + + assertSame(a, copy.get(PojoA.class)); + assertNull(copy.get("keyed", PojoB.class)); + assertEquals(Integer.valueOf(42), copy.get("extra")); + } + @Test @DisplayName("get(Class) for RuntimeContext returns the instance") void selfTypedAccess() { 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 867ab46ed..7b498ac1a 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 @@ -322,25 +322,27 @@ private RuntimeContext ensureSessionDefaults(RuntimeContext ctx) { sessionKey = SimpleSessionKey.of(delegate.getName()); } } + SandboxContext callerSandbox = ctx.get(SandboxContext.class); // Inject default sandbox context if the call doesn't provide one - SandboxContext sandboxCtx = - ctx.get(SandboxContext.class) != null - ? ctx.get(SandboxContext.class) - : defaultSandboxContext; + SandboxContext sandboxCtx = callerSandbox != null ? callerSandbox : defaultSandboxContext; if (session == ctx.getSession() && sessionKey == ctx.getSessionKey() && sandboxCtx == ctx.get(SandboxContext.class)) { return ctx; } - return RuntimeContext.builder() - .sessionId(ctx.getSessionId()) - .userId(ctx.getUserId()) - .session(session) - .sessionKey(sessionKey) - .putAll(ctx.getExtra()) - .put(SandboxContext.class, sandboxCtx) - .build(); + RuntimeContext.Builder builder = + RuntimeContext.builder() + .sessionId(ctx.getSessionId()) + .userId(ctx.getUserId()) + .session(session) + .sessionKey(sessionKey) + .putAll(ctx.getExtra()) + .typedFrom(ctx); + if (callerSandbox == null && defaultSandboxContext != null) { + builder.put(SandboxContext.class, defaultSandboxContext); + } + return builder.build(); } // ==================== Agent interface delegation ==================== diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index d842f1dea..fd0985c1c 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -36,15 +36,18 @@ import io.agentscope.core.session.Session; import io.agentscope.core.tool.AgentTool; import io.agentscope.core.tool.Toolkit; +import io.agentscope.harness.agent.example.support.InMemorySandboxFilesystemSpec; import io.agentscope.harness.agent.filesystem.local.LocalFilesystem; import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec; import io.agentscope.harness.agent.hook.SubagentsHook.SubagentEntry; import io.agentscope.harness.agent.memory.compaction.CompactionConfig; +import io.agentscope.harness.agent.sandbox.SandboxDistributedOptions; import io.agentscope.harness.agent.store.InMemoryStore; import io.agentscope.harness.agent.subagent.AgentSpecLoader; import io.agentscope.harness.agent.subagent.SubagentDeclaration; import io.agentscope.harness.agent.subagent.WorkspaceMode; import io.agentscope.harness.agent.workspace.WorkspaceConstants; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -63,6 +66,14 @@ class HarnessAgentTest { @TempDir Path workspace; + private static final class HarnessContext { + final String value; + + private HarnessContext(String value) { + this.value = value; + } + } + @Test void workspaceAgentsMd_readableViaWorkspaceManager() throws Exception { Files.createDirectories(workspace); @@ -339,6 +350,40 @@ void remoteFilesystemSpec_sharesMemoryMdInNonsandboxMode() throws Exception { store.get(List.of("agents", "agent-a", "users", "_default"), "/MEMORY.md") != null); } + @Test + void ensureSessionDefaults_preservesTypedAttributesWhenInjectingDefaultSandbox() + throws Exception { + Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel("ok")) + .workspace(workspace) + .filesystem(new InMemorySandboxFilesystemSpec()) + .session(mock(Session.class)) + .sandboxDistributed( + SandboxDistributedOptions.builder() + .requireDistributed(false) + .build()) + .build(); + + HarnessContext typed = new HarnessContext("typed-value"); + RuntimeContext input = + RuntimeContext.builder() + .sessionId("sid-typed") + .session(mock(Session.class)) + .sessionKey(io.agentscope.core.state.SimpleSessionKey.of("sid-typed")) + .put(HarnessContext.class, typed) + .build(); + + Method method = + HarnessAgent.class.getDeclaredMethod("ensureSessionDefaults", RuntimeContext.class); + method.setAccessible(true); + RuntimeContext effective = (RuntimeContext) method.invoke(agent, input); + + assertEquals("typed-value", effective.get(HarnessContext.class).value); + } + private static Msg userText(String text) { return Msg.builder() .role(MsgRole.USER)