mocode-ai 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,12 +6,13 @@ import { runAgent } from '../agent/index.js';
6
6
  import { ui } from '../ui/theme.js';
7
7
  import { bannerString, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
8
8
  import * as layout from '../ui/layout.js';
9
- import { promptWithSlashMenu, promptTurnPicker } from '../ui/prompt.js';
9
+ import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, } from '../ui/prompt.js';
10
10
  import { tools } from '../tools/registry.js';
11
11
  import { estimateMessagesTokens, estimateToolSchemaTokens, } from '../llm/index.js';
12
12
  import { compactHistory, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
13
13
  import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
14
14
  import { listSkills, effectiveSystemPrompt } from '../skills/index.js';
15
+ import { buildMemorySection, buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
15
16
  /**
16
17
  * readline 的 prompt 必须是纯文本(无 ANSI):readline 按字符数算光标位置,
17
18
  * 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
@@ -27,7 +28,39 @@ const SLASH_COMMANDS = [
27
28
  { name: '/resume', desc: '续接已保存的会话' },
28
29
  { name: '/think', desc: '展开折叠思考段(/think N)' },
29
30
  { name: '/rollback', desc: '菜单选轮次回滚(↑↓·Enter)' },
31
+ { name: '/memory', desc: '记忆库:条目计数与近期索引' },
32
+ { name: '/reflect', desc: '手动触发后台记忆反思 pass' },
33
+ { name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆' },
30
34
  ];
35
+ /**
36
+ * /init 指令:发给 agent 扫描项目并生成 MOCODE.md(对标 Claude Code /init 生成 CLAUDE.md,
37
+ * 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
38
+ */
39
+ const INIT_PROMPT = `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
40
+
41
+ 先探查(尽量少调用拿全貌):
42
+ - read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
43
+ - glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
44
+ - 若有 .codegraph/:run_command codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读。
45
+ - 若 MOCODE.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
46
+
47
+ MOCODE.md 按以下结构写(每节简短,只写稳定、非显然的事实):
48
+ ## 项目
49
+ 一两句:是什么、技术栈、运行环境。
50
+ ## 命令
51
+ install / dev / build / test / typecheck / lint 等——从 package.json scripts 提炼,写原样命令行(如 \`npm run typecheck\`);没有的注明"无测试"/"无 lint"。
52
+ ## 目录结构
53
+ 顶层各目录与子系统职责,一句话/个;不逐文件列。
54
+ ## 约定
55
+ 从代码与现有文档提炼的硬约定:模块系统(ESM?)、命名、错误处理、工具/函数契约、易踩坑点。只写非显然、会让人踩坑的;不写"保持简洁"这种正确废话。
56
+ ## 扩展点
57
+ 加工具/命令/provider/模块的接缝(改哪个文件、加在哪)。
58
+
59
+ 硬要求:
60
+ - 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
61
+ - 总长 ≤ 1500 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
62
+ - 用 write_file 写入项目根 MOCODE.md。
63
+ - 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
31
64
  /** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
32
65
  async function askLine(prompt) {
33
66
  const rl = readline.createInterface({ input: stdin, output: stdout });
@@ -84,10 +117,14 @@ function runningStateFor(cmd) {
84
117
  return { status: '续接', placeholder: '选择会话…' };
85
118
  case '/rollback':
86
119
  return { status: '回滚', placeholder: '选择轮次…' };
120
+ case '/init':
121
+ return { status: '初始化', placeholder: '生成 MOCODE.md…' };
87
122
  case '/clear':
88
123
  return { status: '清空', placeholder: '…' };
89
124
  default:
90
- return { status: '处理', placeholder: '思考中… Ctrl+C 中断' };
125
+ // 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由状态行 spinner 承载,
126
+ // 不再把「思考中… Ctrl+C 中断」塞进输入框占位——避免看起来像已有输入、妨碍正常输入。
127
+ return { status: '处理', placeholder: '' };
91
128
  }
92
129
  }
93
130
  const emitter = stdin;
@@ -265,7 +302,7 @@ export function renderHistory(history) {
265
302
  export async function startRepl(initialHistory, sessionId, updateNotice = null) {
266
303
  // 有预加载(--resume)则用它,并把 history[0] 刷成当前 system prompt(config 可能已变);
267
304
  // 否则新会话只塞 system 提示。
268
- const systemPrompt = effectiveSystemPrompt(config.systemPrompt);
305
+ const systemPrompt = effectiveSystemPrompt(config.systemPrompt + buildMemorySection() + buildMemoryIndexSection());
269
306
  const history = initialHistory && initialHistory.length
270
307
  ? initialHistory
271
308
  : [{ role: 'system', content: systemPrompt }];
@@ -280,6 +317,8 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
280
317
  rebuildFromHistory(history);
281
318
  }
282
319
  let currentSessionId = sessionId;
320
+ // 反思 cadence 计数:每 reflectEveryN 轮 fire-and-forget 一次后台反思 pass。
321
+ let turnCount = 0;
283
322
  // 本会话累积的折叠思考段,供 /think N 重打原文。
284
323
  const collapsedThinkings = [];
285
324
  const toolsLine = tools.map((t) => t.name).join(' · ');
@@ -381,6 +420,13 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
381
420
  while (true) {
382
421
  // INPUT 态:画底栏输入框 + 状态行,光标入输入框
383
422
  refreshStatusBase(history);
423
+ // 后台反思若已完成(上轮 fire-and-forget),在安全点 flush 一行 dim 摘要:
424
+ // 不在 RUNNING 态写——防与正在跑的 agent 争屏(contentWrite / 状态行)。
425
+ const reflectRes = getLastReflectResult();
426
+ if (reflectRes) {
427
+ layout.contentWrite(` ${ui.gray}↳ ${formatReflectResult(reflectRes)}${ui.reset}\n`);
428
+ clearLastReflectResult();
429
+ }
384
430
  layout.enterInputMode('空闲');
385
431
  let input = null;
386
432
  try {
@@ -402,7 +448,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
402
448
  runningInput = ''; // 预填已消费,清空(下轮运行态从空开始)
403
449
  if (input === null)
404
450
  break; // 空 prompt Ctrl+D
405
- const joined = input.join('\n');
451
+ let joined = input.join('\n');
406
452
  const line = joined.trim();
407
453
  if (!line)
408
454
  continue;
@@ -414,11 +460,16 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
414
460
  const { status, placeholder } = runningStateFor(cmd);
415
461
  refreshStatusBase(history);
416
462
  layout.enterRunningMode(status, placeholder);
463
+ if (line === '/init') {
464
+ // /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
465
+ joined = INIT_PROMPT;
466
+ }
417
467
  if (line === '/clear') {
418
468
  history.length = 1; // 保留 system 提示
419
469
  collapsedThinkings.length = 0; // 同步清空折叠的思考段
420
470
  resetState(); // 同步清空回滚轮次/快照
421
471
  currentSessionId = undefined; // 下轮起新会话文件
472
+ turnCount = 0; // 反思 cadence 重新计数
422
473
  contextState.lastUsage = undefined;
423
474
  layout.clearContent();
424
475
  layout.contentWrite(bannerString(banner()));
@@ -429,6 +480,35 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
429
480
  layout.contentWrite(` ${renderContextBar(history)}\n`);
430
481
  continue;
431
482
  }
483
+ if (line === '/memory') {
484
+ // 记忆库概览:计数 + 近期 active 索引(详情用 memory_search)。
485
+ const all = loadAll();
486
+ const active = all.filter((e) => e.status === 'active');
487
+ const archived = all.filter((e) => e.status === 'archived').length;
488
+ const byType = {};
489
+ for (const e of active)
490
+ byType[e.type] = (byType[e.type] || 0) + 1;
491
+ layout.contentWrite(`${ui.dim}记忆库:active ${active.length}${archived ? ` · archived ${archived}` : ''}${ui.reset}\n`);
492
+ if (Object.keys(byType).length) {
493
+ layout.contentWrite(`${ui.dim}按类:${Object.entries(byType).map(([t, n]) => `${t} ${n}`).join(' ')}${ui.reset}\n`);
494
+ }
495
+ const recent = [...active]
496
+ .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
497
+ .slice(0, 10);
498
+ for (const e of recent) {
499
+ layout.contentWrite(` ${ui.cyan}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
500
+ }
501
+ if (active.length === 0)
502
+ layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 MOCODE.md)${ui.reset}\n`);
503
+ layout.contentWrite(`${ui.dim}(详情用 memory_search;启动索引已注入 systemPrompt)${ui.reset}\n`);
504
+ continue;
505
+ }
506
+ if (line === '/reflect') {
507
+ // 手动触发后台反思 pass(不等;完成后下次 INPUT 态显摘要)。
508
+ kickoffReflection(snapshotTranscript(history, 20));
509
+ layout.contentWrite(`${ui.dim}(反思已触发,后台进行;完成后下次输入态显示摘要。日志见 .mocode/memory.log)${ui.reset}\n`);
510
+ continue;
511
+ }
432
512
  if (line === '/skills') {
433
513
  const skills = listSkills();
434
514
  if (skills.length === 0) {
@@ -458,25 +538,28 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
458
538
  continue;
459
539
  }
460
540
  if (line === '/resume') {
541
+ // /resume:打开会话菜单(↑/↓ 选,Enter 续接,Esc 取消)。默认仅最近 10 条,按 a 展开全部。
542
+ // 仿 /rollback 菜单化(promptSessionPicker);选中项 cyan+bold + ▸ 高亮。
461
543
  const sessions = listSessions();
462
544
  if (sessions.length === 0) {
463
545
  layout.contentWrite(`${ui.dim}(没有已保存的会话)${ui.reset}\n`);
464
546
  continue;
465
547
  }
466
- sessions.forEach((s, i) => {
467
- layout.contentWrite(` ${ui.dim}${i + 1}${ui.reset} ${s.id} ${ui.cyan}${s.firstUser || '(无)'}${ui.reset} ${ui.dim}${s.model}${ui.reset}\n`);
468
- });
469
- let pick = '';
548
+ const items = sessions.map((s) => ({
549
+ id: s.id,
550
+ title: s.firstUser || '(无)',
551
+ subtitle: `${s.id} ${s.model}`,
552
+ }));
553
+ let pick;
470
554
  try {
471
- pick = (await askLine('序号(回车取消): ')).trim();
555
+ pick = await promptSessionPicker(items);
472
556
  }
473
557
  catch {
474
- continue;
558
+ continue; // Ctrl+C(SIGINT)→ 取消
475
559
  }
476
- const idx = Number(pick);
477
- if (!pick || !Number.isInteger(idx) || idx < 1 || idx > sessions.length)
478
- continue;
479
- const loaded = loadSession(sessions[idx - 1].id);
560
+ if (!pick)
561
+ continue; // Esc / Ctrl+D 取消
562
+ const loaded = loadSession(pick.id);
480
563
  if (!loaded || !loaded.history.length) {
481
564
  layout.contentWrite(`${ui.yellow}(加载失败)${ui.reset}\n`);
482
565
  continue;
@@ -539,6 +622,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
539
622
  // 落盘失败不阻断 REPL
540
623
  }
541
624
  persistSnapshots(currentSessionId); // 随会话落盘回滚快照(/resume 后仍可撤销)
625
+ // 后台反思:每 reflectEveryN 轮 fire-and-forget 一次(与下一轮 agent 并发,不阻塞)。
626
+ // 已有在飞任务 / autoReflect 关 → kickoff 内部自守卫。快照同步取(避免下一轮 mutate history 竞态)。
627
+ turnCount++;
628
+ if (turnCount % config.reflectEveryN === 0) {
629
+ kickoffReflection(snapshotTranscript(history, 20));
630
+ }
542
631
  }
543
632
  catch (e) {
544
633
  layout.contentWrite(`${ui.red}[错误]${ui.reset} ${e instanceof Error ? e.message : String(e)}\n`);
@@ -548,5 +637,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
548
637
  }
549
638
  layout.contentWrite('\n'); // 轮次之间空行
550
639
  }
640
+ // 退出前等在飞反思收尾(Ctrl+C 走 SIGINT 直退不等;fire-and-forget 不承诺中断时完成)。
641
+ await drainMemoryBackground();
551
642
  layout.exitAltScreen();
552
643
  }
@@ -1,6 +1,6 @@
1
1
  import { chat, estimateMessagesTokens, estimateToolSchemaTokens, estimateTokens, } from '../llm/index.js';
2
2
  import { config } from '../config/index.js';
3
- import { MAX_HISTORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT } from '../tools/constants.js';
3
+ import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT } from '../tools/constants.js';
4
4
  import { ui } from '../ui/theme.js';
5
5
  import { Spinner } from '../ui/spinner.js';
6
6
  import * as layout from '../ui/layout.js';
@@ -40,6 +40,17 @@ export function capToolResultForHistory(name, output) {
40
40
  return marker.slice(0, MAX_SKILL_RESULT);
41
41
  return output.slice(0, remain) + marker;
42
42
  }
43
+ if (name === 'memory_search') {
44
+ // 召回的记忆正文是高价值事实/指令,中截会劈断语义;同 use_skill 走尾截(保头部、弃尾部)。
45
+ if (output.length <= MAX_MEMORY_RESULT)
46
+ return output;
47
+ const removed = output.length - MAX_MEMORY_RESULT;
48
+ const marker = `…[记忆结果过长,已截断尾部 ${removed} 字符]…`;
49
+ const remain = MAX_MEMORY_RESULT - marker.length;
50
+ if (remain <= 0)
51
+ return marker.slice(0, MAX_MEMORY_RESULT);
52
+ return output.slice(0, remain) + marker;
53
+ }
43
54
  if (output.length <= MAX_HISTORY_RESULT)
44
55
  return output;
45
56
  return truncateMid(output, MAX_HISTORY_RESULT);
@@ -194,7 +205,12 @@ export async function compactHistory(history, opts) {
194
205
  }
195
206
  return noop;
196
207
  }
197
- // 第一层:微压缩——旧区 tool 结果原地截短(保 tool_call_id,无 LLM 调用)
208
+ // 第一层:微压缩——旧区原地截短(保 tool_call_id,无 LLM 调用)
209
+ // 覆盖三类大字段,均只裁模型/工具产物,不动 user 原话与 system(摘要):
210
+ // ① tool 结果 content;
211
+ // ② 旧 assistant 的 tool_calls.arguments —— 保 JSON 合法:整体超长才进,
212
+ // 逐字段值裁中截后重新 stringify(不裸中截会劈断 JSON、严格后端 400);parse 失败跳过;
213
+ // ③ 旧 assistant 正文 content(模型长解释,回看价值低)。
198
214
  let microcompactDone = false;
199
215
  for (const g of oldGroups) {
200
216
  for (const t of g.tools) {
@@ -204,6 +220,38 @@ export async function compactHistory(history, opts) {
204
220
  microcompactDone = true;
205
221
  }
206
222
  }
223
+ const as = g.assistant;
224
+ if (as && as.role === 'assistant') {
225
+ // ③ 旧正文 content
226
+ if (typeof as.content === 'string' && as.content.length > MAX_OLD_TOOL_STUB) {
227
+ as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
228
+ microcompactDone = true;
229
+ }
230
+ // ② tool_calls 参数:整体超长才进,逐字段裁值,保 JSON 合法
231
+ if (Array.isArray(as.tool_calls)) {
232
+ for (const tc of as.tool_calls) {
233
+ const args = tc?.function?.arguments;
234
+ if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
235
+ continue;
236
+ try {
237
+ const parsed = JSON.parse(args);
238
+ if (parsed && typeof parsed === 'object') {
239
+ for (const k of Object.keys(parsed)) {
240
+ const v = parsed[k];
241
+ if (typeof v === 'string' && v.length > MAX_OLD_TOOL_STUB) {
242
+ parsed[k] = truncateMid(v, MAX_OLD_TOOL_STUB);
243
+ }
244
+ }
245
+ tc.function.arguments = JSON.stringify(parsed);
246
+ microcompactDone = true;
247
+ }
248
+ }
249
+ catch {
250
+ // arguments 非合法 JSON(模型偶发):不裁,沿用「调度器永不抛错」
251
+ }
252
+ }
253
+ }
254
+ }
207
255
  }
208
256
  // 第二层:摘要——把旧区(微压缩后)压成一条 system 摘要
209
257
  const older = flattenGroups(oldGroups);
@@ -0,0 +1,50 @@
1
+ import { promptIntervention } from '../../ui/intervention.js';
2
+ // ---------- ask_human ----------
3
+ export const askHumanTool = {
4
+ name: 'ask_human',
5
+ description: [
6
+ '当你在执行任务中遇到需要人类做决策的岔路时调用此工具,在终端弹出问题面板让用户选择。',
7
+ '适用:多种实现方案需要用户拍板、不确定用户意图需要澄清、需要用户提供额外信息才能继续。',
8
+ '调用后阻塞等待用户响应;用户可挑预设选项,也可选"自定义输入"自由作答,结果作为工具返回值返回。',
9
+ '不要在任务明确、可自行决定时频繁调用——会打断用户。options 省略或为空时改为自由文本输入。',
10
+ ].join(''),
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ question: {
15
+ type: 'string',
16
+ description: '要问用户的问题,简明扼要(显示为面板标题)',
17
+ },
18
+ options: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: '供用户选择的选项(2~6 个)。可省略——省略或为空时改为自由文本输入',
22
+ },
23
+ context: {
24
+ type: 'string',
25
+ description: '问题的背景说明(可选,帮助用户理解为何需要他决策;显示在标题下,可多行)',
26
+ },
27
+ },
28
+ required: ['question'],
29
+ },
30
+ async execute(args) {
31
+ const question = String(args.question ?? '');
32
+ const options = Array.isArray(args.options)
33
+ ? args.options.map((o) => String(o))
34
+ : [];
35
+ const context = args.context ? String(args.context) : undefined;
36
+ const result = await promptIntervention({
37
+ type: options.length > 0 ? 'choice' : 'input',
38
+ title: question,
39
+ options: options.length > 0 ? options : undefined,
40
+ detail: context,
41
+ });
42
+ if (result.action === 'cancelled') {
43
+ return '用户取消了选择。请考虑是否有不依赖用户输入的替代方案,或换个角度重新提问。';
44
+ }
45
+ if (result.action === 'submitted') {
46
+ return `用户回答:${result.value ?? ''}`;
47
+ }
48
+ return `用户选择:${result.value ?? ''}`;
49
+ },
50
+ };
@@ -7,6 +7,12 @@ import { grepTool } from './grep.js';
7
7
  import { webSearchTool } from './web-search.js';
8
8
  import { webFetchTool } from './web-fetch.js';
9
9
  import { useSkillTool } from './use-skill.js';
10
+ import { askHumanTool } from './ask-human.js';
11
+ import { memorySaveTool } from './memory-save.js';
12
+ import { memorySearchTool } from './memory-search.js';
13
+ import { memoryListTool } from './memory-list.js';
14
+ import { memoryUpdateTool } from './memory-update.js';
15
+ import { memoryForgetTool } from './memory-forget.js';
10
16
  /**
11
17
  * 所有内置工具,按注册顺序排列。
12
18
  * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。
@@ -21,4 +27,10 @@ export const builtinTools = [
21
27
  webSearchTool,
22
28
  webFetchTool,
23
29
  useSkillTool,
30
+ askHumanTool,
31
+ memorySaveTool,
32
+ memorySearchTool,
33
+ memoryListTool,
34
+ memoryUpdateTool,
35
+ memoryForgetTool,
24
36
  ];
@@ -0,0 +1,32 @@
1
+ import { forgetEntry } from '../../memory/store.js';
2
+ // ---------- memory_forget ----------
3
+ // 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
4
+ export const memoryForgetTool = {
5
+ name: 'memory_forget',
6
+ description: '遗忘一条记忆:默认归档(archived,从索引/默认搜索隐藏,可经 memory_update 复活);mode=delete 硬删。pinned 条目拒删(先 memory_update pinned=false 解钉)。',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ mode: {
12
+ type: 'string',
13
+ enum: ['archive', 'delete'],
14
+ description: '默认 archive',
15
+ },
16
+ },
17
+ required: ['id'],
18
+ },
19
+ async execute(args) {
20
+ const id = String(args.id ?? '').trim();
21
+ if (!id)
22
+ return '错误:缺少 id。';
23
+ const mode = args.mode === 'delete' ? 'delete' : 'archive';
24
+ const r = forgetEntry(id, mode);
25
+ if (r.ok)
26
+ return `已${mode === 'delete' ? '硬删' : '归档'}记忆 [${id}]。`;
27
+ if ('notFound' in r)
28
+ return `错误:找不到记忆 id="${id}"。`;
29
+ // pinned
30
+ return `错误:[${id}] 已钉住,拒绝遗忘。先 memory_update(id="${id}", pinned=false) 解钉。`;
31
+ },
32
+ };
@@ -0,0 +1,34 @@
1
+ import { listEntries } from '../../memory/store.js';
2
+ // ---------- memory_list ----------
3
+ // 列索引(id/name/summary,无正文、不 bump recall)。用于浏览有哪些、拿 id 再 memory_search 取正文。
4
+ export const memoryListTool = {
5
+ name: 'memory_list',
6
+ description: '列出记忆索引(id/name/summary,不含正文)。默认列 active。拿 id 再 memory_search 取正文。',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ type: {
11
+ type: 'string',
12
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
13
+ },
14
+ status: {
15
+ type: 'string',
16
+ enum: ['active', 'superseded', 'archived', 'any'],
17
+ description: '默认 active',
18
+ },
19
+ },
20
+ },
21
+ async execute(args) {
22
+ const items = listEntries({
23
+ type: typeof args.type === 'string' ? args.type : undefined,
24
+ status: typeof args.status === 'string'
25
+ ? args.status
26
+ : undefined,
27
+ });
28
+ if (items.length === 0)
29
+ return '(无记忆条目)';
30
+ return items
31
+ .map((i) => `- ${i.id}: ${i.name} — ${i.summary} (${i.type}, ${i.status})`)
32
+ .join('\n');
33
+ },
34
+ };
@@ -0,0 +1,51 @@
1
+ import { saveEntry } from '../../memory/store.js';
2
+ // ---------- memory_save ----------
3
+ // 存一条长期记忆(跨会话)。启动只把标题/摘要注入索引(几百 token);详情按需 memory_search 取。
4
+ // 撞库(name→id 已存在)拒绝,引导用 memory_update。
5
+ export const memorySaveTool = {
6
+ name: 'memory_save',
7
+ description: '保存一条长期记忆(跨会话)。只存非显然、长期有用的事实/决策/坑。启动时标题进索引,详情按需 memory_search 取。',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ name: { type: 'string', description: '简短唯一标题(转成 id,撞库会拒绝)' },
12
+ summary: { type: 'string', description: '一句话摘要(进索引,尽量短)' },
13
+ body: { type: 'string', description: '完整内容(细节/上下文/证据)' },
14
+ type: {
15
+ type: 'string',
16
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
17
+ description: '分类,默认 fact',
18
+ },
19
+ pinned: { type: 'boolean', description: '钉住(豁免自动遗忘衰减),默认 false' },
20
+ scope: {
21
+ type: 'string',
22
+ enum: ['project', 'global'],
23
+ description: '存到项目级(<cwd>/.mocode/)还是全局(~/.mocode/),默认 project',
24
+ },
25
+ },
26
+ required: ['name', 'summary', 'body'],
27
+ },
28
+ async execute(args) {
29
+ const name = String(args.name ?? '').trim();
30
+ const summary = String(args.summary ?? '').trim();
31
+ const body = String(args.body ?? '').trim();
32
+ if (!name)
33
+ return '错误:缺少 name。';
34
+ if (!summary)
35
+ return '错误:缺少 summary。';
36
+ if (!body)
37
+ return '错误:缺少 body。';
38
+ const type = typeof args.type === 'string' ? args.type : undefined;
39
+ const r = saveEntry({
40
+ name,
41
+ summary,
42
+ body,
43
+ type,
44
+ pinned: args.pinned === true,
45
+ scope: args.scope === 'global' ? 'global' : 'project',
46
+ });
47
+ if (r.ok)
48
+ return `已保存记忆 [${r.id}] "${name}"(下次启动进索引)。`;
49
+ return `已存在同名记忆 [${r.exists}]。改用 memory_update(id="${r.exists}", …) 更新,或换一个 name。`;
50
+ },
51
+ };
@@ -0,0 +1,42 @@
1
+ import { searchEntries } from '../../memory/store.js';
2
+ // ---------- memory_search ----------
3
+ // 关键词搜记忆正文(多词子串匹配,name 权重最高)。命中即 bump recallCount(遗忘衰减依据)。
4
+ // 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
5
+ export const memorySearchTool = {
6
+ name: 'memory_search',
7
+ description: '按关键词搜索记忆正文(多词子串匹配,按相关度排),返回匹配条目全文。命中记一次召回(影响遗忘衰减)。',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ query: { type: 'string', description: '关键词(id 或 name 里的词效果最好)' },
12
+ type: {
13
+ type: 'string',
14
+ enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
15
+ },
16
+ status: {
17
+ type: 'string',
18
+ enum: ['active', 'superseded', 'archived', 'any'],
19
+ description: '默认 active(只搜活的)',
20
+ },
21
+ limit: { type: 'integer', description: '返回条数,默认 5,上限 20' },
22
+ },
23
+ required: ['query'],
24
+ },
25
+ async execute(args) {
26
+ const query = String(args.query ?? '').trim();
27
+ if (!query)
28
+ return '错误:缺少 query。';
29
+ const r = searchEntries(query, {
30
+ type: typeof args.type === 'string' ? args.type : undefined,
31
+ status: typeof args.status === 'string'
32
+ ? args.status
33
+ : undefined,
34
+ limit: typeof args.limit === 'number' ? args.limit : undefined,
35
+ });
36
+ if (r.length === 0)
37
+ return `(无匹配记忆:query="${query}")`;
38
+ return r
39
+ .map((e) => `# [${e.id}] ${e.name} (${e.type}, recalled ${e.recallCount})\nsummary: ${e.summary}\n\n${e.body}`)
40
+ .join('\n\n---\n\n');
41
+ },
42
+ };
@@ -0,0 +1,41 @@
1
+ import { updateEntry } from '../../memory/store.js';
2
+ // ---------- memory_update ----------
3
+ // 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
4
+ export const memoryUpdateTool = {
5
+ name: 'memory_update',
6
+ description: '更新一条记忆(id 不变)。用于事实变了/纠正过时/补 summary 或 body/切换 pinned。id 从 memory_list 或 memory_search 拿。',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ summary: { type: 'string', description: '新摘要(只传要改的字段)' },
12
+ body: { type: 'string', description: '新正文(只传要改的字段)' },
13
+ name: { type: 'string', description: '新标题(id 不随 name 变)' },
14
+ pinned: { type: 'boolean', description: '切换钉住状态(true 钉住豁免衰减 / false 解钉)' },
15
+ reason: { type: 'string', description: '更新理由(记入 lastUpdateReason,便于追溯)' },
16
+ },
17
+ required: ['id'],
18
+ },
19
+ async execute(args) {
20
+ const id = String(args.id ?? '').trim();
21
+ if (!id)
22
+ return '错误:缺少 id。';
23
+ const patch = {};
24
+ if (typeof args.summary === 'string')
25
+ patch.summary = args.summary;
26
+ if (typeof args.body === 'string')
27
+ patch.body = args.body;
28
+ if (typeof args.name === 'string')
29
+ patch.name = args.name;
30
+ if (typeof args.reason === 'string')
31
+ patch.reason = args.reason;
32
+ if (typeof args.pinned === 'boolean')
33
+ patch.pinned = args.pinned;
34
+ if (Object.keys(patch).length === 0)
35
+ return '错误:至少传一个要改的字段(summary/body/name/pinned/reason)。';
36
+ const r = updateEntry(id, patch);
37
+ if (r.ok)
38
+ return `已更新记忆 [${id}]。`;
39
+ return `错误:找不到记忆 id="${id}"。用 memory_list 查可用 id。`;
40
+ },
41
+ };
@@ -4,7 +4,7 @@ import { getSkillBody } from '../../skills/index.js';
4
4
  // 系统提示里已列出可用 skill 的 name + description(何时用),模型据此决定调用。
5
5
  export const useSkillTool = {
6
6
  name: 'use_skill',
7
- description: '加载并返回某个 skill 的完整 SKILL.md 指令。系统提示里列出了可用 skill(name + 何时用的 description)。只在任务相关时调用本工具传 skill 的 name,拿到完整指令后据此行动;不要无脑批量加载。',
7
+ description: '加载并返回某个 skill 的完整 SKILL.md 指令(传 name)。何时用见系统提示的 skill 列表。',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -4,7 +4,7 @@ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
4
4
  // ---------- web_fetch ----------
5
5
  export const webFetchTool = {
6
6
  name: 'web_fetch',
7
- description: '抓取指定 URL 的网页内容并清洗成纯文本(去 HTML 标签/脚本/样式,保留正文)。用于读取搜索结果里的某个链接、或用户给出的具体 URL。注意:只能抓静态 HTML,JS 渲染的页面(正文靠脚本填充)可能拿不到内容——那种情况改用 web_search(其结果自带清洗后的 content)。',
7
+ description: '抓取指定 URL 的网页内容并清洗成纯文本(去 HTML 标签/脚本/样式,保留正文)。用于读取搜索结果里的某个链接、或用户给出的具体 URL。',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -6,7 +6,7 @@ const MAX_CONTENT_CHARS = 800;
6
6
  // ---------- web_search ----------
7
7
  export const webSearchTool = {
8
8
  name: 'web_search',
9
- description: '联网搜索(AnySearch)。返回每条结果的标题/URL/摘要/正文,用于查模型训练数据之外的最新信息(新版本发布、新闻、实时数据、最新 API 文档等)。可选 tag 切换子域:general.general(默认通用网页)/ code.doc(开发文档,须配 params.library)/ code.snippet(代码片段)。',
9
+ description: '联网搜索(AnySearch)。返回每条结果的标题/URL/摘要/正文。可选 tag 切换子域( tag 参数)。',
10
10
  parameters: {
11
11
  type: 'object',
12
12
  properties: {
@@ -8,4 +8,17 @@ export const MAX_HISTORY_RESULT = 8000;
8
8
  export const MAX_SKILL_RESULT = 64000;
9
9
  /** 微压缩时旧工具结果截到的存根长度(字符)。 */
10
10
  export const MAX_OLD_TOOL_STUB = 600;
11
+ // ── 记忆(Tier-2 JSONL 工具库)──────────────────────────────────────────────
12
+ /** 单条记忆 body 上限(字符)。进 history 前 memory_search 结果另有 MAX_MEMORY_RESULT 兜底。 */
13
+ export const MAX_MEMORY_ENTRY = 4000;
14
+ /** 启动注入 systemPrompt 的索引条目上限(遗忘封顶后 active 条目 ≤ MAX_ACTIVE,索引再封一层)。 */
15
+ export const MAX_INDEX_ENTRIES = 50;
16
+ /** active 记忆封顶:超则按 recallCount 低 × lastRecalledAt 老 淘汰到 archived。 */
17
+ export const MAX_ACTIVE = 100;
18
+ /** 衰减:active + !pinned + (lastRecalledAt|createdAt) 早于 DECAY_DAYS → archived。 */
19
+ export const DECAY_DAYS = 30;
20
+ /** GC:archived 超 GC_DAYS → 硬删。 */
21
+ export const GC_DAYS = 90;
22
+ /** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
23
+ export const MAX_MEMORY_RESULT = 64000;
11
24
  export const IGNORE = ['**/node_modules/**', '**/.git/**'];