dsh-compaction-tower 0.1.43 → 0.1.45
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 +8 -2
- package/docs/design/compaction.md +2 -0
- package/docs/design/replenishment.md +281 -0
- package/lib/client.js +5 -2
- package/lib/engine.js +26 -17
- package/lib/select.js +87 -20
- package/lib/tower.js +1530 -1529
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ DSH 的分块自动压缩工具:**无需主动压缩** —— 旧历史提前
|
|
|
8
8
|
|
|
9
9
|
## 它做什么
|
|
10
10
|
|
|
11
|
-
- **压缩由它自己决定,不用模型操心**:观察者在后台把早期历史蒸成「块」写进日志 ——
|
|
11
|
+
- **压缩由它自己决定,不用模型操心**:观察者在后台把早期历史蒸成「块」写进日志 —— 只进日志不进上下文,模型看不见。每次请求前和每个回合结束前各判定一次,**按「缺口」决定蒸几块**:目标 = 离目标线的距离 + 一个单位 + 误差垫,蒸到缺口归零就停。低层优先,低层没材料才由上层补位。推导见 [replenishment.md](docs/design/replenishment.md)。
|
|
12
12
|
- **越线那一刻不等模型**:块是提前蒸好的,提交就是一次查表,再交给宿主的压缩引擎换进表层 —— 不卡顿。
|
|
13
13
|
- **压过的内容还能取回来**:`decompress` 看回被压缩的历史,`search_context` 在含已压缩内容的全部历史里搜。
|
|
14
14
|
|
|
@@ -131,7 +131,13 @@ DSH 的分块自动压缩工具:**无需主动压缩** —— 旧历史提前
|
|
|
131
131
|
|
|
132
132
|
块还能继续合并成更高的层(T2、T3…),层数不设上限;最近 N 步受保护区保护,不会被换走。
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
设计文档:
|
|
135
|
+
|
|
136
|
+
| 文档 | 讲什么 |
|
|
137
|
+
|---|---|
|
|
138
|
+
| [compaction.md](docs/design/compaction.md) | 压缩逻辑的推导:选段、工具配对平衡、两种 token 口径、提示词 |
|
|
139
|
+
| [replenishment.md](docs/design/replenishment.md) | 观察者按缺口补货:目标库存 `target`、逐层抵扣的 `need`、回收率 `r`、紧急态 |
|
|
140
|
+
| [DSH-Tower-插件设计规格.md](docs/design/DSH-Tower-插件设计规格.md) | 完整设计规格与真机验证记录(历史设计记录) |
|
|
135
141
|
|
|
136
142
|
## 界面
|
|
137
143
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# 观察者按缺口补货
|
|
2
|
+
|
|
3
|
+
> 只改**观察者的派发策略**(蒸哪层、蒸几块)。不改账本格式、不改提交循环、不改组装与读回。
|
|
4
|
+
> 目标:把「每层固定备 K 份」换成「离触发线还差多少就蒸多少」。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. 一句话
|
|
9
|
+
|
|
10
|
+
观察者不再问「材料够不够一段」,而是问「还差多少库存」;块数是被切段数出来的,不是预先配的。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. 现状与问题
|
|
15
|
+
|
|
16
|
+
现在的算法是 `reserveUnitsOf(usable)`:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
U = distillMinRunRatio × 可用预算 // 一个单位
|
|
20
|
+
D = (triggerRatio − targetRatio) × 可用预算 // 一次越线要回收的量
|
|
21
|
+
K = ceil(D / U) + 1 // 每层备 K 份
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
然后 `planObservation` 逐层算 `missing = K×U − inventory[tier]`,按缺口发名额。
|
|
25
|
+
|
|
26
|
+
四个问题:
|
|
27
|
+
|
|
28
|
+
| # | 问题 | 后果 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 1 | **每层各备 K 份** | T1 备 7 块、T2 再备 7 块,而越线只需要 `D`。总量约 2 倍冗余,全是 LLM 调用 |
|
|
31
|
+
| 2 | **与内容无关** | 会话才 10% 就开始备货,可能整场都用不上 |
|
|
32
|
+
| 3 | **忽略回收率** | 一块 T1 实际回收 ≈ 0.8U、T2 ≈ 0.6U,算法却按 1U 记账 |
|
|
33
|
+
| 4 | **蒸馏不避开保护区** | 休眠块的 covers 可能整段落在保护区里,被算进库存但提交时提不出来(`headRun` 不含保护区) |
|
|
34
|
+
|
|
35
|
+
第 4 条最隐蔽:库存显示「够了」,越线时却一块都提交不了。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. 符号
|
|
40
|
+
|
|
41
|
+
| 符号 | 含义 | 来源 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `B` | 可用预算 = 窗口 − 固定开销(system + 工具 schema) | `budgetOf` / `overheadOf` |
|
|
44
|
+
| `C` | 内容用量(不含固定开销,已乘事实修正系数) | `planPressure` 里的 `content` |
|
|
45
|
+
| `T` | 触发线 = `triggerRatio × B` | 派生 |
|
|
46
|
+
| `G` | 目标线 = `targetRatio × B` | 派生 |
|
|
47
|
+
| `D` | 一次越线要回收 = `T − G` | **派生,不做配置项** |
|
|
48
|
+
| `U` | 一个单位 = `distillMinRunRatio × B` | 派生 |
|
|
49
|
+
| `r` | 净回收率:一块蒸完能省下覆盖量的百分之几 | 见 §6 |
|
|
50
|
+
| `inv[t]` | 第 t 层的库存净回收 token | `inventoryOf` |
|
|
51
|
+
| `target` | 本轮要备的库存 | §3 |
|
|
52
|
+
| `need` | 还差多少 = 逐层抵扣后的缺口 | §5 |
|
|
53
|
+
|
|
54
|
+
`D` 必须由两条线相减得出。单独配一个 `D`,线和线之间迟早打架。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. 备多少:target
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
target = 0 若 C < G
|
|
62
|
+
= min(D, C − G + U) + U × r(0) 若 C ≥ G
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `C − G`:离目标线多远,最朴素的需求;
|
|
66
|
+
- `+ U`:提前一个段动手,等真缺了再蒸来不及;
|
|
67
|
+
- `+ U × r(0)`:误差垫——`r` 可能估高,观察者跑的时候内容还在涨;
|
|
68
|
+
- `min(D, …)` 只封顶主项,垫加在外面,所以实备上限是 `D + U·r(0)`。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. 手里有多少:库存口径
|
|
73
|
+
|
|
74
|
+
只算一种块:**蒸好了、还没提交、且材料还在表层**。
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// select.js —— 与 planCommits 共用同一个函数,不允许各写一份
|
|
78
|
+
export function usable(block, { surfaceSeqs }) {
|
|
79
|
+
return block.covers.length > 0
|
|
80
|
+
&& block.covers.every((seq) => surfaceSeqs.has(seq))
|
|
81
|
+
&& block.summaryTokens > 0
|
|
82
|
+
&& block.summaryTokens < block.coveredTokens;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// tower.js —— 按层累计净回收
|
|
86
|
+
inventoryOf(session, blocks) // Map<tier, covered − summary>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`usable` 里那条「covers 仍在表层」是 DSH 特有的:宿主压缩会把一段历史换成没有块号的 checkpoint,原料于是离开表层。少了这条,库存会假满。
|
|
90
|
+
|
|
91
|
+
「与提交同源」是硬约束:两处各写一份判据,迟早分叉——库存算进一个提交不掉的块,观察者停手,越线时才发现无货。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 5. 还差多少:need 递推
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
need = target − inv[1] // 第一层只扣第一层的库存
|
|
99
|
+
for level = 0, 1, 2, …:
|
|
100
|
+
if need ≤ 0: break
|
|
101
|
+
if level > 0:
|
|
102
|
+
need -= inv[level + 1] // 轮到自己这层,才扣自己这层的库存
|
|
103
|
+
if need ≤ 0: break
|
|
104
|
+
本层材料 = 切段(level)
|
|
105
|
+
for chunk in 本层材料:
|
|
106
|
+
if need ≤ 0: break
|
|
107
|
+
派发(chunk)
|
|
108
|
+
need -= chunk.tokens × r(level)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
三个要点:
|
|
112
|
+
|
|
113
|
+
1. **逐层抵扣,不要一次扣总账。**
|
|
114
|
+
若写成 `need = target − Σ inv`,T2 的存货会顶掉 T1 的名额:T1 明明又攒出材料,却因为「总库存够了」而不蒸;越线时提交虽然仍是 T1 先,但能动用的只有既有那几块,缺口只能靠 T2 补——备料顺序和提交顺序就反了。
|
|
115
|
+
2. **本层没材料就 `continue` 到上一层,不是 `break`。**
|
|
116
|
+
T1 原文枯竭不代表 T2 没材料:T2 的原料是已提交的 T1 块,与原文余量无关。
|
|
117
|
+
3. **`need` 是 token,不是块数。**
|
|
118
|
+
T1 一块约 1% 预算、T2 约 2%,拿块数走账必然偏。
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 6. 回收率 r
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
r(level) = 缺省 0.8(level 0)/ 0.6(level ≥ 1)
|
|
126
|
+
若同层最近 10 块里有 ≥3 块可算,取中位数(下界 0.5)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
缺省值来自摘要提示词的长度约束(原文材料 ≤ 20%,摘要材料 ≤ 40%)。这是整条链上**唯一需要预报的量**,其余(B/C/T/G/U/covered/库存)都是账上能查的数。
|
|
130
|
+
|
|
131
|
+
中位数抗单块写歪;只看最近 10 块,换模型后旧样本自动淘汰;下界 0.5 防极端短块把 `r` 拉到接近 1。
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 7. 材料与切段
|
|
136
|
+
|
|
137
|
+
| 层 | 材料 | 切法 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `level = 0` | 保护区之外的连续原文 | 每个连续区独立切,洞两侧绝不并段 |
|
|
140
|
+
| `level ≥ 1` | 已提交、未被蒸馏的该层 checkpoint | 按覆盖起点切,id 跳号处断开 |
|
|
141
|
+
|
|
142
|
+
**改动一:T1 材料必须排除保护区。**
|
|
143
|
+
现在是 `protectedSeqs: new Set()`(刻意不限制),理由曾是「提前备着等材料滑出去」。但这样库存与可提交性就对不上(见 §1 第 4 条)。改成与提交同一口径后,新蒸出的块 covers 天然在保护区外,而保护区只会往前移,所以它永远可提交。
|
|
144
|
+
|
|
145
|
+
**改动二:越线时允许切尾巴。**
|
|
146
|
+
`C ≥ T` 时给 `selectRawRun` / `selectMergeRun` 加 `flushTail`,把不足一个单位的尾巴也切出来。没有这条会死锁:内容越线、材料不足一段、观察者「没有可蒸材料」、越线判定又无货可提。
|
|
147
|
+
|
|
148
|
+
注意是 `flushTail`(只放行尾巴),不是把段长降到 1(那会一次切出几十个小块)。
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 8. 时机与并发
|
|
153
|
+
|
|
154
|
+
| 时机 | 行为 | 变化 |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| 每个 step 前 | `pump(wait:false)`,5 秒节流 | 不变 |
|
|
157
|
+
| 回合结束 | `pump(wait:true)` | 不变 |
|
|
158
|
+
|
|
159
|
+
并发保持 `observerConcurrency`:每轮派发总数 ≤ 槽位,`need` 没填完的留到下一轮(节流后自然重来)。Sisyph 那边的单飞等价于槽位无限,我们这边要显式收口。
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 9. 数值走查
|
|
164
|
+
|
|
165
|
+
归一化 `B = 100`,`T = 60`,`G = 40`,`D = 20`,`U = 5`,`r(0) = 0.8`。
|
|
166
|
+
|
|
167
|
+
| C | target = min(20, C−40+5) + 4 | 库存 | need | 派发 |
|
|
168
|
+
|---|---|---|---|---|
|
|
169
|
+
| 44 | min(20, 9) + 4 = 13 | 0 | 13 | T1 切 3 段(15) |
|
|
170
|
+
| 48 | min(20, 13) + 4 = 17 | 15 | 2 | T1 切 1 段(5) |
|
|
171
|
+
| 56 | min(20, 21) + 4 = 24 | 20 | 4 | T1 切 1 段(5) |
|
|
172
|
+
| 60 | 24 | 25 | 0 | 不蒸 |
|
|
173
|
+
|
|
174
|
+
到越线时库存 25 ≥ D = 20,提交 5 块压回 40,第 6 块是垫。
|
|
175
|
+
|
|
176
|
+
**逐层抵扣的例子**:`target = 24`,`inv[1] = 8`(2 个 T1),`inv[2] = 12`(4 个 T2)。
|
|
177
|
+
|
|
178
|
+
- `need = 24 − 8 = 16`
|
|
179
|
+
- level 0 有材料 → 继续蒸 T1,直到 `need` 降下来(**不会因为 T2 有 12 就停手**)
|
|
180
|
+
- level 0 没材料 → `need -= inv[2] = 12` → `need = 4` → T2 再切一段
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 10. 边界情况
|
|
185
|
+
|
|
186
|
+
| # | 情况 | 对策 |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 1 | 越线且材料不足一段 | `flushTail`(§7 改动二) |
|
|
189
|
+
| 2 | 保护区自己超过触发线 | `headRun` 返回 null → `no-range`;面板单列「保护区占用」并告警 |
|
|
190
|
+
| 3 | 观察者连续失败 | 断路器(本设计不做,另开) |
|
|
191
|
+
| 4 | 摘要没比原料短 | `usable` 里含 `summary < covered`(已有) |
|
|
192
|
+
| 5 | 单条消息比 U 还大 | 单段上限 ≤ 观察者窗口一半,超了在消息边界拆 |
|
|
193
|
+
| 6 | 计划算出后块被提交 | `distillOne` 保留 `still` 检查 |
|
|
194
|
+
| 7 | 配置矛盾(`target ≥ trigger`、`U ≥ D`) | 加载时校验并给人话 |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 11. 接口改动
|
|
199
|
+
|
|
200
|
+
### `lib/select.js`
|
|
201
|
+
|
|
202
|
+
| 函数 | 性质 | 说明 |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `usable(block, {surfaceSeqs})` | 不动 | 提交与库存共用 |
|
|
205
|
+
| `planObservation({targetTokens, inventory, materials, recovery, concurrency})` | **重写** | 入参全部换成 token 口径;返回 `[{level, count}]` |
|
|
206
|
+
| `selectRawRun(..., {flushTail})` | 加参数 | 越线时放行尾巴 |
|
|
207
|
+
| `selectMergeRun(..., {flushTail})` | 加参数 | 同上 |
|
|
208
|
+
|
|
209
|
+
### `lib/tower.js`
|
|
210
|
+
|
|
211
|
+
| 成员 | 变化 |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `reserveUnitsOf(usable)` | **删除** |
|
|
214
|
+
| `planTarget({usable, content, targetRatio, triggerRatio})` | **新增**(纯函数,可放 select.js) |
|
|
215
|
+
| `recoveryRatio(session, level)` | **新增** |
|
|
216
|
+
| `inventoryOf(session, blocks)` | 不变(已按层累计净回收) |
|
|
217
|
+
| `availableUnits(...)` | 改:返回 `Map<level, {units, unitTokens}>`;材料排除保护区 |
|
|
218
|
+
| `distillOne(agent, signal, turn, tier, options)` | 加 `emergency`,传给 `selectRawRun` / `selectMergeRun` |
|
|
219
|
+
| `scheduleObservations(...)` | 串起来:target → inventory → materials → plan |
|
|
220
|
+
| 面板 `lastObservation` | 报 `target / need / inv₁`,否则「为什么还在蒸 T1」看不出来 |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 12. 测试清单
|
|
225
|
+
|
|
226
|
+
全部打在 `test/smoke.mjs`(纯函数),`test/patch.mjs` 补一条链路。
|
|
227
|
+
|
|
228
|
+
| 用例 | 断言 |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `target` 在 `C < G` 时为 0 | 没到目标线不备货 |
|
|
231
|
+
| `target` 在 `C ≥ T` 时为 `D + U·r` | 垫不被 `min` 吃掉 |
|
|
232
|
+
| 改 `triggerRatio` 后 target 跟着变 | 配置驱动 |
|
|
233
|
+
| 已提交块不计入库存 | 已进上下文 |
|
|
234
|
+
| 摘要 ≥ 覆盖量不计入库存 | 提交不掉 |
|
|
235
|
+
| 库存判据与提交判据认同一批块 | 同源 |
|
|
236
|
+
| `inv` 够时计划为空 | 不派发 |
|
|
237
|
+
| T1 材料充足只派 level 0 | 低层优先 |
|
|
238
|
+
| T1 不够时缺口漏到 level 1 | 递推 |
|
|
239
|
+
| level 0 无材料、level 1 有 → 仍派 level 1 | continue 不是 break |
|
|
240
|
+
| **T2 补满但 T1 未满 → 仍派 T1** | 逐层抵扣(一次性扣总账会返回空计划) |
|
|
241
|
+
| 越线且材料不足一段 → 仍切 | flushTail |
|
|
242
|
+
| 未越线且材料不足一段 → 不切 | 不浪费 |
|
|
243
|
+
| 3 个 r=0.6 的块 → 校准返回 0.6 | 实测校准 |
|
|
244
|
+
| 实测 r < 0.5 被下界接住 | 防极端 |
|
|
245
|
+
| 并发槽位截断 | 计划总数 ≤ concurrency |
|
|
246
|
+
|
|
247
|
+
**连带重写**:`planObservation` 现有 5 条用例、`reserveUnitsOf` 相关断言。
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 13. 实施顺序
|
|
252
|
+
|
|
253
|
+
| 步 | 内容 | 验证 |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| 1 | `select.js` 里加 `planTarget` / `recoveryRatio` / 新 `planObservation`,全部纯函数 | `smoke` 新用例全绿 |
|
|
256
|
+
| 2 | `tower.js` 接线:`availableUnits` 换 token 口径、材料排除保护区、`reserveUnitsOf` 下架 | `smoke` + `patch` 全绿 |
|
|
257
|
+
| 3 | `flushTail` 紧急态 | 新增两条用例 |
|
|
258
|
+
| 4 | 面板文案(`target / need / inv₁`)与 README | `client` / `state` 测试 |
|
|
259
|
+
|
|
260
|
+
每步单独 commit,测试必须全绿才打包。
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 14. 明确不做
|
|
265
|
+
|
|
266
|
+
- **不做「每层都备满 N 份」**:低层优先意味着上层库存大部分时间不会被消耗。
|
|
267
|
+
- **不用块数当控制量**:跨层块大小差一个量级。
|
|
268
|
+
- **不用「总缺口」当闸门**:见 §5 第 1 条。
|
|
269
|
+
- **不并发观察**:`observerConcurrency` 已经够,并发要额外解决在途记账。
|
|
270
|
+
- **不改提交逻辑**:`planCommits` 的顺序、算术、护栏都不动。
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 附:与 Sisyph 方案的差异
|
|
275
|
+
|
|
276
|
+
| 项 | Sisyph | 这里 | 原因 |
|
|
277
|
+
|---|---|---|---|
|
|
278
|
+
| 库存判据 | `0 < tokens < covered` | 再加 `covers ⊆ 表层` | 宿主压缩会让原料离开表层 |
|
|
279
|
+
| 材料范围 | 近区之外 | 保护区之外(**本次改**) | 否则库存 ≠ 可提交 |
|
|
280
|
+
| 紧急态 | `min_len = 1` | `flushTail`(只放行尾巴) | 我们批量派发,段长降到 1 会一次切出几十块 |
|
|
281
|
+
| 并发 | 单飞(在途无穷) | 显式槽位 | 我们有 `observerConcurrency` |
|
package/lib/client.js
CHANGED
|
@@ -232,12 +232,15 @@ window.__ModuleLoader__.load({
|
|
|
232
232
|
case 'no-window':
|
|
233
233
|
return '上次观察:量不到上下文窗口';
|
|
234
234
|
case 'launched': {
|
|
235
|
-
const jobs = (observation.plan ?? []).map((job) => `T${job.
|
|
235
|
+
const jobs = (observation.plan ?? []).map((job) => `T${(job.level ?? 0) + 1}×${job.count}`).join(' ');
|
|
236
236
|
const planned = (observation.plan ?? []).reduce((sum, job) => sum + job.count, 0);
|
|
237
237
|
const material = (observation.available ?? []).reduce((sum, item) => sum + (item[1] ?? 0), 0);
|
|
238
238
|
// 发起数少于计划时,多半是材料只够这么多份 —— 直接说出来,省得以为并发没生效。
|
|
239
239
|
const short = material > 0 && material <= planned ? ` · 材料只够 ${material} 份` : '';
|
|
240
|
-
|
|
240
|
+
const budget = typeof observation.target === 'number'
|
|
241
|
+
? `,目标 ${fmt(observation.target)} 还差 ${fmt(observation.need ?? 0)}`
|
|
242
|
+
: '';
|
|
243
|
+
return `上次观察:发起 ${jobs || '一批'}(单位 ${observation.unit ?? '?'} tok${budget})${short}`;
|
|
241
244
|
}
|
|
242
245
|
default:
|
|
243
246
|
return `上次观察:${observation.kind ?? '未知'}`;
|
package/lib/engine.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `compactIfNeeded(agent, trigger, signal)` —— 什么时候压、压哪一段;
|
|
9
9
|
* - `summarize(input, agent, signal)` —— 摘要从哪来。
|
|
10
|
+
* - `compactNow(agent, signal, commandId)` —— `/compact` 命令,绕过前者。
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* 三个都在调用点重新读 `this.xxx`(见 regionDependencies 的
|
|
12
13
|
* `summarize: (input, owner, abort) => this.summarize(...)`),
|
|
13
14
|
* 所以换原型方法等于换掉全部隔离实例的行为,且不需要动 preset。
|
|
14
15
|
*/
|
|
@@ -18,18 +19,20 @@ import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic';
|
|
|
18
19
|
const PATCHED = Symbol.for('dsh-compaction-tower.patched');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* 补丁当前指向的实例,**存在原型上**。
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* 不能存在模块级变量里:重新安装插件时 cordis 会重建插件模块,原型上的补丁还在
|
|
25
|
+
* (`PATCHED` 标记挡住重复安装),但方法闭包捕获的是**旧模块**的变量 —— 新模块
|
|
26
|
+
* 换自己的变量,旧闭包读不到,于是补丁永远捏着旧实例。表现就是面板上
|
|
27
|
+
* 「内容越线了却没有任何判定」「观察者说引擎实例还没露面」。
|
|
28
|
+
*
|
|
29
|
+
* 原型的对象是共享的,把实例挂在它上面,任何一次 install 都能被旧闭包读到。
|
|
27
30
|
*/
|
|
28
|
-
|
|
31
|
+
const TOWER = Symbol.for('dsh-compaction-tower.tower');
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* 安装补丁。
|
|
32
|
-
* @param {object} tower Tower 实例(提供 compactIfNeeded / serveSummary)
|
|
35
|
+
* @param {object} tower Tower 实例(提供 compactIfNeeded / serveSummary / manualCompact)
|
|
33
36
|
* @returns {'patched'|'already'|'unavailable'}
|
|
34
37
|
*/
|
|
35
38
|
export function installEnginePatches(tower) {
|
|
@@ -37,34 +40,40 @@ export function installEnginePatches(tower) {
|
|
|
37
40
|
if (proto === undefined) return 'unavailable';
|
|
38
41
|
if (typeof proto.compactIfNeeded !== 'function' || typeof proto.summarize !== 'function') return 'unavailable';
|
|
39
42
|
if (proto[PATCHED] === true) {
|
|
40
|
-
//
|
|
41
|
-
|
|
43
|
+
// 已经打过补丁:只把原型上的引用换成新实例,方法体不动。
|
|
44
|
+
proto[TOWER] = tower;
|
|
42
45
|
return 'already';
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const originalSummarize = proto.summarize;
|
|
46
49
|
|
|
47
50
|
proto.compactIfNeeded = function patchedCompactIfNeeded(agent, trigger, signal) {
|
|
51
|
+
const bound = proto[TOWER];
|
|
52
|
+
if (bound === undefined || bound === null) return null;
|
|
48
53
|
// 顺手记下「这个 agent 用的引擎实例」:某些组合(web)从 agent.ctx 取不到它。
|
|
49
|
-
|
|
54
|
+
bound.rememberEngine?.(agent, this);
|
|
50
55
|
// 不传原方法:塔不交回宿主,溢出也一样(见 Tower.compactIfNeeded)。
|
|
51
|
-
return
|
|
56
|
+
return bound.compactIfNeeded(this, agent, trigger, signal);
|
|
52
57
|
};
|
|
53
58
|
proto.summarize = function patchedSummarize(input, agent, signal) {
|
|
54
|
-
|
|
55
|
-
return
|
|
59
|
+
const bound = proto[TOWER];
|
|
60
|
+
if (bound === undefined || bound === null) return originalSummarize.call(this, input, agent, signal);
|
|
61
|
+
bound.rememberEngine?.(agent, this);
|
|
62
|
+
return bound.serveSummary(this, originalSummarize, input, agent, signal);
|
|
56
63
|
};
|
|
57
64
|
// `/compact` 走的是 compactNow,它直接调 compactSurfaceRegion、绕过 compactIfNeeded。
|
|
58
65
|
// 不接管的话,用户敲一次命令就会压出一个没有块号的 checkpoint,把账本打出洞。
|
|
59
66
|
if (typeof proto.compactNow === 'function') {
|
|
60
67
|
proto.compactNow = function patchedCompactNow(agent, signal, commandId) {
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const bound = proto[TOWER];
|
|
69
|
+
if (bound === undefined || bound === null) return Promise.resolve(null);
|
|
70
|
+
bound.rememberEngine?.(agent, this);
|
|
71
|
+
return bound.manualCompact(this, agent, signal, commandId);
|
|
63
72
|
};
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
Object.defineProperty(proto, PATCHED, { value: true, configurable: true });
|
|
67
|
-
|
|
76
|
+
proto[TOWER] = tower;
|
|
68
77
|
return 'patched';
|
|
69
78
|
}
|
|
70
79
|
|
package/lib/select.js
CHANGED
|
@@ -98,31 +98,97 @@ function overlaps(a, b) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
101
|
+
* 本轮该备多少库存(token)。
|
|
102
102
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* 三段构成:
|
|
104
|
+
* - `C − G`:离目标线多远,最朴素的需求;
|
|
105
|
+
* - `+ U`:提前一个段动手,等真缺了再蒸来不及;
|
|
106
|
+
* - `+ U·r`:误差垫——回收率可能估高、观察者跑的时候内容还在涨。
|
|
105
107
|
*
|
|
108
|
+
* `min(D, …)` 只封顶主项,垫加在外面,所以实备上限是 `D + U·r`。
|
|
106
109
|
* @param {object} params
|
|
107
|
-
* @param {
|
|
108
|
-
* @param {
|
|
109
|
-
* @param {number} params.
|
|
110
|
-
* @param {number} params.
|
|
110
|
+
* @param {number} params.usable 可用预算(窗口 − 固定开销)
|
|
111
|
+
* @param {number} params.content 当前内容用量
|
|
112
|
+
* @param {number} params.triggerRatio 触发线比例
|
|
113
|
+
* @param {number} params.targetRatio 目标线比例
|
|
114
|
+
* @param {number} params.unitRatio 一个单位的比例
|
|
115
|
+
* @param {number} params.recovery 回收率 r(level 0)
|
|
116
|
+
* @returns {number} 期望备货量(token)
|
|
117
|
+
*/
|
|
118
|
+
export function planTarget({ usable, content, triggerRatio, targetRatio, unitRatio, recovery = 0.8 }) {
|
|
119
|
+
const unit = Math.max(1, unitRatio * usable);
|
|
120
|
+
const targetLine = targetRatio * usable;
|
|
121
|
+
if (content < targetLine) return 0;
|
|
122
|
+
const gap = Math.max(0, (triggerRatio - targetRatio) * usable);
|
|
123
|
+
return Math.round(Math.min(gap, content - targetLine + unit) + unit * recovery);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 回收率:一块蒸完能省下覆盖量的百分之几。
|
|
128
|
+
*
|
|
129
|
+
* 缺省值来自摘要提示词的长度约束(原文 ≤ 20%、摘要 ≤ 40%)。有块之后用同层最近
|
|
130
|
+
* 10 块的中位数校准:抗单块写歪,换模型后旧样本自动淘汰,下界 0.5 防极端短块。
|
|
131
|
+
* @param {Array<object>} blocks 账本里的块(按写入顺序)
|
|
132
|
+
* @param {number} tier 目标层
|
|
133
|
+
* @param {number} fallback 样本不足时的缺省值
|
|
134
|
+
* @returns {number}
|
|
135
|
+
*/
|
|
136
|
+
export function recoveryRatio(blocks, tier, fallback) {
|
|
137
|
+
const samples = [];
|
|
138
|
+
for (let index = blocks.length - 1; index >= 0 && samples.length < 10; index -= 1) {
|
|
139
|
+
const block = blocks[index];
|
|
140
|
+
if (Number(block.tier) !== tier) continue;
|
|
141
|
+
const covered = Number(block.coveredTokens ?? 0);
|
|
142
|
+
const summary = Number(block.summaryTokens ?? 0);
|
|
143
|
+
if (!(covered > 0) || !(summary > 0) || summary >= covered) continue;
|
|
144
|
+
samples.push(1 - summary / covered);
|
|
145
|
+
}
|
|
146
|
+
if (samples.length < 3) return fallback;
|
|
147
|
+
samples.sort((left, right) => left - right);
|
|
148
|
+
const middle = Math.floor(samples.length / 2);
|
|
149
|
+
const median = samples.length % 2 === 1
|
|
150
|
+
? samples[middle]
|
|
151
|
+
: (samples[middle - 1] + samples[middle]) / 2;
|
|
152
|
+
return Math.max(0.5, median);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 观察配额:按缺口逐层补货。
|
|
157
|
+
*
|
|
158
|
+
* 关键在**逐层抵扣**:第一层只扣第一层的库存,轮到自己那层才扣自己那层的库存。
|
|
159
|
+
* 若一次扣总账,T2 的存货会顶掉 T1 的名额 —— T1 明明又攒出材料却不再蒸,
|
|
160
|
+
* 越线时能动用的只有既有那几块,缺口只能靠 T2 补,备料顺序和提交顺序就反了。
|
|
161
|
+
*
|
|
162
|
+
* 本层没材料是 `continue` 不是 `break`:T1 原文枯竭不代表 T2 没材料。
|
|
163
|
+
* @param {object} params
|
|
164
|
+
* @param {number} params.targetTokens 本轮要备的库存
|
|
165
|
+
* @param {Map<number, number>} params.inventory 每层库存净回收 token(tier → tokens)
|
|
166
|
+
* @param {Map<number, {units: number, unitTokens: number}>} params.materials 每层材料(level → 量)
|
|
167
|
+
* @param {(level: number) => number} params.recovery 每层的回收率
|
|
111
168
|
* @param {number} params.concurrency 一次最多发几个
|
|
112
|
-
* @returns {Array<{
|
|
169
|
+
* @returns {Array<{level: number, count: number}>}
|
|
113
170
|
*/
|
|
114
|
-
export function planObservation({
|
|
115
|
-
|
|
171
|
+
export function planObservation({ targetTokens, inventory, materials, recovery, concurrency }) {
|
|
172
|
+
let need = targetTokens - (inventory.get(1) ?? 0);
|
|
116
173
|
const plan = [];
|
|
117
174
|
let slots = Math.max(0, Math.floor(concurrency));
|
|
118
|
-
for (const
|
|
119
|
-
if (slots <= 0) break;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
175
|
+
for (const level of [...materials.keys()].sort((left, right) => left - right)) {
|
|
176
|
+
if (need <= 0 || slots <= 0) break;
|
|
177
|
+
if (level > 0) {
|
|
178
|
+
need -= inventory.get(level + 1) ?? 0;
|
|
179
|
+
if (need <= 0) break;
|
|
180
|
+
}
|
|
181
|
+
const material = materials.get(level);
|
|
182
|
+
const units = Number(material?.units ?? 0);
|
|
183
|
+
const unitTokens = Number(material?.unitTokens ?? 0);
|
|
184
|
+
if (units <= 0 || unitTokens <= 0) continue;
|
|
185
|
+
const perUnit = unitTokens * recovery(level);
|
|
186
|
+
if (!(perUnit > 0)) continue;
|
|
187
|
+
const wanted = Math.ceil(need / perUnit);
|
|
188
|
+
const count = Math.min(wanted, units, slots);
|
|
124
189
|
if (count <= 0) continue;
|
|
125
|
-
plan.push({
|
|
190
|
+
plan.push({ level, count });
|
|
191
|
+
need -= count * perUnit;
|
|
126
192
|
slots -= count;
|
|
127
193
|
}
|
|
128
194
|
return plan;
|
|
@@ -144,7 +210,7 @@ export function planObservation({ available, inventory, reserveTokens, unitToken
|
|
|
144
210
|
* @param {number} [options.stopAtTokens] 攒到这个量就停
|
|
145
211
|
* @returns {number[]} 可为空
|
|
146
212
|
*/
|
|
147
|
-
export function selectRawRun(nodes, { systemSeqs, protectedSeqs, coveredSeqs, checkpointSeqs, minTokens, maxTokens, stopAtTokens }) {
|
|
213
|
+
export function selectRawRun(nodes, { systemSeqs, protectedSeqs, coveredSeqs, checkpointSeqs, minTokens, maxTokens, stopAtTokens, flushTail = false }) {
|
|
148
214
|
let start = 0;
|
|
149
215
|
while (start < nodes.length && systemSeqs.has(nodes[start].seq)) start += 1;
|
|
150
216
|
while (start < nodes.length) {
|
|
@@ -164,7 +230,7 @@ export function selectRawRun(nodes, { systemSeqs, protectedSeqs, coveredSeqs, ch
|
|
|
164
230
|
tokens += node.tokens;
|
|
165
231
|
if (stopAtTokens !== undefined && tokens >= stopAtTokens) break;
|
|
166
232
|
}
|
|
167
|
-
if (tokens < minTokens) return [];
|
|
233
|
+
if (!flushTail && tokens < minTokens) return [];
|
|
168
234
|
return seqs;
|
|
169
235
|
}
|
|
170
236
|
|
|
@@ -201,6 +267,7 @@ export function selectMergeRun(nodes, {
|
|
|
201
267
|
minCount,
|
|
202
268
|
minTokens,
|
|
203
269
|
maxTokens,
|
|
270
|
+
flushTail = false,
|
|
204
271
|
}) {
|
|
205
272
|
const covered = coveredSeqs ?? new Set();
|
|
206
273
|
let index = 0;
|
|
@@ -224,7 +291,7 @@ export function selectMergeRun(nodes, {
|
|
|
224
291
|
next += 1;
|
|
225
292
|
if (tokens >= minTokens) break;
|
|
226
293
|
}
|
|
227
|
-
if (run.length >= minCount && tokens >= minTokens) return run;
|
|
294
|
+
if (run.length >= minCount && (flushTail ? tokens > 0 : tokens >= minTokens)) return run;
|
|
228
295
|
index = next;
|
|
229
296
|
}
|
|
230
297
|
return [];
|