mocode-ai 0.6.6 → 0.6.7

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.
@@ -1,93 +1,129 @@
1
- // Relevance Pruner:read_file 相关性裁剪(纯静态分析,零 LLM 调用)。
2
- //
3
- // 场景(用户描述):
4
- // 1) read foo.ts → 后来又 read foo.ts → 旧结果无价值 → 替换为存根
5
- // 2) read foo.ts → edit foo.ts → read foo.ts → 中间那次 read 之前的所有旧 read
6
- // 在 mutation 之后已失效 → 替换为存根
7
- //
8
- // 与现有子系统的关系:
9
- // - Context Optimization Pipeline(`pipeline.ts`):单条上限 + 类型化编码。本层在其外,
10
- // 在 pushToolResult 出口再做一次"跨条"裁剪。
11
- // - drop_context(`session/drop.ts`):agent 主动剔除已知无关的旧 tool 结果。本层是被动自动,
12
- // 不需要 agent 调;两者并存不冲突。
13
- // - compact(`session/compact.ts`):阈值触发的整体微压缩+摘要。本层只裁"明确失效"的旧 read,
14
- // 不触发摘要;门槛更低、零成本。
15
- //
16
- // 不变量(对齐 drop_context / compact):
17
- // - 只改 .content,不删消息、不动 tool_call_id、不动 tool_calls 数组结构。
18
- // - 当前轮保护区:不剔除"最后一个 user 消息及其之后"的 read_file 结果(agent 本轮还在用,
19
- // 剔除会破坏正在进行的推理)。实现复用 drop.ts 的 lastUserIndex 思路。
20
- // - 幂等:已 stub(含「已过时」标记)不重复 stub,避免反复重写同一条消息。
21
- // - 永不抛错(对齐「调度器永不抛错」契约);无匹配 / 解析失败 / 异常 → 静默 no-op。
22
- // - TUI 渲染(hooks.onToolResult)用原始 output,与本层解耦——屏上看全量,LLM 看裁剪后版。
23
- //
24
- // 零行为变化兜底:开关 `config.contextRelprune` 关闭时,pipeline 路径完全不调本模块。
25
- import { canonicalizePath, extractPath, lastUserIndex, toText, toolNameOf } from './utils.js';
26
- /** stub 标记前缀(供幂等判定)。drop_context 用的是「⌦[已剔除:与当前任务无关]」,
27
- * 本层用「⌦[已过时:同 path 已有新 read / 已被 mutation 覆写]」,区分两类剔除来源。 */
28
- const STUB_PREFIX = '⌦[已过时:同 path 已有新 read / 已被 mutation 覆写]';
1
+ // Relevance Pruner: statically removes tool results that a newer observation supersedes.
2
+ // It never deletes messages or changes tool_call_id pairing; only tool content is stubbed.
3
+ import { canonicalizePath, extractPath, isToolResultSuccess, lastUserIndex, toText, } from './utils.js';
4
+ /** Shared prefix lets /context count read and observation supersession together. */
5
+ const STUB_PREFIX = '⌦[已过时:';
6
+ const READ_STUB_REASON = '同 path 已有新 read / 已被 mutation 覆写';
7
+ function parseArgs(raw) {
8
+ try {
9
+ const parsed = raw.trim() ? JSON.parse(raw) : {};
10
+ return parsed && typeof parsed === 'object'
11
+ ? parsed
12
+ : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function normalizedInteger(value, fallback) {
19
+ const n = Number(value);
20
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
21
+ }
22
+ /** Only complete semantic-query equality is safe for whole-message replacement. */
23
+ function observationKey(call) {
24
+ const args = call.args;
25
+ if (!args)
26
+ return null;
27
+ if (call.name === 'grep') {
28
+ if (typeof args.pattern !== 'string')
29
+ return null;
30
+ const rawMax = normalizedInteger(args.max_per_file, 15);
31
+ return JSON.stringify({
32
+ tool: 'grep',
33
+ pattern: args.pattern,
34
+ glob: typeof args.glob === 'string' ? args.glob : '**/*',
35
+ maxPerFile: Math.min(Math.max(rawMax, 1), 50),
36
+ });
37
+ }
38
+ if (call.name === 'codegraph') {
39
+ if (typeof args.action !== 'string' || typeof args.query !== 'string')
40
+ return null;
41
+ const query = args.action === 'explore'
42
+ ? args.query.trim().replace(/\s+/g, ' ')
43
+ : args.query.trim();
44
+ return JSON.stringify({
45
+ tool: 'codegraph',
46
+ action: args.action,
47
+ query,
48
+ file: typeof args.file === 'string' ? args.file.trim().replace(/\\/g, '/') : '',
49
+ offset: args.offset === undefined ? null : normalizedInteger(args.offset, 0),
50
+ limit: args.limit === undefined ? null : normalizedInteger(args.limit, 0),
51
+ });
52
+ }
53
+ return null;
54
+ }
55
+ function observationLabel(call) {
56
+ const args = call.args ?? {};
57
+ if (call.name === 'grep') {
58
+ const pattern = JSON.stringify(String(args.pattern ?? '')).slice(0, 80);
59
+ const glob = JSON.stringify(String(args.glob ?? '**/*')).slice(0, 80);
60
+ return `grep(pattern=${pattern}, glob=${glob})`;
61
+ }
62
+ const action = String(args.action ?? '');
63
+ const query = JSON.stringify(String(args.query ?? '')).slice(0, 100);
64
+ return `codegraph(action=${action}, query=${query})`;
65
+ }
29
66
  /**
30
- * 维护「path → 该 path 所有 read_file tool 消息的 history index」映射。
31
- * - observePush:把刚 push 的 read_file tool 消息登记,并把同 path 的"更早" read 全部 stub。
32
- * - observeMutation:把该 mutation path 的"在 mutation 之前的" read 全部 stub。
33
- *
34
- * 设计:每个 agent 会话(每个 runAgentCore 实例)持有一个 pruner。会话结束/换 plan 时
35
- * 可新建;不持久化(history 重建时索引自然过期)。
36
- *
37
- * 零依赖:仅依赖 ChatMessage 形状;不 import llm / tools / agent。
67
+ * Cross-message relevance pruning:
68
+ * - read_file: a newer successful read of the same canonical path supersedes old reads.
69
+ * - grep/codegraph: a newer successful call with the exact same semantic arguments
70
+ * supersedes old results. Partial file overlap is intentionally not enough.
38
71
  */
39
72
  export class RelevancePruner {
40
- /** path → [history index, ...] 按插入序;最新在末尾。 */
41
73
  readByPath = new Map();
42
- /** 把刚 push 的消息通知 pruner。
43
- * - 只处理成功的 read_file tool 消息;失败读取不能淘汰旧的有效结果。
44
- * - 登记当前 canonical path,并反向 stub 同 path 旧 read。
45
- */
74
+ observationByKey = new Map();
46
75
  observePush(history, msg, succeeded = true) {
47
76
  try {
48
77
  if (!succeeded || msg.role !== 'tool')
49
78
  return;
50
- const m = msg;
51
79
  const idx = history.length - 1;
52
80
  if (idx < 1 || history[idx] !== msg)
53
- return; // 防御:必须刚 push 到末尾
54
- if (toolNameOf(history, idx) !== 'read_file')
55
81
  return;
56
82
  const content = toText(msg.content);
57
83
  if (content.startsWith(STUB_PREFIX))
58
- return; // 已是存根(防御)
59
- const path = this.pathAt(history, idx);
60
- if (!path)
61
- return;
62
- // 先 stub 旧 read(同 canonical path,idx 之前),再登记新 idx。
63
- this.stubPriorReads(history, path, idx);
64
- const list = this.readByPath.get(path);
65
- if (list)
84
+ return;
85
+ const call = this.callAt(history, idx);
86
+ if (!call)
87
+ return;
88
+ if (call.name === 'read_file') {
89
+ const path = canonicalizePath(extractPath(call.argsRaw));
90
+ if (!path)
91
+ return;
92
+ this.stubPriorReads(history, path, idx);
93
+ const list = this.readByPath.get(path) ?? [];
66
94
  list.push(idx);
67
- else
68
- this.readByPath.set(path, [idx]);
95
+ this.readByPath.set(path, list);
96
+ return;
97
+ }
98
+ const key = observationKey(call);
99
+ if (!key)
100
+ return;
101
+ this.stubPriorObservations(history, call.name, key, idx);
102
+ const list = this.observationByKey.get(key) ?? [];
103
+ list.push(idx);
104
+ this.observationByKey.set(key, list);
69
105
  }
70
106
  catch {
71
- /* 永不抛错 */
107
+ // Relevance pruning must never block tool-result insertion.
72
108
  }
73
109
  }
74
- /** 从 tool_call arguments 取得并规范化某条 read_file 的路径。 */
75
- pathAt(history, idx) {
110
+ callAt(history, idx) {
76
111
  const tcId = history[idx]?.tool_call_id;
77
112
  if (!tcId)
78
113
  return null;
79
114
  for (let j = idx - 1; j >= 1; j--) {
80
- const mm = history[j];
81
- if (mm.role !== 'assistant')
115
+ const message = history[j];
116
+ if (message.role !== 'assistant')
117
+ continue;
118
+ const calls = message.tool_calls;
119
+ const hit = calls?.find((tc) => tc?.id === tcId);
120
+ if (!hit?.function?.name)
82
121
  continue;
83
- const tcs = mm.tool_calls;
84
- const hit = tcs?.find((tc) => tc?.id === tcId);
85
- if (hit)
86
- return canonicalizePath(extractPath(hit.function?.arguments));
122
+ const argsRaw = hit.function.arguments ?? '';
123
+ return { name: hit.function.name, argsRaw, args: parseArgs(argsRaw) };
87
124
  }
88
125
  return null;
89
126
  }
90
- /** 成功 mutation 后,把该 canonical path 在 mutation 之前的 read 全部 stub。 */
91
127
  observeMutation(history, path) {
92
128
  try {
93
129
  const canonicalPath = canonicalizePath(path);
@@ -100,93 +136,102 @@ export class RelevancePruner {
100
136
  this.readByPath.delete(canonicalPath);
101
137
  }
102
138
  catch {
103
- /* 永不抛错 */
139
+ // Never throw from mutation cleanup.
104
140
  }
105
141
  }
106
- /**
107
- * 把 history 里 "canonical path 同 + index < beforeIdx + 不在当前轮保护区" 的 read_file
108
- * tool 消息替换为存根。索引是快路径,全表扫描用于恢复 resume 历史;两条路径都重新校验 path。
109
- */
110
142
  stubPriorReads(history, path, beforeIdx) {
111
143
  const targetPath = canonicalizePath(path);
112
144
  if (!targetPath)
113
145
  return;
114
- const guard = lastUserIndex(history);
115
- const protectedFrom = guard > 0 ? guard : 0;
116
- const stubOne = (i) => {
117
- if (i >= beforeIdx)
146
+ const protectedFrom = Math.max(0, lastUserIndex(history));
147
+ const stubOne = (idx) => {
148
+ if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
118
149
  return;
119
- if (i >= protectedFrom && protectedFrom > 0)
120
- return; // 当前轮保护区
121
- const m = history[i];
122
- if (!m || m.role !== 'tool')
150
+ const message = history[idx];
151
+ if (!message || message.role !== 'tool')
123
152
  return;
124
- const content = toText(m.content);
153
+ const content = toText(message.content);
125
154
  if (content.startsWith(STUB_PREFIX))
126
- return; // 幂等
127
- if (toolNameOf(history, i) !== 'read_file')
128
155
  return;
129
- if (this.pathAt(history, i) !== targetPath)
130
- return; // 防止 fallback 扫描误裁其他文件
131
- const tcId = m.tool_call_id;
132
- if (!tcId)
156
+ const call = this.callAt(history, idx);
157
+ if (call?.name !== 'read_file')
158
+ return;
159
+ if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath)
160
+ return;
161
+ if (!message.tool_call_id)
133
162
  return;
134
- const stub = `${STUB_PREFIX} read_file(${targetPath}) ${content.length} 字符 → 已被新 read / mutation 替代 · id …${tcId.slice(-6)}⌫`;
135
- m.content = stub;
163
+ message.content =
164
+ `${STUB_PREFIX}${READ_STUB_REASON}] read_file(${targetPath}) ${content.length} 字符 ` +
165
+ `→ 已被新 read / mutation 替代 · id …${message.tool_call_id.slice(-6)}⌫`;
136
166
  };
137
- const indexed = this.readByPath.get(targetPath);
138
- if (indexed) {
139
- for (const i of indexed)
140
- stubOne(i);
141
- }
167
+ for (const idx of this.readByPath.get(targetPath) ?? [])
168
+ stubOne(idx);
142
169
  const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
143
- for (let i = 1; i < scanEnd; i++)
144
- stubOne(i);
170
+ for (let idx = 1; idx < scanEnd; idx++)
171
+ stubOne(idx);
172
+ }
173
+ stubPriorObservations(history, toolName, key, beforeIdx) {
174
+ const protectedFrom = Math.max(0, lastUserIndex(history));
175
+ const stubOne = (idx) => {
176
+ if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
177
+ return;
178
+ const message = history[idx];
179
+ if (!message || message.role !== 'tool')
180
+ return;
181
+ const content = toText(message.content);
182
+ if (content.startsWith(STUB_PREFIX) || !isToolResultSuccess(content))
183
+ return;
184
+ const call = this.callAt(history, idx);
185
+ if (!call || call.name !== toolName || observationKey(call) !== key)
186
+ return;
187
+ if (!message.tool_call_id)
188
+ return;
189
+ const reason = toolName === 'grep'
190
+ ? '相同 grep 查询已有更新结果'
191
+ : '相同 codegraph 查询已有更新结果';
192
+ message.content =
193
+ `${STUB_PREFIX}${reason}] ${observationLabel(call)} ${content.length} 字符 ` +
194
+ `→ 已被更新查询替代 · id …${message.tool_call_id.slice(-6)}⌫`;
195
+ };
196
+ for (const idx of this.observationByKey.get(key) ?? [])
197
+ stubOne(idx);
198
+ // The fallback scan restores correctness after resume/compact when this instance
199
+ // has no index for older messages.
200
+ const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
201
+ for (let idx = 1; idx < scanEnd; idx++)
202
+ stubOne(idx);
145
203
  }
146
204
  }
147
- /** 默认单例:每个 agent 循环一个。runAgentCore 入口 new 一个,后续 observe 共享。 */
148
205
  export function createRelevancePruner() {
149
206
  return new RelevancePruner();
150
207
  }
151
- /** 解析一条 stub 字符串,提取原 content 长度(若可解析)。失败返 null。 */
208
+ /** Parse the original content length recorded by any relevance stub. */
152
209
  function parseStubOriginalLen(stub) {
153
- // 格式:⌦[已过时:同 path 已有新 read / 已被 mutation 覆写] read_file(<path>) <N> 字符 → ...
154
- const m = / read_file\([^)]+\) (\d+) 字符 /.exec(stub);
155
- if (!m)
210
+ const match = /\) (\d+) 字符 →/.exec(stub);
211
+ if (!match)
156
212
  return null;
157
- const n = Number(m[1]);
158
- return Number.isFinite(n) && n >= 0 ? n : null;
213
+ const value = Number(match[1]);
214
+ return Number.isFinite(value) && value >= 0 ? value : null;
159
215
  }
160
- /**
161
- * 扫 history,统计被相关性裁剪 stub 的 read_file tool 消息(条数 + 原字节数)。
162
- * 供 /context 渲染统计行用(让用户直观看到「prune 帮了多少」)。
163
- * 永不抛错(对齐本模块契约);history 为空 / 无 stub 时返零值。
164
- *
165
- * 注意:stub 后只剩 stub 字符串(原 content 已丢失),故只能从 stub 字符串里 parse
166
- * 原字节数,误差 = stub 时记录的 content.length(精确);token 估算走 estimateTokens。
167
- */
216
+ /** Aggregate relevance/lifecycle compression for the /context panel. */
168
217
  export function computePruneStats(history) {
169
218
  let stubbed = 0;
170
219
  let originalChars = 0;
171
220
  let stubChars = 0;
172
- for (const m of history) {
173
- if (m.role !== 'tool')
221
+ for (const message of history) {
222
+ if (message.role !== 'tool')
174
223
  continue;
175
- const c = toText(m.content);
176
- // Relevance Pruner 的 stub(⌦[已过时:...) 和 Lifecycle Engine 的 DIGEST(⌦[摘要:...) 都统计。
177
- const isPruneStub = c.startsWith(STUB_PREFIX);
178
- const isDigest = c.startsWith('⌦[摘要:');
224
+ const content = toText(message.content);
225
+ const isPruneStub = content.startsWith(STUB_PREFIX);
226
+ const isDigest = content.startsWith('⌦[摘要:');
179
227
  if (!isPruneStub && !isDigest)
180
228
  continue;
181
229
  stubbed++;
182
- stubChars += c.length;
183
- const orig = parseStubOriginalLen(c);
184
- if (orig != null)
185
- originalChars += orig;
230
+ stubChars += content.length;
231
+ const original = parseStubOriginalLen(content);
232
+ if (original != null)
233
+ originalChars += original;
186
234
  }
187
- // token 估算:用 estimateTokens(懒导入,避免循环依赖 llm)
188
- // 这里偷懒:走粗略 chars/4(中文混合下会过估,安全侧)
189
- // 准确应调 estimateTokens,但 /context 已经是粗算,误差可接受
190
235
  const originalTokens = Math.ceil(originalChars / 4);
191
236
  const stubTokens = Math.ceil(stubChars / 4);
192
237
  return {
@@ -6,6 +6,7 @@ import { Spinner } from '../ui/spinner.js';
6
6
  import * as layout from '../ui/layout.js';
7
7
  import { pruneAfterCompaction } from '../rollback/index.js';
8
8
  import { toText } from '../context/utils.js';
9
+ import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
9
10
  export function createContextState() {
10
11
  return { lastEstimate: 0, correction: 1, calibrationSamples: 0 };
11
12
  }
@@ -241,8 +242,8 @@ export async function compactHistory(history, opts) {
241
242
  const estimateBefore = estimatePromptTokens(history, activeTools, state.correction);
242
243
  state.lastEstimate = estimateBefore;
243
244
  const groups = groupFromEnd(history);
244
- // 保近期:按校正后的 token 累积到 40% window(至少保 1 组),永不劈开 group。
245
- const keepBudget = Math.floor(opts.window * 0.4);
245
+ // 保近期:按策略中的 token 比例累积(至少保 1 组),永不劈开 group。
246
+ const keepBudget = Math.floor(opts.window * DEFAULT_BUDGET_POLICY.compactKeepRatio);
246
247
  const kept = [];
247
248
  let keptTokens = 0;
248
249
  for (let k = groups.length - 1; k >= 0; k--) {
@@ -6,9 +6,9 @@
6
6
  */
7
7
  export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, createContextState, } from './compact.js';
8
8
  // ── Context Budget Scheduler 接缝 ────────────────────────────────────────
9
- // agent/core.ts 步前调 runScheduler(history, step):评估五区预算 → 按 ROI 调度
10
- // shrink_cold_tools / cap_hot_tools / compact_history。开关关闭时退化为 maybeCompact。
11
- // repl /compact 命令调 manualCompact(history, focus?):与自动路径完全一致,focus 透传摘要 prompt。
9
+ // agent/core.ts 在 age-aware sweep 后调用 runScheduler(history, step):评估五区预算,
10
+ // 只执行可落地的 warn / compact_history;开关关闭时退化为 maybeCompact。
11
+ // repl /compact 命令调 manualCompact(history, focus?):与自动路径共享决策,focus 透传摘要 prompt。
12
12
  export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
13
13
  export { dropContextFromHistory, formatDropResult, } from './drop.js';
14
14
  export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
@@ -1,30 +1,17 @@
1
- // Context Budget Scheduler(执行层):把 scheduleActions() 产生的动作落到既有闸上。
2
- //
3
- // 关系图:
1
+ // Context Budget Scheduler(执行层):评估五区预算并执行可落地的动作。
4
2
  //
5
3
  // agent/core.ts (步前) repl /compact 命令(手动)
6
4
  // ↓ runScheduler(history, step) ↓ manualCompact(history, focus?)
7
5
  // session/scheduler.ts(本文件)
8
6
  // ├─ evaluateBudget(history, window, step) → BudgetReport
9
- // │ ↓
10
- // ├─ scheduleActions(report) → ScheduleAction[]
11
- // │ ↓
7
+ // ├─ scheduleActions(report) → warn | compact_history
12
8
  // └─ 执行 actions:
13
- // - warn: 仅写日志
14
- // - shrink_cold_tools L1: 由 push-time cap.ts(MAX_HISTORY_RESULT)自动处理;此处 no-op
15
- // - shrink_cold_tools L2: 由 pruner.observePush(relevance.ts)自动处理;此处 no-op
16
- // - shrink_cold_tools L3: 由 lifecycle.pushTool(lifecycle.ts)自动处理;此处 no-op
17
- // - cap_hot_tools: Hot 区只 cap,实际仍由 push-time cap 走;此处 no-op + 记日志
18
- // - compact_history: 调 maybeCompact(history, report)── report 路由到 ROI 调度
9
+ // - warn: 仅写日志
10
+ // - compact_history: 调 maybeCompact(history, report)
19
11
  //
20
- // 设计意图:
21
- // - **L1/L2/L3 不重复实现**——push-time 已经自动跑过这三级。再在调度器做一遍是 Double-Action
22
- // 且破坏「调度器永不抛错 + 幂等」契约。调度器只负责"决策时点",push-time 闸负责"执行"。
23
- // - **hotBoundary** 仍由调度器算出来供 lifecycle 内部用(将来可演进成「仅 Cold 区跑 age stub」)——
24
- // 现版本先全面暴露给 report,暂不传参给 lifecycle。
25
- // - **actionLog**:每次执行的决策落进 contextState.schedulerLog,供 /context 命令与调试用。
26
- // - **manualCompact**:手动入口(用户敲 /compact),与 runScheduler 共享决策路径;唯一差别是
27
- // 即使 history 不超预算也强制产 compact_history(focus 透传)。对齐用户拍板的方案 A。
12
+ // push-time cap / relevance / lifecycle / age-aware sweep 在进入 scheduler 前独立完成,
13
+ // scheduler 不再生成无法执行的 Cold/Hot tool action。每次决策写入 actionLog,供 /context 调试。
14
+ // manualCompact 与自动路径共享 scheduleActions,但用户显式触发时强制追加 compact_history。
28
15
  //
29
16
  // 开关:
30
17
  // - config.contextBudget !== false(默认 true):agent 调 runScheduler
@@ -36,15 +23,10 @@ import { config } from '../config/index.js';
36
23
  import { maybeCompact, contextState } from './compact.js';
37
24
  import * as layout from '../ui/layout.js';
38
25
  import { ui } from '../ui/theme.js';
39
- /** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。
40
- * observePush 当前只是占位:真正 L1/L2/L3 已由 cap / pruner / lifecycle 在 push 时跑;
41
- * 保留接口为后续「调度器注入 hotBoundary 给 lifecycle」演进留接缝。 */
26
+ /** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。 */
42
27
  export function createBudgetScheduler(state = contextState) {
43
28
  const obs = {
44
29
  lastRunLog: null,
45
- observePush(_history, _idx) {
46
- // 占位:push-time 三闸(cap / pruner / lifecycle)已自动跑;此接缝供将来演进。
47
- },
48
30
  async runStep(history, step, activeTools = chatTools) {
49
31
  const report = evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools);
50
32
  const actions = scheduleActions(report);
@@ -53,7 +35,7 @@ export function createBudgetScheduler(state = contextState) {
53
35
  for (const a of actions) {
54
36
  if (a.kind === 'warn') {
55
37
  // system 超:写一行提示(配置漂移应由用户处理,不是调度器压)
56
- layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}调度器警告:${a.layer} ${a.reason}${ui.reset}\n`);
38
+ layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}调度器警告 [${a.layer}] ${a.reason}${ui.reset}\n`);
57
39
  }
58
40
  else if (a.kind === 'compact_history') {
59
41
  // 路由到 maybeCompact;把结构重建信号传回 core,使 lifecycle 按新 index 恢复。
@@ -61,9 +43,6 @@ export function createBudgetScheduler(state = contextState) {
61
43
  compactHistoryCalled = true;
62
44
  historyRebuilt ||= result?.historyRebuilt === true;
63
45
  }
64
- // shrink_cold_tools L1/L2/L3 与 cap_hot_tools:已由 push-time 闸在每次 push 自动跑
65
- // (cap = MAX_HISTORY_RESULT;pruner = same-path 新旧替换;lifecycle = age stub)。
66
- // 调度器不重复,只把决策记录下来供调试。
67
46
  }
68
47
  const log = {
69
48
  step,
@@ -85,9 +64,9 @@ export async function runScheduler(history, step, state = contextState, activeTo
85
64
  const s = createBudgetScheduler(state);
86
65
  return s.runStep(history, step, activeTools);
87
66
  }
88
- /** 手动 /compact 入口(repl):与自动路径完全一致——五区 ROI 调度,但 history 摘要强制执行。
89
- * 即便 layers.history.overBudget=false 或 totalOver=false,manual 仍产 compact_history action
90
- * 把 focus 透传给 LLM 摘要 prompt。其它 ROI 决策(cold tools / cap hot / warn)按 scheduleActions 走。
67
+ /** 手动 /compact 入口(repl):与自动路径共享预算评估和可执行 action,但强制执行 history 摘要。
68
+ * 即便 layers.history.overBudget=false 或 totalOver=false,manual 仍追加 compact_history,
69
+ * 并把 focus 透传给 LLM 摘要 prompt。
91
70
  *
92
71
  * 关系:runScheduler 是「自动触发」,manualCompact 是「用户显式触发」,二者共享 scheduleActions。
93
72
  *
@@ -23,23 +23,6 @@ export const GC_DAYS = 90;
23
23
  /** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
24
24
  export const MAX_MEMORY_RESULT = 64000;
25
25
  export const IGNORE = ['**/node_modules/**', '**/.git/**'];
26
- // ── Context Budget Scheduler(五区分账)────────────────────────────────────
27
- /** Hot/Cold 划分:当前 step 起往前 N 个 user turn 之内的工具结果视为 Hot(绝对不压),
28
- * 之外的视为 Cold(可调度器压)。默认 4 = 跨过 4 个用户问题仍生效。 */
29
- export const HOT_TURN_WINDOW = 4;
30
- /** Cold 区内可被就地 stub 的 tool 消息最低 age(经过的消费者 push 数)。默认 2,
31
- * 与 lifecycle.ts DEFAULT_AGE_THRESHOLD 对齐。 */
32
- export const TOOL_OLD_AGE = 2;
33
- /** 五区预算占比(总和 0.95,留 5% 给 Reserve)。对齐 user 修正版:
34
- * System 15 / History 20 / Hot Tool 25 / Cold Tool 25 / Summary 10。 */
35
- export const BUDGET_RATIO = {
36
- system: 0.15,
37
- history: 0.20,
38
- toolRecent: 0.25,
39
- toolOld: 0.25,
40
- summary: 0.10,
41
- reserve: 0.05,
42
- };
43
26
  // ── plan 模式(只读规划,不执行)──────────────────────────────────────────────
44
27
  /**
45
28
  * plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {