Commit d2f1384322fca806c0c91ed30a18b2637ea57fb2

Authored by yibo
1 parent 2604f5ce

feat: implement Memory Phase 3 — reflect engine + long-term consolidation

Phase 3 features:
- autoReflect: auto-trigger on complex conversations (toolCount>=3 or duration>=60s)
  - Extracts patterns from successful conversations
  - Extracts lessons from failed conversations
  - Auto-solidifies cerebellum plans from 5+ tool-call successes
  - Writes reflection logs to memory/reflections/
  - Updates memory.md 'verified patterns' and 'lessons learned' sections
- consolidateMemory: merge short-term sessions older than 7 days into long-term/
  - Extracts frequent keywords across merged sessions
  - Writes consolidated summary to long-term/consolidate-{date}.md
  - Updates long-term/patterns.md with high-frequency patterns
- generateDailyReport: aggregate today's sessions and reflections into daily/{date}.md
- API endpoints: POST /:agentId/memory/consolidate, POST /:agentId/memory/daily-report

Test results: 16/16 passed (Phase 3 test suite)
data/agents/ceo/memory/cerebellum/创建新的-marketing-agent-并配置所有技能文件.md 0 → 100644
  1 +# 方案: 创建新的 Marketing Agent 并配置所有技能文件
  2 +
  3 +> 创建时间: 2026-03-25
  4 +
  5 +## 适用条件
  6 +- 类似任务: 创建新的 Marketing Agent 并配置所有技能文件
  7 +
  8 +## 执行步骤
  9 +1. 创建 data/agents/marketing 目录结构
  10 +2. 编写 agent.md 和 soul.md
  11 +3. 配置 identity.md (名称: Marketing Agent)
  12 +4. 编写 SKILL.md 主技能
  13 +5. 更新 config.json
  14 +6. 验证 Agent 列表
  15 +
  16 +## 注意事项
  17 +- 自动从对话 test-reflect-001 提取
  18 +- 耗时 120秒, 6 次工具调用
data/agents/ceo/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 02:54:01 [completed] (1 次, 25秒)
  13 +- 任务: [TASK from master]
  14 +任务编号: T-mn5g8r4s
  15 +优先级: P2
  16 +
  17 +请用一句话总结BLM模型的核心思想。不需要使用任何工具,直接回答即可。
  18 +
  19 +请立即执行此任务。完成后调用 cur
  20 +- 结果: **BLM 模型的核心思想**:以差距分析为起点,通过"战略制定"(市场洞察、战略意图、创新焦点、业务设计五要素)与"战略执行"(关键任务、组织、人才、文化)的双轮驱动,实现业绩差距和机会差距的闭合,最终达成业务领先的系统化战略管理框架。
  21 +
  22 +---
  23 +
  24 +现在向 master 汇报任务完成: ✅ 任务 T-mn5g8r4s 已完成并汇报。
  25 +
  26 +### 2026-03-25 02:54:31 [completed] (1 次, 26秒)
  27 +- 任务: [TASK from master]
  28 +任务编号: T-mn5g9cri
  29 +优先级: P2
  30 +
  31 +你还记得刚才我们讨论了什么模型吗?如果你的记忆中有相关信息,请引用它。不需要使用工具。
  32 +
  33 +请立即执行此任务。完
  34 +- 结果: 是的,我记得刚才我们讨论的是 **BLM 模型**(Business Leadership Model,业务领先模型)。
  35 +
  36 +根据我的记忆,BLM 模型的核心思想是:
  37 +> 以差距分析为起点,通过"战略制定"(市场洞察、战略意图、创新焦点、业务设计五要素)与"战略执行"(关键任务、组织、人才、文化)的双轮驱动,实现业绩差距和机会差距的闭合。
  38 +
  39 +现在向 master 汇报任务完成。 ✅ 任务 T-m
  40 +
  41 +### 2026-03-25 03:04:56 [completed] (1 次, 1分钟)
  42 +- 任务: [TASK from master]
  43 +任务编号: T-mn5glp6q
  44 +优先级: P2
  45 +
  46 +帮我制定下季度的OKR目标,公司方向是AI互动剧。简要列出结构即可,不需要工具。
  47 +
  48 +请立即执行此任务。完成后调
  49 +- 结果: ...馈收集机制 | 编辑器产品化 |
  50 +| **技术** | AI 推理性能优化 | 稳定性保障 | 工具链开发 |
  51 +| **内容** | 剧本质量把控 | 社区运营、创作者激励 | 编剧合作拓展 |
  52 +| **运营** | — | 内测招募、口碑传播 | — |
  53 +
  54 +---
  55 +
  56 +## 反思摘要
  57 +
  58 +- ✅ 成功创建/修改了 4 个文件: data/agents/marketing, data/agents/marketing/agent.md, data/agents/marketing/soul.md
  59 +- ✅ 完成了 6 步工具调用的复杂任务 (120秒)
  60 +- ❌ 错误: Connection refused to database. Error: ECONNREFUSED 127.0.0.1:5432
  61 +- ❌ 任务失败 (3 tool calls, 90秒): 部署服务到生产环境
data/agents/ceo/memory/hippocampus-index.json
@@ -30,5 +30,23 @@ @@ -30,5 +30,23 @@
30 "useCount": 0, 30 "useCount": 0,
31 "lastUsed": null, 31 "lastUsed": null,
32 "createdAt": "2026-03-25" 32 "createdAt": "2026-03-25"
  33 + },
  34 + {
  35 + "scenario": "创建新的 Marketing Agent 并配置所有技能文件",
  36 + "keywords": [
  37 + "marketing",
  38 + "agent",
  39 + "创建新的",
  40 + "创建",
  41 + "建新",
  42 + "新的",
  43 + "创建新",
  44 + "建新的"
  45 + ],
  46 + "planFile": "创建新的-marketing-agent-并配置所有技能文件.md",
  47 + "confidence": 0.72,
  48 + "useCount": 0,
  49 + "lastUsed": null,
  50 + "createdAt": "2026-03-25"
33 } 51 }
34 ] 52 ]
35 \ No newline at end of file 53 \ No newline at end of file
data/agents/ceo/memory/hippocampus.md
@@ -5,5 +5,6 @@ @@ -5,5 +5,6 @@
5 5
6 | # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 | 6 | # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 |
7 |---|---------|-----------|---------|--------|---------|----------| 7 |---|---------|-----------|---------|--------|---------|----------|
8 -| 1 | OKR目标制定 | OKR, 目标, 制定, 关键结果, objective, key result | okr目标制定.md | 0.85 | 0 | - | 8 +| 1 | OKR目标制定 | OKR, 目标, 制定, 关键结果, objective, key result | okr目标制定.md | 0.85 | 1 | 2026-03-25 |
9 | 2 | BLM战略规划 | BLM, 战略, 规划, 差距分析, 业务设计, 战略制定 | blm战略规划.md | 0.9 | 0 | - | 9 | 2 | BLM战略规划 | BLM, 战略, 规划, 差距分析, 业务设计, 战略制定 | blm战略规划.md | 0.9 | 0 | - |
  10 +| 3 | 创建新的 Marketing Agent 并配置所有技能文件 | marketing, agent, 创建新的, 创建, 建新, 新的, 创建新, 建新的 | 创建新的-marketing-agent-并配置所有技能文件.md | 0.72 | 0 | - |
data/agents/ceo/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 10:00:00 [] (, )
  16 +- 任务: 测试任务0
  17 +- 结果: 测试结果0
  18 +
  19 +### 2026-03-17 10:10:00 [] (, )
  20 +- 任务: 测试任务1
  21 +- 结果: 测试结果1
  22 +
  23 +### 2026-03-17 10:20:00 [] (, )
  24 +- 任务: 测试任务2
  25 +- 结果: 测试结果2
data/agents/ceo/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/ceo/memory/memory.md
@@ -6,7 +6,9 @@ @@ -6,7 +6,9 @@
6 - [2026-03-25] [TASK from master] → ...馈收集机制 | 编辑器产品化 | 6 - [2026-03-25] [TASK from master] → ...馈收集机制 | 编辑器产品化 |
7 7
8 ## 已验证的模式 8 ## 已验证的模式
9 -(尚无记录) 9 +- [2026-03-25] 成功创建/修改了 4 个文件: data/agents/marketing, data/agents/marketing/agent.md, data/agents/marketing/soul.md
  10 +- [2026-03-25] 完成了 6 步工具调用的复杂任务 (120秒)
10 11
11 ## 失败教训 12 ## 失败教训
12 -(尚无记录) 13 +- [2026-03-25] 错误: Connection refused to database. Error: ECONNREFUSED 127.0.0.1:5432
  14 +- [2026-03-25] 任务失败 (3 tool calls, 90秒): 部署服务到生产环境
data/agents/ceo/memory/reflections/reflect-1774408504165.json 0 → 100644
  1 +{
  2 + "timestamp": "2026-03-25T03:15:04.163Z",
  3 + "convId": "test-reflect-001",
  4 + "task": "创建新的 Marketing Agent 并配置所有技能文件",
  5 + "status": "completed",
  6 + "toolCount": 6,
  7 + "durationSec": 120,
  8 + "patterns": [
  9 + "成功创建/修改了 4 个文件: data/agents/marketing, data/agents/marketing/agent.md, data/agents/marketing/soul.md",
  10 + "完成了 6 步工具调用的复杂任务 (120秒)"
  11 + ],
  12 + "lessons": [],
  13 + "plan": {
  14 + "scenario": "创建新的 Marketing Agent 并配置所有技能文件",
  15 + "keywords": [
  16 + "marketing",
  17 + "agent",
  18 + "创建新的",
  19 + "创建",
  20 + "建新",
  21 + "新的",
  22 + "创建新",
  23 + "建新的"
  24 + ],
  25 + "steps": [
  26 + "创建 data/agents/marketing 目录结构",
  27 + "编写 agent.md 和 soul.md",
  28 + "配置 identity.md (名称: Marketing Agent)",
  29 + "编写 SKILL.md 主技能",
  30 + "更新 config.json",
  31 + "验证 Agent 列表"
  32 + ],
  33 + "confidence": 0.72
  34 + }
  35 +}
0 \ No newline at end of file 36 \ No newline at end of file
data/agents/ceo/memory/reflections/reflect-1774408504167.json 0 → 100644
  1 +{
  2 + "timestamp": "2026-03-25T03:15:04.166Z",
  3 + "convId": "test-fail",
  4 + "task": "部署服务到生产环境",
  5 + "status": "failed",
  6 + "toolCount": 3,
  7 + "durationSec": 90,
  8 + "patterns": [],
  9 + "lessons": [
  10 + "错误: Connection refused to database. Error: ECONNREFUSED 127.0.0.1:5432",
  11 + "任务失败 (3 tool calls, 90秒): 部署服务到生产环境"
  12 + ],
  13 + "plan": null
  14 +}
0 \ No newline at end of file 15 \ No newline at end of file
server/routes/agent-chat.js
@@ -20,7 +20,7 @@ const { ClaudeCliProtocol } = require('@mas/acp-core'); @@ -20,7 +20,7 @@ const { ClaudeCliProtocol } = require('@mas/acp-core');
20 const { Parser } = require('../src/core/parser'); 20 const { Parser } = require('../src/core/parser');
21 const AgentManager = require('../src/core/agent-manager'); 21 const AgentManager = require('../src/core/agent-manager');
22 const { GoalsService } = require('../services/goals'); 22 const { GoalsService } = require('../services/goals');
23 -const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans } = require('../services/memory-service'); 23 +const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans, autoReflect, consolidateMemory, generateDailyReport } = require('../services/memory-service');
24 const systemConfig = require('../config'); 24 const systemConfig = require('../config');
25 const router = express.Router(); 25 const router = express.Router();
26 26
@@ -647,6 +647,17 @@ router.post('/:agentId/memory/cerebellum', (req, res) => { @@ -647,6 +647,17 @@ router.post('/:agentId/memory/cerebellum', (req, res) => {
647 } 647 }
648 }); 648 });
649 649
  650 +// ── Phase 3: 记忆管理 API ──
  651 +router.post('/:agentId/memory/consolidate', (req, res) => {
  652 + const result = consolidateMemory(req.params.agentId);
  653 + res.json(result);
  654 +});
  655 +
  656 +router.post('/:agentId/memory/daily-report', (req, res) => {
  657 + const reportPath = generateDailyReport(req.params.agentId);
  658 + res.json({ success: !!reportPath, path: reportPath });
  659 +});
  660 +
650 router.get('/:agentId/info', (req, res) => { 661 router.get('/:agentId/info', (req, res) => {
651 const agentDir = path.join(DATA_DIR, req.params.agentId); 662 const agentDir = path.join(DATA_DIR, req.params.agentId);
652 if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); 663 if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' });
@@ -1348,6 +1359,13 @@ ${resultsText.join('\n\n')} @@ -1348,6 +1359,13 @@ ${resultsText.join('\n\n')}
1348 console.error('[Memory] Session 摘要保存失败:', memErr.message); 1359 console.error('[Memory] Session 摘要保存失败:', memErr.message);
1349 } 1360 }
1350 1361
  1362 + // ── Phase 3: 自动反思 (toolCallCount>=3 或 duration>=60s 时触发) ──
  1363 + try {
  1364 + autoReflect(agentId, convId, meta, fullOutput);
  1365 + } catch (refErr) {
  1366 + console.error('[Memory] 反思失败:', refErr.message);
  1367 + }
  1368 +
1351 sendSSE('result', { conversationId: convId, agentId, messageId: msgId, output: fullOutput }); 1369 sendSSE('result', { conversationId: convId, agentId, messageId: msgId, output: fullOutput });
1352 1370
1353 } catch (err) { 1371 } catch (err) {
server/scripts/test-memory-phase3.js 0 → 100644
  1 +#!/usr/bin/env node
  2 +/**
  3 + * Memory System Phase 3 — 集成测试
  4 + *
  5 + * 测试:
  6 + * 1. autoReflect: 高复杂度对话自动触发反思
  7 + * 2. consolidateMemory: 过期 short-term 合并到 long-term
  8 + * 3. generateDailyReport: 日报生成
  9 + */
  10 +
  11 +const http = require('http');
  12 +const fs = require('fs');
  13 +const path = require('path');
  14 +
  15 +const BASE = 'http://localhost:3000';
  16 +const AGENT = 'ceo';
  17 +const AGENT_DIR = path.resolve(__dirname, '../../data/agents', AGENT);
  18 +const MEMORY_DIR = path.join(AGENT_DIR, 'memory');
  19 +
  20 +let passed = 0, failed = 0;
  21 +
  22 +function assert(condition, msg) {
  23 + if (condition) { passed++; console.log(` ✅ ${msg}`); }
  24 + else { failed++; console.log(` ❌ ${msg}`); }
  25 +}
  26 +
  27 +function post(url, body) {
  28 + return new Promise((resolve, reject) => {
  29 + const data = JSON.stringify(body);
  30 + const u = new URL(url);
  31 + const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
  32 + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
  33 + }, res => {
  34 + let buf = '';
  35 + res.on('data', c => buf += c);
  36 + res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } });
  37 + });
  38 + req.on('error', reject);
  39 + req.write(data);
  40 + req.end();
  41 + });
  42 +}
  43 +
  44 +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  45 +
  46 +async function main() {
  47 + console.log('\n🧪 Memory System Phase 3 — 集成测试\n');
  48 + console.log('═'.repeat(50));
  49 +
  50 + // ── Test 1: 直接调用 autoReflect (模拟高复杂度对话) ──
  51 + console.log('\n📋 Test 1: autoReflect 引擎');
  52 + const { autoReflect } = require('../../server/services/memory-service');
  53 +
  54 + // 模拟一个 5-tool-call 的成功对话
  55 + const mockMeta = {
  56 + status: 'completed',
  57 + stats: { toolCallCount: 6, messageCount: 3 },
  58 + createdAt: new Date(Date.now() - 120000).toISOString(),
  59 + updatedAt: new Date().toISOString(),
  60 + title: '创建新的 Marketing Agent 并配置所有技能文件',
  61 + };
  62 + const mockOutput = `创建完成!以下是步骤:
  63 +1. 创建 data/agents/marketing 目录结构
  64 +2. 编写 agent.md 和 soul.md
  65 +3. 配置 identity.md (名称: Marketing Agent)
  66 +4. 编写 SKILL.md 主技能
  67 +5. 更新 config.json
  68 +6. 验证 Agent 列表
  69 +
  70 +文件已写入:
  71 +- data/agents/marketing/agent.md
  72 +- data/agents/marketing/soul.md
  73 +- data/agents/marketing/.claude/skills/main_skill/SKILL.md
  74 +
  75 +Agent 已成功创建并加入系统。`;
  76 +
  77 + const reflection = autoReflect(AGENT, 'test-reflect-001', mockMeta, mockOutput);
  78 +
  79 + assert(reflection !== null, 'autoReflect 返回了结果');
  80 + assert(reflection.patterns.length >= 1, `提取了 ${reflection.patterns.length} 个模式`);
  81 + assert(reflection.plan !== null, '自动固化了方案到小脑');
  82 +
  83 + // 验证反思日志
  84 + const reflectDir = path.join(MEMORY_DIR, 'reflections');
  85 + const reflectFiles = fs.existsSync(reflectDir)
  86 + ? fs.readdirSync(reflectDir).filter(f => f.startsWith('reflect-'))
  87 + : [];
  88 + assert(reflectFiles.length >= 1, `reflections/ 有 ${reflectFiles.length} 个日志文件`);
  89 +
  90 + // 验证 memory.md 更新
  91 + const memContent = fs.readFileSync(path.join(MEMORY_DIR, 'memory.md'), 'utf-8');
  92 + assert(!memContent.includes('已验证的模式\n(尚无记录)'), 'memory.md "已验证模式" 段有内容');
  93 +
  94 + // 验证小脑方案
  95 + const cerebellumFiles = fs.readdirSync(path.join(MEMORY_DIR, 'cerebellum'));
  96 + assert(cerebellumFiles.length >= 3, `cerebellum/ 有 ${cerebellumFiles.length} 个方案 (含自动固化的)`);
  97 +
  98 + // ── Test 2: autoReflect 对简单对话不触发 ──
  99 + console.log('\n📋 Test 2: 简单对话不触发反思');
  100 + const simpleMeta = {
  101 + status: 'completed',
  102 + stats: { toolCallCount: 1, messageCount: 1 },
  103 + createdAt: new Date(Date.now() - 10000).toISOString(),
  104 + updatedAt: new Date().toISOString(),
  105 + title: '回答一个简单问题',
  106 + };
  107 + const simpleResult = autoReflect(AGENT, 'test-simple', simpleMeta, '这是一个简单回答');
  108 + assert(simpleResult === null, '简单对话 (1 tool, 10s) 不触发反思');
  109 +
  110 + // ── Test 3: autoReflect 对失败对话提取教训 ──
  111 + console.log('\n📋 Test 3: 失败对话提取教训');
  112 + const failMeta = {
  113 + status: 'failed',
  114 + stats: { toolCallCount: 3, messageCount: 2 },
  115 + createdAt: new Date(Date.now() - 90000).toISOString(),
  116 + updatedAt: new Date().toISOString(),
  117 + title: '部署服务到生产环境',
  118 + };
  119 + const failOutput = 'error: Connection refused to database. Error: ECONNREFUSED 127.0.0.1:5432';
  120 + const failReflect = autoReflect(AGENT, 'test-fail', failMeta, failOutput);
  121 + assert(failReflect !== null, '失败对话触发了反思');
  122 + assert(failReflect.lessons.length >= 1, `提取了 ${failReflect.lessons.length} 个教训`);
  123 +
  124 + // ── Test 4: consolidateMemory ──
  125 + console.log('\n📋 Test 4: 记忆合并');
  126 + // 创建一些"过期"的 short-term 文件 (时间戳 = 8天前)
  127 + const shortDir = path.join(MEMORY_DIR, 'short-term');
  128 + const eightDaysAgo = Date.now() - 8 * 24 * 60 * 60 * 1000;
  129 + for (let i = 0; i < 3; i++) {
  130 + const ts = eightDaysAgo + i * 1000;
  131 + fs.writeFileSync(path.join(shortDir, `session-${ts}.md`),
  132 + `# Session 摘要\n\n- **时间**: 2026-03-17 10:${i}0:00\n\n## 任务\n测试任务${i}\n\n## 结果\n测试结果${i}\n`, 'utf-8');
  133 + }
  134 +
  135 + const consolidateResult = await post(`${BASE}/api/agent/${AGENT}/memory/consolidate`, {});
  136 + assert(consolidateResult.merged === 3, `合并了 ${consolidateResult.merged} 个过期 session (期望 3)`);
  137 +
  138 + // 验证 long-term 目录有文件
  139 + const longTermDir = path.join(MEMORY_DIR, 'long-term');
  140 + const ltFiles = fs.existsSync(longTermDir)
  141 + ? fs.readdirSync(longTermDir).filter(f => f.startsWith('consolidate-'))
  142 + : [];
  143 + assert(ltFiles.length >= 1, `long-term/ 有 ${ltFiles.length} 个合并记录`);
  144 +
  145 + // 验证过期文件已删除
  146 + const remainingShort = fs.readdirSync(shortDir).filter(f => {
  147 + const ts = parseInt(f.replace('session-', '').replace('.md', ''));
  148 + return !isNaN(ts) && ts < eightDaysAgo + 5000;
  149 + });
  150 + assert(remainingShort.length === 0, '过期的 short-term 文件已删除');
  151 +
  152 + // ── Test 5: generateDailyReport ──
  153 + console.log('\n📋 Test 5: 日报生成');
  154 + const reportResult = await post(`${BASE}/api/agent/${AGENT}/memory/daily-report`, {});
  155 + assert(reportResult.success, '日报生成成功');
  156 +
  157 + const today = new Date().toISOString().substring(0, 10);
  158 + const dailyFile = path.join(MEMORY_DIR, 'daily', `${today}.md`);
  159 + assert(fs.existsSync(dailyFile), `daily/${today}.md 文件存在`);
  160 +
  161 + if (fs.existsSync(dailyFile)) {
  162 + const dailyContent = fs.readFileSync(dailyFile, 'utf-8');
  163 + assert(dailyContent.includes('# 日报'), '日报包含标题');
  164 + assert(dailyContent.includes('对话数'), '日报包含统计');
  165 + }
  166 +
  167 + // ── 结果 ──
  168 + console.log('\n' + '═'.repeat(50));
  169 + console.log(`\n🏁 测试结果: ${passed} 通过, ${failed} 失败\n`);
  170 +
  171 + if (failed > 0) process.exit(1);
  172 +}
  173 +
  174 +main().catch(err => { console.error('❌ 测试崩溃:', err); process.exit(1); });
server/services/memory-service.js
@@ -604,6 +604,386 @@ function truncateMemory(content) { @@ -604,6 +604,386 @@ function truncateMemory(content) {
604 return newLines.join('\n'); 604 return newLines.join('\n');
605 } 605 }
606 606
  607 +// ════════════════════════════════════════
  608 +// 6. Phase 3: 自动反思引擎 (Reflect)
  609 +// ════════════════════════════════════════
  610 +
  611 +const REFLECT_MIN_TOOLS = 3; // 至少 N 个 tool call 才触发反思
  612 +const REFLECT_MIN_DURATION = 60; // 或至少持续 N 秒
  613 +const SHORT_TERM_MAX_AGE_DAYS = 7; // short-term 超过 N 天合并到 long-term
  614 +
  615 +/**
  616 + * 自动反思:分析对话结果,提取可复用的模式和教训
  617 + * 无需额外 LLM 调用 — 纯规则提取
  618 + *
  619 + * @param {string} agentId
  620 + * @param {string} convId
  621 + * @param {object} meta - { status, stats, createdAt, updatedAt, title }
  622 + * @param {string} output - Agent 最终输出
  623 + * @returns {object|null} 反思结果 { patterns, lessons, plan }
  624 + */
  625 +function autoReflect(agentId, convId, meta, output) {
  626 + try {
  627 + const toolCount = meta.stats?.toolCallCount || 0;
  628 + const createdAt = new Date(meta.createdAt);
  629 + const updatedAt = new Date(meta.updatedAt || Date.now());
  630 + const durationSec = Math.round((updatedAt.getTime() - createdAt.getTime()) / 1000);
  631 +
  632 + // 判断是否值得反思
  633 + if (toolCount < REFLECT_MIN_TOOLS && durationSec < REFLECT_MIN_DURATION) {
  634 + return null; // 太简单的对话不反思
  635 + }
  636 +
  637 + const agentDir = path.join(DATA_DIR, agentId);
  638 + const task = extractTask(meta.title || '', agentId, convId);
  639 + const isSuccess = meta.status === 'completed';
  640 + const isFailed = meta.status === 'failed';
  641 +
  642 + const reflection = {
  643 + timestamp: new Date().toISOString(),
  644 + convId,
  645 + task: task.substring(0, 200),
  646 + status: meta.status,
  647 + toolCount,
  648 + durationSec,
  649 + patterns: [],
  650 + lessons: [],
  651 + plan: null,
  652 + };
  653 +
  654 + // ── 提取模式 (成功对话) ──
  655 + if (isSuccess && toolCount >= 3) {
  656 + // 检测: 文件创建模式
  657 + const createdFiles = extractMentionedFiles(output);
  658 + if (createdFiles.length > 0) {
  659 + reflection.patterns.push(`成功创建/修改了 ${createdFiles.length} 个文件: ${createdFiles.slice(0, 3).join(', ')}`);
  660 + }
  661 +
  662 + // 检测: 多步骤执行模式
  663 + if (toolCount >= 5) {
  664 + reflection.patterns.push(`完成了 ${toolCount} 步工具调用的复杂任务 (${durationSec}秒)`);
  665 + }
  666 +
  667 + // 检测: dispatch 任务模式
  668 + if (output.includes('dispatch') || output.includes('task/dispatch')) {
  669 + reflection.patterns.push('涉及向其他 Agent 分发子任务');
  670 + }
  671 +
  672 + // 检测: 训练/进化模式
  673 + if (output.includes('benchmark') || output.includes('score') || output.includes('evolve')) {
  674 + reflection.patterns.push('涉及技能训练/评分/进化流程');
  675 + }
  676 + }
  677 +
  678 + // ── 提取教训 (失败对话) ──
  679 + if (isFailed) {
  680 + const errorMatch = output.match(/(?:error|错误|失败|Error)[::]\s*(.{10,200})/i);
  681 + if (errorMatch) {
  682 + reflection.lessons.push(`错误: ${errorMatch[1].trim()}`);
  683 + }
  684 + reflection.lessons.push(`任务失败 (${toolCount} tool calls, ${durationSec}秒): ${task.substring(0, 100)}`);
  685 + }
  686 +
  687 + // ── 自动固化方案 (成功 + 高复杂度) ──
  688 + if (isSuccess && toolCount >= 5) {
  689 + // 从任务和输出中提取关键词作为场景
  690 + const keywords = extractKeywords(task);
  691 + const topKeywords = keywords.slice(0, 8);
  692 +
  693 + // 尝试从输出中提取步骤
  694 + const steps = extractStepsFromOutput(output);
  695 +
  696 + if (steps.length >= 2 && topKeywords.length >= 2) {
  697 + const scenario = task.split('\n')[0].substring(0, 50);
  698 +
  699 + reflection.plan = {
  700 + scenario,
  701 + keywords: topKeywords,
  702 + steps,
  703 + confidence: Math.min(0.6 + (toolCount * 0.02), 0.9), // 工具越多,置信度越高
  704 + };
  705 +
  706 + // 自动保存到小脑
  707 + saveCerebellumPlan(agentId, {
  708 + ...reflection.plan,
  709 + conditions: [`类似任务: ${scenario}`],
  710 + notes: [`自动从对话 ${convId} 提取`, `耗时 ${durationSec}秒, ${toolCount} 次工具调用`],
  711 + });
  712 + }
  713 + }
  714 +
  715 + // ── 写入反思日志 ──
  716 + if (reflection.patterns.length > 0 || reflection.lessons.length > 0) {
  717 + writeReflectionLog(agentDir, reflection);
  718 +
  719 + // 更新 memory.md 的"已验证模式"和"失败教训"段
  720 + updateMemoryMdSections(agentId, reflection);
  721 +
  722 + console.log(`[Memory] 🪞 反思完成: ${agentId} (${reflection.patterns.length} 模式, ${reflection.lessons.length} 教训${reflection.plan ? ', +1 方案固化' : ''})`);
  723 + }
  724 +
  725 + return reflection;
  726 + } catch (err) {
  727 + console.error(`[Memory] ⚠️ 反思失败:`, err.message);
  728 + return null;
  729 + }
  730 +}
  731 +
  732 +/** 从输出中提取步骤(通过数字列表匹配) */
  733 +function extractStepsFromOutput(output) {
  734 + if (!output) return [];
  735 + // 匹配 "1. xxx" "2. xxx" 格式的步骤
  736 + const stepMatches = output.match(/^\d+\.\s+.{5,100}$/gm) || [];
  737 + return stepMatches.slice(0, 8).map(s => s.replace(/^\d+\.\s+/, '').trim());
  738 +}
  739 +
  740 +/** 写入反思日志文件 */
  741 +function writeReflectionLog(agentDir, reflection) {
  742 + const logDir = path.join(agentDir, 'memory', 'reflections');
  743 + if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
  744 +
  745 + const filename = `reflect-${Date.now()}.json`;
  746 + fs.writeFileSync(path.join(logDir, filename), JSON.stringify(reflection, null, 2), 'utf-8');
  747 +
  748 + // 只保留最近 20 个反思日志
  749 + const files = fs.readdirSync(logDir).filter(f => f.startsWith('reflect-')).sort();
  750 + while (files.length > 20) {
  751 + fs.unlinkSync(path.join(logDir, files.shift()));
  752 + }
  753 +}
  754 +
  755 +/** 更新 memory.md 的 "已验证模式" 和 "失败教训" 段 */
  756 +function updateMemoryMdSections(agentId, reflection) {
  757 + const memoryPath = path.join(DATA_DIR, agentId, 'memory', 'memory.md');
  758 + let content = '';
  759 + try { content = fs.readFileSync(memoryPath, 'utf-8'); } catch { return; }
  760 +
  761 + const date = new Date().toISOString().substring(0, 10);
  762 +
  763 + // 更新"已验证的模式"
  764 + if (reflection.patterns.length > 0) {
  765 + const patternEntries = reflection.patterns.map(p => `- [${date}] ${p}`).join('\n');
  766 + if (content.includes('## 已验证的模式')) {
  767 + content = content.replace(
  768 + /## 已验证的模式\n([\s\S]*?)(?=\n## |$)/,
  769 + (match, existing) => {
  770 + const trimmed = existing.trim();
  771 + if (trimmed === '(尚无记录)') return `## 已验证的模式\n${patternEntries}\n`;
  772 + return `## 已验证的模式\n${trimmed}\n${patternEntries}\n`;
  773 + }
  774 + );
  775 + }
  776 + }
  777 +
  778 + // 更新"失败教训"
  779 + if (reflection.lessons.length > 0) {
  780 + const lessonEntries = reflection.lessons.map(l => `- [${date}] ${l}`).join('\n');
  781 + if (content.includes('## 失败教训')) {
  782 + content = content.replace(
  783 + /## 失败教训\n([\s\S]*?)(?=\n## |$)/,
  784 + (match, existing) => {
  785 + const trimmed = existing.trim();
  786 + if (trimmed === '(尚无记录)') return `## 失败教训\n${lessonEntries}\n`;
  787 + return `## 失败教训\n${trimmed}\n${lessonEntries}\n`;
  788 + }
  789 + );
  790 + }
  791 + }
  792 +
  793 + // 大小控制
  794 + if (content.length > MAX_MEMORY_MD_SIZE) {
  795 + content = truncateMemory(content);
  796 + }
  797 +
  798 + fs.writeFileSync(memoryPath, content, 'utf-8');
  799 +}
  800 +
  801 +// ════════════════════════════════════════
  802 +// 7. Phase 3: 记忆合并 (Consolidate)
  803 +// ════════════════════════════════════════
  804 +
  805 +/**
  806 + * 合并过期的 short-term 到 long-term
  807 + * 适合通过 CronCreate 每天凌晨调用
  808 + *
  809 + * @param {string} agentId
  810 + * @returns {object} 合并统计
  811 + */
  812 +function consolidateMemory(agentId) {
  813 + try {
  814 + const agentDir = path.join(DATA_DIR, agentId);
  815 + const shortTermDir = path.join(agentDir, 'memory', 'short-term');
  816 + const longTermDir = path.join(agentDir, 'memory', 'long-term');
  817 +
  818 + if (!fs.existsSync(shortTermDir)) return { merged: 0 };
  819 + if (!fs.existsSync(longTermDir)) fs.mkdirSync(longTermDir, { recursive: true });
  820 +
  821 + const now = Date.now();
  822 + const maxAge = SHORT_TERM_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
  823 +
  824 + const files = fs.readdirSync(shortTermDir)
  825 + .filter(f => f.startsWith('session-') && f.endsWith('.md'))
  826 + .sort();
  827 +
  828 + // 找出过期文件
  829 + const expired = files.filter(f => {
  830 + const ts = parseInt(f.replace('session-', '').replace('.md', ''));
  831 + return !isNaN(ts) && (now - ts) > maxAge;
  832 + });
  833 +
  834 + if (expired.length === 0) return { merged: 0 };
  835 +
  836 + // 读取所有过期文件内容
  837 + const summaries = expired.map(f => {
  838 + const content = fs.readFileSync(path.join(shortTermDir, f), 'utf-8');
  839 + return compactSession(content);
  840 + });
  841 +
  842 + // 提取反复出现的关键词 (简单频率统计)
  843 + const allKeywords = [];
  844 + for (const s of summaries) {
  845 + allKeywords.push(...extractKeywords(s));
  846 + }
  847 + const kwFreq = {};
  848 + allKeywords.forEach(kw => { kwFreq[kw] = (kwFreq[kw] || 0) + 1; });
  849 + const frequentKW = Object.entries(kwFreq)
  850 + .filter(([_, count]) => count >= 2)
  851 + .sort((a, b) => b[1] - a[1])
  852 + .slice(0, 10)
  853 + .map(([kw, count]) => `${kw} (x${count})`);
  854 +
  855 + // 写入 long-term 合并记录
  856 + const consolidateDate = new Date().toISOString().substring(0, 10);
  857 + const ltContent = [
  858 + `# 长期记忆合并 — ${consolidateDate}`,
  859 + '',
  860 + `> 合并了 ${expired.length} 个过期 session (>${SHORT_TERM_MAX_AGE_DAYS} 天)`,
  861 + '',
  862 + frequentKW.length > 0 ? `## 高频关键词\n${frequentKW.map(k => `- ${k}`).join('\n')}` : '',
  863 + '',
  864 + '## 合并的对话摘要',
  865 + '',
  866 + ...summaries,
  867 + ].filter(Boolean).join('\n');
  868 +
  869 + const ltFilename = `consolidate-${consolidateDate}.md`;
  870 + fs.writeFileSync(path.join(longTermDir, ltFilename), ltContent, 'utf-8');
  871 +
  872 + // 更新 long-term/patterns.md (追加高频模式)
  873 + if (frequentKW.length > 0) {
  874 + const patternsPath = path.join(longTermDir, 'patterns.md');
  875 + let patterns = '';
  876 + try { patterns = fs.readFileSync(patternsPath, 'utf-8'); } catch {}
  877 + if (!patterns) patterns = '# 长期模式\n\n> 从 short-term 多次出现的模式沉淀而来\n\n';
  878 + patterns += `\n## ${consolidateDate}\n${frequentKW.map(k => `- ${k}`).join('\n')}\n`;
  879 + fs.writeFileSync(patternsPath, patterns, 'utf-8');
  880 + }
  881 +
  882 + // 删除已合并的 short-term 文件
  883 + for (const f of expired) {
  884 + fs.unlinkSync(path.join(shortTermDir, f));
  885 + }
  886 +
  887 + console.log(`[Memory] 📦 记忆合并: ${agentId} (${expired.length} sessions → long-term/${ltFilename})`);
  888 + return { merged: expired.length, file: ltFilename, keywords: frequentKW };
  889 + } catch (err) {
  890 + console.error(`[Memory] ⚠️ 记忆合并失败:`, err.message);
  891 + return { merged: 0, error: err.message };
  892 + }
  893 +}
  894 +
  895 +// ════════════════════════════════════════
  896 +// 8. Phase 3: 日报生成 (Daily Report)
  897 +// ════════════════════════════════════════
  898 +
  899 +/**
  900 + * 生成当天的日报摘要
  901 + *
  902 + * @param {string} agentId
  903 + * @returns {string|null} 日报文件路径
  904 + */
  905 +function generateDailyReport(agentId) {
  906 + try {
  907 + const agentDir = path.join(DATA_DIR, agentId);
  908 + const shortTermDir = path.join(agentDir, 'memory', 'short-term');
  909 + const dailyDir = path.join(agentDir, 'memory', 'daily');
  910 + const reflectDir = path.join(agentDir, 'memory', 'reflections');
  911 +
  912 + if (!fs.existsSync(dailyDir)) fs.mkdirSync(dailyDir, { recursive: true });
  913 +
  914 + const today = new Date().toISOString().substring(0, 10);
  915 +
  916 + // 收集今天的 session 摘要
  917 + const todaySessions = [];
  918 + if (fs.existsSync(shortTermDir)) {
  919 + const files = fs.readdirSync(shortTermDir).filter(f => f.startsWith('session-'));
  920 + for (const f of files) {
  921 + const content = fs.readFileSync(path.join(shortTermDir, f), 'utf-8');
  922 + if (content.includes(today)) {
  923 + todaySessions.push(compactSession(content));
  924 + }
  925 + }
  926 + }
  927 +
  928 + // 收集今天的反思
  929 + const todayReflections = [];
  930 + if (fs.existsSync(reflectDir)) {
  931 + const files = fs.readdirSync(reflectDir).filter(f => f.startsWith('reflect-'));
  932 + for (const f of files) {
  933 + try {
  934 + const r = JSON.parse(fs.readFileSync(path.join(reflectDir, f), 'utf-8'));
  935 + if (r.timestamp && r.timestamp.startsWith(today)) {
  936 + todayReflections.push(r);
  937 + }
  938 + } catch {}
  939 + }
  940 + }
  941 +
  942 + if (todaySessions.length === 0 && todayReflections.length === 0) {
  943 + return null; // 今天没活动
  944 + }
  945 +
  946 + // 统计
  947 + const totalPatterns = todayReflections.reduce((sum, r) => sum + (r.patterns?.length || 0), 0);
  948 + const totalLessons = todayReflections.reduce((sum, r) => sum + (r.lessons?.length || 0), 0);
  949 + const totalPlans = todayReflections.filter(r => r.plan).length;
  950 +
  951 + const report = [
  952 + `# 日报 — ${today}`,
  953 + '',
  954 + `## 概览`,
  955 + `- 对话数: ${todaySessions.length}`,
  956 + `- 反思数: ${todayReflections.length}`,
  957 + `- 新模式: ${totalPatterns}`,
  958 + `- 新教训: ${totalLessons}`,
  959 + `- 固化方案: ${totalPlans}`,
  960 + '',
  961 + ];
  962 +
  963 + if (todaySessions.length > 0) {
  964 + report.push('## 对话记录', '', ...todaySessions);
  965 + }
  966 +
  967 + if (todayReflections.length > 0) {
  968 + report.push('## 反思摘要', '');
  969 + for (const r of todayReflections) {
  970 + if (r.patterns?.length) report.push(...r.patterns.map(p => `- ✅ ${p}`));
  971 + if (r.lessons?.length) report.push(...r.lessons.map(l => `- ❌ ${l}`));
  972 + }
  973 + report.push('');
  974 + }
  975 +
  976 + const reportPath = path.join(dailyDir, `${today}.md`);
  977 + fs.writeFileSync(reportPath, report.join('\n'), 'utf-8');
  978 +
  979 + console.log(`[Memory] 📰 日报已生成: ${agentId}/daily/${today}.md`);
  980 + return reportPath;
  981 + } catch (err) {
  982 + console.error(`[Memory] ⚠️ 日报生成失败:`, err.message);
  983 + return null;
  984 + }
  985 +}
  986 +
607 module.exports = { 987 module.exports = {
608 // Phase 1 988 // Phase 1
609 saveSessionSummary, 989 saveSessionSummary,
@@ -613,6 +993,10 @@ module.exports = { @@ -613,6 +993,10 @@ module.exports = {
613 retrieveRelevantPlans, 993 retrieveRelevantPlans,
614 saveCerebellumPlan, 994 saveCerebellumPlan,
615 extractKeywords, 995 extractKeywords,
  996 + // Phase 3
  997 + autoReflect,
  998 + consolidateMemory,
  999 + generateDailyReport,
616 // 内部 (测试用) 1000 // 内部 (测试用)
617 loadHippocampusIndex, 1001 loadHippocampusIndex,
618 saveHippocampusIndex, 1002 saveHippocampusIndex,