Commit c45e4968ce99ae34497882b18c805f3420814d6e

Authored by yibo
1 parent c0955699

fix(memory): prioritize current task relative to history for hippocampus; optimi…

…ze Chinese keyword extraction

- Fix: The long trailing history in todo.md drowned out the actual current task keywords because English tokens (e.g. TASK, P2, from) padded the 30-keyword limit beforehand.
- Updated extractKeywords: Process Chinese characters first as they carry more weight for NLP context intent.
- Added userMessage prefix to searchText in buildSystemPrompt so newly dispatched tasks dominate keyword extraction.
- End-to-end e2e test created and passed (27/27 asserts passed)
data/agents/poet/.claude/scheduled_tasks.lock 0 → 100644
  1 +{"sessionId":"151999eb-9535-4a75-9abe-b6a0f9383139","pid":55368,"acquiredAt":1774410478451}
0 \ No newline at end of file 2 \ No newline at end of file
data/agents/poet/memory/cerebellum/为国庆节创作一组主题诗集-包含5首诗.md 0 → 100644
  1 +# 方案: 为国庆节创作一组主题诗集,包含5首诗
  2 +
  3 +> 创建时间: 2026-03-25
  4 +
  5 +## 适用条件
  6 +- 类似任务: 为国庆节创作一组主题诗集,包含5首诗
  7 +
  8 +## 执行步骤
  9 +1. 确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来)
  10 +2. 分别确定每首诗的体裁(五绝、七绝、五律、七律、词)
  11 +3. 创作第一首《旗颂》五言绝句
  12 +4. 创作第二首《山河赋》七言律诗
  13 +5. 创作第三首《英雄辞》五言律诗
  14 +6. 创作第四首《和平吟》七言绝句
  15 +7. 创作第五首《展望》词·水调歌头
  16 +8. 整体校对格律和意境
  17 +
  18 +## 注意事项
  19 +- 自动从对话 test-complex 提取
  20 +- 耗时 180秒, 8 次工具调用
data/agents/poet/memory/cerebellum/写古典诗词.md 0 → 100644
  1 +# 方案: 写古典诗词
  2 +
  3 +> 创建时间: 2026-03-25
  4 +
  5 +## 适用条件
  6 +- 需要创作古典诗词
  7 +- 需要遵循格律
  8 +
  9 +## 执行步骤
  10 +1. 确认体裁(五绝/七绝/五律/七律/词)
  11 +2. 确认主题和意境
  12 +3. 起句: 点明时间/地点/情境
  13 +4. 承句: 展开描写,丰富画面
  14 +5. 转句: 转折或深化情感
  15 +6. 合句: 收束全诗,升华主题
  16 +
  17 +## 注意事项
  18 +- 五绝20字,七绝28字
  19 +- 注意平仄和押韵
  20 +- 避免直白说理,用意象表达
data/agents/poet/memory/daily/2026-03-25.md 0 → 100644
  1 +# 日报 — 2026-03-25
  2 +
  3 +## 概览
  4 +- 对话数: 3
  5 +- 反思数: 2
  6 +- 新模式: 2
  7 +- 新教训: 2
  8 +- 固化方案: 1
  9 +
  10 +## 对话记录
  11 +
  12 +### 2026-03-25 03:48:29 [completed] (2 次, 47秒)
  13 +- 任务: [TASK from master]
  14 +任务编号: T-mn5i6b4q
  15 +优先级: P2
  16 +
  17 +请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。
  18 +
  19 +请立即执行此任务。完成后调用 c
  20 +- 结果: ...墨为君绘就一方诗意天地。
  21 +
  22 +先恢复我的定时任务,然后为您创作这首踏青绝句。 定时任务已恢复。现在为您创作这首踏青绝句:
  23 +
  24 +---
  25 +
  26 +【作品】
  27 +《踏青》
  28 +(五言绝句,平水韵 十三元)
  29 +
  30 +草色侵衣绿,花风拂面温。
  31 +踏青人醉处,春在小溪痕。
  32 +
  33 +【格律】
  34 +```
  35 +平仄:仄仄平平仄,平平仄仄平。
  36 + 仄平平仄仄,平仄仄平平。
  37 +押韵:温、痕(上平声 十三元韵部)
  38 +```
  39 +
  40 +【简评】
  41 +此诗以"
  42 +
  43 +### 2026-03-25 03:49:14 [completed] (1 次, 42秒)
  44 +- 任务: [TASK from master]
  45 +任务编号: T-mn5i7dqk
  46 +优先级: P2
  47 +
  48 +你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。
  49 +
  50 +请立即执行此任务。
  51 +- 结果: 我记得刚才创作了一首关于春天踏青的五言绝句。根据我的近期记忆,任务编号 **T-mn5i6b4q** 中我完成了以下作品:
  52 +
  53 +---
  54 +
  55 +**【作品】《踏青》**
  56 +(五言绝句,平水韵 十三元)
  57 +
  58 +草色侵衣绿,花风拂面温。
  59 +踏青人醉处,春在小溪痕。
  60 +
  61 +---
  62 +
  63 +此诗通过「草色」「花风」「溪痕」等意象,描绘春日踏青时衣染草绿、面拂花香的惬意场景,末句以"春在小溪痕"收束,将抽象的春意化为流水留下的痕迹,
  64 +
  65 +### 2026-03-25 03:49:50 [completed] (1 次, 33秒)
  66 +- 任务: [TASK from master]
  67 +任务编号: T-mn5i8d96
  68 +优先级: P2
  69 +
  70 +请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。
  71 +
  72 +请立即执行此任务。完成后调用 curl 汇报:
  73 +- 结果: 在下诗人,愿以笔墨为君绘就一方诗意天地。
  74 +
  75 +---
  76 +
  77 +【作品】
  78 +《中秋望月》
  79 +(七言绝句,平水韵 十四寒)
  80 +
  81 +玉轮初上碧云端,万里清辉照画栏。
  82 +此夜人间同仰望,不知秋思落谁边。
  83 +
  84 +【格律】
  85 +```
  86 +平仄:仄平平仄仄平平,仄仄平平仄仄平。
  87 + 仄仄平平平仄仄,仄平平仄仄平平。
  88 +押韵:端、栏、边(上平声 十四寒韵部)
  89 +```
  90 +
  91 +【简评】
  92 +首句"玉轮初上"点出中秋月升之景,次句"万里清辉"写月光
  93 +
  94 +## 反思摘要
  95 +
  96 +- ✅ 成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md
  97 +- ✅ 完成了 8 步工具调用的复杂任务 (180秒)
  98 +- ❌ 错误: 输入格式无法解析 Error: Invalid poem format at line 3
  99 +- ❌ 任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误
data/agents/poet/memory/hippocampus-index.json 0 → 100644
  1 +[
  2 + {
  3 + "scenario": "写古典诗词",
  4 + "keywords": [
  5 + "写诗",
  6 + "诗词",
  7 + "古诗",
  8 + "五言",
  9 + "七言",
  10 + "绝句",
  11 + "律诗",
  12 + "创作"
  13 + ],
  14 + "planFile": "写古典诗词.md",
  15 + "confidence": 0.9,
  16 + "useCount": 1,
  17 + "lastUsed": "2026-03-25",
  18 + "createdAt": "2026-03-25"
  19 + },
  20 + {
  21 + "scenario": "为国庆节创作一组主题诗集,包含5首诗",
  22 + "keywords": [
  23 + "为国",
  24 + "国庆",
  25 + "庆节",
  26 + "节创",
  27 + "创作",
  28 + "作一",
  29 + "一组",
  30 + "组主"
  31 + ],
  32 + "planFile": "为国庆节创作一组主题诗集-包含5首诗.md",
  33 + "confidence": 0.76,
  34 + "useCount": 0,
  35 + "lastUsed": null,
  36 + "createdAt": "2026-03-25"
  37 + }
  38 +]
0 \ No newline at end of file 39 \ No newline at end of file
data/agents/poet/memory/hippocampus.md
1 # 海马体 — 经验记忆索引 1 # 海马体 — 经验记忆索引
2 2
3 > 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。 3 > 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。
  4 +> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。
4 5
5 -| # | 情景模式 | 匹配条件 | 方案文件 | 置信度 | 使用次数 | 上次使用 |  
6 -|---|---------|---------|---------|--------|---------|----------|  
7 -| | (暂无经验记录) | | | | | | 6 +| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 |
  7 +|---|---------|-----------|---------|--------|---------|----------|
  8 +| 1 | 写古典诗词 | 写诗, 诗词, 古诗, 五言, 七言, 绝句, 律诗, 创作 | 写古典诗词.md | 0.9 | 1 | 2026-03-25 |
  9 +| 2 | 为国庆节创作一组主题诗集,包含5首诗 | 为国, 国庆, 庆节, 节创, 创作, 作一, 一组, 组主 | 为国庆节创作一组主题诗集-包含5首诗.md | 0.76 | 0 | - |
data/agents/poet/memory/long-term/consolidate-2026-03-25.md 0 → 100644
  1 +# 长期记忆合并 — 2026-03-25
  2 +> 合并了 3 个过期 session (>7 天)
  3 +## 高频关键词
  4 +- 任务 (x3)
  5 +- 旧任务 (x3)
  6 +- 旧任 (x3)
  7 +- 写一 (x3)
  8 +- 一首 (x3)
  9 +- 首关 (x3)
  10 +- 关于 (x3)
  11 +- 于秋 (x3)
  12 +- 秋天 (x3)
  13 +- 天的 (x3)
  14 +## 合并的对话摘要
  15 +### 2026-03-17 00:00:00 [] (, )
  16 +- 任务: 旧任务-0: 写一首关于秋天的诗
  17 +- 结果: 秋诗完成-0
  18 +
  19 +### 2026-03-17 01:00:00 [] (, )
  20 +- 任务: 旧任务-1: 写一首关于秋天的诗
  21 +- 结果: 秋诗完成-1
  22 +
  23 +### 2026-03-17 02:00:00 [] (, )
  24 +- 任务: 旧任务-2: 写一首关于秋天的诗
  25 +- 结果: 秋诗完成-2
data/agents/poet/memory/long-term/patterns.md 0 → 100644
  1 +# 长期模式
  2 +
  3 +> 从 short-term 多次出现的模式沉淀而来
  4 +
  5 +
  6 +## 2026-03-25
  7 +- 任务 (x3)
  8 +- 旧任务 (x3)
  9 +- 旧任 (x3)
  10 +- 写一 (x3)
  11 +- 一首 (x3)
  12 +- 首关 (x3)
  13 +- 关于 (x3)
  14 +- 于秋 (x3)
  15 +- 秋天 (x3)
  16 +- 天的 (x3)
data/agents/poet/memory/memory.md
1 # 我的记忆 1 # 我的记忆
2 2
3 ## 关键事实 3 ## 关键事实
4 -(尚无记录) 4 +- [2026-03-25] [TASK from master] → ...墨为君绘就一方诗意天地。
  5 +- [2026-03-25] [TASK from master] → 我记得刚才创作了一首关于春天踏青的五言绝句。根据我的近期记忆,任务编号 **T-mn5i6b4q** 中我完成了以下作品:
  6 +- [2026-03-25] [TASK from master] → 在下诗人,愿以笔墨为君绘就一方诗意天地。
5 7
6 ## 已验证的模式 8 ## 已验证的模式
7 -(尚无记录) 9 +- [2026-03-25] 成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md
  10 +- [2026-03-25] 完成了 8 步工具调用的复杂任务 (180秒)
8 11
9 ## 失败教训 12 ## 失败教训
10 -(尚无记录) 13 +- [2026-03-25] 错误: 输入格式无法解析 Error: Invalid poem format at line 3
  14 +- [2026-03-25] 任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误
data/agents/poet/memory/reflections/reflect-1774410599260.json 0 → 100644
  1 +{
  2 + "timestamp": "2026-03-25T03:49:59.259Z",
  3 + "convId": "test-complex",
  4 + "task": "为国庆节创作一组主题诗集,包含5首诗",
  5 + "status": "completed",
  6 + "toolCount": 8,
  7 + "durationSec": 180,
  8 + "patterns": [
  9 + "成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md",
  10 + "完成了 8 步工具调用的复杂任务 (180秒)"
  11 + ],
  12 + "lessons": [],
  13 + "plan": {
  14 + "scenario": "为国庆节创作一组主题诗集,包含5首诗",
  15 + "keywords": [
  16 + "为国",
  17 + "国庆",
  18 + "庆节",
  19 + "节创",
  20 + "创作",
  21 + "作一",
  22 + "一组",
  23 + "组主"
  24 + ],
  25 + "steps": [
  26 + "确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来)",
  27 + "分别确定每首诗的体裁(五绝、七绝、五律、七律、词)",
  28 + "创作第一首《旗颂》五言绝句",
  29 + "创作第二首《山河赋》七言律诗",
  30 + "创作第三首《英雄辞》五言律诗",
  31 + "创作第四首《和平吟》七言绝句",
  32 + "创作第五首《展望》词·水调歌头",
  33 + "整体校对格律和意境"
  34 + ],
  35 + "confidence": 0.76
  36 + }
  37 +}
0 \ No newline at end of file 38 \ No newline at end of file
data/agents/poet/memory/reflections/reflect-1774410599261.json 0 → 100644
  1 +{
  2 + "timestamp": "2026-03-25T03:49:59.260Z",
  3 + "convId": "test-fail",
  4 + "task": "翻译古诗为英文但格式错误",
  5 + "status": "failed",
  6 + "toolCount": 4,
  7 + "durationSec": 60,
  8 + "patterns": [],
  9 + "lessons": [
  10 + "错误: 输入格式无法解析 Error: Invalid poem format at line 3",
  11 + "任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误"
  12 + ],
  13 + "plan": null
  14 +}
0 \ No newline at end of file 15 \ No newline at end of file
data/agents/poet/todo.md
@@ -5,3 +5,25 @@ @@ -5,3 +5,25 @@
5 | # | 任务 | 状态 | 分配人 | 优先级 | 接收时间 | 上次检查 | 5 | # | 任务 | 状态 | 分配人 | 优先级 | 接收时间 | 上次检查 |
6 |---|------|------|--------|--------|----------|----------| 6 |---|------|------|--------|--------|----------|----------|
7 | T-mn405cps | 测试你的诗词创作功能。请创作一首七言绝句,主题:春天,情感基调:明快。要求:1) 严格按照七律格律;2) 标注平仄和韵部;3) 提供简评赏析 | ✅ 完成 | master-001 | P1 | 2026-03-24 02:35 | - | 7 | T-mn405cps | 测试你的诗词创作功能。请创作一首七言绝句,主题:春天,情感基调:明快。要求:1) 严格按照七律格律;2) 标注平仄和韵部;3) 提供简评赏析 | ✅ 完成 | master-001 | P1 | 2026-03-24 02:35 | - |
  8 +| T-mn5h7iux | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:20 | - |
  9 +| T-mn5h8dr7 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:21 | - |
  10 +| T-mn5h95kh | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:21 | - |
  11 +| T-mn5hcpbb | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:24 | - |
  12 +| T-mn5hdrxo | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:25 | - |
  13 +| T-mn5hei6x | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:26 | - |
  14 +| T-mn5hgw6y | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:27 | - |
  15 +| T-mn5hkltv | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:30 | - |
  16 +| T-mn5hlt3f | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:31 | - |
  17 +| T-mn5hmx9j | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:32 | - |
  18 +| T-mn5hpbq3 | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:34 | - |
  19 +| T-mn5hqqp4 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:35 | - |
  20 +| T-mn5hr98v | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:35 | - |
  21 +| T-mn5hxnyf | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:40 | - |
  22 +| T-mn5hyke4 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:41 | - |
  23 +| T-mn5hzjx9 | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:42 | - |
  24 +| T-mn5i2hot | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:44 | - |
  25 +| T-mn5i3ne9 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:45 | - |
  26 +| T-mn5i4rjr | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:46 | - |
  27 +| T-mn5i6b4q | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:47 | - |
  28 +| T-mn5i7dqk | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:48 | - |
  29 +| T-mn5i8d96 | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:49 | - |
server/routes/agent-chat.js
@@ -241,7 +241,7 @@ const BASE_DIR = path.join(__dirname, '../../packages/agent-core/base'); @@ -241,7 +241,7 @@ const BASE_DIR = path.join(__dirname, '../../packages/agent-core/base');
241 * - tools.md: base/common/tools-base.md + agent/tools.md (追加) 241 * - tools.md: base/common/tools-base.md + agent/tools.md (追加)
242 * - 其他文件 (soul.md, identity.md 等): 只从 agent/ 读取(纯个性化) 242 * - 其他文件 (soul.md, identity.md 等): 只从 agent/ 读取(纯个性化)
243 */ 243 */
244 -function buildSystemPrompt(agentDir) { 244 +function buildSystemPrompt(agentDir, userMessage) {
245 const config = loadAgentConfig(agentDir); 245 const config = loadAgentConfig(agentDir);
246 const agentType = config?.type || 'skill'; 246 const agentType = config?.type || 'skill';
247 247
@@ -298,9 +298,10 @@ function buildSystemPrompt(agentDir) { @@ -298,9 +298,10 @@ function buildSystemPrompt(agentDir) {
298 if (recentMemory) parts.push('---\n\n' + recentMemory); 298 if (recentMemory) parts.push('---\n\n' + recentMemory);
299 299
300 // hippocampus — 海马体检索相关经验 (Phase 2) 300 // hippocampus — 海马体检索相关经验 (Phase 2)
301 - // 使用当前任务文本作为检索 key(从 todo.md 或最后一条消息提取) 301 + // 将 userMessage 放在前,避免 todo.md 历史堆积导致当前任务的关键词被截断
302 const todoContent = loadFile(agentDir, 'todo.md'); 302 const todoContent = loadFile(agentDir, 'todo.md');
303 - const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, todoContent); 303 + const searchText = [userMessage || '', todoContent].join('\n\n');
  304 + const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, searchText);
304 if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans); 305 if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans);
305 306
306 // skills 307 // skills
@@ -551,7 +552,7 @@ function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) { @@ -551,7 +552,7 @@ function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) {
551 return userMessage; 552 return userMessage;
552 } 553 }
553 554
554 - const systemPrompt = buildSystemPrompt(agentDir); 555 + const systemPrompt = buildSystemPrompt(agentDir, userMessage);
555 const cronInstructions = buildCronInstructions(agentDir); 556 const cronInstructions = buildCronInstructions(agentDir);
556 const history = loadHistory(agentId, convId); 557 const history = loadHistory(agentId, convId);
557 558
server/scripts/test-memory-e2e.js 0 → 100644
  1 +#!/usr/bin/env node
  2 +/**
  3 + * Memory System — 完整端到端测试
  4 + *
  5 + * 用真实 Agent 对话验证三个 Phase 的完整效果:
  6 + *
  7 + * Phase 1 验证:
  8 + * ① 对话完成 → short-term/ 自动出现 session 文件
  9 + * ② memory.md 自动追加关键事实
  10 + * ③ 第二次对话 → Agent 能引用第一次的内容(近期记忆注入)
  11 + *
  12 + * Phase 2 验证:
  13 + * ④ 保存小脑方案 → hippocampus-index.json 更新
  14 + * ⑤ 发送匹配任务 → 海马体命中方案 → prompt 体积增大
  15 + * ⑥ 使用次数自动递增
  16 + *
  17 + * Phase 3 验证:
  18 + * ⑦ 复杂任务(多 tool call)→ 自动触发反思 → reflections/ 出现日志
  19 + * ⑧ memory.md "已验证模式" / "失败教训" 段被更新
  20 + * ⑨ 过期 short-term 合并 → long-term/ 出现合并记录
  21 + * ⑩ 日报 API → daily/{today}.md 生成
  22 + */
  23 +
  24 +const http = require('http');
  25 +const fs = require('fs');
  26 +const path = require('path');
  27 +
  28 +const BASE = 'http://localhost:3000';
  29 +const AGENT = 'poet'; // 用 poet 做测试(轻量级、不影响 ceo 数据)
  30 +const DATA_DIR = path.resolve(__dirname, '../../data/agents');
  31 +const AGENT_DIR = path.join(DATA_DIR, AGENT);
  32 +const MEM = path.join(AGENT_DIR, 'memory');
  33 +
  34 +let passed = 0, failed = 0, total = 0;
  35 +
  36 +function test(condition, msg) {
  37 + total++;
  38 + if (condition) { passed++; console.log(` ✅ ${msg}`); }
  39 + else { failed++; console.log(` ❌ FAIL: ${msg}`); }
  40 +}
  41 +
  42 +// ── HTTP 工具 ──
  43 +function post(urlStr, body) {
  44 + return new Promise((resolve, reject) => {
  45 + const data = JSON.stringify(body);
  46 + const u = new URL(urlStr);
  47 + const req = http.request({
  48 + hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
  49 + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
  50 + }, res => {
  51 + let buf = '';
  52 + res.on('data', c => buf += c);
  53 + res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } });
  54 + });
  55 + req.on('error', reject);
  56 + req.write(data);
  57 + req.end();
  58 + });
  59 +}
  60 +
  61 +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  62 +
  63 +function waitForStatus(agentId, convId, timeoutMs = 120000) {
  64 + const start = Date.now();
  65 + return new Promise((resolve, reject) => {
  66 + const check = () => {
  67 + if (Date.now() - start > timeoutMs) return reject(new Error(`Timeout waiting for ${convId}`));
  68 + try {
  69 + const metaPath = path.join(DATA_DIR, agentId, 'conversations', convId, 'meta.json');
  70 + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
  71 + if (meta.status === 'completed' || meta.status === 'failed') return resolve(meta);
  72 + } catch {}
  73 + setTimeout(check, 2000);
  74 + };
  75 + check();
  76 + });
  77 +}
  78 +
  79 +function readFile(p) { try { return fs.readFileSync(p, 'utf-8'); } catch { return ''; } }
  80 +function listDir(p) { try { return fs.readdirSync(p); } catch { return []; } }
  81 +
  82 +// ════════════════════════════════════════
  83 +// Clean Slate
  84 +// ════════════════════════════════════════
  85 +function cleanMemory() {
  86 + const dirs = ['short-term', 'long-term', 'reflections', 'cerebellum', 'daily'];
  87 + for (const d of dirs) {
  88 + const dir = path.join(MEM, d);
  89 + if (fs.existsSync(dir)) {
  90 + for (const f of fs.readdirSync(dir)) {
  91 + if (f === '.' || f === '..') continue;
  92 + try { fs.unlinkSync(path.join(dir, f)); } catch {}
  93 + }
  94 + }
  95 + }
  96 + // 重置 memory.md
  97 + fs.writeFileSync(path.join(MEM, 'memory.md'),
  98 + '# 我的记忆\n\n## 关键事实\n(尚无记录)\n\n## 已验证的模式\n(尚无记录)\n\n## 失败教训\n(尚无记录)\n', 'utf-8');
  99 + // 清 hippocampus
  100 + const hippIdx = path.join(MEM, 'hippocampus-index.json');
  101 + if (fs.existsSync(hippIdx)) fs.unlinkSync(hippIdx);
  102 +}
  103 +
  104 +// ════════════════════════════════════════
  105 +// Main Test
  106 +// ════════════════════════════════════════
  107 +async function main() {
  108 + console.log('\n🧠 Memory System — 端到端集成测试');
  109 + console.log('═'.repeat(56));
  110 + console.log(` Agent: ${AGENT}`);
  111 + console.log(` Server: ${BASE}`);
  112 + console.log('═'.repeat(56));
  113 +
  114 + // ── 0. Clean ──
  115 + console.log('\n🗑️ Step 0: 清理历史数据');
  116 + cleanMemory();
  117 + console.log(' Done.\n');
  118 +
  119 + // ═══════════════════════════════════
  120 + // PHASE 1: 基础记忆读写
  121 + // ═══════════════════════════════════
  122 + console.log('┌──────────────────────────────────┐');
  123 + console.log('│ PHASE 1: 基础记忆读写 │');
  124 + console.log('└──────────────────────────────────┘');
  125 +
  126 + // ── 1a. 第一次对话 ──
  127 + console.log('\n📋 1a. 发送第一次对话:让 Poet 写一首关于春天的五言绝句');
  128 + const r1 = await post(`${BASE}/api/agent/task/dispatch`, {
  129 + from: 'master', to: AGENT,
  130 + task: '请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。',
  131 + priority: 'P2', autoExecute: true,
  132 + });
  133 + test(r1.success, `Dispatch 成功 (taskId: ${r1.taskId})`);
  134 + const conv1 = r1.executionConvId;
  135 +
  136 + console.log(' ⏳ 等待对话完成...');
  137 + const meta1 = await waitForStatus(AGENT, conv1);
  138 + test(meta1.status === 'completed', `对话完成 (toolCalls: ${meta1.stats?.toolCallCount})`);
  139 + await sleep(2000);
  140 +
  141 + // ① session 文件
  142 + const sessions1 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-'));
  143 + test(sessions1.length >= 1, `① short-term/ 有 ${sessions1.length} 个 session 文件`);
  144 +
  145 + // ② memory.md 更新
  146 + const mem1 = readFile(path.join(MEM, 'memory.md'));
  147 + const hasFact = mem1.includes('[2026-') || !mem1.includes('(尚无记录)');
  148 + test(hasFact, `② memory.md 已更新关键事实`);
  149 +
  150 + // ── 1b. 第二次对话(验证近期记忆注入) ──
  151 + console.log('\n📋 1b. 发送第二次对话:要求 Agent 回忆之前写了什么');
  152 + const r2 = await post(`${BASE}/api/agent/task/dispatch`, {
  153 + from: 'master', to: AGENT,
  154 + task: '你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。',
  155 + priority: 'P2', autoExecute: true,
  156 + });
  157 + test(r2.success, `Dispatch 成功 (taskId: ${r2.taskId})`);
  158 + const conv2 = r2.executionConvId;
  159 +
  160 + console.log(' ⏳ 等待对话完成...');
  161 + const meta2 = await waitForStatus(AGENT, conv2);
  162 + test(meta2.status === 'completed', `对话完成`);
  163 + await sleep(2000);
  164 +
  165 + // ③ 验证 Agent 引用了之前的内容
  166 + const log2Path = path.join(AGENT_DIR, 'conversations', conv2, 'messages.log');
  167 + const log2 = readFile(log2Path);
  168 + const remembered = log2.includes('春') || log2.includes('踏青') || log2.includes('诗') || log2.includes('记忆');
  169 + test(remembered, `③ Agent 在第二次对话中引用了之前的春天/踏青内容`);
  170 +
  171 + const sessions2 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-'));
  172 + test(sessions2.length >= 2, `short-term/ 现在有 ${sessions2.length} 个 session 文件`);
  173 +
  174 + // ═══════════════════════════════════
  175 + // PHASE 2: 海马体检索 + 小脑
  176 + // ═══════════════════════════════════
  177 + console.log('\n┌──────────────────────────────────┐');
  178 + console.log('│ PHASE 2: 海马体检索 + 小脑固化 │');
  179 + console.log('└──────────────────────────────────┘');
  180 +
  181 + // ④ 保存小脑方案
  182 + console.log('\n📋 2a. 保存一个"写诗"小脑方案');
  183 + const planRes = await post(`${BASE}/api/agent/${AGENT}/memory/cerebellum`, {
  184 + scenario: '写古典诗词',
  185 + keywords: ['写诗', '诗词', '古诗', '五言', '七言', '绝句', '律诗', '创作'],
  186 + conditions: ['需要创作古典诗词', '需要遵循格律'],
  187 + steps: [
  188 + '确认体裁(五绝/七绝/五律/七律/词)',
  189 + '确认主题和意境',
  190 + '起句: 点明时间/地点/情境',
  191 + '承句: 展开描写,丰富画面',
  192 + '转句: 转折或深化情感',
  193 + '合句: 收束全诗,升华主题',
  194 + ],
  195 + notes: ['五绝20字,七绝28字', '注意平仄和押韵', '避免直白说理,用意象表达'],
  196 + confidence: 0.9,
  197 + });
  198 + test(planRes.success, `④ 小脑方案保存成功`);
  199 +
  200 + // 验证 hippocampus 索引
  201 + const hippIdx = readFile(path.join(MEM, 'hippocampus-index.json'));
  202 + test(hippIdx.includes('写古典诗词'), `hippocampus-index.json 包含新条目`);
  203 +
  204 + // ⑤ 发送匹配任务 → 验证海马体命中
  205 + console.log('\n📋 2b. 发送"写诗"任务 → 验证海马体检索');
  206 + const r3 = await post(`${BASE}/api/agent/task/dispatch`, {
  207 + from: 'master', to: AGENT,
  208 + task: '请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。',
  209 + priority: 'P2', autoExecute: true,
  210 + });
  211 + test(r3.success, `Dispatch 成功 (taskId: ${r3.taskId})`);
  212 + const conv3 = r3.executionConvId;
  213 +
  214 + console.log(' ⏳ 等待对话完成...');
  215 + await waitForStatus(AGENT, conv3);
  216 + await sleep(2000);
  217 +
  218 + // ⑥ 验证使用次数递增
  219 + // 海马体 useCount 在 buildSystemPrompt 时更新,需等服务端写完文件
  220 + await sleep(5000);
  221 + const hippRaw = readFile(path.join(MEM, 'hippocampus-index.json'));
  222 + console.log(` 🔍 hippocampus raw: ${hippRaw.substring(0, 200)}`);
  223 + const hippIdxAfter = JSON.parse(hippRaw || '[]');
  224 + const poetryEntry = hippIdxAfter.find(e => e.scenario === '写古典诗词');
  225 + console.log(` 🔍 poetryEntry: ${JSON.stringify(poetryEntry)}`);
  226 + test(poetryEntry && poetryEntry.useCount >= 1, `⑥ 海马体使用次数: ${poetryEntry?.useCount} (期望 ≥1)`);
  227 + test(poetryEntry && poetryEntry.lastUsed !== null, `lastUsed 已更新: ${poetryEntry?.lastUsed}`);
  228 +
  229 + // ═══════════════════════════════════
  230 + // PHASE 3: 反思 + 合并 + 日报
  231 + // ═══════════════════════════════════
  232 + console.log('\n┌──────────────────────────────────┐');
  233 + console.log('│ PHASE 3: 反思 + 合并 + 日报 │');
  234 + console.log('└──────────────────────────────────┘');
  235 +
  236 + // ⑦ 模拟复杂对话触发反思
  237 + console.log('\n📋 3a. 模拟复杂对话 → 验证自动反思');
  238 + const { autoReflect } = require('../../server/services/memory-service');
  239 + const complexMeta = {
  240 + status: 'completed',
  241 + stats: { toolCallCount: 8, messageCount: 4 },
  242 + createdAt: new Date(Date.now() - 180000).toISOString(),
  243 + updatedAt: new Date().toISOString(),
  244 + title: '为国庆节创作一组主题诗集,包含5首诗',
  245 + };
  246 + const complexOutput = `创作完成!以下是创作步骤:
  247 +1. 确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来)
  248 +2. 分别确定每首诗的体裁(五绝、七绝、五律、七律、词)
  249 +3. 创作第一首《旗颂》五言绝句
  250 +4. 创作第二首《山河赋》七言律诗
  251 +5. 创作第三首《英雄辞》五言律诗
  252 +6. 创作第四首《和平吟》七言绝句
  253 +7. 创作第五首《展望》词·水调歌头
  254 +8. 整体校对格律和意境
  255 +
  256 +产出文件:
  257 +- workspace/poet/guoqing-collection.md
  258 +- workspace/poet/guoqing-notes.md`;
  259 +
  260 + const reflectResult = autoReflect(AGENT, 'test-complex', complexMeta, complexOutput);
  261 + test(reflectResult !== null, `⑦ 复杂对话触发了反思`);
  262 + test(reflectResult?.patterns?.length >= 1, `提取了 ${reflectResult?.patterns?.length} 个模式`);
  263 + test(reflectResult?.plan !== null, `自动固化了方案`);
  264 +
  265 + // 验证 reflections 目录
  266 + const reflectFiles = listDir(path.join(MEM, 'reflections')).filter(f => f.startsWith('reflect-'));
  267 + test(reflectFiles.length >= 1, `reflections/ 有 ${reflectFiles.length} 个日志`);
  268 +
  269 + // ⑧ memory.md 更新
  270 + const mem3 = readFile(path.join(MEM, 'memory.md'));
  271 + test(!mem3.includes('已验证的模式\n(尚无记录)'), `⑧ memory.md "已验证模式" 段有内容`);
  272 +
  273 + // 模拟失败对话
  274 + console.log('\n📋 3b. 模拟失败对话 → 验证教训提取');
  275 + const failReflect = autoReflect(AGENT, 'test-fail', {
  276 + status: 'failed',
  277 + stats: { toolCallCount: 4, messageCount: 2 },
  278 + createdAt: new Date(Date.now() - 60000).toISOString(),
  279 + updatedAt: new Date().toISOString(),
  280 + title: '翻译古诗为英文但格式错误',
  281 + }, 'error: 输入格式无法解析 Error: Invalid poem format at line 3');
  282 +
  283 + test(failReflect?.lessons?.length >= 1, `失败对话提取了 ${failReflect?.lessons?.length} 个教训`);
  284 + const mem3b = readFile(path.join(MEM, 'memory.md'));
  285 + test(!mem3b.includes('失败教训\n(尚无记录)'), `memory.md "失败教训" 段有内容`);
  286 +
  287 + // ⑨ 记忆合并
  288 + console.log('\n📋 3c. 记忆合并 (short-term → long-term)');
  289 + // 创建 3 个"过期"session(8天前)
  290 + const shortDir = path.join(MEM, 'short-term');
  291 + const oldTs = Date.now() - 8 * 24 * 60 * 60 * 1000;
  292 + for (let i = 0; i < 3; i++) {
  293 + fs.writeFileSync(path.join(shortDir, `session-${oldTs + i}.md`),
  294 + `# Session 摘要\n\n- **时间**: 2026-03-17 0${i}:00:00\n\n## 任务\n旧任务-${i}: 写一首关于秋天的诗\n\n## 结果\n秋诗完成-${i}\n`, 'utf-8');
  295 + }
  296 +
  297 + const shortBefore = listDir(shortDir).filter(f => f.startsWith('session-'));
  298 + console.log(` short-term/ 合并前: ${shortBefore.length} 文件`);
  299 +
  300 + const consolidateRes = await post(`${BASE}/api/agent/${AGENT}/memory/consolidate`, {});
  301 + test(consolidateRes.merged === 3, `⑨ 合并了 ${consolidateRes.merged} 个过期 session (期望 3)`);
  302 +
  303 + const ltFiles = listDir(path.join(MEM, 'long-term'));
  304 + test(ltFiles.some(f => f.startsWith('consolidate-')), `long-term/ 有合并记录`);
  305 +
  306 + const shortAfter = listDir(shortDir).filter(f => {
  307 + const ts = parseInt(f.replace('session-', '').replace('.md', ''));
  308 + return !isNaN(ts) && ts < oldTs + 5;
  309 + });
  310 + test(shortAfter.length === 0, `过期 session 已从 short-term/ 删除`);
  311 +
  312 + // ⑩ 日报
  313 + console.log('\n📋 3d. 日报生成');
  314 + const dailyRes = await post(`${BASE}/api/agent/${AGENT}/memory/daily-report`, {});
  315 + test(dailyRes.success, `⑩ 日报生成成功`);
  316 +
  317 + const today = new Date().toISOString().substring(0, 10);
  318 + const dailyContent = readFile(path.join(MEM, 'daily', `${today}.md`));
  319 + test(dailyContent.includes('# 日报'), `daily/${today}.md 包含日报标题`);
  320 + test(dailyContent.includes('对话数'), `日报包含统计数据`);
  321 + test(dailyContent.includes('反思摘要') || dailyContent.includes('反思数'), `日报包含反思信息`);
  322 +
  323 + // ═══════════════════════════════════
  324 + // 最终汇总
  325 + // ═══════════════════════════════════
  326 + console.log('\n' + '═'.repeat(56));
  327 + console.log('\n📊 Memory 目录最终状态:');
  328 + const finalState = {
  329 + 'short-term/': listDir(path.join(MEM, 'short-term')).filter(f => f.endsWith('.md')).length,
  330 + 'long-term/': listDir(path.join(MEM, 'long-term')).length,
  331 + 'cerebellum/': listDir(path.join(MEM, 'cerebellum')).length,
  332 + 'reflections/': listDir(path.join(MEM, 'reflections')).length,
  333 + 'daily/': listDir(path.join(MEM, 'daily')).filter(f => f.endsWith('.md')).length,
  334 + 'memory.md': `${readFile(path.join(MEM, 'memory.md')).length} 字`,
  335 + 'hippocampus': hippIdxAfter.length + ' 条索引',
  336 + };
  337 + for (const [k, v] of Object.entries(finalState)) {
  338 + console.log(` 📁 ${k.padEnd(18)} ${typeof v === 'number' ? v + ' 文件' : v}`);
  339 + }
  340 +
  341 + console.log('\n' + '═'.repeat(56));
  342 + const status = failed === 0 ? '🎉 ALL PASSED' : '⚠️ SOME FAILED';
  343 + console.log(`\n${status}: ${passed}/${total} 通过, ${failed} 失败\n`);
  344 +
  345 + if (failed > 0) process.exit(1);
  346 +}
  347 +
  348 +main().catch(err => { console.error('💥 测试崩溃:', err); process.exit(1); });
server/services/memory-service.js
@@ -222,6 +222,7 @@ function retrieveRelevantPlans(agentId, taskText) { @@ -222,6 +222,7 @@ function retrieveRelevantPlans(agentId, taskText) {
222 const score = entryKeywords.length > 0 222 const score = entryKeywords.length > 0
223 ? hits.length / entryKeywords.length // 命中了 entry 的多少比例关键词 223 ? hits.length / entryKeywords.length // 命中了 entry 的多少比例关键词
224 : 0; 224 : 0;
  225 + console.log(`[HippocampusDebug] agent=${agentId} scenario="${entry.scenario}" score=${score} hits=${hits.length} taskKwLength=${taskKeywords.length} entryKws=${entryKeywords.length}`);
225 return { ...entry, score, hits }; 226 return { ...entry, score, hits };
226 }); 227 });
227 228
@@ -440,35 +441,22 @@ const STOP_WORDS = new Set([ @@ -440,35 +441,22 @@ const STOP_WORDS = new Set([
440 441
441 /** 442 /**
442 * 从文本中提取关键词 443 * 从文本中提取关键词
443 - * 策略: 中文按 2-4 字切片 + 英文按空格分词 + 去停用词 444 + * 取前 1500 字符,优先处理中文分词(重要任务内容),再处理英文,避免英文占位符号挤占中文。
444 */ 445 */
445 function extractKeywords(text) { 446 function extractKeywords(text) {
446 if (!text || text.length < 2) return []; 447 if (!text || text.length < 2) return [];
447 448
  449 + text = text.trim().substring(0, 1500); // 截断避免太长
448 const keywords = new Set(); 450 const keywords = new Set();
449 451
450 - // 1. 英文词 (≥2字母)  
451 - const englishWords = text.match(/[a-zA-Z]{2,}/g) || [];  
452 - englishWords.forEach(w => {  
453 - const lower = w.toLowerCase();  
454 - if (!STOP_WORDS.has(lower) && lower.length >= 3) {  
455 - keywords.add(lower);  
456 - }  
457 - });  
458 -  
459 - // 2. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram 452 + // 1. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram
460 const chineseSegments = text.match(/[\u4e00-\u9fff]{2,}/g) || []; 453 const chineseSegments = text.match(/[\u4e00-\u9fff]{2,}/g) || [];
461 for (const seg of chineseSegments) { 454 for (const seg of chineseSegments) {
462 - // 完整段 (如果 ≤6 字)  
463 - if (seg.length <= 6 && !STOP_WORDS.has(seg)) {  
464 - keywords.add(seg);  
465 - }  
466 - // 2-gram 455 + if (seg.length <= 6 && !STOP_WORDS.has(seg)) keywords.add(seg);
467 for (let i = 0; i <= seg.length - 2; i++) { 456 for (let i = 0; i <= seg.length - 2; i++) {
468 const bi = seg.substring(i, i + 2); 457 const bi = seg.substring(i, i + 2);
469 if (!STOP_WORDS.has(bi)) keywords.add(bi); 458 if (!STOP_WORDS.has(bi)) keywords.add(bi);
470 } 459 }
471 - // 3-gram (如果段够长)  
472 if (seg.length >= 3) { 460 if (seg.length >= 3) {
473 for (let i = 0; i <= seg.length - 3; i++) { 461 for (let i = 0; i <= seg.length - 3; i++) {
474 const tri = seg.substring(i, i + 3); 462 const tri = seg.substring(i, i + 3);
@@ -477,7 +465,14 @@ function extractKeywords(text) { @@ -477,7 +465,14 @@ function extractKeywords(text) {
477 } 465 }
478 } 466 }
479 467
480 - return [...keywords].slice(0, 30); // 最多 30 个关键词 468 + // 2. 英文词 (≥2字母)
  469 + const englishWords = text.match(/[a-zA-Z]{2,}/g) || [];
  470 + englishWords.forEach(w => {
  471 + const lower = w.toLowerCase();
  472 + if (!STOP_WORDS.has(lower) && lower.length >= 3) keywords.add(lower);
  473 + });
  474 +
  475 + return [...keywords].slice(0, 50); // 最多 50 个关键词
481 } 476 }
482 477
483 // ════════════════════════════════════════ 478 // ════════════════════════════════════════