dsh-compact-relay 0.3.6 → 0.4.1

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,27 +1,74 @@
1
1
  /**
2
- * 模型入口(0.3.4)——「当前在可压缩区间」的动态提示。
2
+ * 模型入口(0.3.4 / 0.4 P0-1)——「当前可以记一个压缩节点」的动态提示。
3
3
  *
4
4
  * BUG-010:0.3.1 的 `mark_stage` 工具在 427 个真实会话里 0 次调用。根因不是模型
5
5
  * 不听话,而是工具描述承诺「系统会告诉你容量」但系统从没说过 —— 条件不可判断。
6
- * 现在宿主每轮把一条 runtime context 注入 system prompt,告诉模型当前容量落在
7
- * 可压缩区间;模型据此判断「刚完成一个自然节点」时该不该调 `mark_compress_point`。
6
+ * 0.3.4 起宿主每轮把一条 runtime context 注入 system prompt,告诉模型当前容量。
7
+ *
8
+ * 0.4 P0-1(Win 173 会话里 cue 注入 18 次、工具仍 0 调用):
9
+ * 1. 文案 v2 与区间 v2 在 core/compress-cue.js(4 类完成信号 + 被拒零副作用);
10
+ * 2. **注入门控**在本文件(方案 §3.2):只有全部满足才注入——
11
+ * a) 容量落在 cue 区间(10%–40%,且不越过压缩线,见 cueBandOf);
12
+ * b) 距上次注入 ≥ CUE_MIN_TURNS 轮(本轮按 text 回调调用次数计,纯内存不落盘);
13
+ * c) 一轮内最多一次(连续调用时 b 天然保证);
14
+ * 3. 实际注入一次就 `deps.diag.bumpCueInjected()`(供 P0-2 诊断包看数),
15
+ * 只记次数、不记提示原文(隐私口径与 P0-2 一致)。
8
16
  *
9
17
  * 约束:
10
18
  * - `systemPrompt.context` 的 text 回调必须是**同步**函数 → 只读
11
19
  * `dsh/state-hub.js` 维护的内存缓存,不 await、不 measure;
12
20
  * - 拿不到会话/缓存/阈值就返回空串(宿主不会展示空 context);
13
- * - 全程 try/catch:提示是锦上添花,任何异常都不能影响宿主组装 prompt。
21
+ * - 全程 try/catch:提示是锦上添花,任何异常都不能影响宿主组装 prompt;
22
+ * - 本文件(adapters)不 import lib/dsh/:计数走 deps.diag 既有注入。
14
23
  */
15
24
  import {
16
25
  COMPRESS_CUE_NAME,
17
26
  COMPRESS_CUE_ORDER,
18
27
  compressCueText,
28
+ cueBandOf,
19
29
  } from '../core/compress-cue.js';
20
- import { compressionMinRatio } from '../core/policy.js';
30
+ import { CUE_MIN_TURNS, compressionMinRatio } from '../core/policy.js';
21
31
 
22
32
  /** 供测试/文档引用的 context 名称。 */
23
33
  export { COMPRESS_CUE_NAME };
24
34
 
35
+ /**
36
+ * 每会话的注入节奏(进程内,不新增持久状态):
37
+ * calls text 回调被调用次数(≈轮数)
38
+ * lastInjectedCall 上次真正注入发生在第几次调用(null = 还没注入过)
39
+ *
40
+ * 进程内 Map 只服务宿主生命周期;上限 200 个会话,避免长期运行无限增长。
41
+ */
42
+ const MAX_TRACKED_SESSIONS = 200;
43
+ const cueTurns = new Map();
44
+
45
+ /**
46
+ * 取(并前移)某会话的注入节奏计数。
47
+ * @param {string} sessionId
48
+ * @returns {{calls:number, lastInjectedCall:number|null}}
49
+ */
50
+ function cueTurnOf(sessionId) {
51
+ let turn = cueTurns.get(sessionId);
52
+ if (turn === undefined) {
53
+ turn = { calls: 0, lastInjectedCall: null };
54
+ cueTurns.set(sessionId, turn);
55
+ if (cueTurns.size > MAX_TRACKED_SESSIONS) {
56
+ const oldest = cueTurns.keys().next().value;
57
+ cueTurns.delete(oldest);
58
+ }
59
+ }
60
+ turn.calls += 1;
61
+ return turn;
62
+ }
63
+
64
+ /**
65
+ * 测试用:清空注入节奏(相当于宿主重启)。生产路径不需要调用。
66
+ * @returns {void}
67
+ */
68
+ export function resetCueTurns() {
69
+ cueTurns.clear();
70
+ }
71
+
25
72
  /**
26
73
  * 计算某个 agent 本轮该显示的动态提示(同步、异常安全)。
27
74
  * @param {{context?:object, deps?:object, hub?:object}} input
@@ -42,14 +89,28 @@ export function compressCueFor({ context, deps, hub } = {}) {
42
89
  if (!Number.isFinite(thresholds.compactRatio)) return '';
43
90
  if (!Number.isFinite(cached.ratio) || !Number.isFinite(cached.window) || cached.window <= 0) return '';
44
91
 
45
- // 最小线是「配置值 vs 动态下限(保留尾部 + 最小可压 token)/窗口」的较大者(纯函数)。
46
- const minRatio = compressionMinRatio(thresholds, deps?.cfg?.retention, cached.window);
47
- return compressCueText({
92
+ // 每次 text 回调(≈一轮)都计数:即使本轮不注入,也要推进轮次,限频才有意义。
93
+ const turn = cueTurnOf(sessionId);
94
+
95
+ // 门控 a:区间 = max(10%, 动态最小线) ≤ ratio < min(40%, 压缩线)。
96
+ const dynamicMinRatio = compressionMinRatio(thresholds, deps?.cfg?.retention, cached.window);
97
+ const band = cueBandOf({ compactRatio: thresholds.compactRatio, dynamicMinRatio });
98
+ if (!(cached.ratio >= band.minRatio && cached.ratio < band.maxRatio)) return '';
99
+
100
+ // 门控 b/c:距上次注入 ≥ CUE_MIN_TURNS 轮(同一轮内多次调用必然不足)。
101
+ if (turn.lastInjectedCall !== null && turn.calls - turn.lastInjectedCall < CUE_MIN_TURNS) return '';
102
+
103
+ const text = compressCueText({
48
104
  ratio: cached.ratio,
49
- minRatio,
50
- compactRatio: thresholds.compactRatio,
105
+ minRatio: band.minRatio,
106
+ maxRatio: band.maxRatio,
51
107
  compressHint: cached.compressHint ?? null,
52
108
  });
109
+ if (text === '') return '';
110
+
111
+ turn.lastInjectedCall = turn.calls;
112
+ deps?.diag?.bumpCueInjected?.();
113
+ return text;
53
114
  } catch {
54
115
  return '';
55
116
  }
@@ -8,6 +8,11 @@
8
8
  * 模型无法判断条件 → 427 个真实会话 0 次调用。0.3.4 起由宿主每轮注入可压缩区间
9
9
  * 提示(lib/adapters/prompt.js),工具描述只描述「它自己会做什么」:
10
10
  * 服务端工具会返回当前容量并复核区间,太低/到线都拒绝且不改历史。
11
+ *
12
+ * 0.4 P0-1(方案 §3.3):Win 173 会话 cue 注入 18 次、工具 0 调用 → 描述 v2:
13
+ * - 明确「用户和模型都可以调用」(不只是模型的自发动作);
14
+ * - 「被拒不需要致歉」:拒绝零副作用、不改历史、不需要重试或道歉(降低调用心理成本);
15
+ * - 给出典型调用时机;把「不需要每个小步骤都调用」弱化后挪到句尾,不再抢在条件前面。
11
16
  */
12
17
  import { markCompressHint } from '../actions/compress-hint.js';
13
18
  import engine from '../dsh/engine.js';
@@ -22,10 +27,10 @@ export function registerTool(ctx, deps) {
22
27
  name: 'mark_compress_point',
23
28
  description:
24
29
  '记录一个「压缩节点」:在指示灯环上标出「这里适合把前面的旧历史压成摘要」。' +
25
- '在你刚完成一个明确的任务/阶段、并且认为之后可以安全地压缩旧历史继续往下做时调用。' +
26
- '不确定当前是否合适时也可以调用:工具会返回当前上下文容量并做判断,' +
27
- '容量太低(压缩不划算)或已经到压缩线(直接建议用户压缩即可)会被拒绝,拒绝不会改动任何历史。' +
28
- '不需要每个小步骤都调用。每次判断只记一条,压缩或交接后旧记录自动失效。',
30
+ '用户和模型都可以调用;不确定时调用是安全的:工具会自己判断容量,太早或已到压缩线会拒绝,' +
31
+ '被拒不需要致歉、不会改动历史、也不影响会话,不需要重试。' +
32
+ '典型调用时机:阶段完成(功能已实现并验证 / 文档已交付 / 问题已定位并修复)、准备交接前、长任务中途确认可回退点。' +
33
+ '调用后工具会返回当前容量与判断结果;每次判断只记一条,压缩或交接后旧记录自动失效。小步骤不用调用。',
29
34
  parameters: {},
30
35
  output: {
31
36
  schema: { type: 'string' },