mocode-ai 0.6.2 → 0.6.3
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.
- package/dist/agent/core.js +3 -1
- package/dist/context/lifecycle.js +248 -18
- package/dist/context/relevance.js +4 -1
- package/package.json +1 -1
package/dist/agent/core.js
CHANGED
|
@@ -186,7 +186,9 @@ export async function runAgentCore(opts) {
|
|
|
186
186
|
// 观察者生命周期引擎:每个 runAgentCore 实例一个,纯静态、自动维护 grep/glob/codegraph 等
|
|
187
187
|
// producer 与 read/edit/write 的 consumer 引用关系;孤立+老化的非观察类工具自动 STUB。
|
|
188
188
|
// 开关关闭时为 null,所有 pushToolResult / mutation 调用走无 lifecycle 路径(零行为变化)。
|
|
189
|
-
|
|
189
|
+
// 引擎需要从已有会话 history 恢复观察结果的年龄和 path 索引;不能只追踪本次
|
|
190
|
+
// runAgentCore,否则跨用户轮次的 grep/glob 永远不会衰减。
|
|
191
|
+
const lifecycle = config.contextLifecycle ? createLifecycleEngine(history) : null;
|
|
190
192
|
// 预算调度器:每个 runAgentCore 实例一个,步前 evaluateBudget + scheduleActions。
|
|
191
193
|
// 决策按 ROI 分发(cold tools 优先 / history 摘要最后);contextBudget 开关关闭时为 null。
|
|
192
194
|
const scheduler = config.contextBudget !== false
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// - OBSOLETE:无任何消费者引用,且距离当前 push 已老化 N 步(默认 2)。
|
|
10
10
|
// - STUB:已被替换为存根(物理上 content 变成「⌦[无消费者:...]」)。
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// - grep/glob/codegraph/web_search/web_fetch
|
|
14
|
-
//
|
|
12
|
+
// 观察类工具两阶段衰减(避免误伤):
|
|
13
|
+
// - grep/glob/codegraph/web_search/web_fetch 等「观察/检索类」工具两阶段衰减:
|
|
14
|
+
// Phase 1(8 步):LIVE → REFERENCED,保留完整内容(返回多个候选,剩余候选可能后续被消费)。
|
|
15
|
+
// Phase 2(+5 步):REFERENCED → DIGEST,替换为摘要存根(保留文件列表+命中数+参数,丢弃详情),
|
|
16
|
+
// 释放 ~90% token。states 仍为 REFERENCED,不引入新状态。
|
|
15
17
|
// - 当前轮保护区(最后一个 user 之后的工具结果)完全不动。
|
|
16
18
|
// - 已 STUB(含「⌦[已过时:...]」或「⌦[已剔除:...]」或本层的「⌦[无消费者:...]」)不重复处理。
|
|
17
19
|
// - 永不抛错(对齐上下文管道的「调度器永不抛错」契约)。
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
//
|
|
28
30
|
// 开关:`config.contextLifecycle`(默认 true;MOCODE_LIFECYCLE=false 回退)。
|
|
29
31
|
import { extractPath, lastUserIndex, toText, toolNameOf } from './utils.js';
|
|
30
|
-
/** 观察类工具(
|
|
32
|
+
/** 观察类工具(LIVE → REFERENCED → DIGEST 两阶段衰减,永不自动 STUB)。 */
|
|
31
33
|
const OBSERVER_TOOLS = new Set([
|
|
32
34
|
'grep',
|
|
33
35
|
'glob',
|
|
@@ -42,10 +44,19 @@ const CONSUMER_TOOLS = new Set(['read_file', 'edit_file', 'write_file']);
|
|
|
42
44
|
const MUTATION_TOOLS = new Set(['edit_file', 'write_file']);
|
|
43
45
|
/** 存根前缀(区分 Relevance Pruner 与 drop_context)。 */
|
|
44
46
|
const STUB_PREFIX_NO_CONSUMER = '⌦[无消费者:观察结果已无引用价值]';
|
|
47
|
+
/** 观察类工具摘要前缀(DIGEST 状态标记)。 */
|
|
48
|
+
const DIGEST_PREFIX = '⌦[摘要:';
|
|
45
49
|
/** 老化阈值:某条工具消息自 push 以来经历的「消费者 push」次数。
|
|
46
50
|
* ≥ 这个值且仍为 LIVE 且非观察类 → 视为 OBSOLETE → STUB。
|
|
47
51
|
* 默认 2:等价于「跨过两个消费者 push 仍无人引用」= 跨过整轮最末尾的工具调用。 */
|
|
48
52
|
const DEFAULT_AGE_THRESHOLD = 2;
|
|
53
|
+
/** 观察类工具 Phase 1:LIVE → REFERENCED 的老化阈值(普通工具用 DEFAULT_AGE_THRESHOLD=2)。
|
|
54
|
+
* 10 步后才降为 REFERENCED,保留完整内容;理由:返回多个候选(grep 10 文件但只读 1 个),
|
|
55
|
+
* 剩余候选可能后续被消费,给足够时间窗口。 */
|
|
56
|
+
const OBSERVER_REFERENCED_AGE = 10;
|
|
57
|
+
/** 观察类工具 Phase 2:REFERENCED → DIGEST 的老化阈值(从 REFERENCED 起再累积)。
|
|
58
|
+
* +5 步后替换为摘要存根,保留文件列表+命中数+参数,丢弃详情;释放 ~90% token。 */
|
|
59
|
+
const OBSERVER_DIGEST_AGE = 5;
|
|
49
60
|
/** 从 tool 结果的 content 中提取「生产者命中过的 path 列表」。
|
|
50
61
|
* - read_file:没有 path 列表(本身就是单 path 消费者,无需再追生产者)。
|
|
51
62
|
* - grep:content 是 `file:line: ...` 行,提取每行的 file 段(只保留绝对路径形态或与 pattern 匹配的)。
|
|
@@ -113,12 +124,102 @@ export class LifecycleEngine {
|
|
|
113
124
|
/** producer 路径 → 生产者工具消息 idx 列表(逆查用:某个 path 被消费时,反查上游 producer)。
|
|
114
125
|
* 注意:不存 read_file,因为 read 自身就是消费者不充当 producer。 */
|
|
115
126
|
producersByPath = new Map();
|
|
127
|
+
/** 已被 DIGEST 的 tool 消息 idx 集合(避免重复检查;states 保持 REFERENCED)。 */
|
|
128
|
+
digestedIdxs = new Set();
|
|
129
|
+
/** 摘要不比原文短而保留原文的 idx;仅抑制当前 run 内的重复计算。 */
|
|
130
|
+
digestRetainedIdxs = new Set();
|
|
116
131
|
/** 当前步序号(用于 age 老化:每次 pushTool 自增,对比 age 阈值)。 */
|
|
117
132
|
step = 0;
|
|
118
133
|
/** 最后 user 索引缓存(pushTool 时重算;pushMutation 时也重算,因为 mutation 可能跟 user 同行)。 */
|
|
119
134
|
lastUser = -1;
|
|
120
|
-
constructor(ageThreshold = DEFAULT_AGE_THRESHOLD) {
|
|
135
|
+
constructor(ageThreshold = DEFAULT_AGE_THRESHOLD, history) {
|
|
121
136
|
this.ageThreshold = ageThreshold;
|
|
137
|
+
if (history)
|
|
138
|
+
this.rehydrate(history);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 从会话历史恢复观察者状态。
|
|
142
|
+
*
|
|
143
|
+
* LifecycleEngine 是每次 runAgentCore 新建的,但 history 是会话级的;若不回放,
|
|
144
|
+
* 跨轮的 observer 不在 states 中,后续工具调用也就无法使它老化。这里按原始
|
|
145
|
+
* pushTool 的顺序重放登记、消费关系和 age,并使用当时的 user 保护边界推进阶段。
|
|
146
|
+
* 只纳入 observer:非 observer 的短期 orphan 处理刻意保持原有「本轮」语义。
|
|
147
|
+
*/
|
|
148
|
+
rehydrate(history) {
|
|
149
|
+
try {
|
|
150
|
+
let replayLastUser = -1;
|
|
151
|
+
const pendingDigests = new Set();
|
|
152
|
+
for (let idx = 1; idx < history.length; idx++) {
|
|
153
|
+
const m = history[idx];
|
|
154
|
+
if (m.role === 'user') {
|
|
155
|
+
replayLastUser = idx;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (m.role !== 'tool')
|
|
159
|
+
continue;
|
|
160
|
+
const toolName = toolNameOf(history, idx);
|
|
161
|
+
if (!toolName)
|
|
162
|
+
continue;
|
|
163
|
+
const content = toText(m.content);
|
|
164
|
+
const isDigest = content.startsWith(DIGEST_PREFIX);
|
|
165
|
+
if (OBSERVER_TOOLS.has(toolName) && !content.startsWith('⌦[')) {
|
|
166
|
+
this.states.set(idx, 'LIVE');
|
|
167
|
+
this.consumerCount.set(idx, 0);
|
|
168
|
+
this.age.set(idx, 0);
|
|
169
|
+
for (const path of extractProducerPaths(toolName, content)) {
|
|
170
|
+
const producers = this.producersByPath.get(path) ?? [];
|
|
171
|
+
producers.push(idx);
|
|
172
|
+
this.producersByPath.set(path, producers);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (OBSERVER_TOOLS.has(toolName) && isDigest) {
|
|
176
|
+
this.states.set(idx, 'REFERENCED');
|
|
177
|
+
this.consumerCount.set(idx, 0);
|
|
178
|
+
this.age.set(idx, 0);
|
|
179
|
+
this.digestedIdxs.add(idx);
|
|
180
|
+
}
|
|
181
|
+
if (CONSUMER_TOOLS.has(toolName)) {
|
|
182
|
+
const path = extractPath(this.findToolArgs(history, idx));
|
|
183
|
+
if (path) {
|
|
184
|
+
for (const producerIdx of this.producersByPath.get(path) ?? []) {
|
|
185
|
+
if (this.states.get(producerIdx) === 'LIVE') {
|
|
186
|
+
this.states.set(producerIdx, 'REFERENCED');
|
|
187
|
+
}
|
|
188
|
+
this.consumerCount.set(producerIdx, (this.consumerCount.get(producerIdx) ?? 0) + 1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const key of this.states.keys())
|
|
193
|
+
this.age.set(key, (this.age.get(key) ?? 0) + 1);
|
|
194
|
+
this.step++;
|
|
195
|
+
// 和 pushTool 一致:本轮 user 及之后的结果不处理;旧轮结果可以推进。
|
|
196
|
+
for (const [observerIdx, state] of this.states) {
|
|
197
|
+
if (observerIdx >= replayLastUser || this.isDigestFinalized(observerIdx))
|
|
198
|
+
continue;
|
|
199
|
+
const observerName = toolNameOf(history, observerIdx);
|
|
200
|
+
if (!observerName || !OBSERVER_TOOLS.has(observerName))
|
|
201
|
+
continue;
|
|
202
|
+
const age = this.age.get(observerIdx) ?? 0;
|
|
203
|
+
if (state === 'LIVE' && age >= OBSERVER_REFERENCED_AGE) {
|
|
204
|
+
this.states.set(observerIdx, 'REFERENCED');
|
|
205
|
+
}
|
|
206
|
+
else if (state === 'REFERENCED'
|
|
207
|
+
&& age >= OBSERVER_REFERENCED_AGE + OBSERVER_DIGEST_AGE) {
|
|
208
|
+
pendingDigests.add(observerIdx);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.lastUser = lastUserIndex(history);
|
|
213
|
+
// 回放期间只计算状态;最后统一落盘,避免迭代时破坏 path 提取。
|
|
214
|
+
for (const idx of pendingDigests) {
|
|
215
|
+
const toolName = toolNameOf(history, idx);
|
|
216
|
+
if (toolName && idx < Math.max(0, this.lastUser))
|
|
217
|
+
this.digestOne(history, idx, toolName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// 历史中出现非标准消息时降级为本轮行为,不能影响 agent 主循环。
|
|
222
|
+
}
|
|
122
223
|
}
|
|
123
224
|
/** 新工具结果 push 进 history 时调;idx = history.length - 1。
|
|
124
225
|
* mutation 工具(edit_file/write_file)的 push 跳过本轮的 autoStubOrphans(由调用方在
|
|
@@ -233,27 +334,44 @@ export class LifecycleEngine {
|
|
|
233
334
|
// 永不抛错。
|
|
234
335
|
}
|
|
235
336
|
}
|
|
236
|
-
/**
|
|
337
|
+
/** 老化自动处理:
|
|
338
|
+
* - 非观察类 LIVE 且 age ≥ 阈值 → OBSOLETE → STUB(完全丢弃)。
|
|
339
|
+
* - 观察类工具两阶段衰减:
|
|
340
|
+
* Phase 1: LIVE → REFERENCED(age ≥ OBSERVER_REFERENCED_AGE,保留完整内容)。
|
|
341
|
+
* Phase 2: REFERENCED → DIGEST(增量 age ≥ OBSERVER_DIGEST_AGE,替换为摘要存根)。
|
|
342
|
+
* - 当前轮保护区(最后一个 user 之后)完全不动。
|
|
343
|
+
*/
|
|
237
344
|
autoStubOrphans(history) {
|
|
238
345
|
try {
|
|
239
346
|
const protectedFrom = Math.max(0, this.lastUser);
|
|
240
347
|
for (const [idx, state] of this.states) {
|
|
241
|
-
if (state !== 'LIVE')
|
|
242
|
-
continue;
|
|
243
348
|
if (idx >= protectedFrom)
|
|
244
349
|
continue; // 当前轮保护区
|
|
245
350
|
const age = this.age.get(idx) ?? 0;
|
|
246
|
-
if (age < this.ageThreshold)
|
|
247
|
-
continue;
|
|
248
351
|
const tn = toolNameOf(history, idx);
|
|
249
352
|
if (!tn)
|
|
250
353
|
continue;
|
|
251
|
-
// 观察类工具永远只到 REFERENCED,不自动 STUB(用户拍板)。
|
|
252
354
|
if (OBSERVER_TOOLS.has(tn)) {
|
|
253
|
-
|
|
355
|
+
// Phase 1: LIVE → REFERENCED(保留完整内容)。
|
|
356
|
+
if (state === 'LIVE' && age >= OBSERVER_REFERENCED_AGE) {
|
|
357
|
+
this.states.set(idx, 'REFERENCED');
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// Phase 2: REFERENCED → DIGEST(替换为摘要存根,状态不变)。
|
|
361
|
+
if (state === 'REFERENCED' && !this.isDigestFinalized(idx)) {
|
|
362
|
+
// read_file 可能在很早时就把 producer 标成 REFERENCED;不能因此跳过
|
|
363
|
+
// Phase 1 的保留窗口。摘要始终以 observer 自身的总 age 为准。
|
|
364
|
+
if (age >= OBSERVER_REFERENCED_AGE + OBSERVER_DIGEST_AGE) {
|
|
365
|
+
this.digestOne(history, idx, tn);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
254
368
|
continue;
|
|
255
369
|
}
|
|
256
|
-
//
|
|
370
|
+
// 非观察类:只在 LIVE 状态下老化 STUB。
|
|
371
|
+
if (state !== 'LIVE')
|
|
372
|
+
continue;
|
|
373
|
+
if (age < this.ageThreshold)
|
|
374
|
+
continue;
|
|
257
375
|
this.stubOne(history, idx, tn);
|
|
258
376
|
}
|
|
259
377
|
}
|
|
@@ -279,6 +397,115 @@ export class LifecycleEngine {
|
|
|
279
397
|
// 永不抛错。
|
|
280
398
|
}
|
|
281
399
|
}
|
|
400
|
+
/** 观察类工具 Phase 2:替换为摘要存根,保留文件列表+命中数+参数,丢弃详情。
|
|
401
|
+
* 幂等:已是 ⌦[ 前缀的跳过。states 保持 REFERENCED,只替换 content。 */
|
|
402
|
+
digestOne(history, idx, toolName) {
|
|
403
|
+
try {
|
|
404
|
+
const m = history[idx];
|
|
405
|
+
if (!m)
|
|
406
|
+
return;
|
|
407
|
+
const c = toText(m.content);
|
|
408
|
+
if (c.startsWith('⌦['))
|
|
409
|
+
return; // 幂等(已 STUB/DIGEST)
|
|
410
|
+
const origLen = c.length;
|
|
411
|
+
const argsRaw = this.findToolArgs(history, idx);
|
|
412
|
+
const summary = this.buildDigestSummary(toolName, c, argsRaw, origLen, extractProducerPaths(toolName, c));
|
|
413
|
+
// 摘要的唯一目标是降低上下文成本。短结果(如“未命中”)不能被固定文案放大。
|
|
414
|
+
if (summary.length >= origLen) {
|
|
415
|
+
this.digestRetainedIdxs.add(idx);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
m.content = summary;
|
|
419
|
+
// states 保持 REFERENCED;digestedIdxs 由 stats() 扣除显示。
|
|
420
|
+
this.digestedIdxs.add(idx);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// 永不抛错。
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** 为各观察类工具生成摘要字符串。只保留统计 + 参数,不保留详情(文件列表/URL/正文),
|
|
427
|
+
* 明确标注"这是历史摘要,需要最新信息请重新调用"。 */
|
|
428
|
+
buildDigestSummary(toolName, content, argsRaw, origLen, paths) {
|
|
429
|
+
const files = this.formatDigestPaths(paths);
|
|
430
|
+
switch (toolName) {
|
|
431
|
+
case 'grep': {
|
|
432
|
+
// 命中行数:content 里形如 `file:line:` 的行数
|
|
433
|
+
let lineCount = 0;
|
|
434
|
+
const re = /^[^\s:][^:]*?\.[A-Za-z0-9]+:\d+:/gm;
|
|
435
|
+
while (re.exec(content))
|
|
436
|
+
lineCount++;
|
|
437
|
+
const args = (() => { try {
|
|
438
|
+
const a = JSON.parse(argsRaw);
|
|
439
|
+
let s = `"${a.pattern ?? ''}"`;
|
|
440
|
+
if (a.glob)
|
|
441
|
+
s += `, glob="${a.glob}"`;
|
|
442
|
+
return s;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return '...';
|
|
446
|
+
} })();
|
|
447
|
+
return `${DIGEST_PREFIX}grep(${args}) — 历史结果 ${lineCount} 行命中,文件 ${files},${origLen}→摘要]\n如需完整详情或最新信息请重新调用 grep`;
|
|
448
|
+
}
|
|
449
|
+
case 'glob': {
|
|
450
|
+
const args = (() => { try {
|
|
451
|
+
return `"${JSON.parse(argsRaw).pattern ?? ''}"`;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return '...';
|
|
455
|
+
} })();
|
|
456
|
+
return `${DIGEST_PREFIX}glob(${args}) — 历史结果文件 ${files},${origLen}→摘要]\n如需完整列表或最新信息请重新调用 glob`;
|
|
457
|
+
}
|
|
458
|
+
case 'codegraph': {
|
|
459
|
+
const args = (() => { try {
|
|
460
|
+
const a = JSON.parse(argsRaw);
|
|
461
|
+
return `"${a.query ?? ''}"`;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return '...';
|
|
465
|
+
} })();
|
|
466
|
+
return `${DIGEST_PREFIX}codegraph(${args}) — 历史结果文件 ${files},${origLen}→摘要]\n如需完整详情或最新信息请重新调用 codegraph`;
|
|
467
|
+
}
|
|
468
|
+
case 'web_search': {
|
|
469
|
+
// 统计结果数
|
|
470
|
+
let resultCount = 0;
|
|
471
|
+
const lines = content.split(/\r?\n/);
|
|
472
|
+
for (const line of lines) {
|
|
473
|
+
if (/^\[\d+\]\s*.+/.test(line))
|
|
474
|
+
resultCount++;
|
|
475
|
+
}
|
|
476
|
+
const query = (() => { try {
|
|
477
|
+
return JSON.parse(argsRaw).query ?? '';
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return '';
|
|
481
|
+
} })();
|
|
482
|
+
return `${DIGEST_PREFIX}web_search("${query}") — 历史结果 ${resultCount} 条, ${origLen}→摘要]\n如需最新信息请重新调用 web_search`;
|
|
483
|
+
}
|
|
484
|
+
case 'web_fetch': {
|
|
485
|
+
const url = (() => { try {
|
|
486
|
+
return JSON.parse(argsRaw).url ?? '';
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return '';
|
|
490
|
+
} })();
|
|
491
|
+
return `${DIGEST_PREFIX}web_fetch(${url}) — 历史结果已归档, ${origLen}→摘要]\n如需最新信息请重新调用 web_fetch`;
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
return `${DIGEST_PREFIX}${toolName} — 历史结果已归档, ${origLen}→摘要]\n如需最新信息请重新调用 ${toolName}`;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
isDigestFinalized(idx) {
|
|
498
|
+
return this.digestedIdxs.has(idx) || this.digestRetainedIdxs.has(idx);
|
|
499
|
+
}
|
|
500
|
+
/** 文件候选是摘要的关键可用信息;限量且去重,避免摘要本身重新膨胀。 */
|
|
501
|
+
formatDigestPaths(paths) {
|
|
502
|
+
const unique = [...new Set(paths)];
|
|
503
|
+
if (unique.length === 0)
|
|
504
|
+
return '0 个(未能从输出解析路径)';
|
|
505
|
+
const limit = 12;
|
|
506
|
+
const shown = unique.slice(0, limit).join(', ');
|
|
507
|
+
return unique.length > limit ? `${unique.length} 个 [${shown}, …]` : `${unique.length} 个 [${shown}]`;
|
|
508
|
+
}
|
|
282
509
|
/** 找某条 tool 消息对应的 assistant.tool_calls.arguments。 */
|
|
283
510
|
findToolArgs(history, idx) {
|
|
284
511
|
try {
|
|
@@ -305,7 +532,8 @@ export class LifecycleEngine {
|
|
|
305
532
|
getState(idx) {
|
|
306
533
|
return this.states.get(idx) ?? null;
|
|
307
534
|
}
|
|
308
|
-
/** 拿当前各状态计数。供 /context 显示「live=N, referenced=M,
|
|
535
|
+
/** 拿当前各状态计数。供 /context 显示「live=N, referenced=M, digest=K, obsolete=J, stubbed=S」。
|
|
536
|
+
* digested:已被摘要的观察类工具(states 仍为 REFERENCED,但 content 已替换为摘要)。 */
|
|
309
537
|
stats() {
|
|
310
538
|
let live = 0;
|
|
311
539
|
let referenced = 0;
|
|
@@ -321,10 +549,12 @@ export class LifecycleEngine {
|
|
|
321
549
|
else if (s === 'STUB')
|
|
322
550
|
stubbed++;
|
|
323
551
|
}
|
|
324
|
-
|
|
552
|
+
// digested 的 states 仍为 REFERENCED,从总 referenced 中扣除得到纯 REFERENCED 数。
|
|
553
|
+
return { live, referenced: referenced - this.digestedIdxs.size, digested: this.digestedIdxs.size, obsolete, stubbed };
|
|
325
554
|
}
|
|
326
555
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
556
|
+
export function createLifecycleEngine(historyOrThreshold, ageThreshold) {
|
|
557
|
+
const history = Array.isArray(historyOrThreshold) ? historyOrThreshold : undefined;
|
|
558
|
+
const threshold = typeof historyOrThreshold === 'number' ? historyOrThreshold : ageThreshold;
|
|
559
|
+
return new LifecycleEngine(threshold, history);
|
|
330
560
|
}
|
|
@@ -200,7 +200,10 @@ export function computePruneStats(history) {
|
|
|
200
200
|
if (m.role !== 'tool')
|
|
201
201
|
continue;
|
|
202
202
|
const c = toText(m.content);
|
|
203
|
-
|
|
203
|
+
// Relevance Pruner 的 stub(⌦[已过时:...) 和 Lifecycle Engine 的 DIGEST(⌦[摘要:...) 都统计。
|
|
204
|
+
const isPruneStub = c.startsWith(STUB_PREFIX);
|
|
205
|
+
const isDigest = c.startsWith('⌦[摘要:');
|
|
206
|
+
if (!isPruneStub && !isDigest)
|
|
204
207
|
continue;
|
|
205
208
|
stubbed++;
|
|
206
209
|
stubChars += c.length;
|