pi-think-anchor 1.0.3 → 1.0.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.
- package/README.md +3 -2
- package/index.ts +63 -28
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 来源与借鉴
|
|
6
6
|
|
|
7
|
-
蒸馏自 [dsh-anchored-monitor](https://dsh-plugin.org/zh/plugins/aik358/dsh-anchored-monitor) 0.3.1(DSH
|
|
7
|
+
蒸馏自 [dsh-anchored-monitor](https://dsh-plugin.org/zh/plugins/aik358/dsh-anchored-monitor) 0.3.1(DSH 生态):机制思想来源,非代码移植(回合级而非流内实时、无独立进程)。判据经 6151 条日志三次校准:**v2.3 = 单轮证据门槛 + α 平滑专注/发散比**(α=1;react <0.5 / mixed <1.2 / spec ≥1.2)。命中不足的轮次不评分(避免把「长而散」判成反复试探),回放 react 11.3% → 3.4%。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
| 项 | 默认 | 说明 |
|
|
20
20
|
| --- | --- | --- |
|
|
21
21
|
| 启用 | 开 | `THINK_ANCHOR=0` 或配置文件 `enabled: false` 关闭 |
|
|
22
|
-
| 波段阈值 | 1.2 / 0.5 |
|
|
22
|
+
| 波段阈值 | 1.2 / 0.5 | α 平滑比率:高专注 ≥1.2;发散 <0.5 |
|
|
23
|
+
| 证据门槛 | 3 命中/轮 | 本轮词频命中不足 3 个 → 不评分、不干预 |
|
|
23
24
|
| 聚合窗口 | 5 轮 | 滑动窗口 |
|
|
24
25
|
| L1 冷却 | 10 分钟 | 首次跌入发散 |
|
|
25
26
|
| L2 冷却 | 15 分钟 | 持续发散,会话上限 2 次 |
|
package/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ interface TAConfig {
|
|
|
31
31
|
windowSize: number;
|
|
32
32
|
bands: { spec: number; react: number };
|
|
33
33
|
/** v2 主判据:证据门槛 + 发散词占比(与长度无关) */
|
|
34
|
-
score: { minHits: number;
|
|
34
|
+
score: { minHits: number; spec: number; react: number; alpha: number };
|
|
35
35
|
weights: { beta: number; eps: number };
|
|
36
36
|
intervention: { l1CooldownMin: number; l2CooldownMin: number; maxL2: number };
|
|
37
37
|
}
|
|
@@ -41,7 +41,7 @@ const DEF_CFG: TAConfig = {
|
|
|
41
41
|
lexicon: { positive: DEF_POS, negative: DEF_NEG, neutral: DEF_NEU },
|
|
42
42
|
windowSize: 5,
|
|
43
43
|
bands: { spec: 1.2, react: 0.5 },
|
|
44
|
-
score: { minHits: 3,
|
|
44
|
+
score: { minHits: 3, spec: 1.2, react: 0.5, alpha: 1 },
|
|
45
45
|
weights: { beta: 0.4, eps: 0.5 },
|
|
46
46
|
intervention: { l1CooldownMin: 10, l2CooldownMin: 15, maxL2: 2 },
|
|
47
47
|
};
|
|
@@ -58,6 +58,8 @@ function loadCfg(): TAConfig {
|
|
|
58
58
|
...DEF_CFG,
|
|
59
59
|
...raw,
|
|
60
60
|
lexicon: { ...DEF_CFG.lexicon, ...(raw.lexicon ?? {}) },
|
|
61
|
+
// score 是 v2 判据;未配置 score 时兼容旧 bands(否则改 bands 静默无效)
|
|
62
|
+
score: { ...DEF_CFG.score, ...(raw.score ?? {}), ...(raw.score ? {} : (raw.bands ?? {})) },
|
|
61
63
|
bands: { ...DEF_CFG.bands, ...(raw.bands ?? {}) },
|
|
62
64
|
weights: { ...DEF_CFG.weights, ...(raw.weights ?? {}) },
|
|
63
65
|
intervention: { ...DEF_CFG.intervention, ...(raw.intervention ?? {}) },
|
|
@@ -103,15 +105,17 @@ export function bandOf(ratio: number, bands: { spec: number; react: number } = D
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
|
-
* v2
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* v2.1 判据:证据门槛 + α 平滑的专注/发散比。
|
|
109
|
+
* 回放 2069 条可评分日志:旧判据 react 占 18.3%(其中 57% 是命中 <3 的长而散);
|
|
110
|
+
* α=1(伪计数)+ minHits=3 后 react 占 9.7%,且选中样本 negRate 中位 0.86(真发散)。
|
|
111
|
+
* α 的作用:低命中时比率趋向 1(mixed),不再因「命中少」被误判成 react。
|
|
110
112
|
*/
|
|
111
|
-
export function classify(
|
|
113
|
+
export function classify(pos: number, neg: number, neu: number, c: { minHits: number; spec: number; react: number; alpha: number }): TABand {
|
|
114
|
+
const hits = pos + neg + neu;
|
|
112
115
|
if (hits < c.minHits) return "none";
|
|
113
|
-
|
|
114
|
-
if (
|
|
116
|
+
const r = (pos + 0.4 * neu + c.alpha) / (neg + 0.5 + c.alpha);
|
|
117
|
+
if (r < c.react) return "react";
|
|
118
|
+
if (r < c.spec) return "mixed";
|
|
115
119
|
return "spec";
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -164,6 +168,11 @@ export function decideNext(phase: TAPhase, band: TABand, l2Tried: number, maxL2:
|
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
// ── 干预文案(中性建议式——原版措辞纪律:不指责、不自称审查者) ──
|
|
171
|
+
export function downgradeIfIneffective(level: "none" | "L1" | "L2", lastEff: number | null): "none" | "L1" | "L2" {
|
|
172
|
+
if (level === "L2" && lastEff !== null && lastEff >= 0) return "L1";
|
|
173
|
+
return level;
|
|
174
|
+
}
|
|
175
|
+
|
|
167
176
|
const L1_TEXT = "「think-anchor」信号(词频启发式,非判据):思考偏离收敛。动作:给结论 + 下一步,只留依据。";
|
|
168
177
|
const L2_TEXT = "「think-anchor」信号(词频启发式,非判据):连续偏离。动作:停止探索,立即执行一个具体动作。";
|
|
169
178
|
|
|
@@ -176,14 +185,14 @@ interface TAState {
|
|
|
176
185
|
l1Count: number;
|
|
177
186
|
l2Count: number;
|
|
178
187
|
turns: number;
|
|
179
|
-
bandHist: { spec: number; mixed: number; react: number };
|
|
188
|
+
bandHist: { spec: number; mixed: number; react: number; none: number };
|
|
180
189
|
lastL1: number;
|
|
181
190
|
lastL2: number;
|
|
182
191
|
lastRatio: number;
|
|
183
192
|
lastBand: TABand;
|
|
184
193
|
/** v3:效果闭环与自适应(借鉴 ptc-meter 的数据自校正 + patch-keeper 的条件触发) */
|
|
185
194
|
recent: number[];
|
|
186
|
-
pending: { turn: number; pre: number } | null;
|
|
195
|
+
pending: { turn: number; pre: number; ts: number; seen: number } | null;
|
|
187
196
|
effSamples: number;
|
|
188
197
|
effImproved: number;
|
|
189
198
|
autoMuted: boolean;
|
|
@@ -200,7 +209,7 @@ function newState(cfg: TAConfig): TAState {
|
|
|
200
209
|
l1Count: 0,
|
|
201
210
|
l2Count: 0,
|
|
202
211
|
turns: 0,
|
|
203
|
-
bandHist: { spec: 0, mixed: 0, react: 0 },
|
|
212
|
+
bandHist: { spec: 0, mixed: 0, react: 0, none: 0 },
|
|
204
213
|
lastL1: 0,
|
|
205
214
|
lastL2: 0,
|
|
206
215
|
lastRatio: 0,
|
|
@@ -220,6 +229,8 @@ const SAMPLE_EVERY = 5;
|
|
|
220
229
|
/** 自适应(v3):有效样本 ≥ AUTO_MIN 且下降率 < AUTO_RATE(不及抛硬币)即转静默,只记录不注入 */
|
|
221
230
|
const AUTO_MIN = 8;
|
|
222
231
|
const AUTO_RATE = 0.5;
|
|
232
|
+
/** 上次干预的实测效果(eff,负 = 改善);用于「无效则不升级」 */
|
|
233
|
+
let lastEffValue: number | null = null;
|
|
223
234
|
|
|
224
235
|
/** 日志上限 1 MiB(与 ptc-meter 同口径):超限保留尾部,避免无界增长。 */
|
|
225
236
|
const MAX_LOG = 1 << 20;
|
|
@@ -228,9 +239,10 @@ export function appendLog(row: Record<string, unknown>): void {
|
|
|
228
239
|
try {
|
|
229
240
|
appendFileSync(LOG, JSON.stringify(row) + "\n");
|
|
230
241
|
if (statSync(LOG).size > MAX_LOG) {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
242
|
+
const buf = readFileSync(LOG);
|
|
243
|
+
const keep = buf.subarray(Math.max(0, buf.length - MAX_LOG));
|
|
244
|
+
const cut = keep.indexOf(10);
|
|
245
|
+
writeFileSync(LOG, cut >= 0 ? keep.subarray(cut + 1) : keep);
|
|
234
246
|
}
|
|
235
247
|
} catch {
|
|
236
248
|
/* 静默 */
|
|
@@ -241,7 +253,12 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
241
253
|
let st = newState(loadCfg());
|
|
242
254
|
|
|
243
255
|
pi.on("session_start", () => {
|
|
256
|
+
// 跨会话保住效果闭环:干预常发生在会话末尾,3 轮结算窗口会跨过 session 边界
|
|
257
|
+
const carryRecent = st.recent.slice(-3);
|
|
258
|
+
const carryPending = st.pending;
|
|
244
259
|
st = newState(loadCfg());
|
|
260
|
+
st.recent = carryRecent;
|
|
261
|
+
st.pending = carryPending;
|
|
245
262
|
});
|
|
246
263
|
|
|
247
264
|
pi.on("turn_end", async (event, ctx) => {
|
|
@@ -258,6 +275,17 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
258
275
|
if (st.turns % SAMPLE_EVERY === 0) appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 3, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none", eff: null });
|
|
259
276
|
return;
|
|
260
277
|
}
|
|
278
|
+
// v2.3:证据门槛作用于**本轮**。窗口聚合 hits 中位 27(仅 1.4% 低于门槛)→ 门槛形同虚设;
|
|
279
|
+
// 而 42.7% 的轮次单轮命中 <3(多为长而散),正是旧口径下 react 偏高的来源(回放 11.3% → 3.4%)。
|
|
280
|
+
const turnHits = fp.pos + fp.neg + fp.neu;
|
|
281
|
+
if (turnHits < st.cfg.score.minHits) {
|
|
282
|
+
st.bandHist.none++; // 会话内计数:v3 采样下 none 轮通常不写日志,只有这里能观测 v2.3 生效
|
|
283
|
+
st.window.push(fp);
|
|
284
|
+
if (st.window.length > st.cfg.windowSize) st.window.shift();
|
|
285
|
+
st.turns++;
|
|
286
|
+
if (st.turns % SAMPLE_EVERY === 0) appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 3, len: thinking.length, pos: fp.pos, neg: fp.neg, neu: fp.neu, ratio: null, hits: turnHits, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none", eff: null });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
261
289
|
st.window.push(fp);
|
|
262
290
|
if (st.window.length > st.cfg.windowSize) st.window.shift();
|
|
263
291
|
const agg = st.window.reduce((a, b) => ({ pos: a.pos + b.pos, neg: a.neg + b.neg, neu: a.neu + b.neu }), { pos: 0, neg: 0, neu: 0 });
|
|
@@ -266,11 +294,14 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
266
294
|
lastHits = hits;
|
|
267
295
|
lastNegRate = negRate;
|
|
268
296
|
const ratio = ratioOf(agg.pos, agg.neg, agg.neu, st.cfg.weights);
|
|
269
|
-
const band = classify(
|
|
297
|
+
const band = classify(agg.pos, agg.neg, agg.neu, st.cfg.score);
|
|
270
298
|
const prevPhase = st.phase;
|
|
271
|
-
const
|
|
299
|
+
const r = (agg.pos + 0.4 * agg.neu + st.cfg.score.alpha) / (agg.neg + 0.5 + st.cfg.score.alpha);
|
|
300
|
+
const d0 = decideNext(st.phase, band, st.l2Tried, st.cfg.intervention.maxL2);
|
|
301
|
+
// 升级前先看上次干预有没有用:无效则本次不升 L2(实测 L2 无收益)
|
|
302
|
+
const d = { ...d0, level: downgradeIfIneffective(d0.level, lastEffValue) };
|
|
272
303
|
st.turns++;
|
|
273
|
-
st.lastRatio =
|
|
304
|
+
st.lastRatio = r;
|
|
274
305
|
st.lastBand = band;
|
|
275
306
|
if (band !== "none") st.bandHist[band]++; // none = 证据不足,不计波段(否则 NaN)
|
|
276
307
|
let acted = "none";
|
|
@@ -281,22 +312,22 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
281
312
|
st.lastL1 = now;
|
|
282
313
|
st.l1Count++;
|
|
283
314
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
284
|
-
{ customType: "think-anchor", content: L1_TEXT +
|
|
315
|
+
{ customType: "think-anchor", content: L1_TEXT + `(比率 ${r.toFixed(2)} · 命中 ${hits};react<0.5)`, display: false },
|
|
285
316
|
{ deliverAs: "nextTurn" },
|
|
286
317
|
);
|
|
287
318
|
acted = "L1";
|
|
288
|
-
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
|
|
319
|
+
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)), ts: Date.now(), seen: 0 };
|
|
289
320
|
st.phase = d.phase;
|
|
290
321
|
} else if (d.level === "L2" && !st.autoMuted && now - st.lastL2 > st.cfg.intervention.l2CooldownMin * 60_000) {
|
|
291
322
|
st.lastL2 = now;
|
|
292
323
|
st.l2Tried++;
|
|
293
324
|
st.l2Count++;
|
|
294
325
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
295
|
-
{ customType: "think-anchor", content: L2_TEXT +
|
|
326
|
+
{ customType: "think-anchor", content: L2_TEXT + `(比率 ${r.toFixed(2)} · 命中 ${hits};L2 ${st.l2Tried}/${st.cfg.intervention.maxL2})`, display: false },
|
|
296
327
|
{ deliverAs: "nextTurn" },
|
|
297
328
|
);
|
|
298
329
|
acted = "L2";
|
|
299
|
-
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
|
|
330
|
+
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)), ts: Date.now(), seen: 0 };
|
|
300
331
|
st.phase = d.phase;
|
|
301
332
|
} else {
|
|
302
333
|
// 被冷却/上限挡下:记 blocked,避免与「无需动作」混淆(实测 583 次 want=L2 只有 55 次真发)
|
|
@@ -307,11 +338,15 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
307
338
|
// v3 效果闭环:干预后第 3 轮结算(pre = 干预前最近 3 轮 ratio 均值)
|
|
308
339
|
st.recent.push(ratio);
|
|
309
340
|
if (st.recent.length > 6) st.recent.shift();
|
|
341
|
+
if (st.pending) st.pending.seen++; // 用独立计数而非 turns(新会话 turns 归零,差值会为负)
|
|
310
342
|
let eff: number | null = null;
|
|
311
|
-
|
|
343
|
+
// 跨会话只在 30 分钟内结算(避免隔天比率混算)
|
|
344
|
+
const pendingFresh = st.pending !== null && Date.now() - st.pending.ts < 30 * 60_000;
|
|
345
|
+
if (st.pending && pendingFresh && st.pending.seen >= 3) {
|
|
312
346
|
const tail = st.recent.slice(-3);
|
|
313
347
|
const post = tail.reduce((a, b) => a + b, 0) / Math.max(1, tail.length);
|
|
314
|
-
eff = Math.round((
|
|
348
|
+
eff = Math.round((st.pending.pre - post) * 100) / 100; // 负 = 改善(比率上升)
|
|
349
|
+
lastEffValue = eff;
|
|
315
350
|
st.effSamples++;
|
|
316
351
|
if (eff < 0) st.effImproved++;
|
|
317
352
|
st.pending = null;
|
|
@@ -333,7 +368,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
333
368
|
neu: fp.neu,
|
|
334
369
|
hits,
|
|
335
370
|
negRate: Math.round(negRate * 100) / 100,
|
|
336
|
-
ratio: Math.round(
|
|
371
|
+
ratio: Math.round(r * 100) / 100,
|
|
337
372
|
band,
|
|
338
373
|
phase: st.phase,
|
|
339
374
|
want: d.level,
|
|
@@ -381,7 +416,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
381
416
|
"think-anchor 统计:" + size + " MB / 上限 1.00 · " + rows.length + " 行",
|
|
382
417
|
" 版本 " + group("v") + " · 波段 " + group("band"),
|
|
383
418
|
" 动作 " + group("acted") + " · want " + group("want"),
|
|
384
|
-
" 效果结算 " + improved + "/" + effRows.length + (effRows.length ? "(下降 " + Math.round((100 * improved) / effRows.length) + "%)" : ""),
|
|
419
|
+
" 效果结算 " + improved + "/" + effRows.length + (effRows.length ? "(下降 " + Math.round((100 * improved) / effRows.length) + "%)" : "(样本未满:干预后需再走 3 轮)"),
|
|
385
420
|
];
|
|
386
421
|
if (ctx.hasUI) ctx.ui.notify(lines.join("\n"), "info");
|
|
387
422
|
} catch { if (ctx.hasUI) ctx.ui.notify("think-anchor: 统计失败(日志读取异常)", "warning"); }
|
|
@@ -392,9 +427,9 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
392
427
|
(st.cfg.enabled ? "ON" : "OFF") +
|
|
393
428
|
" · phase=" + st.phase +
|
|
394
429
|
" · 消息 " + st.turns +
|
|
395
|
-
" · 波段 spec/mixed/react=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react +
|
|
430
|
+
" · 波段 spec/mixed/react/none=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react + "/" + st.bandHist.none +
|
|
396
431
|
" · 干预 L1×" + st.l1Count + " L2×" + st.l2Count +
|
|
397
|
-
" · 有效率 " + st.effImproved + "/" + st.effSamples + (st.autoMuted ? "(不及抛硬币→已静默)" : "") +
|
|
432
|
+
" · 有效率 " + st.effImproved + "/" + st.effSamples + (st.autoMuted ? "(不及抛硬币→已静默)" : st.effSamples === 0 ? "(样本未满:干预后 3 轮结算)" : st.effSamples < AUTO_MIN ? "(样本 " + st.effSamples + "/" + AUTO_MIN + ")" : "") +
|
|
398
433
|
" · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")" +
|
|
399
434
|
" · 采样跳过 " + st.skipped;
|
|
400
435
|
if (ctx.hasUI) ctx.ui.notify(line, "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-think-anchor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Monitors thinking focus drift and nudges the model back on track.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,5 +22,6 @@
|
|
|
22
22
|
],
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@earendil-works/pi-coding-agent": "*"
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"author": "15066212pp"
|
|
26
27
|
}
|