dsh-whale-loli 0.5.3 → 0.5.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 +16 -7
- package/package.json +1 -1
- package/src/history-window.js +42 -26
package/README.md
CHANGED
|
@@ -96,17 +96,26 @@ Cannot read properties of undefined (reading 'kind')
|
|
|
96
96
|
连 `step/start` 都没有,极难定位。所以无论裁剪成功、失败还是跳过,都必须 `return next()`。
|
|
97
97
|
`verify-history-window.mjs` 第 9 组用例复刻了这个 waterfall 语义,并保留了一个「老写法必崩」的反例。
|
|
98
98
|
|
|
99
|
-
#### ⚠️
|
|
99
|
+
#### ⚠️ 裁剪写的事件必须是宿主认得的形状(这条踩了三次)
|
|
100
100
|
|
|
101
|
-
宿主的会话格式校验(`dsh-session-format-v3-to-v4
|
|
102
|
-
写死 `turn: 0` 会抛:
|
|
101
|
+
宿主的会话格式校验(`dsh-session-format-v3-to-v4`)对消息事件有硬性要求,**缺一项就抛错、整个回合失败**:
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
| 事件类型 | 要求 | 踩过的错 |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `developer/message` | `turn`/`step` 正整数 + `message.id` + `message.role='developer'` + `message.content` 数组 + `message.source`(producer-owned,且 ≠ `'plugin'`) | `developer/message turn must be positive`<br>`format v4 developer message requires id, role, content, and a producer-owned source` |
|
|
106
|
+
| `user/message` | `role !== 'developer'`、`content` 是数组、`source.kind` 非空且 ≠ `'plugin'` | `message requires a producer-owned source kind` |
|
|
107
|
+
|
|
108
|
+
所以本插件**改用 `user/message`**(形状照 compaction 的 checkpoint 抄 —— 那是宿主自己在跑、
|
|
109
|
+
必然通过校验的路径),并带上:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
{ id: randomUUID(), role: 'user', content: [...], source: { kind: 'history-window' } }
|
|
106
113
|
```
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
`source.kind = 'history-window'` 有两个用处:满足宿主的 producer-owned 要求,以及让
|
|
116
|
+
`isUserNode()` 能把自己的提示条和**真实用户开口**区分开。后者很重要 —— 宿主的运行时上下文
|
|
117
|
+
(`source.kind = 'runtime-context'`)同样是 `user/message`,此前会把轮数算多、导致过早裁剪。
|
|
118
|
+
第 10 组用例同时验证「提示条合法」「提示条不计轮」「运行时上下文不计轮」「重复裁剪不会越积越多」。
|
|
110
119
|
|
|
111
120
|
### 为什么用「提示变量」而不是 section(重要)
|
|
112
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-whale-loli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
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",
|
package/src/history-window.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* `agent/pre-step`:循环在每个步骤**派生历史之前**运行它,是唯一既在请求前、
|
|
31
31
|
* 又能拿到本 agent 的时机。(`turn/start` 是 session 事件而非 ctx 事件,挂不上。)
|
|
32
32
|
*/
|
|
33
|
+
import { randomUUID } from 'node:crypto';
|
|
33
34
|
import { homedir } from 'node:os';
|
|
34
35
|
import { join } from 'node:path';
|
|
35
36
|
|
|
@@ -56,9 +57,27 @@ function dshHome() {
|
|
|
56
57
|
return process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* 本插件写进会话的「已裁剪」提示条所用的 source.kind。
|
|
62
|
+
* 宿主只要求 message.source.kind 是非空字符串且不是 `'plugin'`,
|
|
63
|
+
* 但我们必须能把自己的提示条和「真实用户开口」区分开,否则每裁一次就多算一轮。
|
|
64
|
+
*/
|
|
65
|
+
export const NOTICE_SOURCE_KIND = 'history-window';
|
|
66
|
+
|
|
67
|
+
/** 不算「用户开口」的 source.kind:本插件的提示条 + 宿主注入的运行时上下文。 */
|
|
68
|
+
const NON_TURN_SOURCE_KINDS = new Set([NOTICE_SOURCE_KIND, 'runtime-context']);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 一个「轮」的边界 = 一条**真实用户**发出的 user 消息。
|
|
72
|
+
*
|
|
73
|
+
* 不能只看 `type === 'user/message'`:宿主的运行时上下文(`source.kind = 'runtime-context'`)
|
|
74
|
+
* 和本插件自己的提示条也是 user/message,会把轮数算多、导致过早裁剪。
|
|
75
|
+
* 来源未知(老日志没有 source)时保守地当成用户开口。
|
|
76
|
+
*/
|
|
60
77
|
function isUserNode(event) {
|
|
61
|
-
|
|
78
|
+
if (event?.type !== 'user/message') return false;
|
|
79
|
+
const kind = event.data?.source?.kind;
|
|
80
|
+
return typeof kind !== 'string' || !NON_TURN_SOURCE_KINDS.has(kind);
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
/**
|
|
@@ -138,22 +157,24 @@ export function shadowedNodes(session, startSeq, endSeq) {
|
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* `
|
|
146
|
-
*
|
|
147
|
-
*
|
|
160
|
+
* 执行裁剪。返回计划;无需裁剪返回 null。
|
|
161
|
+
*
|
|
162
|
+
* 替换消息用 **`user/message`**,形状照 compaction 的 checkpoint 抄 —— 这是宿主
|
|
163
|
+
* 已经在跑、必然通过全部校验的那条路径。相比之下 `developer/message` 的规则苛刻得多
|
|
164
|
+
* (`assertDeveloperMessage` 要求 `id` / `role` / `content` / **producer-owned `source`**,
|
|
165
|
+
* 且 `turn`、`step` 必须是正整数),少一项就抛:
|
|
166
|
+
* format v4 developer message requires id, role, content, and a producer-owned source
|
|
167
|
+
*
|
|
168
|
+
* `user/message` 这边只有两条硬要求:
|
|
169
|
+
* 1. `assertOrdinaryMessage`:`role !== 'developer'`、`content` 是数组
|
|
170
|
+
* 2. `assertV4MessageSources`:`source.kind` 是非空字符串且不等于 `'plugin'`
|
|
171
|
+
* 所以带上 `id` 和 `source: { kind: 'history-window' }` 即可,且能被 {@link isUserNode}
|
|
172
|
+
* 识别出来、不计入轮数。
|
|
148
173
|
*/
|
|
149
|
-
export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS
|
|
174
|
+
export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS) {
|
|
150
175
|
const session = agent?.session;
|
|
151
176
|
if (session === undefined) return null;
|
|
152
177
|
|
|
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
|
-
|
|
157
178
|
const plan = planPrune(session, keepTurns);
|
|
158
179
|
if (plan === null) return null;
|
|
159
180
|
|
|
@@ -161,14 +182,12 @@ export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS, position = {
|
|
|
161
182
|
if (shadowed.length === 0) return null;
|
|
162
183
|
|
|
163
184
|
session.append(
|
|
164
|
-
'
|
|
185
|
+
'user/message',
|
|
165
186
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
content: [{ type: 'text', text: noticeText(plan.removedTurns, plan.keptTurns) }],
|
|
171
|
-
},
|
|
187
|
+
id: randomUUID(),
|
|
188
|
+
role: 'user',
|
|
189
|
+
content: [{ type: 'text', text: noticeText(plan.removedTurns, plan.keptTurns) }],
|
|
190
|
+
source: { kind: NOTICE_SOURCE_KIND },
|
|
172
191
|
},
|
|
173
192
|
{
|
|
174
193
|
surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },
|
|
@@ -209,16 +228,13 @@ export function apply(ctx, config = {}) {
|
|
|
209
228
|
|
|
210
229
|
const keepTurns = typeof config.keepTurns === 'number' ? config.keepTurns : DEFAULT_KEEP_TURNS;
|
|
211
230
|
|
|
212
|
-
// agent/pre-step 在派生历史之前运行,且拿得到本 agent
|
|
231
|
+
// agent/pre-step 在派生历史之前运行,且拿得到本 agent。
|
|
213
232
|
ctx.effect(
|
|
214
233
|
() =>
|
|
215
234
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
216
235
|
try {
|
|
217
236
|
const agent = payload?.agent ?? payload?.handle?.agent;
|
|
218
|
-
if (agent?.session !== undefined)
|
|
219
|
-
// turn/step 必须原样透传给 pruneHistory:宿主校验器要求 turn 为正整数。
|
|
220
|
-
pruneHistory(agent, keepTurns, { turn: payload?.turn, step: payload?.step });
|
|
221
|
-
}
|
|
237
|
+
if (agent?.session !== undefined) pruneHistory(agent, keepTurns);
|
|
222
238
|
} catch {
|
|
223
239
|
// 裁剪失败绝不影响正常对话
|
|
224
240
|
}
|