From 0809da898feb9037d582d696acf1686b064fd3f2 Mon Sep 17 00:00:00 2001 From: changningbo Date: Fri, 28 Aug 2026 23:50:21 +0800 Subject: [PATCH 1/2] fix(core): pass builder permission context when loading legacy v1 session state Commit 605978292 (#2760) rewrote loadOrCreateAgentStateForSlot and dropped the permCtx argument, calling the 3-arg LegacyStateLoader.loadFromLegacySessionWithPresence overload. Legacy v1 keys (memory_messages / toolkit_activeGroups) never carry a permission context, so the first turn after a 1.x -> 2.0 migration silently downgraded the caller-configured permission mode to DEFAULT, re-introducing the bug fixed by #2769. Pass the builder-time permission context to the 4-arg overload and add an end-to-end regression test (ReActAgentLegacyPermissionContextTest) that fails without the fix: expected BYPASS but was DEFAULT. Re-lands #2769 (fixes #2768). --- .../java/io/agentscope/core/ReActAgent.java | 3 +- ...ReActAgentLegacyPermissionContextTest.java | 106 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java 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 f2b532d1b8..19ba75a15b 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -435,7 +435,8 @@ private static VersionedState loadOrCreateAgentStateForSlot( return versioned; } LegacyStateLoader.LegacyLoadResult legacy = - LegacyStateLoader.loadFromLegacySessionWithPresence(stateStore, userId, sessionId); + LegacyStateLoader.loadFromLegacySessionWithPresence( + stateStore, userId, sessionId, permCtx); if (legacy.found()) { // Legacy keys have no version; treat as create-if-absent baseline. long version = stateStore.supportsVersioning() ? 0L : AgentStateStore.UNVERSIONED; diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java new file mode 100644 index 0000000000..5fee00f3a2 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java @@ -0,0 +1,106 @@ +/* + * 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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.ReActAgent; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +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.permission.PermissionContextState; +import io.agentscope.core.permission.PermissionMode; +import io.agentscope.core.state.AgentState; +import io.agentscope.core.state.InMemoryAgentStateStore; +import io.agentscope.core.state.legacy.ToolkitState; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +/** + * End-to-end regression test for #2769: when a session has only v1 legacy keys + * ({@code memory_messages} / {@code toolkit_activeGroups}), the first call reconstructs state via + * {@code LegacyStateLoader}. Legacy keys cannot carry a permission context, so the builder-supplied + * {@code permissionContext} must be forwarded to the loader instead of silently falling back to + * {@link PermissionMode#DEFAULT}. + */ +class ReActAgentLegacyPermissionContextTest { + + private static final String SESSION = "session-legacy-perm"; + + private final InMemoryAgentStateStore stateStore = new InMemoryAgentStateStore(); + + @Test + void legacySessionLoad_keepsBuilderPermissionContext() { + Msg legacyMsg = Msg.builder().role(MsgRole.USER).textContent("hello from v1").build(); + stateStore.save(null, SESSION, "memory_messages", List.of(legacyMsg)); + stateStore.save( + null, SESSION, "toolkit_activeGroups", new ToolkitState(List.of("team-alpha"))); + + ReActAgent agent = + ReActAgent.builder() + .name("asst") + .model(new TextModel()) + .stateStore(stateStore) + .defaultSessionId(SESSION) + .permissionContext( + PermissionContextState.builder() + .mode(PermissionMode.BYPASS) + .build()) + .build(); + + Msg reply = agent.call(List.of()).block(); + assertNotNull(reply); + + AgentState state = agent.getAgentState(); + assertEquals( + PermissionMode.BYPASS, + state.getPermissionContext().getMode(), + "builder-supplied permission context must survive legacy v1 session loading"); + assertTrue( + state.getContext().stream() + .anyMatch(m -> "hello from v1".equals(m.getTextContent())), + "legacy conversation context must be preserved"); + assertEquals( + List.of("team-alpha"), + state.getToolContext().getActivatedGroups(), + "legacy tool activation groups must be preserved"); + } + + private static final class TextModel extends ChatModelBase { + @Override + public String getModelName() { + return "text"; + } + + @Override + protected Flux doStream( + List messages, List tools, GenerateOptions options) { + return Flux.just( + ChatResponse.builder() + .content( + List.of(TextBlock.builder().text("done").build())) + .build()); + } + } +} From d9efd6189bed4d19ec68d1c26fe127e9cf498a5e Mon Sep 17 00:00:00 2001 From: changningbo Date: Mon, 31 Aug 2026 18:16:36 +0800 Subject: [PATCH 2/2] fix(core): keep slot identity while loading legacy v1 session state (#2768) --- .../core/state/LegacyStateLoader.java | 22 +++++++++++- ...ReActAgentLegacyPermissionContextTest.java | 34 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/state/LegacyStateLoader.java b/agentscope-core/src/main/java/io/agentscope/core/state/LegacyStateLoader.java index 9491b2f900..5e27b0dfc4 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/state/LegacyStateLoader.java +++ b/agentscope-core/src/main/java/io/agentscope/core/state/LegacyStateLoader.java @@ -51,7 +51,13 @@ private LegacyStateLoader() {} * @param userId nullable user identifier * @param sessionId the session identifier (required) * @return a new AgentState populated with the legacy data + * @deprecated Use + * {@link #loadFromLegacySession(AgentStateStore, String, String, PermissionContextState)} + * instead. Legacy keys never carry a permission context, so this variant reconstructs the + * state with the default permission mode and silently discards any caller-configured + * context. */ + @Deprecated public static AgentState loadFromLegacySession( AgentStateStore stateStore, String userId, String sessionId) { return loadFromLegacySessionWithPresence(stateStore, userId, sessionId, null).state(); @@ -65,6 +71,10 @@ public static AgentState loadFromLegacySession( * preserve a non-default permission mode (for example {@code BYPASS}) when reconstructing * state from a v1 session. * + *

Unlike {@code freshState}, v1 keys must be upgraded in place: the reconstructed state + * keeps the {@code userId} / {@code sessionId} identity of the migrated session so it stays + * the same slot. + * * @param stateStore the state store to read from * @param userId nullable user identifier * @param sessionId the session identifier (required) @@ -90,7 +100,14 @@ public static AgentState loadFromLegacySession( * context to the reconstructed state; legacy keys themselves never carry one. Callers that * do not need a specific permission mode may use the {@code (stateStore, userId, sessionId)} * variant. + * + * @deprecated Use + * {@link #loadFromLegacySessionWithPresence(AgentStateStore, String, String, PermissionContextState)} + * instead. Legacy keys never carry a permission context, so this variant reconstructs the + * state with the default permission mode and silently discards any caller-configured + * context. */ + @Deprecated public static LegacyLoadResult loadFromLegacySessionWithPresence( AgentStateStore stateStore, String userId, String sessionId) { return loadFromLegacySessionWithPresence(stateStore, userId, sessionId, null); @@ -119,7 +136,10 @@ public static LegacyLoadResult loadFromLegacySessionWithPresence( Optional toolkitState = stateStore.get(userId, sessionId, "toolkit_activeGroups", ToolkitState.class); - AgentState.Builder builder = AgentState.builder().context(msgs); + AgentState.Builder builder = AgentState.builder().sessionId(sessionId).context(msgs); + if (userId != null) { + builder.userId(userId); + } if (permCtx != null) { builder.permissionContext(permCtx); } diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java index 5fee00f3a2..21fafbbda6 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentLegacyPermissionContextTest.java @@ -17,6 +17,7 @@ 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.assertTrue; import io.agentscope.core.ReActAgent; @@ -77,6 +78,11 @@ void legacySessionLoad_keepsBuilderPermissionContext() { PermissionMode.BYPASS, state.getPermissionContext().getMode(), "builder-supplied permission context must survive legacy v1 session loading"); + assertEquals( + SESSION, + state.getSessionId(), + "migrated state must keep the slot's session id instead of a random value"); + assertNull(state.getUserId(), "anonymous slot keeps a null user id"); assertTrue( state.getContext().stream() .anyMatch(m -> "hello from v1".equals(m.getTextContent())), @@ -87,6 +93,34 @@ void legacySessionLoad_keepsBuilderPermissionContext() { "legacy tool activation groups must be preserved"); } + @Test + void existingV2State_winsOverBuilderPermissionContext() { + // A pre-existing 2.0 state is authoritative: its own permission context must not be + // clobbered by the builder template on a legacy-triggered load. + stateStore.save( + null, SESSION, "agent_state", AgentState.builder().sessionId(SESSION).build()); + + ReActAgent agent = + ReActAgent.builder() + .name("asst") + .model(new TextModel()) + .stateStore(stateStore) + .defaultSessionId(SESSION) + .permissionContext( + PermissionContextState.builder() + .mode(PermissionMode.BYPASS) + .build()) + .build(); + + Msg reply = agent.call(List.of()).block(); + assertNotNull(reply); + + assertEquals( + PermissionMode.DEFAULT, + agent.getAgentState().getPermissionContext().getMode(), + "an existing v2 state keeps its own permission context over the builder template"); + } + private static final class TextModel extends ChatModelBase { @Override public String getModelName() {