dsh-whale-loli 0.5.2 → 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 CHANGED
@@ -96,6 +96,27 @@ Cannot read properties of undefined (reading 'kind')
96
96
  连 `step/start` 都没有,极难定位。所以无论裁剪成功、失败还是跳过,都必须 `return next()`。
97
97
  `verify-history-window.mjs` 第 9 组用例复刻了这个 waterfall 语义,并保留了一个「老写法必崩」的反例。
98
98
 
99
+ #### ⚠️ 裁剪写的事件必须是宿主认得的形状(这条踩了三次)
100
+
101
+ 宿主的会话格式校验(`dsh-session-format-v3-to-v4`)对消息事件有硬性要求,**缺一项就抛错、整个回合失败**:
102
+
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' } }
113
+ ```
114
+
115
+ `source.kind = 'history-window'` 有两个用处:满足宿主的 producer-owned 要求,以及让
116
+ `isUserNode()` 能把自己的提示条和**真实用户开口**区分开。后者很重要 —— 宿主的运行时上下文
117
+ (`source.kind = 'runtime-context'`)同样是 `user/message`,此前会把轮数算多、导致过早裁剪。
118
+ 第 10 组用例同时验证「提示条合法」「提示条不计轮」「运行时上下文不计轮」「重复裁剪不会越积越多」。
119
+
99
120
  ### 为什么用「提示变量」而不是 section(重要)
100
121
 
101
122
  `@deepseek-ai/dsh-system-prompt` 的 `assemble()` 末尾有这样一段:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-loli",
3
- "version": "0.5.2",
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",
@@ -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
- /** 一个「轮」的边界 = 一条 user 消息。表层里只有消息节点,turn 标记不进表层。 */
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
- return event?.type === 'user/message';
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
  /**
@@ -137,10 +156,25 @@ export function shadowedNodes(session, startSeq, endSeq) {
137
156
  return nodes.filter((seq) => seq >= startSeq && seq <= endSeq);
138
157
  }
139
158
 
140
- /** 执行裁剪。返回计划;无需裁剪返回 null。 */
159
+ /**
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
+ * 识别出来、不计入轮数。
173
+ */
141
174
  export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS) {
142
175
  const session = agent?.session;
143
176
  if (session === undefined) return null;
177
+
144
178
  const plan = planPrune(session, keepTurns);
145
179
  if (plan === null) return null;
146
180
 
@@ -148,14 +182,12 @@ export function pruneHistory(agent, keepTurns = DEFAULT_KEEP_TURNS) {
148
182
  if (shadowed.length === 0) return null;
149
183
 
150
184
  session.append(
151
- 'developer/message',
185
+ 'user/message',
152
186
  {
153
- turn: 0,
154
- step: 0,
155
- message: {
156
- role: 'developer',
157
- content: [{ type: 'text', text: noticeText(plan.removedTurns, plan.keptTurns) }],
158
- },
187
+ id: randomUUID(),
188
+ role: 'user',
189
+ content: [{ type: 'text', text: noticeText(plan.removedTurns, plan.keptTurns) }],
190
+ source: { kind: NOTICE_SOURCE_KIND },
159
191
  },
160
192
  {
161
193
  surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },