mocode-ai 1.6.4 → 1.6.5

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.
Files changed (64) hide show
  1. package/README.md +376 -365
  2. package/README.zh-CN.md +13 -2
  3. package/dist/agent/model-turn.js +14 -1
  4. package/dist/agent/run-coordinator.js +5 -0
  5. package/dist/agent/spawn.js +38 -5
  6. package/dist/agent/stages/model-runner.js +1 -1
  7. package/dist/agent/stages/tool-dispatcher.js +1 -0
  8. package/dist/agent/work-discipline.js +3 -1
  9. package/dist/config/index.js +52 -2
  10. package/dist/config/presets.js +40 -1
  11. package/dist/config/profiles.js +9 -1
  12. package/dist/context/clearing.js +53 -0
  13. package/dist/context/relevance.js +6 -0
  14. package/dist/context/text-search.js +113 -0
  15. package/dist/llm/index.js +35 -8
  16. package/dist/llm/providers/anthropic.js +14 -4
  17. package/dist/llm/reasoning.js +141 -0
  18. package/dist/memory/reflect.js +1 -1
  19. package/dist/memory/store.js +31 -28
  20. package/dist/models/budgets.js +11 -0
  21. package/dist/models/catalog.js +130 -0
  22. package/dist/models/map-preset.js +50 -0
  23. package/dist/models/model-panel.js +227 -0
  24. package/dist/models/protocol.js +44 -0
  25. package/dist/models/reasoning-cap.js +88 -0
  26. package/dist/models/search.js +58 -0
  27. package/dist/models/types.js +8 -0
  28. package/dist/repl/commands/effort.js +81 -0
  29. package/dist/repl/commands/model-actions.js +138 -0
  30. package/dist/repl/commands/model-catalog.js +67 -0
  31. package/dist/repl/commands/model-panel-ui.js +107 -0
  32. package/dist/repl/commands/model.js +17 -0
  33. package/dist/repl/commands/registry.js +6 -0
  34. package/dist/repl/commands/session.js +54 -0
  35. package/dist/repl/commands/stats.js +28 -0
  36. package/dist/repl/commands.js +3 -0
  37. package/dist/repl/runtime.js +5 -1
  38. package/dist/repl/status-bar.js +4 -2
  39. package/dist/rollback/store.js +17 -0
  40. package/dist/runtime/runtime.js +31 -1
  41. package/dist/session/compact.js +84 -3
  42. package/dist/session/index.js +2 -0
  43. package/dist/session/retention.js +297 -0
  44. package/dist/session/scheduler.js +30 -3
  45. package/dist/session/store.js +108 -6
  46. package/dist/session/usage-stats.js +90 -0
  47. package/dist/skills/activation.js +2 -0
  48. package/dist/skills/builtin-skills.js +153 -0
  49. package/dist/skills/discover.js +3 -1
  50. package/dist/skills/runner.js +4 -1
  51. package/dist/tools/builtins/index.js +4 -0
  52. package/dist/tools/builtins/read-file.js +19 -1
  53. package/dist/tools/builtins/session-search.js +33 -0
  54. package/dist/tools/builtins/task.js +2 -0
  55. package/dist/tools/constants.js +8 -0
  56. package/dist/tools/read-dedup.js +62 -0
  57. package/dist/tools/tool-runtime.js +1 -0
  58. package/dist/ui/batch.js +109 -103
  59. package/dist/ui/fuzzy-picker.js +227 -0
  60. package/dist/ui/hierarchical-picker.js +302 -0
  61. package/dist/ui/intervention.js +11 -1
  62. package/dist/ui/layout-internal/statusbar.js +18 -7
  63. package/dist/ui/render.js +2 -0
  64. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ /** 从 ChatUsage 构造记录;usage 缺失返 null。 */
2
+ export function toUsageRecord(step, phase, usage, estimatedTotal) {
3
+ if (!usage)
4
+ return null;
5
+ return {
6
+ step,
7
+ phase,
8
+ inputTokens: usage.promptTokens,
9
+ outputTokens: usage.completionTokens,
10
+ cachedTokens: usage.cachedTokens,
11
+ reasoningTokens: usage.reasoningTokens,
12
+ ...(usage.cacheCreationTokens ? { cacheCreationTokens: usage.cacheCreationTokens } : {}),
13
+ ...(estimatedTotal != null ? { estimatedTotal } : {}),
14
+ };
15
+ }
16
+ function emptyTotals() {
17
+ return { input: 0, output: 0, cached: 0, reasoning: 0, cacheCreation: 0, calls: 0 };
18
+ }
19
+ function add(totals, r) {
20
+ totals.input += r.inputTokens;
21
+ totals.output += r.outputTokens;
22
+ totals.cached += r.cachedTokens;
23
+ totals.reasoning += r.reasoningTokens;
24
+ totals.cacheCreation += r.cacheCreationTokens ?? 0;
25
+ totals.calls++;
26
+ }
27
+ function hitRate(t) {
28
+ if (t.calls === 0)
29
+ return null;
30
+ // 后端从未上报过 cached → 无法计算,不是 0%。
31
+ if (t.cached === 0)
32
+ return null;
33
+ return t.cached / Math.max(t.input, 1);
34
+ }
35
+ export function summarizeUsage(records) {
36
+ const main = emptyTotals();
37
+ const compact = emptyTotals();
38
+ let cacheReported = false;
39
+ let estimateRatioSum = 0;
40
+ let estimateSamples = 0;
41
+ for (const r of records) {
42
+ add(r.phase === 'compact' ? compact : main, r);
43
+ if (r.cachedTokens > 0)
44
+ cacheReported = true;
45
+ if (r.estimatedTotal && r.estimatedTotal > 0) {
46
+ estimateRatioSum += r.inputTokens / r.estimatedTotal;
47
+ estimateSamples++;
48
+ }
49
+ }
50
+ const total = emptyTotals();
51
+ for (const r of records)
52
+ add(total, r);
53
+ // 全部记录里从未出现 cached 字段 → 所有命中率都 n/a;
54
+ // 有过 cached(即使某 phase 为 0)则该 phase 按真实 0 计算。
55
+ const rate = (t) => {
56
+ if (!cacheReported || t.calls === 0)
57
+ return null;
58
+ return t.cached / Math.max(t.input, 1);
59
+ };
60
+ return {
61
+ records: records.length,
62
+ compactCalls: compact.calls,
63
+ main,
64
+ compact,
65
+ total,
66
+ cacheHitRate: hitRate(total),
67
+ mainCacheHitRate: rate(main),
68
+ compactCacheHitRate: rate(compact),
69
+ reasoningShare: total.output > 0 ? total.reasoning / total.output : 0,
70
+ estimateRatio: estimateSamples > 0 ? estimateRatioSum / estimateSamples : null,
71
+ };
72
+ }
73
+ function pct(n) {
74
+ return n == null ? 'n/a' : `${(n * 100).toFixed(1)}%`;
75
+ }
76
+ function fmtTotals(t) {
77
+ return `${t.calls} calls · in ${t.input} · out ${t.output} · cached ${t.cached} · reasoning ${t.reasoning}`;
78
+ }
79
+ /** 拍平成人类可读(/stats 命令输出)。 */
80
+ export function formatUsageSummary(s) {
81
+ const lines = [];
82
+ lines.push(`Token usage — ${s.records} record(s), ${s.compactCalls} compact call(s)`);
83
+ lines.push(` Cache hit rate: total ${pct(s.cacheHitRate)} · main ${pct(s.mainCacheHitRate)} · compact ${pct(s.compactCacheHitRate)}`);
84
+ lines.push(` Main ${fmtTotals(s.main)}`);
85
+ if (s.compact.calls > 0)
86
+ lines.push(` Compact ${fmtTotals(s.compact)}`);
87
+ lines.push(` Total in ${s.total.input} · out ${s.total.output} · reasoning share ${(s.reasoningShare * 100).toFixed(1)}%` +
88
+ (s.estimateRatio != null ? ` · measured/estimate ${s.estimateRatio.toFixed(2)}` : ''));
89
+ return lines.join('\n');
90
+ }
@@ -20,6 +20,8 @@ export function activateSkill(skill) {
20
20
  active = {
21
21
  name: skill.name,
22
22
  disallowed: disallowed.size > 0 ? disallowed : null,
23
+ // 后者带 effort 则覆盖;没带则继承前一 skill 的设置。
24
+ effort: skill.effort ?? active?.effort,
23
25
  };
24
26
  }
25
27
  /** 用户新轮开始时清激活态。 */
@@ -40,6 +40,111 @@ run_command({ command: "codegraph node <symbol-or-path>" })
40
40
 
41
41
  CLI dump 一般是 \`path/to/file.ts:line:col symbol\` 形式,与 grep 一样可被 read_file 消费。
42
42
  `;
43
+ const BRAINSTORM_BODY = `# Brainstorming(需求澄清)
44
+
45
+ 开工前的需求澄清流程:当需求可能被理解错、或信息不足以决定交付物时,先用对话把需求问清楚,再动手。
46
+
47
+ ## 何时进入
48
+
49
+ - 用户的需求存在两种以上实质不同的理解,会导致不同交付物;
50
+ - 缺少关键信息(目标、范围、约束、输入输出、验收标准),且靠仓库证据无法补全;
51
+ - 改动涉及不可逆操作 / 公共 API / 多个会实质改变产品行为的方案;
52
+ - 用户只给了粗略想法(如「帮我做个 X」),期望先讨论。
53
+
54
+ ## 何时不要进入
55
+
56
+ - 任务清晰且存在合理默认做法 → 直接干,在最终回复中披露假设;
57
+ - 只是命名或实现细节 → 遵循仓库惯例自主决定;
58
+ - 用户已明确表示「直接做 / 别问我」。
59
+
60
+ ## 流程
61
+
62
+ 1. **最小探查**:只读取提出好问题所必需的代码和上下文(有 .codegraph/ 时优先 codegraph skill),不要在澄清阶段做大范围调研。
63
+ 2. **复述理解**:用一两句向用户复述你对需求的理解,并点出关键分歧或未知点。
64
+ 3. **一次只问一个问题**:每轮 \`ask_human\` 只问当前最关键的一个问题,自由文本优先(\`options: []\`);仅当答案能收敛为少量明确选项时才给 2-4 个选项。不要一次甩多个问题。
65
+ 4. **据回答继续**:必要时追问下一个最关键的问题;回答引出新歧义时先澄清新歧义。不要重复问已回答的内容。
66
+ 5. **收敛确认**:需求清晰后,用一次 \`ask_human\` 分节呈现方案——目标 / 范围 / 做法与受影响文件 / 不做什么 / 验证方式,选项固定为「确认,开始做」「再调整」「取消」。
67
+ 6. **转入执行**:用户确认后再开始改动。
68
+
69
+ ## 边界
70
+
71
+ - 澄清阶段的 \`ask_human\` 不计入「执行阶段每轮最多 2 次」的预算,但仍要克制:每个问题都必须实质推进理解。用户表现出不耐烦或说「你决定」时,采用最安全的可逆默认做法并说明。
72
+ - 用户取消:停止,不要按自己的猜测继续。
73
+ - 问题与方案用用户的语言表述。
74
+ - 澄清期间保持只读:不改文件、不跑有副作用的命令。
75
+ `;
76
+ const DEBUGGING_BODY = `# Systematic Debugging(系统化调试)
77
+
78
+ 遇到 bug、测试失败或报错时,用「先理解再修」的流程,禁止靠猜测随机改动。
79
+
80
+ ## 核心原则
81
+
82
+ - 没有找到根因之前,不要改代码。症状不是原因。
83
+ - 一次只改一处;改完无法解释为什么有效,就不算修好。
84
+ - 不要用「删掉重试 / 换个写法试试 / 加个 await 碰碰运气」这类无假设的尝试。
85
+
86
+ ## 流程
87
+
88
+ 1. **复现**:先稳定复现问题。复现不了,先弄清楚触发条件(输入、环境、顺序),必要时向用户索取复现步骤或报错全文。
89
+ 2. **收集证据**:读完整报错和堆栈,定位到具体文件/行;用日志或最小用例确认数据在**哪一步**开始出错。有 .codegraph/ 时用它追调用链。
90
+ 3. **形成根因假设**:用一句话说明「为什么当前行为会必然产生这个症状」,并指出能证伪它的检查点。
91
+ 4. **验证假设**:先做最小检查确认假设成立,再动手。如果证据与假设矛盾,回到第 2 步,不要硬修。
92
+ 5. **最小修复**:只改根因,不动无关代码;避免为绕过症状而加补丁、吞异常、放宽校验。
93
+ 6. **验证修复**:复现用例通过;补一个能覆盖该 bug 的回归测试(成本合理时);检查同类路径是否也受影响。
94
+ 7. **复盘**:说明根因、改动、验证方式;仍有不确定就如实说。
95
+
96
+ ## 反模式
97
+
98
+ - 报错还没看完就开始改;
99
+ - 同一处反复改了 3 次仍失败却不换思路;
100
+ - 修复让错误「消失」但说不清机制(往往只是被掩盖);
101
+ - 顺手重构与 bug 无关的代码。
102
+ `;
103
+ const CODE_REVIEW_BODY = `# Code Review(自审 diff)
104
+
105
+ 提交/收尾前,对**自己本次的改动**做一遍审查。改动越多越要做;几行的机械改动可快速过。
106
+
107
+ ## 流程
108
+
109
+ 1. **拿到完整 diff**:只审本次相关改动(\`git diff\` / 未提交改动),不要凭记忆。
110
+ 2. **意图对齐**:每处改动是否都服务于任务?有没有夹带无关重构、调试残留(console.log、注释掉的代码、临时文件)?
111
+ 3. **正确性**:逻辑分支是否完整;边界(空值、空数组、并发/重复调用、错误路径);条件取反、off-by-one;异步是否有未 await 或未处理的 rejection。
112
+ 4. **错误处理**:失败时是否给出有用信息而非静默吞掉;资源(文件句柄、锁、子进程)是否正确释放。
113
+ 5. **兼容与契约**:公共 API、类型、配置键、默认值是否保持兼容;是否违背仓库既有约定(如纯 ESM 的 .js 扩展名、工具契约)。
114
+ 6. **安全与副作用**:是否引入不可逆操作、权限绕过、注入面;写入/执行是否走了既有护栏。
115
+ 7. **完整性**:文档、调用方、测试是否需要同步;用户要求的场景是否都覆盖。
116
+ 8. **修复后复审**:发现问题就改,改完重新看那部分 diff,确认没有引入新问题。
117
+
118
+ ## 输出
119
+
120
+ 如实质问题:直接修复,最终回复中列出发现并修复了什么。
121
+ 如无问题:简要说明审过哪些点。不要为了凑数报无关紧要的风格问题——格式交给 Prettier。
122
+ `;
123
+ const VERIFICATION_BODY = `# Verification Before Completion(完成前证据验证)
124
+
125
+ 宣布任务「完成」之前,必须用实际运行的证据确认改动真的生效。禁止把「代码看起来对 / 应该能跑」当作完成。
126
+
127
+ ## 硬性规则
128
+
129
+ 1. **先验证,再宣称完成**:任何「已修复 / 已完成 / 能用了」的结论,前面必须有一次实际执行及其真实输出。
130
+ 2. **验证必须对得上声称**:改了行为就跑能体现该行为的场景;修了 bug 就用能复现该 bug 的输入确认;不能用「编译通过」代替「功能正确」。
131
+ 3. **看完整结果**:确认退出码、断言/测试通过数、关键输出行;有 warning 或失败项要逐条判断,不能只看最后一行没报错。
132
+ 4. **验证不了就如实说**:环境缺失、需要用户凭据/设备、或验证成本明显不合理时,说明已验证到哪一步、什么没验证、用户可以怎么验证。不要假称已验证。
133
+
134
+ ## 怎么选验证(按成本,够小即可)
135
+
136
+ - 优先最小相关检查:拥有该改动的包的 typecheck / 一个针对性测试 / 直接运行受影响入口;
137
+ - 不要为了「保险」默认跑全量 test/build;
138
+ - UI/交互类改动:能自动化就自动化(浏览器工具/webapp 测试),否则明确告知未做可视化验证;
139
+ - 纯文档、注释等零行为改动:可不跑验证。
140
+
141
+ ## 反模式
142
+
143
+ - 「理论上没问题」「逻辑上应该可以」——没有运行证据就不算完成;
144
+ - 测试其实失败/被跳过,却报告成功;
145
+ - 跑了一个与改动无关的检查,用它冒充验证;
146
+ - 改完代码不重新运行,沿用改动前的旧结果。
147
+ `;
43
148
  /** 单一事实源:加新内置 skill 在此数组加一条。 */
44
149
  export const builtinSkills = [
45
150
  {
@@ -56,6 +161,54 @@ export const builtinSkills = [
56
161
  origin: 'builtin',
57
162
  warnings: [],
58
163
  },
164
+ {
165
+ name: 'brainstorming',
166
+ description: '需求澄清(开工前对话):当用户需求可能被误解、信息不足、存在多种实质不同方案,或只给了粗略想法时,' +
167
+ '先用「一次一个问题」的对话澄清需求,分节呈现方案并获得确认,再开始写代码。任务清晰时不要使用。',
168
+ body: BRAINSTORM_BODY,
169
+ dir: 'builtin',
170
+ skillMdPath: 'builtin',
171
+ context: 'inline',
172
+ modelInvocable: true,
173
+ origin: 'builtin',
174
+ warnings: [],
175
+ },
176
+ {
177
+ name: 'systematic-debugging',
178
+ description: '系统化调试:遇到 bug、测试失败或报错时,先稳定复现并定位根因(读完堆栈、用证据验证假设),再做最小修复并回归验证。' +
179
+ '禁止没找到根因就靠猜测反复改代码。',
180
+ body: DEBUGGING_BODY,
181
+ dir: 'builtin',
182
+ skillMdPath: 'builtin',
183
+ context: 'inline',
184
+ modelInvocable: true,
185
+ origin: 'builtin',
186
+ warnings: [],
187
+ },
188
+ {
189
+ name: 'code-review',
190
+ description: '收尾前自审本次 diff:意图对齐、正确性与边界、错误处理、API/约定兼容性、安全副作用、文档与测试完整性,' +
191
+ '发现问题直接修复后复审。改动较多或涉及核心逻辑时使用;纯机械小改可快速过。',
192
+ body: CODE_REVIEW_BODY,
193
+ dir: 'builtin',
194
+ skillMdPath: 'builtin',
195
+ context: 'inline',
196
+ modelInvocable: true,
197
+ origin: 'builtin',
198
+ warnings: [],
199
+ },
200
+ {
201
+ name: 'verification-before-completion',
202
+ description: '完成前证据验证:宣布「已修复/已完成」前必须实际运行过相关检查并看到真实输出(退出码、测试通过数、关键输出),' +
203
+ '验证内容要对得上声称;无法验证时如实说明。适用于一切有行为改动的收尾,纯文档改动除外。',
204
+ body: VERIFICATION_BODY,
205
+ dir: 'builtin',
206
+ skillMdPath: 'builtin',
207
+ context: 'inline',
208
+ modelInvocable: true,
209
+ origin: 'builtin',
210
+ warnings: [],
211
+ },
59
212
  ];
60
213
  /** 内置 skill 名字集,供 discoverSkills 跳过/覆盖时使用。 */
61
214
  export const builtinSkillNames = new Set(builtinSkills.map((s) => s.name));
@@ -8,6 +8,7 @@
8
8
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
+ import { parseReasoningEffort } from '../llm/reasoning.js';
11
12
  import { builtinSkillNames } from './builtin-skills.js';
12
13
  /** 把开头的 ~/ 或 ~ 展开为 home 目录(Windows 与 POSIX 通用)。 */
13
14
  function expandHome(p) {
@@ -94,7 +95,7 @@ function resolveContext(meta) {
94
95
  * 该字段不再产生任何效果,按"已忽略"提示作者,避免其误以为仍有限权语义。 */
95
96
  function collectWarnings(meta) {
96
97
  const warnings = [];
97
- for (const k of ['hooks', 'model', 'effort', 'paths', 'agent']) {
98
+ for (const k of ['hooks', 'model', 'paths', 'agent']) {
98
99
  if (k in meta)
99
100
  warnings.push(`字段 "${k}" 当前不支持,已忽略`);
100
101
  }
@@ -238,6 +239,7 @@ export function discoverSkills() {
238
239
  disallowedTools: asArray(meta, 'disallowed-tools'),
239
240
  modelInvocable: !truthy(meta, 'disable-model-invocation'),
240
241
  maxSteps: num(meta, 'max-steps'),
242
+ effort: parseReasoningEffort(scalar(meta, 'effort')),
241
243
  argumentHint: scalar(meta, 'argument-hint')?.trim() || undefined,
242
244
  whenToUse: scalar(meta, 'when_to_use')?.trim() || undefined,
243
245
  origin,
@@ -13,6 +13,7 @@ import { isSkillTrusted, ensureSkillTrust } from './trust.js';
13
13
  import { mapSkillTools } from './toolmap.js';
14
14
  import { spawnAgent } from '../agent/spawn.js';
15
15
  import { runCommandRaw } from '../tools/builtins/run-command.js';
16
+ import { runWithEffortScope } from '../llm/reasoning.js';
16
17
  const MAX_INJECTIONS = 4;
17
18
  const MAX_INJECTION_OUTPUT = 4096;
18
19
  const INJECT_TIMEOUT = 10_000;
@@ -168,7 +169,7 @@ export async function runSkill(a, ctx) {
168
169
  // 仅记到正文前导,模型可感知哪些 allowed-tools 被忽略(不阻断执行)。
169
170
  body = `> 注意:以下 allowed-tools 无法映射到 mocode 工具,已忽略: ${unknown.join(', ')}\n\n` + body;
170
171
  }
171
- const res = await spawnAgent({
172
+ const execute = () => spawnAgent({
172
173
  prompt: SKILL_PROTOCOL_HEADER + '\n\n' + body,
173
174
  tools: tools ?? undefined,
174
175
  maxSteps: skill.maxSteps,
@@ -180,5 +181,7 @@ export async function runSkill(a, ctx) {
180
181
  parentAllowedToolNames: ctx?.allowedToolNames,
181
182
  delegation: ctx?.delegation,
182
183
  });
184
+ // skill frontmatter effort 覆盖会话级(ALS 沿子 agent 异步树继承)。
185
+ const res = skill.effort ? await runWithEffortScope(skill.effort, execute) : await execute();
183
186
  return toOutcome(res);
184
187
  }
@@ -23,6 +23,7 @@ import { memoryForgetTool } from './memory-forget.js';
23
23
  import { memoryGraphTool } from './memory-graph.js';
24
24
  import { subAgentTool } from './task.js';
25
25
  import { computerTool } from './computer.js';
26
+ import { sessionSearchTool } from './session-search.js';
26
27
  /**
27
28
  * 所有内置工具,按注册顺序排列。
28
29
  * 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
@@ -70,6 +71,8 @@ export const CAPABILITIES = {
70
71
  memory_forget: { effect: 'write', concurrency: 'serial', resources: memoryResource },
71
72
  // memory_graph:search/neighbors/stats 只读、add 写,统一按写处理走串行(调用不频繁,简化)。
72
73
  memory_graph: { effect: 'write', concurrency: 'serial', resources: memoryResource },
74
+ // session_search 只读归档索引,纯本地无副作用;串行(每次重建 BM25 索引,避免并发交错)。
75
+ session_search: { effect: 'read', concurrency: 'serial' },
73
76
  // computer 桌面操控:桌面状态全局唯一,任何两个 computer 调用都不允许并发;
74
77
  // effect=process(不写工作区文件,不触发 rollback/diff 追踪),supportsAbort 中断长 wait/拖拽。
75
78
  computer: { effect: 'process', concurrency: 'serial', resources: () => ['desktop'], supportsAbort: true },
@@ -111,6 +114,7 @@ const rawBuiltinTools = [
111
114
  memoryGraphTool,
112
115
  subAgentTool,
113
116
  computerTool,
117
+ sessionSearchTool,
114
118
  ];
115
119
  /** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
116
120
  export const builtinTools = rawBuiltinTools.map((tool) => ({
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
3
3
  import { contentHash } from '../../changeset/index.js';
4
4
  import { IMAGE_READ_MARKER, MAX_FILE_LINES } from '../constants.js';
5
5
  import { MAX_INLINE_BYTES_DEFAULT, isProbablyBinary, loadImageAttachmentWithDownscale, sniffImageMime, } from '../../attachments/image.js';
6
+ import { config } from '../../config/index.js';
6
7
  /** 默认单次 read_file 拉取的行数。刻意压低,逼 LLM 分块读大文件,
7
8
  * 配合 description 中的 PAGINATION IS MANDATORY 引导。
8
9
  * 300 行 ≈ 一个屏幕的源码量,够定位一段逻辑而不至于吃光上下文。 */
@@ -56,7 +57,7 @@ export const readFileTool = {
56
57
  },
57
58
  required: ['path'],
58
59
  },
59
- async execute(args) {
60
+ async execute(args, toolCtx) {
60
61
  const path = String(args.path);
61
62
  const offset = Number(args.offset ?? 1);
62
63
  // 无论 LLM 传多大,单次硬钳到 MAX_FILE_LINES,杜绝「绕过分页引导一把全拿」。
@@ -122,9 +123,26 @@ export const readFileTool = {
122
123
  // 让模型误以为读到了带一行空白的文件。data.length===0 才是真·空的判据。
123
124
  if (data.length === 0)
124
125
  return { status: 'success', code: 'OK', retryable: false, output: `${artifactHeader}\n(空文件)` };
126
+ // P2 重复读短路:同区间 + hash 未变 + 内容仍在 context → 短指针。
127
+ const reqStart = start + 1;
128
+ const reqEnd = end;
129
+ const dedup = toolCtx?.readDedup;
130
+ if (config.readDedup && dedup) {
131
+ const hit = dedup.findUnchanged(path, reqStart, reqEnd, contentHash(data));
132
+ if (hit) {
133
+ return {
134
+ status: 'success',
135
+ code: 'OK',
136
+ retryable: false,
137
+ output: `${artifactHeader}\n[unchanged since step ${hit.step}, hash=${hit.hash}] ` +
138
+ '该区间内容已在上方上下文中,未重复返回;若已被压缩或确需重读,请忽略本提示再读一次。',
139
+ };
140
+ }
141
+ }
125
142
  const output = end < lines.length
126
143
  ? `${artifactHeader}\n${body}\n\n... (${lines.length - end} 行未显示,共 ${lines.length} 行)`
127
144
  : `${artifactHeader}\n${body}`;
145
+ dedup?.record(path, reqStart, reqEnd, contentHash(data));
128
146
  return { status: 'success', code: 'OK', retryable: false, output };
129
147
  },
130
148
  };
@@ -0,0 +1,33 @@
1
+ import { config } from '../../config/index.js';
2
+ import { searchArchiveIndex } from '../../session/retention.js';
3
+ // ---------- session_search ----------
4
+ // episodic 层入口:跨会话搜索已归档会话(首条用户消息 + 摘要,BM25)。
5
+ // 已 purge(正文已删)的会话同样可命中——meta+摘要永久保留。需要整段恢复时,
6
+ // 未 purge 的会话仍可用 --resume <id> 从 archive 取回(见 retention loadArchivedSession)。
7
+ export const sessionSearchTool = {
8
+ name: 'session_search',
9
+ description: 'Search archived past sessions in this workspace by keyword (BM25 over the first user message and the session summary). Returns session id, date, and summary. Use to recall what was decided or done in earlier conversations.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ query: { type: 'string', description: 'Keywords describing the past session' },
14
+ limit: { type: 'integer', description: 'Number of sessions to return, default 10' },
15
+ },
16
+ required: ['query'],
17
+ },
18
+ async execute(args) {
19
+ const query = String(args.query ?? '').trim();
20
+ if (!query)
21
+ return '错误:缺少 query。';
22
+ const limit = typeof args.limit === 'number' ? args.limit : 10;
23
+ const hits = searchArchiveIndex(config.sessionDir, query, limit);
24
+ if (hits.length === 0)
25
+ return `(无匹配的归档会话:query="${query}")`;
26
+ return hits
27
+ .map((h) => {
28
+ const status = h.purgedAt ? ' [正文已清除,仅摘要]' : '';
29
+ return `# ${h.id} · ${h.createdAt}${status}\n${h.summary || h.firstUser}`;
30
+ })
31
+ .join('\n\n---\n\n');
32
+ },
33
+ };
@@ -81,6 +81,8 @@ export const subAgentTool = {
81
81
  retryable: false,
82
82
  output,
83
83
  usage: result.usage,
84
+ // P4 闸3:只归属本子 agent 树真实改动;tool-runtime 允许工具自报覆盖整轮快照。
85
+ changedFiles: result.changedFiles ?? [],
84
86
  };
85
87
  return outcome;
86
88
  },
@@ -18,6 +18,14 @@ export const MAX_HISTORY_RESULT = 8000;
18
18
  export const MAX_SKILL_RESULT = 64000;
19
19
  /** 微压缩时旧工具结果截到的存根长度(字符)。 */
20
20
  export const MAX_OLD_TOOL_STUB = 600;
21
+ // ── 磁盘侧治理(见 session/store.ts)──────────────────────────────────────
22
+ /** trace.jsonl 单文件大小上限(字节):超过即把整份 gzip 轮转为 trace.N.jsonl.gz 并清空。
23
+ * trace 是纯诊断数据,可激进轮转;512KB 约含千余条事件。 */
24
+ export const MAX_TRACE_BYTES = 512 * 1024;
25
+ /** 保留的历史 trace 轮转份数(trace.1 .. trace.N),更老的硬删。 */
26
+ export const MAX_TRACE_ROTATIONS = 3;
27
+ /** rollback snapshots 压缩前滚动窗口:无论是否 compact,仅保留最近 N 个 user turn 的快照。 */
28
+ export const MAX_SNAPSHOT_TURNS = 50;
21
29
  // ── 上下文压缩摘要(见 session/compact.ts defaultSummarize)────────────────
22
30
  /** 摘要输入单条消息封顶:逐条封顶保证每轮对话都有代表,替代整段中截(会切掉中间整轮)。
23
31
  * 转录总量超 SUMMARY_TRANSCRIPT_WINDOW_RATIO 时,这些封顶会被等比缩小重拼。 */
@@ -0,0 +1,62 @@
1
+ const MAX_RANGES_PER_PATH = 5;
2
+ const MAX_PATHS = 200;
3
+ export class ReadDedup {
4
+ /** Map 保序:超容量时删最老(第一个)key。 */
5
+ entries = new Map();
6
+ currentStep = 0;
7
+ /** model-turn 每步调用:钉住 step(指针消息里展示 "since step K")。 */
8
+ beginStep(step) {
9
+ this.currentStep = step;
10
+ }
11
+ /** trim 变更 history 内容后调用:所有存活条目失活(其内容可能已不在 context)。 */
12
+ markContextChanged() {
13
+ for (const list of this.entries.values())
14
+ for (const e of list)
15
+ e.hot = false;
16
+ }
17
+ touch(path) {
18
+ let list = this.entries.get(path);
19
+ if (list) {
20
+ // LRU:命中路径挪到末尾。
21
+ this.entries.delete(path);
22
+ }
23
+ else {
24
+ list = [];
25
+ if (this.entries.size >= MAX_PATHS) {
26
+ const oldest = this.entries.keys().next().value;
27
+ if (oldest !== undefined)
28
+ this.entries.delete(oldest);
29
+ }
30
+ }
31
+ this.entries.set(path, list);
32
+ return list;
33
+ }
34
+ /** 记录一次实际返回的读取(全量返回之后调用)。 */
35
+ record(path, startLine, endLine, hash) {
36
+ const list = this.touch(path);
37
+ list.push({ startLine, endLine, hash, step: this.currentStep, hot: true });
38
+ while (list.length > MAX_RANGES_PER_PATH)
39
+ list.shift();
40
+ }
41
+ /**
42
+ * 查询重复读命中:存在 hot 条目,其行区间覆盖请求区间、hash 相同。
43
+ * 无命中返 null(调用方照常全量返回)。
44
+ */
45
+ findUnchanged(path, startLine, endLine, hash) {
46
+ const list = this.entries.get(path);
47
+ if (!list)
48
+ return null;
49
+ // 从新到旧找。
50
+ for (let i = list.length - 1; i >= 0; i--) {
51
+ const e = list[i];
52
+ if (e.hot && e.hash === hash && e.startLine <= startLine && e.endLine >= endLine) {
53
+ return { step: e.step, hash };
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ }
59
+ /** 每用户 turn 一个 scope。 */
60
+ export function createReadDedup() {
61
+ return new ReadDedup();
62
+ }
@@ -292,6 +292,7 @@ export class ToolRuntime {
292
292
  callId: opts?.callId,
293
293
  allowedToolNames: opts?.allowedToolNames,
294
294
  delegation: opts?.delegation,
295
+ readDedup: opts?.readDedup,
295
296
  });
296
297
  }
297
298
  finally {