dsh-compaction-tower 0.0.0-stage → 0.1.39
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/LICENSE +21 -0
- package/README.md +210 -3
- package/docs/compression.png +0 -0
- package/docs/config.png +0 -0
- package/docs/design/DSH-Tower-/346/217/222/344/273/266/350/256/276/350/256/241/350/247/204/346/240/274.md +723 -0
- package/docs/design/compaction.md +157 -0
- package/docs/panel-narrow.png +0 -0
- package/docs/panel.png +0 -0
- package/dsh.bundle.patch.yml +9 -0
- package/lib/client.js +841 -0
- package/lib/config-store.js +146 -0
- package/lib/config.js +323 -0
- package/lib/core.js +268 -0
- package/lib/engine.js +66 -0
- package/lib/index.js +101 -0
- package/lib/prompts.js +124 -0
- package/lib/select.js +248 -0
- package/lib/server.js +253 -0
- package/lib/state.js +192 -0
- package/lib/summarize.js +91 -0
- package/lib/tools.js +262 -0
- package/lib/tower.js +1366 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 压缩逻辑
|
|
2
|
+
|
|
3
|
+
## 记号
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
W = 上下文窗口
|
|
7
|
+
F = 固定开销 = system 提示词 + 工具 schema (恒占窗口、永不压缩)
|
|
8
|
+
B = 可用预算 = W − F (下面所有比例的**分母**)
|
|
9
|
+
U = 一个单位 = distillMinRunRatio × B (默认 5%;1M 窗口下 ≈ 52K)
|
|
10
|
+
T = 触发线 = triggerRatio × B (默认 45%;下文示例按 30% = 305.6K 算)
|
|
11
|
+
G = 目标线 = targetRatio × B (默认 15% = 152.8K)
|
|
12
|
+
K = 每层库存 = ceil((T−G)/U) + 1 (自动推导,不做成配置项;0.45/0.15/0.05 时是 7 块)
|
|
13
|
+
C = 并发上限 = observerConcurrency (默认 10 个)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**分母为什么扣掉 F**:system 与工具 schema 恒定占着窗口,既不会增长也永远不会被压缩。把它们算进分母,等于把「能用的空间」和「整块窗口」混为一谈 —— 面板会显示「还能再塞 700K」,其实能动的只有 970K 里剩下的那部分。所以阈值、单位、目标线全部按 `B` 算,进度条把 `F` 单独画成最前面一截黑色。
|
|
17
|
+
|
|
18
|
+
## 两种 token 口径
|
|
19
|
+
|
|
20
|
+
宿主的 token 数有两个来源,**它们不是同一把尺子**:
|
|
21
|
+
|
|
22
|
+
| 口径 | 来源 | 覆盖范围 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 真实 | provider 每次响应里的 `usage`(input + cacheRead + output) | 整条请求一个总数,**没有逐条拆分**,请求成功之后才有 |
|
|
25
|
+
| 估算 | token meter 的固定密度 `字符数 ÷ 4`(每块再加 4) | 每条消息一个数,随时可用,但中文被低估三倍多 |
|
|
26
|
+
|
|
27
|
+
实测一条 47 万 token 的会话:真实 0.327 tok/字符,估算 0.255 —— 差 **1.25 倍**。宿主里没有分词器(`@deepseek-ai` 下没有任何 tokenizer 包),所以逐条消息的真值拿不到。
|
|
28
|
+
|
|
29
|
+
插件用两个办法把口径统一:
|
|
30
|
+
|
|
31
|
+
1. **块的 `coveredTokens` 用 usage 差分实测**:同一段材料在「区间后第一次请求」和「区间前最后一次请求」的 prompt 之差,就是它真实占的 token(`realTokensOf`)。测不到(跨过压缩点、缺 usage、量级离谱)才退回「估算 × 系数」。
|
|
32
|
+
2. **其余一律乘「事实修正系数」**(`calibration`):`系数 = 真实用量 ÷ 同一时刻的估算量`,每次请求成功后重算、做平滑、**保留两位小数**。长会话里它会收敛成一个稳定值,面板上的 token 数不会每轮抖一下;实测这条会话收敛在 1.25 左右。
|
|
33
|
+
|
|
34
|
+
系数作用在所有估算值上 —— 阈值分母、蒸馏单位、块大小、面板显示 —— 所以它们始终是同一把尺子。它同时把 system + 工具 schema 也放大了,那部分只有 4K 量级,放大后仍偏小(实测真实约 26K),差的 20K 在 1M 窗口里是 2%,不值得为它再引入第二个参数。
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
每一层的一块都恰好是「一个单位换一个单位」:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
T1:52.4K 原文 → 2.5K 摘要
|
|
41
|
+
T2:21 个 T1 checkpoint(52.5K) → 2.5K
|
|
42
|
+
T3:21 个 T2 checkpoint(52.5K) → 2.5K
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 一、观察者:分层库存 + 并发配额
|
|
46
|
+
|
|
47
|
+
**每次模型请求前**(`agent/pre-step`)判定一次,**每个回合结束前**(`agent/turn-stopping`)再判定一次。判定内容一样:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
1. 分层盘点 inv[k] = Σ 未提交的 tier=k 块 (coveredTokens − summaryTokens)
|
|
51
|
+
「未提交」= 没被合并进上层、没在表层生成 checkpoint、covers 仍在表层
|
|
52
|
+
2. 分层备料 avail[k] = 这一层现在有几个「单位」的材料
|
|
53
|
+
T1 看最早那段连续可蒸原文;T2+ 看同层相邻 checkpoint 攒了多少
|
|
54
|
+
3. 发名额 从 k=1 往上:缺口 = K×U − inv[k];够就不发
|
|
55
|
+
count = min(ceil(缺口/U), avail[k], 剩余并发名额)
|
|
56
|
+
名额发满就停 —— 低层有料就先蒸低层
|
|
57
|
+
4. 并发蒸馏 每个任务一次旁路 LLM 调用,写一条 plugin:tower/block
|
|
58
|
+
在飞的 seq 会被占位,别的任务挑不到同一段材料
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**为什么分层记账**:提交顺序是低层优先,高层块要等低层用完才会被动用。所以「T1 有 2 个 + T2 有 2 个」并不等于「库存够了」—— 把它们混成一个数会导致该蒸 T1 的时候不蒸,越线时又被迫动用高层块。
|
|
62
|
+
|
|
63
|
+
**观察不受保护区限制**:蒸馏只是往账本里写块,**不改上下文**,所以早期、近期、甚至最近一步的材料都可以蒸。保护区只在**提交**那一步生效:覆盖区间落在最近 N 步里的块先不提交,等它滑出保护区再提交。这样越线时手里始终有货,而"最近的内容不压"这条承诺也不会被打破。
|
|
64
|
+
|
|
65
|
+
**为什么库存够就不蒸**:这就是防浪费的闸门。上限被钉死在「每层 K 块」,不会因为回合多就一直蒸。
|
|
66
|
+
|
|
67
|
+
**为什么请求前不等**:那一刻回合是开着的,块写进内存后由这次请求前后的持久化检查点落盘;等它只会拖慢请求。回合尾那次要等 —— 回合结束会立刻关会话,不等就丢块。
|
|
68
|
+
|
|
69
|
+
## 二、提交:越线才动,底层优先,够到目标就停
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
每次请求前,若「内容」≥ T(内容 = 整条请求 − F):
|
|
73
|
+
候选 = 覆盖区间仍在可压区、且摘要小于原文的块
|
|
74
|
+
排序 = 层号升序 → 同层最老优先
|
|
75
|
+
累加:每选一块就按 (summary − covered) 折减 total,直到 total ≤ G
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
三条性质:
|
|
79
|
+
|
|
80
|
+
- **只用已就绪的块**,绝不等还在蒸的 —— 所以越线那一刻不会被观察者拖住;
|
|
81
|
+
- **压不到目标也照样提交**:候选耗尽就停在能到的位置,下一次判定继续;
|
|
82
|
+
- **绝不为了凑目标去提交高层块**:高层只会在低层候选耗尽后才被选中。
|
|
83
|
+
|
|
84
|
+
提交本身零 LLM 调用:块是提前蒸好的,这里只是查表 + 交给引擎的 `compactRegion` 换进表层。
|
|
85
|
+
|
|
86
|
+
**压不下去也不交回宿主**:量不到窗口、可压区为空、没有就绪块 —— 这三种情况一律**这次不压**,请求照发。交给宿主压的代价是它会在塔外做出一段没有块号的 checkpoint:既不能 `decompress`,也不参与分层,账本从此对不上账。溢出(provider 报上下文超限)同样不交回,而是当成一次强制提交:用手上的块把空间腾出来,腾不动就让这条请求自己失败(宁可报错,也不留无法追溯的压缩)。
|
|
87
|
+
|
|
88
|
+
**为什么块要落在工具配对平衡的位置上**:宿主的 `compactRegion(start, end)` 拒绝「工具调用和它的结果被切在两边」的区间(`not a balanced boundary`)。注意它**不看 step** —— `dsh-compaction` 的 tool-pairing 文档原话是: safe cuts 由 tool-call/result 的内容推出,"rather than step markers"(那句报错文案里的 step 是旧语义的遗留)。所以:
|
|
89
|
+
|
|
90
|
+
- **蒸原材料时**补齐到 step 末尾:step 结束意味着工具调用和结果都齐了,天然平衡,块从此不用再调边界;
|
|
91
|
+
- **合并块不用管**:它的材料是 checkpoint 节点,替换的区间两端本来就是平衡的;
|
|
92
|
+
- **提交时再兜一次**:万一历史块边界不齐(老版本、或工具调用跨了切点),就在块自己的 covers 里往前缩到最近的平衡点,缩不出来就跳过这一块 —— 绝不向后扩,那会把没进摘要的消息一起盖掉。
|
|
93
|
+
|
|
94
|
+
## 三、层级无上限
|
|
95
|
+
|
|
96
|
+
层数不是配置出来的,是被材料逼出来的:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
要凑 1 个 T2 → 需要 21 个 T1 checkpoint → 21 次 T1 提交
|
|
100
|
+
要凑 1 个 T3 → 需要 21 个 T2 checkpoint → 441 次 T1 提交
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
所以 T7、T8 理论上存在(需要极长会话),代码里没有任何「最多两层」的假设:`tier = max(子块层) + 1`,排序按 `Number(tier)`,块头解析用 `T\d+`(两位数以上的层号也读得出来)。
|
|
104
|
+
|
|
105
|
+
## 四、谁在什么时候动
|
|
106
|
+
|
|
107
|
+
| 时机 | 观察者 | 提交 |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| 每次模型请求前(pre-step) | 判定并发起,**不等** | 越线时执行 |
|
|
110
|
+
| 回合结束前(turn-stopping) | 判定并发起,**等它写完** | — |
|
|
111
|
+
| 用户点「立即蒸馏 / 立即提交」 | 强制跑一批 | 排队到下一回合 |
|
|
112
|
+
|
|
113
|
+
块记录用 `plugin:` 命名空间 + `ignorable: true` 写入,所以**卸载插件后会话仍然读得出来**。
|
|
114
|
+
|
|
115
|
+
> 为什么不能是"滚动摘要":`compactRegion(start, end)` 要求区间仍在表层。T1 一提交,它覆盖的 seq 就离开表层,任何以它们为 covers 的父块便再也提交不了。所以上层块只能以**已提交的 checkpoint 节点**为成员。
|
|
116
|
+
|
|
117
|
+
## 提示词
|
|
118
|
+
|
|
119
|
+
分两部分,归属不同。
|
|
120
|
+
|
|
121
|
+
**观察者(蒸馏):三档可选。** 默认用宿主的英文原文,也可以换成中文预设或自己写的。
|
|
122
|
+
|
|
123
|
+
| `summaryPromptMode` | 行为 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `host`(默认) | 调宿主的 `engine.summarize()`,用 `COMPACTION_INSTRUCTION`;格式与宿主原生压缩完全一致,上游改提示词我们自动跟上 |
|
|
126
|
+
| `zh` | **同一条 8 段骨架**,正文改成中文;小节标题仍是英文(结构是协议,标题不能翻) |
|
|
127
|
+
| `custom` | 用 `summaryPrompt` 的文本;这一次请求由 Tower 自己发 |
|
|
128
|
+
|
|
129
|
+
三档共用同一个骨架:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
## Primary Request and Intent ## Key Technical Concepts
|
|
133
|
+
## Files and Code ## Errors and Fixes
|
|
134
|
+
## Pending Jobs ## Current Work
|
|
135
|
+
## Next Step ## Critical Context
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
规则:短横线、不写段落、空小节写 `(none)`、不许删小节;保留精确路径 / 命令 / 错误串 / 标识符 / 数值;不提及这次压缩;若材料里已有 `<compacted-summary>`,那是旧检查点,要合并而不是照抄。
|
|
139
|
+
|
|
140
|
+
`zh` / `custom` 走 `lib/summarize.js` 自己发请求,逐条照抄宿主的形状,少一条都会退化:
|
|
141
|
+
|
|
142
|
+
- 指令作为**最后一条 user 消息**(不是 system)—— 前缀缓存仍然复用;
|
|
143
|
+
- `toolHistory` 与 `tools` 必带,否则摘要看不到工具调用;
|
|
144
|
+
- `purpose: 'compaction'` 是路由与计费标记;
|
|
145
|
+
- `maxTokens` 是**保险丝不是剪刀**(`summaryMaxTokens`,默认 200000;0 = 不传,交给模型自己的输出上限):设小会让摘要撞上限 → 整块作废,等于让蒸馏反复失败。实测本机网关接受 `max_tokens: 200000`(HTTP 200),所以默认值取大是安全的;
|
|
146
|
+
- 出错 / 中断 / **截断**一律算失败,绝不把半截摘要拿去写块(否则那一块覆盖的原文就真丢了)。取舍很明确:**宁可这一次不压缩、原文继续留在表层,也不拿半截摘要去覆盖原文**——前者只是没省下空间,后者是真丢了内容。
|
|
147
|
+
|
|
148
|
+
观察者路由不完整(provider 或 model 为空)时,`custom` 档这一回合会退回宿主,不会把蒸馏搞停。
|
|
149
|
+
|
|
150
|
+
**面板里的交互:下拉选档,文本框跟着变。** 选 `host` / `zh` / `builtin` 时,下面的文本框**只读展示那一刻生效的预设正文**(可以滚动、可以选中复制,但改不了);只有切到 `custom` 才变成可编辑。切档时如果自定义框还是空的,会先把刚才展示的预设正文复制进去,让你在已知可用的文本上改,而不是对着空白框重写。
|
|
151
|
+
|
|
152
|
+
| 档位字段 | 文本框字段 | 预设来源 |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `summaryPromptMode` | `summaryPrompt` | `host` → 宿主英文原文(展示副本)、`zh` → 中文预设 |
|
|
155
|
+
|
|
156
|
+
> 宿主的英文指令是包内常量、没有导出,所以 `host` 档展示的是**抄来的副本**(`SUMMARY_PROMPT_HOST_DISPLAY`)。真正执行仍然由宿主的 `summarize` 用它自己的常量——副本过时只会让面板文字落后,不影响压缩结果。
|
|
157
|
+
|
|
Binary file
|
package/docs/panel.png
ADDED
|
Binary file
|