mirror of
https://gitee.com/dromara/liteFlow.git
synced 2026-06-10 03:07:32 +08:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,8 @@
|
||||
THEN(prepare, stubAgent, recordReply);
|
||||
</chain>
|
||||
|
||||
<chain name="collabAgentChain">
|
||||
THEN(prepare, collabAgentA, collabAgentB, recordReply);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
||||
Reference in New Issue
Block a user