无状态 Agent,比如我们的QueryRewritingAgent、IntentRecognitionAgent 这类"单次调用、不持有 Memory"的分析型 Agent。我们也需要针对他们做session的持久化,但是这类Agent他并不是StateModule,无法直接做session的读取和存储。 于是,我们在SessionPersistenceHook需要针对这种情况做特殊处理:
/**
* 非 ReActAgent 的 AgentBase(单次调用型分析 Agent,如 QueryRewritingAgent、IntentRecognitionAgent)
* 的 Session 持久化。
*
* <p>这类 Agent 每次 {@code doCall} 都是「系统提示词 + 本轮输入」的一次性调用,自身不维护 Memory,
* 因此这里绕开 StateModule(默认 no-op),直接读写 {@link Session} 的原始消息列表:
* <ul>
* <li>{@link PreCallEvent}:读取此前累计的历史消息,与本轮输入拼接后写回
* {@link PreCallEvent#setInputMessages},使 {@code doCall} 能感知历史上下文;</li>
* <li>{@link PostCallEvent}:取出 PreCall 阶段暂存的「历史 + 本轮输入」,追加本轮回复后
* 整体写回 Session,供下一轮加载;超过 {@link #MAX_HISTORY_MESSAGES} 时从头部裁剪。</li>
* </ul>
*/
private void persistStatelessAgent(HookEvent event, String sessionKey, String sessionId,
String agentName, boolean resuming) {
SessionKey key = SimpleSessionKey.of(sessionKey);
if (event instanceof PreCallEvent preCallEvent) {
if (resuming) {
logger.debug("[SessionPersistence] 恢复暂停 Agent,跳过历史记忆加载: sessionId={}, agent={}", sessionId, agentName);
return;
}
List<Msg> history = session.exists(key)
? session.getList(key, MEMORY_MESSAGES_KEY, Msg.class)
: List.<Msg>of();
List<Msg> merged = new ArrayList<>(history);
merged.addAll(preCallEvent.getInputMessages());
if (!history.isEmpty()) {
preCallEvent.setInputMessages(merged);
logger.debug("[SessionPersistence] 已加载历史记忆(无状态 Agent): sessionId={}, agent={}, historySize={}",
sessionId, agentName, history.size());
}
// 暂存本轮「历史 + 输入」,供 PostCall 追加回复后一并写回 Session
pendingStatelessInput.put(sessionKey, merged);
} else {
PostCallEvent postCallEvent = (PostCallEvent) event;
List<Msg> merged = pendingStatelessInput.remove(sessionKey);
List<Msg> newHistory = merged != null ? new ArrayList<>(merged) : new ArrayList<>();
newHistory.add(postCallEvent.getFinalMessage());
if (newHistory.size() > MAX_HISTORY_MESSAGES) {
newHistory = new ArrayList<>(
newHistory.subList(newHistory.size() - MAX_HISTORY_MESSAGES, newHistory.size()));
}
session.save(key, MEMORY_MESSAGES_KEY, newHistory);
logger.debug("[SessionPersistence] 已保存对话记忆(无状态 Agent): sessionId={}, agent={}, size={}",
sessionId, agentName, newHistory.size());
}
}
它还暴露了 appendStatelessHistory(sessionId, agentName, userMsg, assistantMsg):当快路径(如意图直接命中、跳过了某无状态 Agent 的真实 call())也想让该 Agent"以为自己被调用过",可合成一对 USER→ASSISTANT 消息写入其历史,存储契约与上面完全一致,下一轮 PreCall 能无缝加载。
/**
* 对外暴露的「无状态 Agent 影子历史追加」接口。
*
* <p>适用于快路径(例如 L1/L2 意图命中)绕过了某个无状态 Agent 的实际 {@code call()},
* 却又希望下一轮该 Agent 依然能读到本轮上下文的场景:调用方可以合成一对
* {@code USER → ASSISTANT} 消息,通过本方法写入对应 Agent 的 Session 历史,
* 效果等价于该 Agent 真正被调用过一次。
*
* <p>存储契约与 {@link #persistStatelessAgent} 完全一致(同 key、同 MEMORY_MESSAGES_KEY、
* 同 MAX_HISTORY_MESSAGES 裁剪),因此下一轮 {@link PreCallEvent} 能被无缝加载。
*
* @param sessionId 对话会话 ID
* @param agentName 目标无状态 Agent 名(如 {@code QueryRewritingAgent})
* @param userMsg 合成的用户消息(不能为 null)
* @param assistantMsg 合成的 Agent 回复消息(不能为 null)
*/
public void appendStatelessHistory(String sessionId, String agentName, Msg userMsg, Msg assistantMsg) {
if (sessionId == null || agentName == null || userMsg == null || assistantMsg == null) {
return;
}
String sessionKey = sessionId + ":" + agentName;
SessionKey key = SimpleSessionKey.of(sessionKey);
try {
List<Msg> history = session.exists(key)
? session.getList(key, MEMORY_MESSAGES_KEY, Msg.class)
: List.<Msg>of();
List<Msg> newHistory = new ArrayList<>(history);
newHistory.add(userMsg);
newHistory.add(assistantMsg);
if (newHistory.size() > MAX_HISTORY_MESSAGES) {
newHistory = new ArrayList<>(
newHistory.subList(newHistory.size() - MAX_HISTORY_MESSAGES, newHistory.size()));
}
session.save(key, MEMORY_MESSAGES_KEY, newHistory);
logger.debug("[SessionPersistence] 已追加影子历史(无状态 Agent): sessionId={}, agent={}, size={}",
sessionId, agentName, newHistory.size());
} catch (Exception e) {
logger.warn("[SessionPersistence] 追加影子历史失败 sessionId={}, agent={}: {}",
sessionId, agentName, e.getMessage());
}
}
暂存用 ConcurrentHashMap