dsh-whale-loli 0.5.1 → 0.5.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/README.md +18 -1
- package/package.json +2 -2
- package/src/history-window.js +22 -6
- package/src/memory.js +43 -5
package/README.md
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
- **每轮都记**:不是挑着记。短对话直接记;长对话**先精简概括**再记(压成一两句说清聊了什么、结论是什么)。
|
|
26
26
|
- **80 条上限只算「滚动摘要」**:`# 固定记忆` 不占这个名额。
|
|
27
27
|
- **想长期留的就往上挪**:某条滚动摘要以后还用得上,就总结一句写进 `# 固定记忆`;否则到点会滚出去(并归档)。
|
|
28
|
-
- 上限有两道保险:注入时只取最近 80 条(文件里堆再多也只进 80
|
|
28
|
+
- 上限有两道保险:注入时只取最近 80 条(文件里堆再多也只进 80 条);**每轮结束(agent 转 idle)时自动**把文件本身裁到 80 条,超出的写进 `archive/<预设名>/`。
|
|
29
|
+
- 为什么放在 idle 而不是组装时:组装是热路径、每次都要跑,在那里写盘既慢又容易和 agent 正在编辑的文件打架。
|
|
30
|
+
- 不足 80 条时**完全不动文件**(不产生无谓写盘)。
|
|
31
|
+
- **记忆文件还是空的时候也会注入一段带绝对路径的提示**:否则新预设开局拿到一片空白,agent 连该往哪个文件写都不知道。
|
|
29
32
|
- 总长顶到 `maxChars`(16000)时,**先丢最旧的滚动摘要,再丢详细区**,固定记忆最后才动。
|
|
30
33
|
|
|
31
34
|
### 每轮先看什么
|
|
@@ -93,6 +96,18 @@ Cannot read properties of undefined (reading 'kind')
|
|
|
93
96
|
连 `step/start` 都没有,极难定位。所以无论裁剪成功、失败还是跳过,都必须 `return next()`。
|
|
94
97
|
`verify-history-window.mjs` 第 9 组用例复刻了这个 waterfall 语义,并保留了一个「老写法必崩」的反例。
|
|
95
98
|
|
|
99
|
+
#### ⚠️ 裁剪写的事件必须带合法 `turn`
|
|
100
|
+
|
|
101
|
+
宿主的会话格式校验(`dsh-session-format-v3-to-v4`)要求 `developer/message` 的 `turn` 是**正整数**。
|
|
102
|
+
写死 `turn: 0` 会抛:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
developer/message turn must be positive
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
所以 `pruneHistory()` 从 `agent/pre-step` 的 payload 里取真实的 `turn` / `step` 透传;
|
|
109
|
+
**拿不到正数 turn 就跳过本次裁剪**(宁可不裁,也不写非法事件)。第 10 组用例覆盖这个分支。
|
|
110
|
+
|
|
96
111
|
### 为什么用「提示变量」而不是 section(重要)
|
|
97
112
|
|
|
98
113
|
`@deepseek-ai/dsh-system-prompt` 的 `assemble()` 末尾有这样一段:
|
|
@@ -213,6 +228,7 @@ dsh-whale-loli/
|
|
|
213
228
|
├── verify-memory-injection.mjs # 证明变量通道有效、section 通道被丢弃
|
|
214
229
|
├── verify-memory-isolation.mjs # 实测三个记忆文件不串
|
|
215
230
|
├── verify-memory-rolling.mjs # 实测四区结构与 80 条滚动 + 归档
|
|
231
|
+
├── verify-memory-archive.mjs # 超 80 条时真的归档并瘦身(idle 触发,三预设互不覆盖)
|
|
216
232
|
├── verify-peers.mjs # 校验 peer 范围覆盖 harness 各发布档
|
|
217
233
|
├── verify-listing.mjs # 复刻插件目录的收录门槛自查
|
|
218
234
|
└── prove-collision.mjs # 复现并证明重复注册冲突
|
|
@@ -235,6 +251,7 @@ node verify-history-window.mjs # 真的裁到最近 N 轮(不切
|
|
|
235
251
|
node verify-memory-injection.mjs # 变量通道有效 / section 通道被丢弃
|
|
236
252
|
node verify-memory-isolation.mjs # 记忆文件端到端隔离(三个预设)
|
|
237
253
|
node verify-memory-rolling.mjs # 固定记忆永久 + 滚动摘要 80 条 + 归档
|
|
254
|
+
node verify-memory-archive.mjs # 超 80 条自动归档瘦身(idle 触发、三预设隔离)
|
|
238
255
|
node smoke-memory.mjs # 记忆变量冒烟
|
|
239
256
|
node verify-peers.mjs # peer 范围无预发布坑
|
|
240
257
|
node verify-listing.mjs --topics=dsh-plugin # 收录门槛自查
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-whale-loli",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "鲸鱼娘 Agent 预设(三个)— 极简模式工具面 + 鲸鱼娘·小鱼人设 + 跨会话长期记忆 + 真正生效的 20 轮历史窗口。记忆分「固定记忆」(永久保留)/「滚动摘要」(每轮都记,最多 80 条 FIFO
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "鲸鱼娘 Agent 预设(三个)— 极简模式工具面 + 鲸鱼娘·小鱼人设 + 跨会话长期记忆 + 真正生效的 20 轮历史窗口。记忆分「固定记忆」(永久保留)/「滚动摘要」(每轮都记,最多 80 条 FIFO,超限每轮结束自动归档到 archive/<预设名>/)/「最近详细区」/「归档索引」四区,经提示变量注入(persona 为 complete:true,独立 section 会被宿主丢弃);history-window 用宿主表层替换真的裁掉 20 轮以前的历史,不调 LLM、不切断工具配对,并正确放行 agent/pre-step 的 waterfall。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
package/src/history-window.js
CHANGED
|
@@ -137,10 +137,23 @@ export function shadowedNodes(session, startSeq, endSeq) {
|
|
|
137
137
|
return nodes.filter((seq) => seq >= startSeq && seq <= endSeq);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
140
|
+
/**
|
|
141
|
+
* 执行裁剪。返回计划;无需裁剪(或拿不到合法 position)返回 null。
|
|
142
|
+
*
|
|
143
|
+
* `position` = `{ turn, step }`,直接来自 `agent/pre-step` 的 payload。
|
|
144
|
+
* **必须传真实 turn**:宿主 `dsh-session-format-v3-to-v4` 的校验器要求
|
|
145
|
+
* `developer/message` 的 `turn` 为正整数,写死 0 会抛
|
|
146
|
+
* `developer/message turn must be positive`
|
|
147
|
+
* 拿不到正数 turn 就跳过这一轮(宁可不裁,也不写非法事件)。
|
|
148
|
+
*/
|
|
149
|
+
export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS, position = {}) {
|
|
142
150
|
const session = agent?.session;
|
|
143
151
|
if (session === undefined) return null;
|
|
152
|
+
|
|
153
|
+
const turn = Number.isInteger(position?.turn) && position.turn > 0 ? position.turn : null;
|
|
154
|
+
if (turn === null) return null;
|
|
155
|
+
const step = Number.isInteger(position?.step) && position.step > 0 ? position.step : 1;
|
|
156
|
+
|
|
144
157
|
const plan = planPrune(session, keepTurns);
|
|
145
158
|
if (plan === null) return null;
|
|
146
159
|
|
|
@@ -150,8 +163,8 @@ export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS) {
|
|
|
150
163
|
session.append(
|
|
151
164
|
'developer/message',
|
|
152
165
|
{
|
|
153
|
-
turn
|
|
154
|
-
step
|
|
166
|
+
turn,
|
|
167
|
+
step,
|
|
155
168
|
message: {
|
|
156
169
|
role: 'developer',
|
|
157
170
|
content: [{ type: 'text', text: noticeText(plan.removedTurns, plan.keptTurns) }],
|
|
@@ -196,13 +209,16 @@ export function apply(ctx, config = {}) {
|
|
|
196
209
|
|
|
197
210
|
const keepTurns = typeof config.keepTurns === 'number' ? config.keepTurns : DEFAULT_KEEP_TURNS;
|
|
198
211
|
|
|
199
|
-
// agent/pre-step 在派生历史之前运行,且拿得到本 agent。
|
|
212
|
+
// agent/pre-step 在派生历史之前运行,且拿得到本 agent 与当前 turn/step。
|
|
200
213
|
ctx.effect(
|
|
201
214
|
() =>
|
|
202
215
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
203
216
|
try {
|
|
204
217
|
const agent = payload?.agent ?? payload?.handle?.agent;
|
|
205
|
-
if (agent?.session !== undefined)
|
|
218
|
+
if (agent?.session !== undefined) {
|
|
219
|
+
// turn/step 必须原样透传给 pruneHistory:宿主校验器要求 turn 为正整数。
|
|
220
|
+
pruneHistory(agent, keepTurns, { turn: payload?.turn, step: payload?.step });
|
|
221
|
+
}
|
|
206
222
|
} catch {
|
|
207
223
|
// 裁剪失败绝不影响正常对话
|
|
208
224
|
}
|
package/src/memory.js
CHANGED
|
@@ -547,21 +547,59 @@ export function apply(ctx, config = {}) {
|
|
|
547
547
|
|
|
548
548
|
/**
|
|
549
549
|
* 每次都重新读盘:改完文件下一个模型步就生效,不用重载宿主。
|
|
550
|
-
*
|
|
550
|
+
*
|
|
551
|
+
* 注意两点:
|
|
552
|
+
* 1. 必须返回**字符串** —— 变量值为空串时 interpolate 只是替换成空,不会报错。
|
|
553
|
+
* 2. **文件空(或不存在)时也要给出绝对路径**。否则新预设开局记忆是空骨架,
|
|
554
|
+
* agent 拿到的是一段空白,连该往哪个文件写都不知道(只能靠猜 ~/.dsh/…)。
|
|
555
|
+
* 所以空文件回一段带路径的提示,注入标记也照样出现,便于审计。
|
|
551
556
|
*/
|
|
552
557
|
const read = () => {
|
|
558
|
+
const path = memoryPath(file);
|
|
553
559
|
try {
|
|
554
560
|
const { text } = composeMemory(file, { maxChars, turnLimit, detailLimit });
|
|
555
|
-
if (!text)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
561
|
+
if (!text) {
|
|
562
|
+
return [
|
|
563
|
+
`【鲸鱼记忆 · ${file}】`,
|
|
564
|
+
`文件:${path}`,
|
|
565
|
+
'(现在还是空的 —— 想到什么就直接写进这个文件;只写这一个,别动别的预设的记忆。)',
|
|
566
|
+
].join('\n');
|
|
567
|
+
}
|
|
568
|
+
return `【鲸鱼记忆 · ${file}】\n文件:${path}\n\n${text}`;
|
|
569
|
+
} catch (err) {
|
|
570
|
+
// 读失败也要让 agent 知道路径,而不是默默变成空串
|
|
571
|
+
return [`【鲸鱼记忆 · ${file}】`, `文件:${path}`, `(这次没读出来:${String(err?.message ?? err)})`].join('\n');
|
|
559
572
|
}
|
|
560
573
|
};
|
|
561
574
|
|
|
562
575
|
// 主通道:变量。人设 prefix 里写 {{variableName}},随 complete section 一起进提示。
|
|
563
576
|
ctx.effect(() => ctx.systemPrompt.variable(variableName, read), `whale-memory.variable(${variableName})`);
|
|
564
577
|
|
|
578
|
+
/**
|
|
579
|
+
* 每轮结束(agent 转为 idle)时,把超出 turnLimit 的滚动条目**归档并瘦身文件**。
|
|
580
|
+
*
|
|
581
|
+
* 为什么放这里而不是 `read()`:`read()` 是组装热路径(每次组装都会跑),
|
|
582
|
+
* 在那里写盘既慢又容易和 agent 自己的编辑打架 —— `pruneMemoryFile` 的注释里
|
|
583
|
+
* 也写明了「组装热路径不会调用」。而 idle 一轮只发生一次,时机正好。
|
|
584
|
+
*
|
|
585
|
+
* 不这么做的话:注入会按 turnLimit 截断(只喂最新 80 条),但文件本身无限增长,
|
|
586
|
+
* `archive/` 目录永远空着 —— 与「超过 80 条就不断覆盖」的约定不符。
|
|
587
|
+
*/
|
|
588
|
+
ctx.effect(
|
|
589
|
+
() =>
|
|
590
|
+
typeof ctx.on === 'function' &&
|
|
591
|
+
ctx.on('agent/status', ({ status }) => {
|
|
592
|
+
if (status !== 'idle') return;
|
|
593
|
+
try {
|
|
594
|
+
const { totalRoll } = composeMemory(file, { maxChars, turnLimit, detailLimit });
|
|
595
|
+
if (totalRoll > turnLimit) pruneMemoryFile(file, turnLimit, { detailLimit });
|
|
596
|
+
} catch {
|
|
597
|
+
// 整理失败不影响对话
|
|
598
|
+
}
|
|
599
|
+
}),
|
|
600
|
+
`whale-memory.trim(${file})`,
|
|
601
|
+
);
|
|
602
|
+
|
|
565
603
|
// 备用通道:仅在明确要求时注册独立 section(非 complete 预设才用得上)。
|
|
566
604
|
if (config.emitSection === true) {
|
|
567
605
|
const sectionName =
|