feat(agent): refactor session management to dual-key (conversationId + agentKey)

AgentSessionManager 从单一 sessionId 改为 (conversationId, agentKey) 双 key 架构:
同一 conversationId 下多个 agent 共享 workspace 目录但各自拥有独立的 AgentSession
和对话记忆。删除 NanoIdSessionIdGenerator(conversationId 生成职责上移到 core 层
的 ConversationIdGenerator)。新增 ReActAgentConversationContinuityTest 和
ReActAgentMultiAgentChainTest 覆盖对话连续性和多 agent 协作场景。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-05-09 15:01:39 +08:00
parent a5735c3f4d
commit 3175af400d
9 changed files with 325 additions and 68 deletions

View File

@@ -5,23 +5,73 @@ import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
/**
* 单个 agent 在某次会话中的运行时状态。
*
* <p>会话标识被拆分为两个维度:
* <ul>
* <li>{@code conversationId}:业务/对话维度,由调用方决定,整条 chain 内所有 agent 共享,
* 决定 workspace 目录与对话连续性。</li>
* <li>{@code agentKey}:组件维度,默认是 {@code nodeId},用于在同一段对话中区分
* 不同 agent 的 ReActAgent 实例和持久化记忆。</li>
* </ul>
*
* <p>{@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();
}
}

View File

@@ -25,22 +25,27 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* 跟踪当前 JVM 中存活的 AgentSession并桥接到可插拔的
* 跟踪当前 JVM 中存活的 {@link AgentSession},并桥接到可插拔的
* {@link Session}(由 {@link AgentSessionFactoryRegistry} 提供)。
*
* <p>这里将两类职责保持独立
* <p>会话标识被拆分为两个维度
* <ul>
* <li>JVM 内缓存、加锁和 LRU 淘汰(本类负责)</li>
* <li>持久化存储(委托给 AgentScope 的 Session 抽象)</li>
* <li>{@code conversationId}:业务/对话维度,决定 workspace 目录与连续对话的恢复。</li>
* <li>{@code agentKey}:组件维度(默认为 {@code nodeId}),用于在同一段对话内
* 区分不同 agent 的 ReActAgent 实例与对话记忆。</li>
* </ul>
*
* <p>淘汰(空闲或超过容量)只移除缓存的 agent 实例。
* workspace 文件以及磁盘、Redis、MySQL 中的持久化 session 数据会被保留。
* <p>缓存与持久化都按 {@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<String, AgentSession> sessions = new ConcurrentHashMap<>();
@@ -73,26 +78,37 @@ public class AgentSessionManager implements AutoCloseable {
cleaner.scheduleWithFixedDelay(this::cleanup, every, every, TimeUnit.MILLISECONDS);
}
public AgentSession acquire(String sessionId) {
String safe = safeId(sessionId);
AgentSession s = sessions.computeIfAbsent(safe, id -> {
Path ws = root.resolve(id);
try { Files.createDirectories(ws); }
catch (IOException e) { throw new AgentConfigException("cannot create workspace: " + ws, e); }
return new AgentSession(id, ws);
/**
* 获取(或创建)一个 agent 会话。
*
* <p>同一 {@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)) {

View File

@@ -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);
}
}

View File

@@ -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 维度上的语义:
* <ul>
* <li>{@code .autoConversationId()} 让框架自动生成 NanoId 标识response 可读到;</li>
* <li>{@code .conversationId(cid)} 按调用方传入的标识落到 slot</li>
* <li>跨次调用使用同一 conversationId 时,第二次执行能读到第一次写入 workspace 的文件
* (即同一段对话的状态被保留),可直接支持"连续对话"场景;</li>
* <li>未声明 cid 时不主动写入 slot由组件按需自行处理不破坏既有 chain 行为)。</li>
* </ul>
*/
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());
}
}

View File

@@ -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);

View File

@@ -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 时的会话/工作区行为:
* <ul>
* <li>整个 chain 共享同一个 conversationId首个 agent 通过默认 resolver 从 requestData 中解析后
* 通过 {@code slot.setConversationId} 写回,后续 agent 直接读取);</li>
* <li>workspace 目录按 conversationId 创建一次,多个 agent 共享,可以彼此读取对方写入的文件;</li>
* <li>每个 agent 的 {@code agentKey}(默认为 nodeId不同进而拥有独立的 ReActAgent 实例与持久化记忆 key。</li>
* </ul>
*/
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<String, Object> req = Map.of("conversationId", CONV_ID, "userInput", "go");
LiteflowResponse response = flowExecutor.execute2Resp("collabAgentChain", req);
Assertions.assertTrue(response.isSuccess(),
"chain failed: " + (response.getCause() == null ? "" : response.getCause().getMessage()));
// 1. chain 整体共享 conversationId来自请求 Map 中的 "conversationId"
Assertions.assertEquals(CONV_ID, response.getConversationId(),
"conversationId in request map should propagate to LiteflowResponse");
Assertions.assertEquals(CONV_ID, CollabAgentACmp.SEEN_CONVERSATION_ID.get(),
"first agent should resolve conversationId from request data");
Assertions.assertEquals(CONV_ID, CollabAgentBCmp.SEEN_CONVERSATION_ID.get(),
"second agent should reuse the same conversationId via slot");
// 2. workspace 共享A 和 B 看到的 workspace 路径一致
Assertions.assertNotNull(CollabAgentACmp.SEEN_WORKSPACE.get());
Assertions.assertEquals(CollabAgentACmp.SEEN_WORKSPACE.get(), CollabAgentBCmp.SEEN_WORKSPACE.get(),
"both agents in the same conversation should resolve to the same workspace dir");
// 3. agentKey 不同:默认对应各自的 nodeId
Assertions.assertEquals("collabAgentA", CollabAgentACmp.SEEN_AGENT_KEY.get());
Assertions.assertEquals("collabAgentB", CollabAgentBCmp.SEEN_AGENT_KEY.get());
// 4. 文件协作A 写入的 marker 在 B 中可读
Assertions.assertEquals(CollabAgentACmp.MARKER_CONTENT, CollabAgentBCmp.READ_MARKER.get(),
"agent B should be able to read the marker file written by agent A in the shared workspace");
}
@Test
public void testDifferentConversationIdsGetSeparateWorkspaces() {
flowExecutor.execute2Resp("collabAgentChain",
Map.of("conversationId", "convX", "userInput", "x"));
String workspaceX = CollabAgentACmp.SEEN_WORKSPACE.get();
flowExecutor.execute2Resp("collabAgentChain",
Map.of("conversationId", "convY", "userInput", "y"));
String workspaceY = CollabAgentACmp.SEEN_WORKSPACE.get();
Assertions.assertNotNull(workspaceX);
Assertions.assertNotNull(workspaceY);
Assertions.assertNotEquals(workspaceX, workspaceY,
"different conversationIds must resolve to different workspace directories");
}
}

View File

@@ -8,25 +8,25 @@ import org.junit.jupiter.api.Test;
/**
* 测试 ReAct Agent 的 Session 管理行为。
*
* <p>测试桩组件固定返回同一个 sessionId因此同一个测试方法内连续执行两次链路时
* <p>测试桩组件固定返回同一个 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());
}
}

View File

@@ -10,16 +10,16 @@ import java.nio.file.Path;
/**
* 测试 ReAct Agent 的 Workspace 目录管理。
*
* <p>ReActAgentComponent 会通过 AgentSessionManager 为每个 session 创建独立工作目录。
* 这个类只验证 workspace 的创建和 sessionId 到目录名的映射,不测试工具注册细节。
* <p>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());
}
}

View File

@@ -22,4 +22,8 @@
THEN(prepare, stubAgent, recordReply);
</chain>
<chain name="collabAgentChain">
THEN(prepare, collabAgentA, collabAgentB, recordReply);
</chain>
</flow>