diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSession.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSession.java index fce034562..f011233b8 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSession.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSession.java @@ -5,23 +5,73 @@ import java.time.Instant; import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; +/** + * 单个 agent 在某次会话中的运行时状态。 + * + *
会话标识被拆分为两个维度: + *
{@code workspaceDir} 仅按 {@code conversationId} 创建,因此同一段对话中的多个 agent + * 共享同一个工作区目录(实现 agent 之间的文件协作)。 + */ public class AgentSession { - private final String sessionId; + + private final String conversationId; + private final String agentKey; + private final String cacheKey; private final Path workspaceDir; private final ReentrantLock lock = new ReentrantLock(); private volatile Object agent; private volatile Instant lastActive = Instant.now(); - public AgentSession(String sessionId, Path workspaceDir) { - this.sessionId = Objects.requireNonNull(sessionId); - this.workspaceDir = Objects.requireNonNull(workspaceDir); + public AgentSession(String conversationId, String agentKey, String cacheKey, Path workspaceDir) { + this.conversationId = Objects.requireNonNull(conversationId, "conversationId"); + this.agentKey = Objects.requireNonNull(agentKey, "agentKey"); + this.cacheKey = Objects.requireNonNull(cacheKey, "cacheKey"); + this.workspaceDir = Objects.requireNonNull(workspaceDir, "workspaceDir"); } - public String getSessionId() { return sessionId; } - public Path getWorkspaceDir() { return workspaceDir; } - public ReentrantLock getLock() { return lock; } - public Object getAgent() { return agent; } - public void setAgent(Object agent) { this.agent = agent; } - public Instant getLastActive() { return lastActive; } - public void touch() { this.lastActive = Instant.now(); } + public String getConversationId() { + return conversationId; + } + + public String getAgentKey() { + return agentKey; + } + + /** + * JVM 内的缓存 key 与持久化 key,由 {@code conversationId} 与 {@code agentKey} 组合并安全编码后得到。 + */ + public String getCacheKey() { + return cacheKey; + } + + public Path getWorkspaceDir() { + return workspaceDir; + } + + public ReentrantLock getLock() { + return lock; + } + + public Object getAgent() { + return agent; + } + + public void setAgent(Object agent) { + this.agent = agent; + } + + public Instant getLastActive() { + return lastActive; + } + + public void touch() { + this.lastActive = Instant.now(); + } } diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java index 30301e927..798e4b6a6 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java @@ -25,22 +25,27 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** - * 跟踪当前 JVM 中存活的 AgentSession,并桥接到可插拔的 + * 跟踪当前 JVM 中存活的 {@link AgentSession},并桥接到可插拔的 * {@link Session}(由 {@link AgentSessionFactoryRegistry} 提供)。 * - *
这里将两类职责保持独立: + *
会话标识被拆分为两个维度: *
淘汰(空闲或超过容量)只移除缓存的 agent 实例。 - * workspace 文件以及磁盘、Redis、MySQL 中的持久化 session 数据会被保留。 + *
缓存与持久化都按 {@code (conversationId, agentKey)} 组合 key 隔离;
+ * workspace 目录则只按 {@code conversationId} 建一份,让同一段对话中的多个 agent
+ * 通过共享文件协作。
*/
public class AgentSessionManager implements AutoCloseable {
private static final Pattern SAFE = Pattern.compile("[a-zA-Z0-9_\\-]+");
+ /** 缓存 key 内部使用的分隔符;不在 SAFE 字符集中以外,且不会出现在 NanoId 输出里。 */
+ static final String KEY_SEPARATOR = "__";
+
private final AgentConfig config;
private final Path root;
private final Map 同一 {@code conversationId} 下的多个 {@code agentKey} 共享同一个 workspace 目录,
+ * 但分别拥有独立的 {@link AgentSession}(独立的 ReActAgent 实例、独立的记忆持久化 key)。
+ */
+ public AgentSession acquire(String conversationId, String agentKey) {
+ String safeCid = safeId(conversationId);
+ String safeKey = safeId(agentKey);
+ String cacheKey = safeCid + KEY_SEPARATOR + safeKey;
+ AgentSession s = sessions.computeIfAbsent(cacheKey, k -> {
+ Path ws = root.resolve(safeCid);
+ try {
+ Files.createDirectories(ws);
+ } catch (IOException e) {
+ throw new AgentConfigException("cannot create workspace: " + ws, e);
+ }
+ return new AgentSession(safeCid, safeKey, k, ws);
});
s.touch();
enforceMaxSessions();
return s;
}
- public boolean contains(String sessionId) {
- return sessions.containsKey(safeId(sessionId));
+ public boolean contains(String conversationId, String agentKey) {
+ return sessions.containsKey(safeId(conversationId) + KEY_SEPARATOR + safeId(agentKey));
}
/**
* 将之前持久化的状态懒加载恢复到 agent 中。
- * 同一个 session id 在当前 JVM 生命周期内应只调用一次,并且应在 agent
+ * 同一个 {@code (conversationId, agentKey)} 在当前 JVM 生命周期内应只调用一次,并且应在 agent
* 构建完成后、首次 {@code agent.call(...)} 前调用。
*/
public void loadIfExists(AgentSession session, ReActAgent agent) {
@@ -100,7 +116,7 @@ public class AgentSessionManager implements AutoCloseable {
MemoryStorageConfig mc = config.getSession().getMemory();
if (!mc.isLoadOnFirstUse()) return;
if (mc.getMode() == MemoryStorageMode.NONE) return;
- SessionManager.forSessionId(session.getSessionId())
+ SessionManager.forSessionId(session.getCacheKey())
.withSession(storage)
.addComponent(agent)
.loadIfExists();
@@ -111,7 +127,7 @@ public class AgentSessionManager implements AutoCloseable {
if (storage == null || agent == null) return;
MemoryStorageConfig mc = config.getSession().getMemory();
if (mc.getMode() == MemoryStorageMode.NONE) return;
- SessionManager.forSessionId(session.getSessionId())
+ SessionManager.forSessionId(session.getCacheKey())
.withSession(storage)
.addComponent(agent)
.saveSession();
@@ -149,18 +165,27 @@ public class AgentSessionManager implements AutoCloseable {
}
/**
- * @param cleanWorkspace 为 true 时,同时删除磁盘上的 workspace 目录
- * (保留历史行为)。存储在其他位置的持久化 session
- * 状态(例如 workspaceRoot/.agent-session、Redis、
- * MySQL)不会在这里被删除。
+ * @param cleanWorkspace 为 true 时同时尝试删除磁盘上的 workspace 目录。由于同一 workspace
+ * 可能被 {@code (conversationId, *)} 下的多个 agent 共享,仅在该
+ * conversation 下没有其他存活会话时才会真正删除目录。
+ * 存储在其他位置的持久化 session 状态(例如 workspaceRoot/.agent-session、
+ * Redis、MySQL)不会在这里被删除。
*/
private void evictFromCache(AgentSession s, boolean cleanWorkspace) {
- sessions.remove(s.getSessionId(), s);
- if (cleanWorkspace) {
+ sessions.remove(s.getCacheKey(), s);
+ if (cleanWorkspace && !hasSiblingInSameConversation(s)) {
deleteRecursively(s.getWorkspaceDir());
}
}
+ private boolean hasSiblingInSameConversation(AgentSession evicted) {
+ String prefix = evicted.getConversationId() + KEY_SEPARATOR;
+ for (String key : sessions.keySet()) {
+ if (key.startsWith(prefix)) return true;
+ }
+ return false;
+ }
+
private static void deleteRecursively(Path p) {
if (!Files.exists(p)) return;
try (var walk = Files.walk(p)) {
diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/NanoIdSessionIdGenerator.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/NanoIdSessionIdGenerator.java
deleted file mode 100644
index 3fa6085bc..000000000
--- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/NanoIdSessionIdGenerator.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.yomahub.liteflow.agent.session;
-
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.lang.id.NanoId;
-import cn.hutool.core.util.StrUtil;
-
-import java.util.Date;
-
-public class NanoIdSessionIdGenerator {
-
- private static final char[] CODE_ALPHABET =
- "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".toCharArray();
-
- public static String generate() {
- String date = DateUtil.format(new Date(), "yyyyMMdd");
- String code = NanoId.randomNanoId(null, CODE_ALPHABET, 12);
- return StrUtil.format("{}_{}", date, code);
- }
-}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentConversationContinuityTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentConversationContinuityTest.java
new file mode 100644
index 000000000..3e3cca76d
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentConversationContinuityTest.java
@@ -0,0 +1,118 @@
+package com.yomahub.liteflow.test.agent;
+
+import com.yomahub.liteflow.core.ExecuteOption;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.test.agent.cmp.CollabAgentACmp;
+import com.yomahub.liteflow.test.agent.cmp.CollabAgentBCmp;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * 验证 {@code flowExecutor.execute2Resp(chainId, param, ExecuteOption)} 入口
+ * 在 conversationId 维度上的语义:
+ * 测试桩组件固定返回同一个 sessionId,因此同一个测试方法内连续执行两次链路时,
+ * 测试桩组件固定返回同一个 conversationId,因此同一个测试方法内连续执行两次链路时,
* 第二次应该复用第一次构建好的 ReActAgent 实例,而不是重新构建模型、工具和系统提示词。
*/
public class ReActAgentSessionTest extends AbstractReActAgentSpringbootTest {
/**
- * 验证相同 sessionId 会复用同一个受管 ReActAgent 实例。
+ * 验证相同 conversationId + 同一 agent 节点会复用受管 ReActAgent 实例。
*/
@Test
- public void testStubAgentReusesManagedSessionForSameSessionId() {
+ public void testStubAgentReusesManagedSessionForSameConversationId() {
LiteflowResponse first = flowExecutor.execute2Resp("stubAgentChain", "first");
LiteflowResponse second = flowExecutor.execute2Resp("stubAgentChain", "second");
Assertions.assertTrue(first.isSuccess());
Assertions.assertTrue(second.isSuccess());
- // 同一个 sessionId 只会触发一次模型解析,说明第二次执行复用了已有 agent。
+ // 同一个 conversationId 在同一 agent 上只会触发一次模型解析,说明第二次执行复用了已有 agent。
Assertions.assertEquals(1, StubReActAgentCmp.SPEC_RESOLVE_COUNT.get(),
- "same session id should reuse built ReActAgent");
+ "same conversation id + agent key should reuse built ReActAgent");
// 系统提示词只在 agent 构建阶段使用一次,用户提示词则每次执行都要重新生成。
Assertions.assertEquals(1, StubReActAgentCmp.SYSTEM_PROMPT_COUNT.get(),
@@ -40,5 +40,8 @@ public class ReActAgentSessionTest extends AbstractReActAgentSpringbootTest {
// 第二次执行仍然能读取新的 LiteFlow 请求数据,而不是复用第一次的用户输入。
Assertions.assertTrue(StubReActAgentCmp.MODEL_PROBES.get(1).inputTexts().contains("second"));
+
+ // LiteflowResponse 应该能透出 chain 内解析到的 conversationId。
+ Assertions.assertEquals(StubReActAgentCmp.FIXED_CONVERSATION_ID, first.getConversationId());
}
}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentWorkspaceTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentWorkspaceTest.java
index aac86e664..2f17cb670 100644
--- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentWorkspaceTest.java
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentWorkspaceTest.java
@@ -10,16 +10,16 @@ import java.nio.file.Path;
/**
* 测试 ReAct Agent 的 Workspace 目录管理。
*
- * ReActAgentComponent 会通过 AgentSessionManager 为每个 session 创建独立工作目录。
- * 这个类只验证 workspace 的创建和 sessionId 到目录名的映射,不测试工具注册细节。
+ * ReActAgentComponent 会通过 AgentSessionManager 为每个 conversation 创建独立工作目录。
+ * 这个类只验证 workspace 的创建和 conversationId 到目录名的映射,不测试工具注册细节。
*/
public class ReActAgentWorkspaceTest extends AbstractReActAgentSpringbootTest {
/**
- * 验证执行 agent 节点时会为当前 session 创建 workspace。
+ * 验证执行 agent 节点时会按 conversationId 创建 workspace 目录。
*/
@Test
- public void testWorkspaceIsCreatedForResolvedSessionId() {
+ public void testWorkspaceIsCreatedForResolvedConversationId() {
LiteflowResponse response = flowExecutor.execute2Resp("stubAgentChain", "workspace");
Assertions.assertTrue(response.isSuccess());
@@ -27,12 +27,12 @@ public class ReActAgentWorkspaceTest extends AbstractReActAgentSpringbootTest {
StubReActAgentCmp.ModelProbe probe = StubReActAgentCmp.MODEL_PROBES.get(0);
- // 模型桩会记录 ReActAgentContext 中的 sessionId 和 workspace 路径。
- Assertions.assertEquals(StubReActAgentCmp.FIXED_SESSION_ID, probe.sessionId());
+ // 模型桩会记录 ReActAgentContext 中的 conversationId 和 workspace 路径。
+ Assertions.assertEquals(StubReActAgentCmp.FIXED_CONVERSATION_ID, probe.conversationId());
Assertions.assertTrue(probe.workspaceExists());
- // 固定 sessionId 应该出现在 workspace 目录末尾,便于排查和隔离不同会话文件。
- Assertions.assertEquals(StubReActAgentCmp.FIXED_SESSION_ID,
+ // 固定 conversationId 应该出现在 workspace 目录末尾,便于排查和隔离不同会话文件。
+ Assertions.assertEquals(StubReActAgentCmp.FIXED_CONVERSATION_ID,
Path.of(probe.workspaceDir()).getFileName().toString());
}
}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml
index 008537733..8ff3d8576 100644
--- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml
@@ -22,4 +22,8 @@
THEN(prepare, stubAgent, recordReply);
+
+ *
+ */
+public class ReActAgentConversationContinuityTest extends AbstractReActAgentSpringbootTest {
+
+ @BeforeEach
+ public void resetCollabState() {
+ CollabAgentACmp.reset();
+ CollabAgentBCmp.reset();
+ }
+
+ @Test
+ public void testAutoConversationIdGeneratesNanoId() {
+ LiteflowResponse response = flowExecutor.execute2Resp("collabAgentChain", "go",
+ ExecuteOption.of().autoConversationId());
+
+ Assertions.assertTrue(response.isSuccess());
+ String generated = response.getConversationId();
+ Assertions.assertNotNull(generated, "autoConversationId() should produce a non-null cid");
+ Assertions.assertFalse(generated.isBlank());
+ Assertions.assertEquals(generated, CollabAgentACmp.SEEN_CONVERSATION_ID.get());
+ Assertions.assertEquals(generated, CollabAgentBCmp.SEEN_CONVERSATION_ID.get());
+ }
+
+ @Test
+ public void testExplicitConversationIdHonored() {
+ String cid = "task-explicit-001";
+ LiteflowResponse response = flowExecutor.execute2Resp("collabAgentChain", "go",
+ ExecuteOption.of().conversationId(cid));
+
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals(cid, response.getConversationId());
+ Assertions.assertEquals(cid, CollabAgentACmp.SEEN_CONVERSATION_ID.get());
+ Assertions.assertEquals(cid, CollabAgentBCmp.SEEN_CONVERSATION_ID.get());
+ }
+
+ /**
+ * 同一 conversationId 跨次调用:第二次进入 agent A 时应能看到第一次留下的标记文件。
+ */
+ @Test
+ public void testSameConversationIdSharesWorkspaceAcrossCalls() {
+ // 每次运行用唯一 cid,避免上一次 mvn test 残留在 target/wk_root 下的目录干扰断言。
+ String cid = "continuity-" + System.nanoTime();
+
+ LiteflowResponse first = flowExecutor.execute2Resp("collabAgentChain", "first",
+ ExecuteOption.of().conversationId(cid));
+ Assertions.assertTrue(first.isSuccess());
+ String workspaceFromFirst = CollabAgentACmp.SEEN_WORKSPACE.get();
+ Assertions.assertNotNull(workspaceFromFirst);
+ Assertions.assertEquals(Boolean.FALSE, CollabAgentACmp.MARKER_EXISTED_BEFORE_WRITE.get(),
+ "first call should see no pre-existing marker file");
+
+ CollabAgentACmp.reset();
+ CollabAgentBCmp.reset();
+
+ LiteflowResponse second = flowExecutor.execute2Resp("collabAgentChain", "second",
+ ExecuteOption.of().conversationId(cid));
+ Assertions.assertTrue(second.isSuccess());
+ Assertions.assertEquals(cid, second.getConversationId());
+ Assertions.assertEquals(workspaceFromFirst, CollabAgentACmp.SEEN_WORKSPACE.get(),
+ "second call with same conversationId should resolve to the same workspace dir");
+ Assertions.assertEquals(Boolean.TRUE, CollabAgentACmp.MARKER_EXISTED_BEFORE_WRITE.get(),
+ "second call should observe the marker written by the previous call - workspace state persists across calls");
+ Assertions.assertEquals(CollabAgentACmp.MARKER_CONTENT, CollabAgentBCmp.READ_MARKER.get());
+ }
+
+ /**
+ * 不在 ExecuteOption 中声明 cid 时(既不调用 .conversationId 也不调用 .autoConversationId),
+ * 框架不应主动写入 slot.conversationId,沿用既有 chain 行为。
+ * 但 ReActAgentComponent 自己的 resolver 仍会兜底为本次执行生成一个,这是组件层面的契约,
+ * 与 FlowExecutor 入口语义独立。
+ */
+ @Test
+ public void testNullExecuteOptionDoesNotForceCidAtExecutorLayer() {
+ LiteflowResponse response = flowExecutor.execute2Resp("collabAgentChain", "no-cid",
+ ExecuteOption.of()); // 既未 .conversationId 也未 .autoConversationId
+
+ Assertions.assertTrue(response.isSuccess());
+ // 在没有显式声明的情况下,由 ReActAgentComponent 默认 resolver 生成 cid 并写回 slot;
+ // 因此 response 仍能取到 cid,只是这个 cid 来自组件而非 FlowExecutor 入口。
+ Assertions.assertNotNull(response.getConversationId());
+ }
+
+ /**
+ * rid + cid 同时设置:验证组合不爆炸,所有维度都生效。
+ */
+ @Test
+ public void testRequestIdAndConversationIdComposed() {
+ String rid = "req-id-xyz";
+ String cid = "conv-id-xyz";
+ LiteflowResponse response = flowExecutor.execute2Resp("collabAgentChain", "compose",
+ ExecuteOption.of()
+ .requestId(rid)
+ .conversationId(cid));
+
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals(rid, response.getRequestId());
+ Assertions.assertEquals(cid, response.getConversationId());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELChainTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELChainTest.java
index c1cd751b4..8e10416da 100644
--- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELChainTest.java
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELChainTest.java
@@ -31,7 +31,7 @@ public class ReActAgentELChainTest extends AbstractReActAgentSpringbootTest {
// recordReply 会把 agent 回复保存到本节点 output,测试由此确认后置节点拿到了回复。
Object reply = response.getSlot().getOutput(RecordReplyCmp.NODE_ID);
Assertions.assertNotNull(reply);
- Assertions.assertTrue(reply.toString().contains("reply:" + StubReActAgentCmp.FIXED_SESSION_ID));
+ Assertions.assertTrue(reply.toString().contains("reply:" + StubReActAgentCmp.FIXED_CONVERSATION_ID));
// userPrompt 必须来自 prepare 写入的 chainReqData,不能绕开 LiteFlow slot 上下文。
Assertions.assertEquals(List.of("hello-liteflow-agent"), StubReActAgentCmp.USER_PROMPTS);
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentMultiAgentChainTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentMultiAgentChainTest.java
new file mode 100644
index 000000000..7682ff7f5
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentMultiAgentChainTest.java
@@ -0,0 +1,76 @@
+package com.yomahub.liteflow.test.agent;
+
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.test.agent.cmp.CollabAgentACmp;
+import com.yomahub.liteflow.test.agent.cmp.CollabAgentBCmp;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+/**
+ * 验证一条 chain 内编排多个 ReAct Agent 时的会话/工作区行为:
+ *
+ *
+ */
+public class ReActAgentMultiAgentChainTest extends AbstractReActAgentSpringbootTest {
+
+ private static final String CONV_ID = "collab-conv-1024";
+
+ @BeforeEach
+ public void resetCollabState() {
+ CollabAgentACmp.reset();
+ CollabAgentBCmp.reset();
+ }
+
+ @Test
+ public void testMultipleAgentsShareWorkspaceButHaveDistinctAgentKeys() {
+ Map