From 7e8560c481b08350c5f75e84e273d65ea643010f Mon Sep 17 00:00:00 2001
From: zichun <26684461+reporkey@users.noreply.github.com>
Date: Tue, 28 Jul 2026 13:23:54 +0800
Subject: [PATCH] P1: event log as single source of truth; 4-zone LLM projection
---
src/main/java/com/xly/agent/EventLogChatMemory.java | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xly/agent/ProjectedChatMemory.java | 173 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src/main/java/com/xly/config/AgentFactory.java | 26 +++++++++++++++++++-------
src/main/java/com/xly/config/RedisChatMemoryStore.java | 37 ++++++++++---------------------------
src/main/java/com/xly/service/ConversationService.java | 51 +++++++++++++++++----------------------------------
src/main/java/com/xly/service/EventProjectionService.java | 375 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xly/service/LedgerService.java | 45 ++++++++++++++++++++++++++++++++++++---------
src/main/java/com/xly/web/AgentChatController.java | 50 ++++++++++----------------------------------------
src/main/java/com/xly/web/OpController.java | 16 ++--------------
src/test/java/com/xly/service/ConversationScopeTest.java | 3 ++-
src/test/java/com/xly/service/EventLogConcurrencyTest.java | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/test/java/com/xly/service/EventProjectionTest.java | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/test/java/com/xly/service/InMemoryLedger.java | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 1045 insertions(+), 305 deletions(-)
create mode 100644 src/main/java/com/xly/agent/EventLogChatMemory.java
delete mode 100644 src/main/java/com/xly/agent/ProjectedChatMemory.java
create mode 100644 src/main/java/com/xly/service/EventProjectionService.java
create mode 100644 src/test/java/com/xly/service/EventLogConcurrencyTest.java
create mode 100644 src/test/java/com/xly/service/EventProjectionTest.java
create mode 100644 src/test/java/com/xly/service/InMemoryLedger.java
diff --git a/src/main/java/com/xly/agent/EventLogChatMemory.java b/src/main/java/com/xly/agent/EventLogChatMemory.java
new file mode 100644
index 0000000..ad7c2d1
--- /dev/null
+++ b/src/main/java/com/xly/agent/EventLogChatMemory.java
@@ -0,0 +1,157 @@
+package com.xly.agent;
+
+import com.xly.service.EventProjectionService;
+import com.xly.service.LedgerService;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.data.message.ChatMessageSerializer;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.memory.ChatMemory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 事件日志上的对话记忆:{@link #add} 把模型环消息逐条 append 成事件(rightPush 原子——
+ * 对话流与确认端点并发写互不覆盖,替代旧 chat:mem 整包读改写),{@link #messages()} 读取
+ * {@link EventProjectionService} 的四段式投影。日志即唯一事实源,本类不持有任何会话状态。
+ *
+ *
写者分工:用户事件由控制器在收到请求时先落账(前端立即可见),本类对 UserMessage 做
+ * 去重跳过;{@code internalUserTurn=true} 时(反编造护栏重试的注入话术)例外——落一条
+ * {@code internal} 标记的用户事件,LLM 可见、前端历史不显示。模型消息(tool_call/tool_result/ai)
+ * 只由本类落账。
+ */
+public class EventLogChatMemory implements ChatMemory {
+
+ private static final int LLM_EVENT_WINDOW = 400;
+
+ private final String convId;
+ private final LedgerService log;
+ private final EventProjectionService projection;
+ private final int charBudget;
+ private final boolean internalUserTurn;
+
+ /** system prompt 每次由 provider 提供,不落日志(保持日志纯业务事件)。 */
+ private volatile String systemText;
+ /**
+ * 本轮实际喂给模型的用户文本(原话 + 编排层附加的 grounding/状态后缀)。日志只存原话;
+ * 投影时把当前轮用户消息替换为它。P2 编排层不再加后缀后,此字段恒空。
+ */
+ private volatile String currentTurnUserText;
+
+ public EventLogChatMemory(String convId, LedgerService log, EventProjectionService projection,
+ int charBudget, boolean internalUserTurn) {
+ this.convId = convId;
+ this.log = log;
+ this.projection = projection;
+ this.charBudget = charBudget;
+ this.internalUserTurn = internalUserTurn;
+ }
+
+ @Override
+ public Object id() {
+ return convId;
+ }
+
+ @Override
+ public void add(ChatMessage m) {
+ if (m instanceof SystemMessage sm) {
+ systemText = sm.text();
+ return;
+ }
+ if (m instanceof UserMessage um) {
+ String text = um.hasSingleText() ? um.singleText() : String.valueOf(um.contents());
+ if (!internalUserTurn && alreadyLoggedPrefixOf(text)) {
+ currentTurnUserText = text;
+ return;
+ }
+ Map data = new LinkedHashMap<>();
+ data.put("text", text);
+ if (internalUserTurn) {
+ data.put("internal", true);
+ }
+ log.append(convId, "user", data);
+ return;
+ }
+ if (m instanceof AiMessage am) {
+ if (am.hasToolExecutionRequests()) {
+ Map data = new LinkedHashMap<>();
+ data.put("payload", ChatMessageSerializer.messagesToJson(List.of(am)));
+ data.put("text", am.text() == null ? "" : am.text());
+ List tools = new ArrayList<>();
+ for (ToolExecutionRequest r : am.toolExecutionRequests()) {
+ tools.add(r.name());
+ }
+ data.put("tools", tools);
+ log.append(convId, "tool_call", data);
+ } else if (am.text() != null && !am.text().isBlank()) {
+ log.append(convId, "ai", Map.of("text", am.text()));
+ }
+ return;
+ }
+ if (m instanceof ToolExecutionResultMessage tr) {
+ String text = tr.text() == null ? "" : tr.text();
+ Map data = new LinkedHashMap<>();
+ data.put("tcId", tr.id() == null ? "" : tr.id());
+ data.put("name", tr.toolName() == null ? "" : tr.toolName());
+ data.put("text", text);
+ data.put("digest", digest(text));
+ log.append(convId, "tool_result", data);
+ }
+ }
+
+ /**
+ * 控制器已把本轮用户**原话**落账(最近的用户事件是喂给模型文本的前缀、且其后无模型事件)
+ * → 跳过,避免重复。编排层可能在原话后附加 grounding/状态后缀,故用前缀而非全等判定。
+ */
+ private boolean alreadyLoggedPrefixOf(String text) {
+ List