dsh-plugin-om 0.0.28 → 0.0.30
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 -14
- package/dist/client/definition.d.ts +2 -2
- package/dist/client/definition.d.ts.map +1 -1
- package/dist/client/locales.d.ts +1 -1
- package/dist/client.js +9 -9
- package/dist/client.js.map +1 -1
- package/dist/compaction-log.d.ts +27 -26
- package/dist/compaction-log.d.ts.map +1 -1
- package/dist/compress-loop.d.ts +85 -0
- package/dist/compress-loop.d.ts.map +1 -0
- package/dist/compress-tools.d.ts +79 -0
- package/dist/compress-tools.d.ts.map +1 -0
- package/dist/compress-view.d.ts +82 -0
- package/dist/compress-view.d.ts.map +1 -0
- package/dist/compress.d.ts +10 -9
- package/dist/compress.d.ts.map +1 -1
- package/dist/config.d.ts +1 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1244 -954
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/summarize.d.ts +0 -125
- package/dist/summarize.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
2
2
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
3
|
+
import { BlockAssembler, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
4
5
|
import { SUBAGENT_DESCRIPTOR_VERSION } from "@deepseek-ai/dsh-subagent";
|
|
5
6
|
import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -28,6 +29,8 @@ const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
|
|
|
28
29
|
* 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
|
|
29
30
|
*/
|
|
30
31
|
const COMPLETE_MESSAGE_DEFINITION = "`完整消息`指一条`用户消息`、`系统消息`、`模型输出文本`或`具有result的toolcall`;首条 index 为 0,按会话顺序递增。";
|
|
32
|
+
/** 最终 <history> 块内文块首的格式说明注释(XML 注释,完整消息定义 + 条目标签语义)。 */
|
|
33
|
+
const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
|
|
31
34
|
/**
|
|
32
35
|
* 压缩因 signal 中止而放弃时 compaction/end 的 error 标识(服务端写入、客户端过滤):
|
|
33
36
|
* 中止不是失败,客户端据此隐藏失败行(宿主不变量要求无 summary 的 end 必须带 error)。
|
|
@@ -39,109 +42,25 @@ function isPluginOwnedSource(source) {
|
|
|
39
42
|
return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
|
|
40
43
|
}
|
|
41
44
|
//#endregion
|
|
42
|
-
//#region src/
|
|
43
|
-
/** om 信封 text 前缀:标识该 feedback/record 记录由本插件写入。 */
|
|
44
|
-
const OM_EVENT_PREFIX = "om:1:";
|
|
45
|
-
/** 校验载荷字段与类别匹配(运行时守卫,保证返回类型的诚实性)。 */
|
|
46
|
-
function isValidPayload(kind, data) {
|
|
47
|
-
switch (kind) {
|
|
48
|
-
case "om/warning": return typeof data.problem === "string" && typeof data.message === "string";
|
|
49
|
-
case "om/observe-pending": return typeof data.triggerMessageIndex === "number" && Number.isSafeInteger(data.triggerMessageIndex);
|
|
50
|
-
case "om/observe-invalidate": return typeof data.pendingSeq === "number" && Number.isSafeInteger(data.pendingSeq);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
45
|
+
//#region src/logger.ts
|
|
53
46
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
47
|
+
* 插件日志门面:step 为步骤级(debug)日志,按配置开关过滤;info/warn 始终输出。
|
|
48
|
+
* 导出 PluginLogger / makeLogger。
|
|
56
49
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const text = event.data?.text;
|
|
60
|
-
if (typeof text !== "string" || !text.startsWith("om:1:")) return void 0;
|
|
61
|
-
let envelope;
|
|
62
|
-
try {
|
|
63
|
-
envelope = JSON.parse(text.slice(5));
|
|
64
|
-
} catch {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) return void 0;
|
|
68
|
-
const { kind, ...rest } = envelope;
|
|
69
|
-
if (typeof kind !== "string") return void 0;
|
|
70
|
-
if (![
|
|
71
|
-
"om/warning",
|
|
72
|
-
"om/observe-pending",
|
|
73
|
-
"om/observe-invalidate"
|
|
74
|
-
].includes(kind)) return void 0;
|
|
75
|
-
const omKind = kind;
|
|
76
|
-
if (!isValidPayload(omKind, rest)) return void 0;
|
|
50
|
+
/** 构建插件日志门面:统一加 PLUGIN_LABEL 前缀,step 按 debug 开关过滤。 */
|
|
51
|
+
function makeLogger(ctx, debug) {
|
|
77
52
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
step(message) {
|
|
54
|
+
if (debug) ctx.logger.debug(`${PLUGIN_LABEL}: ${message}`);
|
|
55
|
+
},
|
|
56
|
+
info(message) {
|
|
57
|
+
ctx.logger.info(`${PLUGIN_LABEL}: ${message}`);
|
|
58
|
+
},
|
|
59
|
+
warn(message) {
|
|
60
|
+
ctx.logger.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
61
|
+
}
|
|
81
62
|
};
|
|
82
63
|
}
|
|
83
|
-
/** 编码一条 om 私有事件为 feedback/record 的 text 信封。 */
|
|
84
|
-
function encodeOmEvent(kind, data) {
|
|
85
|
-
return `${OM_EVENT_PREFIX}${JSON.stringify({
|
|
86
|
-
kind,
|
|
87
|
-
...data
|
|
88
|
-
})}`;
|
|
89
|
-
}
|
|
90
|
-
/** 追加一条 om 私有事件(借用 feedback/record,log-only,不进 surface),返回事件 seq。 */
|
|
91
|
-
function appendOmEvent(session, kind, data) {
|
|
92
|
-
return session.append("feedback/record", { text: encodeOmEvent(kind, data) }).seq;
|
|
93
|
-
}
|
|
94
|
-
function findOmEvents(session, kind) {
|
|
95
|
-
const result = [];
|
|
96
|
-
for (const event of session.events) {
|
|
97
|
-
const om = readOmEvent(event);
|
|
98
|
-
if (om !== void 0 && (kind === void 0 || om.kind === kind)) result.push(om);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/degrade.ts
|
|
104
|
-
/**
|
|
105
|
-
* 降级报告:压缩流程中的挂载失败统一出口。
|
|
106
|
-
* 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
|
|
107
|
-
*
|
|
108
|
-
* - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
|
|
109
|
-
* 并向会话日志追加 log-only 的 om 警告事件(借用 feedback/record 的 om 信封,
|
|
110
|
-
* 见 om-event.ts;客户端渲染为「功能降级」警告行);
|
|
111
|
-
* 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
|
|
112
|
-
* - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
|
|
113
|
-
* - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
|
|
114
|
-
*/
|
|
115
|
-
/** 各降级问题面向用户的简短说明(om/warning 信封载荷 message;客户端警告行直接展示)。 */
|
|
116
|
-
const DEGRADE_PROBLEMS = {
|
|
117
|
-
"systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
|
|
118
|
-
"tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
|
|
119
|
-
};
|
|
120
|
-
/**
|
|
121
|
-
* 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
|
|
122
|
-
* 出现时,向会话日志追加 log-only om 警告事件(客户端渲染警告行,每会话最多
|
|
123
|
-
* 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
|
|
124
|
-
* 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
|
|
125
|
-
*/
|
|
126
|
-
function reportDegrade(session, logger, problem) {
|
|
127
|
-
const message = DEGRADE_PROBLEMS[problem];
|
|
128
|
-
let first = true;
|
|
129
|
-
try {
|
|
130
|
-
first = !findOmEvents(session, "om/warning").some((om) => om.data.problem === problem);
|
|
131
|
-
} catch {}
|
|
132
|
-
if (!first) return;
|
|
133
|
-
console.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
134
|
-
logger.warn(message);
|
|
135
|
-
try {
|
|
136
|
-
appendOmEvent(session, "om/warning", {
|
|
137
|
-
problem,
|
|
138
|
-
message
|
|
139
|
-
});
|
|
140
|
-
} catch (error) {
|
|
141
|
-
const text = error instanceof Error ? error.message : String(error);
|
|
142
|
-
logger.warn(`om 警告事件追加失败: ${text}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
64
|
//#endregion
|
|
146
65
|
//#region src/utils.ts
|
|
147
66
|
/** 判断值是否为普通对象(非 null、非数组)。 */
|
|
@@ -202,204 +121,66 @@ function routedTarget(session) {
|
|
|
202
121
|
} catch {}
|
|
203
122
|
}
|
|
204
123
|
//#endregion
|
|
205
|
-
//#region src/log
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
* 完整消息分四类:user(用户消息)、sys(系统消息,压缩日志中以 <sys> 空块表示)、
|
|
214
|
-
* assistant(模型输出文本)、toolcall(单个工具调用及其结果,result 按 callId 匹配并入)。
|
|
215
|
-
* index 从 0 起、按日志顺序递增、只追加不重排(压缩后旧摘要条目引用的 index 仍然有效)。
|
|
216
|
-
*/
|
|
217
|
-
/** 查找 seq 在表层节点序列中的下标(不在则返回 -1)。 */
|
|
218
|
-
function surfaceIndexOf(nodes, seq) {
|
|
219
|
-
for (let i = 0; i < nodes.length; i += 1) if (nodes[i] === seq) return i;
|
|
220
|
-
return -1;
|
|
124
|
+
//#region src/compaction-log.ts
|
|
125
|
+
/** 诊断子会话的 descriptor provider(宿主子代理列表识别用)。 */
|
|
126
|
+
const COMPACTION_LOG_PROVIDER = "om-compaction-log";
|
|
127
|
+
/** 压缩 pass 的中文标签(会话记录 label 用;未知阶段回落「压缩」)。 */
|
|
128
|
+
function phaseLabel(phase) {
|
|
129
|
+
if (phase === "observe") return "观察";
|
|
130
|
+
if (phase === "reflect") return "反思";
|
|
131
|
+
return "压缩";
|
|
221
132
|
}
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* 本插件自产消息不占位;压缩在 agent/pre-step 触发(call-result 完备),不存在未闭合调用。
|
|
226
|
-
*/
|
|
227
|
-
function indexCompleteMessages(session) {
|
|
228
|
-
const cms = [];
|
|
229
|
-
const pending = /* @__PURE__ */ new Map();
|
|
230
|
-
const events = session.events;
|
|
231
|
-
for (let seq = 0; seq < events.length; seq += 1) {
|
|
232
|
-
const event = events[seq];
|
|
233
|
-
if (!event) continue;
|
|
234
|
-
if (event.type === "user/message") {
|
|
235
|
-
const source = event.data.source;
|
|
236
|
-
if (isPluginOwnedSource(source)) continue;
|
|
237
|
-
if (source?.kind === "user") cms.push({
|
|
238
|
-
index: cms.length,
|
|
239
|
-
type: "user",
|
|
240
|
-
seqs: [seq]
|
|
241
|
-
});
|
|
242
|
-
else cms.push({
|
|
243
|
-
index: cms.length,
|
|
244
|
-
type: "sys",
|
|
245
|
-
seqs: [seq],
|
|
246
|
-
...source?.kind === void 0 ? {} : { kind: source.kind }
|
|
247
|
-
});
|
|
248
|
-
} else if (event.type === "assistant/message") {
|
|
249
|
-
const message = event.data.message;
|
|
250
|
-
if (!message || !Array.isArray(message.content)) continue;
|
|
251
|
-
let hasText = false;
|
|
252
|
-
for (const block of message.content) if (block.type === "text") {
|
|
253
|
-
hasText = true;
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
if (hasText) cms.push({
|
|
257
|
-
index: cms.length,
|
|
258
|
-
type: "assistant",
|
|
259
|
-
seqs: [seq]
|
|
260
|
-
});
|
|
261
|
-
for (const block of message.content) {
|
|
262
|
-
if (block.type !== "tool-call") continue;
|
|
263
|
-
const callId = String(block.id ?? "");
|
|
264
|
-
const cm = {
|
|
265
|
-
index: cms.length,
|
|
266
|
-
type: "toolcall",
|
|
267
|
-
seqs: [seq],
|
|
268
|
-
...callId === "" ? {} : { callId }
|
|
269
|
-
};
|
|
270
|
-
cms.push(cm);
|
|
271
|
-
if (callId !== "") pending.set(callId, cm);
|
|
272
|
-
}
|
|
273
|
-
} else if (event.type === "tool/result") {
|
|
274
|
-
const source = event.data.message?.source;
|
|
275
|
-
const callId = String(source?.callId ?? "");
|
|
276
|
-
const cm = callId === "" ? void 0 : pending.get(callId);
|
|
277
|
-
if (cm) {
|
|
278
|
-
cm.seqs.push(seq);
|
|
279
|
-
pending.delete(callId);
|
|
280
|
-
} else cms.push({
|
|
281
|
-
index: cms.length,
|
|
282
|
-
type: "toolcall",
|
|
283
|
-
seqs: [seq],
|
|
284
|
-
...callId === "" ? {} : { callId }
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return cms;
|
|
133
|
+
/** 压缩会话记录 label:成功为会话记录、失败为失败日志,均含压缩阶段与轮数。 */
|
|
134
|
+
function compressionRecordLabel(phase, rounds, success) {
|
|
135
|
+
return `OM 压缩${success ? "会话记录" : "失败日志"}(${phaseLabel(phase)} · ${rounds} 轮)`;
|
|
289
136
|
}
|
|
290
137
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
138
|
+
* 把一次压缩工具循环的完整会话消息组落盘为子会话:ctx.sessions.create 创建子会话
|
|
139
|
+
* (header origin 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、
|
|
140
|
+
* cwd 继承主会话),追加 one-shot descriptor(provider om-compaction-log,label 含
|
|
141
|
+
* 压缩阶段与轮数),逐消息原样追加(user 指令/提醒与 tool-result 为 user/message,
|
|
142
|
+
* assistant 含 tool-call 块为 assistant/message),flush 持久化,返回子会话 id。
|
|
143
|
+
* 成功与失败均调用;落盘自身绝不抛错,任何失败仅 logger.warn 并返回 undefined。
|
|
293
144
|
*/
|
|
294
|
-
function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
145
|
+
async function recordCompressionSession(ctx, parentSession, options) {
|
|
146
|
+
const logger = makeLogger(ctx, options.debug);
|
|
147
|
+
try {
|
|
148
|
+
const header = parentSession.header;
|
|
149
|
+
const child = ctx.sessions.create(SessionId(`om-compaction-log-${uuid()}`), { meta: {
|
|
150
|
+
...header.cwd === void 0 ? {} : { cwd: header.cwd },
|
|
151
|
+
parentSession: parentSession.id,
|
|
152
|
+
origin: "subagent",
|
|
153
|
+
delegationDepth: (header.delegationDepth ?? 0) + 1
|
|
154
|
+
} });
|
|
155
|
+
child.append("subagent/descriptor", {
|
|
156
|
+
version: SUBAGENT_DESCRIPTOR_VERSION,
|
|
157
|
+
mode: "one-shot",
|
|
158
|
+
provider: COMPACTION_LOG_PROVIDER,
|
|
159
|
+
label: compressionRecordLabel(options.phase, options.rounds, options.success)
|
|
160
|
+
});
|
|
161
|
+
let step = 0;
|
|
162
|
+
for (const message of options.messages) {
|
|
163
|
+
step += 1;
|
|
164
|
+
if (message.role === "assistant") child.append("assistant/message", {
|
|
165
|
+
turn: 0,
|
|
166
|
+
step,
|
|
167
|
+
message
|
|
168
|
+
}, { surfaceOp: "append" });
|
|
169
|
+
else child.append("user/message", message, { surfaceOp: "append" });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await ctx.sessions.flush(child);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
logger.warn(`压缩会话记录子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
|
|
176
|
+
}
|
|
177
|
+
return child.id;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
logger.warn(`压缩会话记录子会话落盘失败: ${message}`);
|
|
181
|
+
return;
|
|
309
182
|
}
|
|
310
183
|
}
|
|
311
|
-
/**
|
|
312
|
-
* 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
|
|
313
|
-
* user/sys 取消息原文,assistant 取文本块,toolcall 为调用块 + 结果文本
|
|
314
|
-
* (pruner 裁剪超大结果);同时收集该条完整消息携带的图片附件(含 tool-result 嵌套,
|
|
315
|
-
* pruner 裁剪掉的图片不收集)。
|
|
316
|
-
*/
|
|
317
|
-
function renderCompleteMessageParts(session, cm, pruner) {
|
|
318
|
-
const images = [];
|
|
319
|
-
if (cm.type === "user" || cm.type === "sys") {
|
|
320
|
-
const seq = cm.seqs[0];
|
|
321
|
-
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
322
|
-
const message = event ? session.deriveEventMessage(event) : null;
|
|
323
|
-
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
324
|
-
return {
|
|
325
|
-
text: message ? renderMessageText(message) : "",
|
|
326
|
-
images
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
if (cm.type === "assistant") {
|
|
330
|
-
const seq = cm.seqs[0];
|
|
331
|
-
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
332
|
-
const message = event ? session.deriveEventMessage(event) : null;
|
|
333
|
-
if (!message || !Array.isArray(message.content)) return {
|
|
334
|
-
text: "",
|
|
335
|
-
images
|
|
336
|
-
};
|
|
337
|
-
collectImageRefs(message.content, images);
|
|
338
|
-
const texts = [];
|
|
339
|
-
for (const block of message.content) if (block.type === "text") texts.push(String(block.text));
|
|
340
|
-
return {
|
|
341
|
-
text: texts.join("\n"),
|
|
342
|
-
images
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
const parts = [];
|
|
346
|
-
const callSeq = cm.seqs[0];
|
|
347
|
-
const callEvent = callSeq === void 0 ? void 0 : session.events[callSeq];
|
|
348
|
-
if (callEvent?.type === "assistant/message") {
|
|
349
|
-
const message = session.deriveEventMessage(callEvent);
|
|
350
|
-
if (message && Array.isArray(message.content)) {
|
|
351
|
-
let call;
|
|
352
|
-
for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) {
|
|
353
|
-
call = block;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
if (call) parts.push(`[tool-call ${String(call.name ?? "")} id=${String(call.id ?? "")}]\n${safeJson(call.arguments)}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
const resultSeq = cm.seqs[1];
|
|
360
|
-
const resultEvent = resultSeq === void 0 ? void 0 : session.events[resultSeq];
|
|
361
|
-
if (resultEvent?.type === "tool/result") {
|
|
362
|
-
let message = session.deriveEventMessage(resultEvent);
|
|
363
|
-
if (message && pruner?.pruneContent) {
|
|
364
|
-
const pruned = pruner.pruneContent(message.content);
|
|
365
|
-
if (pruned) message = {
|
|
366
|
-
...message,
|
|
367
|
-
content: pruned
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
371
|
-
const text = message ? renderMessageText(message) : "";
|
|
372
|
-
if (text.trim() !== "") parts.push(`[result]\n${text}`);
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
text: parts.join("\n"),
|
|
376
|
-
images
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
/** 渲染一条完整消息的文本(压缩输入与语义嵌入共用):renderCompleteMessageParts 的纯文本投影。 */
|
|
380
|
-
function renderCompleteMessage(session, cm, pruner) {
|
|
381
|
-
return renderCompleteMessageParts(session, cm, pruner).text;
|
|
382
|
-
}
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/logger.ts
|
|
385
|
-
/**
|
|
386
|
-
* 插件日志门面:step 为步骤级(debug)日志,按配置开关过滤;info/warn 始终输出。
|
|
387
|
-
* 导出 PluginLogger / makeLogger。
|
|
388
|
-
*/
|
|
389
|
-
/** 构建插件日志门面:统一加 PLUGIN_LABEL 前缀,step 按 debug 开关过滤。 */
|
|
390
|
-
function makeLogger(ctx, debug) {
|
|
391
|
-
return {
|
|
392
|
-
step(message) {
|
|
393
|
-
if (debug) ctx.logger.debug(`${PLUGIN_LABEL}: ${message}`);
|
|
394
|
-
},
|
|
395
|
-
info(message) {
|
|
396
|
-
ctx.logger.info(`${PLUGIN_LABEL}: ${message}`);
|
|
397
|
-
},
|
|
398
|
-
warn(message) {
|
|
399
|
-
ctx.logger.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
184
|
//#endregion
|
|
404
185
|
//#region node_modules/.pnpm/@xmldom+xmldom@0.9.11/node_modules/@xmldom/xmldom/lib/conventions.js
|
|
405
186
|
var require_conventions = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -7491,7 +7272,7 @@ var require_dom_parser = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
7491
7272
|
exports.onWarningStopParsing = onWarningStopParsing;
|
|
7492
7273
|
}));
|
|
7493
7274
|
//#endregion
|
|
7494
|
-
//#region src/
|
|
7275
|
+
//#region src/log-index.ts
|
|
7495
7276
|
var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
7496
7277
|
var conventions = require_conventions();
|
|
7497
7278
|
exports.assign = conventions.assign;
|
|
@@ -7531,236 +7312,237 @@ var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
7531
7312
|
exports.onErrorStopParsing = domParser.onErrorStopParsing;
|
|
7532
7313
|
exports.onWarningStopParsing = domParser.onWarningStopParsing;
|
|
7533
7314
|
})))();
|
|
7534
|
-
/** 诊断子会话的 descriptor provider(宿主子代理列表识别用)。 */
|
|
7535
|
-
const COMPACTION_LOG_PROVIDER = "om-compaction-log";
|
|
7536
|
-
/** 压缩 pass 的中文标签(诊断子会话 label 用;未知阶段回落「压缩」)。 */
|
|
7537
|
-
function phaseLabel(phase) {
|
|
7538
|
-
if (phase === "observe") return "观察";
|
|
7539
|
-
if (phase === "reflect") return "反思";
|
|
7540
|
-
return "压缩";
|
|
7541
|
-
}
|
|
7542
|
-
/** 诊断子会话 label:含压缩阶段与尝试序号。 */
|
|
7543
|
-
function compactionLogLabel(phase, attemptNo) {
|
|
7544
|
-
return `OM 压缩日志(${phaseLabel(phase)} · 第 ${attemptNo} 次尝试)`;
|
|
7545
|
-
}
|
|
7546
|
-
/** 追加一次尝试的「提示词 → 原始输出」消息组(surfaceOp append;id 为品牌类型,session.append 运行时校验)。 */
|
|
7547
|
-
function appendAttemptMessages(child, attempt, step, target) {
|
|
7548
|
-
const userMessage = {
|
|
7549
|
-
id: uuid(),
|
|
7550
|
-
role: "user",
|
|
7551
|
-
content: [{
|
|
7552
|
-
type: "text",
|
|
7553
|
-
text: attempt.prompt
|
|
7554
|
-
}],
|
|
7555
|
-
source: {
|
|
7556
|
-
kind: "plugin",
|
|
7557
|
-
plugin: PLUGIN_LABEL
|
|
7558
|
-
}
|
|
7559
|
-
};
|
|
7560
|
-
child.append("user/message", userMessage, { surfaceOp: "append" });
|
|
7561
|
-
const assistantMessage = {
|
|
7562
|
-
id: uuid(),
|
|
7563
|
-
role: "assistant",
|
|
7564
|
-
content: [{
|
|
7565
|
-
type: "text",
|
|
7566
|
-
text: attempt.rawOutput
|
|
7567
|
-
}],
|
|
7568
|
-
source: {
|
|
7569
|
-
kind: "model",
|
|
7570
|
-
provider: target.provider,
|
|
7571
|
-
model: target.model
|
|
7572
|
-
}
|
|
7573
|
-
};
|
|
7574
|
-
child.append("assistant/message", {
|
|
7575
|
-
turn: 0,
|
|
7576
|
-
step,
|
|
7577
|
-
message: assistantMessage
|
|
7578
|
-
}, { surfaceOp: "append" });
|
|
7579
|
-
}
|
|
7580
7315
|
/**
|
|
7581
|
-
*
|
|
7582
|
-
*
|
|
7583
|
-
*
|
|
7584
|
-
*
|
|
7585
|
-
*
|
|
7316
|
+
* 会话日志索引:完整消息索引与渲染。
|
|
7317
|
+
* 导出 indexCompleteMessages(完整消息四类折叠索引,recall 与摘要共用同一套编号)、
|
|
7318
|
+
* indexMessages / surfaceIndexOf / messageIdOfEvent(消息级定位辅助)、
|
|
7319
|
+
* collectImageRefs / renderCompleteMessageParts / renderCompleteMessage(完整消息渲染
|
|
7320
|
+
* 与图片附件收集)。事件日志仅追加(被遮蔽的事件仍可读,recall 依赖此性质)。
|
|
7321
|
+
*
|
|
7322
|
+
* 完整消息分四类:user(用户消息)、sys(系统消息,压缩日志中以 <sys> 空块表示)、
|
|
7323
|
+
* assistant(模型输出文本)、toolcall(单个工具调用及其结果,result 按 callId 匹配并入)。
|
|
7324
|
+
* index 从 0 起、按日志顺序递增、只追加不重排(压缩后旧摘要条目引用的 index 仍然有效)。
|
|
7586
7325
|
*/
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
const child = ctx.sessions.create(SessionId(`om-compaction-log-${uuid()}`), { meta: {
|
|
7592
|
-
...header.cwd === void 0 ? {} : { cwd: header.cwd },
|
|
7593
|
-
parentSession: parentSession.id,
|
|
7594
|
-
origin: "subagent",
|
|
7595
|
-
delegationDepth: (header.delegationDepth ?? 0) + 1
|
|
7596
|
-
} });
|
|
7597
|
-
child.append("subagent/descriptor", {
|
|
7598
|
-
version: SUBAGENT_DESCRIPTOR_VERSION,
|
|
7599
|
-
mode: "one-shot",
|
|
7600
|
-
provider: COMPACTION_LOG_PROVIDER,
|
|
7601
|
-
label: compactionLogLabel(options.phase, options.attemptNo)
|
|
7602
|
-
});
|
|
7603
|
-
appendAttemptMessages(child, options.attempt, 1, options.target);
|
|
7604
|
-
try {
|
|
7605
|
-
await ctx.sessions.flush(child);
|
|
7606
|
-
} catch (error) {
|
|
7607
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7608
|
-
logger.warn(`压缩日志子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
|
|
7609
|
-
}
|
|
7610
|
-
return child.id;
|
|
7611
|
-
} catch (error) {
|
|
7612
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7613
|
-
logger.warn(`压缩日志子会话落盘失败(第 ${options.attemptNo} 次尝试): ${message}`);
|
|
7614
|
-
return;
|
|
7615
|
-
}
|
|
7326
|
+
/** 查找 seq 在表层节点序列中的下标(不在则返回 -1)。 */
|
|
7327
|
+
function surfaceIndexOf(nodes, seq) {
|
|
7328
|
+
for (let i = 0; i < nodes.length; i += 1) if (nodes[i] === seq) return i;
|
|
7329
|
+
return -1;
|
|
7616
7330
|
}
|
|
7617
|
-
//#endregion
|
|
7618
|
-
//#region src/rate-limit.ts
|
|
7619
7331
|
/**
|
|
7620
|
-
*
|
|
7621
|
-
*
|
|
7622
|
-
*
|
|
7623
|
-
* 导出 isRateLimitError / noteRateLimit / gateRateLimit / RATE_LIMIT_WAIT_MS_DEFAULT /
|
|
7624
|
-
* resetRateLimitGate。
|
|
7332
|
+
* 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(四类,见文件头)。
|
|
7333
|
+
* 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
|
|
7334
|
+
* 本插件自产消息不占位;压缩在 agent/pre-step 触发(call-result 完备),不存在未闭合调用。
|
|
7625
7335
|
*/
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7336
|
+
function indexCompleteMessages(session) {
|
|
7337
|
+
const cms = [];
|
|
7338
|
+
const pending = /* @__PURE__ */ new Map();
|
|
7339
|
+
const events = session.events;
|
|
7340
|
+
for (let seq = 0; seq < events.length; seq += 1) {
|
|
7341
|
+
const event = events[seq];
|
|
7342
|
+
if (!event) continue;
|
|
7343
|
+
if (event.type === "user/message") {
|
|
7344
|
+
const source = event.data.source;
|
|
7345
|
+
if (isPluginOwnedSource(source)) continue;
|
|
7346
|
+
if (source?.kind === "user") cms.push({
|
|
7347
|
+
index: cms.length,
|
|
7348
|
+
type: "user",
|
|
7349
|
+
seqs: [seq]
|
|
7350
|
+
});
|
|
7351
|
+
else cms.push({
|
|
7352
|
+
index: cms.length,
|
|
7353
|
+
type: "sys",
|
|
7354
|
+
seqs: [seq],
|
|
7355
|
+
...source?.kind === void 0 ? {} : { kind: source.kind }
|
|
7356
|
+
});
|
|
7357
|
+
} else if (event.type === "assistant/message") {
|
|
7358
|
+
const message = event.data.message;
|
|
7359
|
+
if (!message || !Array.isArray(message.content)) continue;
|
|
7360
|
+
let hasText = false;
|
|
7361
|
+
for (const block of message.content) if (block.type === "text") {
|
|
7362
|
+
hasText = true;
|
|
7363
|
+
break;
|
|
7364
|
+
}
|
|
7365
|
+
if (hasText) cms.push({
|
|
7366
|
+
index: cms.length,
|
|
7367
|
+
type: "assistant",
|
|
7368
|
+
seqs: [seq]
|
|
7369
|
+
});
|
|
7370
|
+
for (const block of message.content) {
|
|
7371
|
+
if (block.type !== "tool-call") continue;
|
|
7372
|
+
const callId = String(block.id ?? "");
|
|
7373
|
+
const cm = {
|
|
7374
|
+
index: cms.length,
|
|
7375
|
+
type: "toolcall",
|
|
7376
|
+
seqs: [seq],
|
|
7377
|
+
...callId === "" ? {} : { callId }
|
|
7378
|
+
};
|
|
7379
|
+
cms.push(cm);
|
|
7380
|
+
if (callId !== "") pending.set(callId, cm);
|
|
7381
|
+
}
|
|
7382
|
+
} else if (event.type === "tool/result") {
|
|
7383
|
+
const source = event.data.message?.source;
|
|
7384
|
+
const callId = String(source?.callId ?? "");
|
|
7385
|
+
const cm = callId === "" ? void 0 : pending.get(callId);
|
|
7386
|
+
if (cm) {
|
|
7387
|
+
cm.seqs.push(seq);
|
|
7388
|
+
pending.delete(callId);
|
|
7389
|
+
} else cms.push({
|
|
7390
|
+
index: cms.length,
|
|
7391
|
+
type: "toolcall",
|
|
7392
|
+
seqs: [seq],
|
|
7393
|
+
...callId === "" ? {} : { callId }
|
|
7394
|
+
});
|
|
7642
7395
|
}
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
resolve(true);
|
|
7646
|
-
}, ms);
|
|
7647
|
-
const onAbort = () => {
|
|
7648
|
-
cleanup();
|
|
7649
|
-
resolve(false);
|
|
7650
|
-
};
|
|
7651
|
-
const cleanup = () => {
|
|
7652
|
-
clearTimeout(timer);
|
|
7653
|
-
signal?.removeEventListener("abort", onAbort);
|
|
7654
|
-
};
|
|
7655
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7656
|
-
});
|
|
7396
|
+
}
|
|
7397
|
+
return cms;
|
|
7657
7398
|
}
|
|
7658
7399
|
/**
|
|
7659
|
-
*
|
|
7660
|
-
*
|
|
7400
|
+
* 递归收集内容块中的图片附件元数据(recall 输出保留图片用):
|
|
7401
|
+
* image 块按附件元数据收集(字段不全则忽略);tool-result 块递归收集其 content。
|
|
7661
7402
|
*/
|
|
7662
|
-
|
|
7663
|
-
if (
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
if (
|
|
7667
|
-
|
|
7403
|
+
function collectImageRefs(content, out) {
|
|
7404
|
+
if (!Array.isArray(content)) return;
|
|
7405
|
+
for (const block of content) {
|
|
7406
|
+
if (!isRecord(block)) continue;
|
|
7407
|
+
if (block.type === "image") {
|
|
7408
|
+
const a = block.attachment;
|
|
7409
|
+
if (isRecord(a) && typeof a.attachmentId === "string" && a.attachmentId !== "" && typeof a.mediaType === "string" && typeof a.bytes === "number" && Number.isFinite(a.bytes) && typeof a.width === "number" && Number.isFinite(a.width) && typeof a.height === "number" && Number.isFinite(a.height)) out.push({
|
|
7410
|
+
attachmentId: a.attachmentId,
|
|
7411
|
+
mediaType: a.mediaType,
|
|
7412
|
+
bytes: a.bytes,
|
|
7413
|
+
width: a.width,
|
|
7414
|
+
height: a.height,
|
|
7415
|
+
...typeof a.name === "string" && a.name !== "" ? { name: a.name } : {}
|
|
7416
|
+
});
|
|
7417
|
+
} else if (block.type === "tool-result") collectImageRefs(block.content, out);
|
|
7668
7418
|
}
|
|
7669
|
-
return true;
|
|
7670
7419
|
}
|
|
7671
|
-
//#endregion
|
|
7672
|
-
//#region src/summarize.ts
|
|
7673
7420
|
/**
|
|
7674
|
-
*
|
|
7675
|
-
*
|
|
7676
|
-
*
|
|
7677
|
-
*
|
|
7421
|
+
* 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
|
|
7422
|
+
* user/sys 取消息原文,assistant 取文本块,toolcall 为调用块 + 结果文本
|
|
7423
|
+
* (pruner 裁剪超大结果);同时收集该条完整消息携带的图片附件(含 tool-result 嵌套,
|
|
7424
|
+
* pruner 裁剪掉的图片不收集)。
|
|
7678
7425
|
*/
|
|
7679
|
-
function
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
"
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7426
|
+
function renderCompleteMessageParts(session, cm, pruner) {
|
|
7427
|
+
const images = [];
|
|
7428
|
+
if (cm.type === "user" || cm.type === "sys") {
|
|
7429
|
+
const seq = cm.seqs[0];
|
|
7430
|
+
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
7431
|
+
const message = event ? session.deriveEventMessage(event) : null;
|
|
7432
|
+
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
7433
|
+
return {
|
|
7434
|
+
text: message ? renderMessageText(message) : "",
|
|
7435
|
+
images
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
if (cm.type === "assistant") {
|
|
7439
|
+
const seq = cm.seqs[0];
|
|
7440
|
+
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
7441
|
+
const message = event ? session.deriveEventMessage(event) : null;
|
|
7442
|
+
if (!message || !Array.isArray(message.content)) return {
|
|
7443
|
+
text: "",
|
|
7444
|
+
images
|
|
7445
|
+
};
|
|
7446
|
+
collectImageRefs(message.content, images);
|
|
7447
|
+
const texts = [];
|
|
7448
|
+
for (const block of message.content) if (block.type === "text") texts.push(String(block.text));
|
|
7449
|
+
return {
|
|
7450
|
+
text: texts.join("\n"),
|
|
7451
|
+
images
|
|
7452
|
+
};
|
|
7453
|
+
}
|
|
7454
|
+
const parts = [];
|
|
7455
|
+
const callSeq = cm.seqs[0];
|
|
7456
|
+
const callEvent = callSeq === void 0 ? void 0 : session.events[callSeq];
|
|
7457
|
+
if (callEvent?.type === "assistant/message") {
|
|
7458
|
+
const message = session.deriveEventMessage(callEvent);
|
|
7459
|
+
if (message && Array.isArray(message.content)) {
|
|
7460
|
+
let call;
|
|
7461
|
+
for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) {
|
|
7462
|
+
call = block;
|
|
7463
|
+
break;
|
|
7464
|
+
}
|
|
7465
|
+
if (call) parts.push(`[tool-call ${String(call.name ?? "")} id=${String(call.id ?? "")}]\n${safeJson(call.arguments)}`);
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
const resultSeq = cm.seqs[1];
|
|
7469
|
+
const resultEvent = resultSeq === void 0 ? void 0 : session.events[resultSeq];
|
|
7470
|
+
if (resultEvent?.type === "tool/result") {
|
|
7471
|
+
let message = session.deriveEventMessage(resultEvent);
|
|
7472
|
+
if (message && pruner?.pruneContent) {
|
|
7473
|
+
const pruned = pruner.pruneContent(message.content);
|
|
7474
|
+
if (pruned) message = {
|
|
7475
|
+
...message,
|
|
7476
|
+
content: pruned
|
|
7477
|
+
};
|
|
7478
|
+
}
|
|
7479
|
+
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
7480
|
+
const text = message ? renderMessageText(message) : "";
|
|
7481
|
+
if (text.trim() !== "") parts.push(`[result]\n${text}`);
|
|
7482
|
+
}
|
|
7483
|
+
return {
|
|
7484
|
+
text: parts.join("\n"),
|
|
7485
|
+
images
|
|
7486
|
+
};
|
|
7720
7487
|
}
|
|
7721
|
-
/**
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7488
|
+
/** 渲染一条完整消息的文本(压缩输入与语义嵌入共用):renderCompleteMessageParts 的纯文本投影。 */
|
|
7489
|
+
function renderCompleteMessage(session, cm, pruner) {
|
|
7490
|
+
return renderCompleteMessageParts(session, cm, pruner).text;
|
|
7491
|
+
}
|
|
7492
|
+
//#endregion
|
|
7493
|
+
//#region src/compress-view.ts
|
|
7494
|
+
/** 静默 DOMParser:非致命解析问题不刷 console,fatalError 仍抛 ParseError、解析语义不变。 */
|
|
7725
7495
|
function newQuietParser() {
|
|
7726
7496
|
return new import_lib.DOMParser({ onError: () => {} });
|
|
7727
7497
|
}
|
|
7728
|
-
/**
|
|
7729
|
-
|
|
7498
|
+
/**
|
|
7499
|
+
* 提取用户消息条目的文本与注释(文本块拼接为原文;图片/其他块降级为注释文本,
|
|
7500
|
+
* 渲染时输出为 XML 注释)。无任何内容返回 null(该 index 在视图中不占条目)。
|
|
7501
|
+
*/
|
|
7502
|
+
function userEntryParts(session, cm) {
|
|
7730
7503
|
const seq = cm.seqs[0];
|
|
7731
7504
|
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
7732
7505
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
7733
7506
|
if (!message || !Array.isArray(message.content)) return null;
|
|
7734
|
-
const
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
el.appendChild(doc.createTextNode(String(block.text)));
|
|
7739
|
-
hasContent = true;
|
|
7740
|
-
} else if (block.type === "image") {
|
|
7507
|
+
const texts = [];
|
|
7508
|
+
const notes = [];
|
|
7509
|
+
for (const block of message.content) if (block.type === "text") texts.push(String(block.text));
|
|
7510
|
+
else if (block.type === "image") {
|
|
7741
7511
|
const ref = block.attachment;
|
|
7742
7512
|
const name = ref?.name ? `:${ref.name}` : "";
|
|
7743
7513
|
const meta = ref ? `(${String(ref.mediaType ?? "")} ${String(ref.width ?? "")}×${String(ref.height ?? "")},${String(ref.bytes ?? "")} bytes)` : "";
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
return el;
|
|
7514
|
+
notes.push(` 图片附件${name}${meta} `);
|
|
7515
|
+
} else notes.push(` ${String(block.type)} 块 `);
|
|
7516
|
+
if (texts.length === 0 && notes.length === 0) return null;
|
|
7517
|
+
return {
|
|
7518
|
+
text: texts.join("\n"),
|
|
7519
|
+
notes
|
|
7520
|
+
};
|
|
7752
7521
|
}
|
|
7753
7522
|
/**
|
|
7754
|
-
*
|
|
7755
|
-
*
|
|
7756
|
-
* assistant/toolcall → <assistant>(原样文本);skipReasoning=false 时另把
|
|
7757
|
-
* assistant 的 reasoning → <reasoning>(参考条目)。
|
|
7758
|
-
* 文本经 XML 序列化自动转义;仅渲染 seqs 全部落在给定集合内的完整消息。
|
|
7523
|
+
* 提取 toolcall 完整消息的工具名(按 callId 在所属 assistant 消息中定位 tool-call 块)。
|
|
7524
|
+
* 找不到返回 undefined。
|
|
7759
7525
|
*/
|
|
7760
|
-
function
|
|
7526
|
+
function toolCallNameOf(session, cm) {
|
|
7527
|
+
if (cm.type !== "toolcall") return void 0;
|
|
7528
|
+
const seq = cm.seqs[0];
|
|
7529
|
+
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
7530
|
+
if (event?.type !== "assistant/message") return void 0;
|
|
7531
|
+
const message = event.data.message;
|
|
7532
|
+
if (!message || !Array.isArray(message.content)) return void 0;
|
|
7533
|
+
for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) return String(block.name ?? "");
|
|
7534
|
+
}
|
|
7535
|
+
/**
|
|
7536
|
+
* 观察视图:被压缩区间(表层 seq 集合)内的完整消息投影为条目——
|
|
7537
|
+
* user → 原文(图片等非文本块为注释)、sys → 空条目、assistant/toolcall → 原文渲染,
|
|
7538
|
+
* reasoning 作为参考条目置于其所属 assistant 条目之前(每条 assistant 消息输出一次;
|
|
7539
|
+
* skipReasoning 时省略)。
|
|
7540
|
+
* 要求区间为区间内完整消息的首尾 index。
|
|
7541
|
+
*/
|
|
7542
|
+
function buildObserveView(session, seqs, options = {}) {
|
|
7761
7543
|
const shadowed = new Set(seqs);
|
|
7762
7544
|
const reasoningBySeq = /* @__PURE__ */ new Map();
|
|
7763
|
-
|
|
7545
|
+
for (const seq of seqs) {
|
|
7764
7546
|
const event = session.events[seq];
|
|
7765
7547
|
if (event?.type !== "assistant/message") continue;
|
|
7766
7548
|
const message = event.data.message;
|
|
@@ -7769,76 +7551,661 @@ function renderMessages(session, seqs, skipReasoning = true) {
|
|
|
7769
7551
|
for (const block of message.content) if (block.type === "reasoning" && typeof block.text === "string") reasonings.push(block.text);
|
|
7770
7552
|
if (reasonings.length > 0) reasoningBySeq.set(seq, reasonings);
|
|
7771
7553
|
}
|
|
7772
|
-
const emittedReasoning = /* @__PURE__ */ new Set();
|
|
7773
|
-
const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
7774
7554
|
const entries = [];
|
|
7555
|
+
const emittedReasoning = /* @__PURE__ */ new Set();
|
|
7775
7556
|
for (const cm of indexCompleteMessages(session)) {
|
|
7776
7557
|
if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
|
|
7777
7558
|
if (cm.type === "sys") {
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7559
|
+
entries.push({
|
|
7560
|
+
kind: "sys",
|
|
7561
|
+
lo: cm.index,
|
|
7562
|
+
hi: cm.index,
|
|
7563
|
+
text: "",
|
|
7564
|
+
...cm.kind === void 0 ? {} : { sysKind: cm.kind }
|
|
7565
|
+
});
|
|
7783
7566
|
continue;
|
|
7784
7567
|
}
|
|
7785
7568
|
if (cm.type === "user") {
|
|
7786
|
-
const
|
|
7787
|
-
if (
|
|
7788
|
-
entries.push(
|
|
7789
|
-
|
|
7569
|
+
const parts = userEntryParts(session, cm);
|
|
7570
|
+
if (parts === null) continue;
|
|
7571
|
+
entries.push({
|
|
7572
|
+
kind: "user",
|
|
7573
|
+
lo: cm.index,
|
|
7574
|
+
hi: cm.index,
|
|
7575
|
+
text: parts.text,
|
|
7576
|
+
...parts.notes.length === 0 ? {} : { notes: parts.notes }
|
|
7577
|
+
});
|
|
7578
|
+
continue;
|
|
7579
|
+
}
|
|
7580
|
+
if (options.skipReasoning !== true) {
|
|
7790
7581
|
const callSeq = cm.seqs[0];
|
|
7791
7582
|
const reasonings = callSeq === void 0 ? void 0 : reasoningBySeq.get(callSeq);
|
|
7792
|
-
if (
|
|
7583
|
+
if (callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
|
|
7793
7584
|
emittedReasoning.add(callSeq);
|
|
7794
|
-
for (const text of reasonings) {
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7585
|
+
for (const text of reasonings) entries.push({
|
|
7586
|
+
kind: "reasoning",
|
|
7587
|
+
lo: cm.index,
|
|
7588
|
+
hi: cm.index,
|
|
7589
|
+
text
|
|
7590
|
+
});
|
|
7799
7591
|
}
|
|
7800
|
-
const text = renderCompleteMessage(session, cm);
|
|
7801
|
-
if (text.trim() === "") continue;
|
|
7802
|
-
const el = doc.createElement("assistant");
|
|
7803
|
-
el.setAttribute("index", String(cm.index));
|
|
7804
|
-
el.appendChild(doc.createTextNode(text));
|
|
7805
|
-
entries.push(el);
|
|
7806
7592
|
}
|
|
7593
|
+
const text = renderCompleteMessage(session, cm);
|
|
7594
|
+
if (text.trim() === "") continue;
|
|
7595
|
+
const toolName = cm.type === "toolcall" ? toolCallNameOf(session, cm) : void 0;
|
|
7596
|
+
entries.push({
|
|
7597
|
+
kind: "assistant",
|
|
7598
|
+
lo: cm.index,
|
|
7599
|
+
hi: cm.index,
|
|
7600
|
+
text,
|
|
7601
|
+
...toolName === void 0 ? {} : { toolName }
|
|
7602
|
+
});
|
|
7603
|
+
}
|
|
7604
|
+
return {
|
|
7605
|
+
entries,
|
|
7606
|
+
...viewBounds(entries)
|
|
7607
|
+
};
|
|
7608
|
+
}
|
|
7609
|
+
/** 提取视图要求区间:全部可定位条目的最小 lo 与最大 hi(无可定位条目时均 undefined)。 */
|
|
7610
|
+
function viewBounds(entries) {
|
|
7611
|
+
let minIndex;
|
|
7612
|
+
let maxIndex;
|
|
7613
|
+
for (const entry of entries) {
|
|
7614
|
+
if (entry.lo === void 0 || entry.hi === void 0) continue;
|
|
7615
|
+
if (minIndex === void 0 || entry.lo < minIndex) minIndex = entry.lo;
|
|
7616
|
+
if (maxIndex === void 0 || entry.hi > maxIndex) maxIndex = entry.hi;
|
|
7807
7617
|
}
|
|
7618
|
+
return {
|
|
7619
|
+
...minIndex === void 0 ? {} : { minIndex },
|
|
7620
|
+
...maxIndex === void 0 ? {} : { maxIndex }
|
|
7621
|
+
};
|
|
7622
|
+
}
|
|
7623
|
+
/** 提取 <history> 块的内文(去开/闭标签;非块文本原样返回)。 */
|
|
7624
|
+
function historyInner(text) {
|
|
7625
|
+
const closeTag = `</${HISTORY_TAG}>`;
|
|
7626
|
+
const close = text.lastIndexOf(closeTag);
|
|
7627
|
+
if (close === -1) return text;
|
|
7628
|
+
const open = text.indexOf(`<${HISTORY_TAG}`);
|
|
7629
|
+
if (open === -1) return text;
|
|
7630
|
+
const gt = text.indexOf(">", open);
|
|
7631
|
+
if (gt === -1 || gt >= close) return text;
|
|
7632
|
+
return text.slice(gt + 1, close);
|
|
7633
|
+
}
|
|
7634
|
+
/** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
|
|
7635
|
+
function intAttr(el, name) {
|
|
7636
|
+
const raw = el.getAttribute(name);
|
|
7637
|
+
if (raw === null || raw === "") return void 0;
|
|
7638
|
+
if (!/^\d+$/.test(raw)) return void 0;
|
|
7639
|
+
return Number(raw);
|
|
7640
|
+
}
|
|
7641
|
+
/**
|
|
7642
|
+
* 解析一个已有 <history> 块的内条目(反思视图):user_message / sys / assistant
|
|
7643
|
+
* (index 单条或 start/end 区间)/ reasoning。整块无法解析或根非 <history> 时降级为
|
|
7644
|
+
* 单条不可定位的历史遗留条目(text 为块内文原文,构建最终块时原样保留)。
|
|
7645
|
+
*/
|
|
7646
|
+
function parseBlockEntries(blockText, blockSeq) {
|
|
7647
|
+
const opaque = () => [{
|
|
7648
|
+
kind: "assistant",
|
|
7649
|
+
text: historyInner(blockText),
|
|
7650
|
+
blockSeq
|
|
7651
|
+
}];
|
|
7652
|
+
let doc;
|
|
7653
|
+
try {
|
|
7654
|
+
doc = newQuietParser().parseFromString(blockText, "text/xml");
|
|
7655
|
+
} catch {
|
|
7656
|
+
return opaque();
|
|
7657
|
+
}
|
|
7658
|
+
const root = doc.documentElement;
|
|
7659
|
+
if (!root || root.nodeName !== "history") return opaque();
|
|
7660
|
+
const entries = [];
|
|
7661
|
+
const children = root.childNodes;
|
|
7662
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
7663
|
+
const node = children[i];
|
|
7664
|
+
if (node?.nodeType !== 1) continue;
|
|
7665
|
+
const el = node;
|
|
7666
|
+
const text = el.textContent ?? "";
|
|
7667
|
+
if (el.nodeName === "user_message") {
|
|
7668
|
+
const index = intAttr(el, "index");
|
|
7669
|
+
if (index === void 0) continue;
|
|
7670
|
+
entries.push({
|
|
7671
|
+
kind: "user",
|
|
7672
|
+
lo: index,
|
|
7673
|
+
hi: index,
|
|
7674
|
+
text,
|
|
7675
|
+
blockSeq
|
|
7676
|
+
});
|
|
7677
|
+
} else if (el.nodeName === "sys") {
|
|
7678
|
+
const index = intAttr(el, "index");
|
|
7679
|
+
if (index === void 0) continue;
|
|
7680
|
+
const type = el.getAttribute("type");
|
|
7681
|
+
entries.push({
|
|
7682
|
+
kind: "sys",
|
|
7683
|
+
lo: index,
|
|
7684
|
+
hi: index,
|
|
7685
|
+
text: "",
|
|
7686
|
+
...type === null ? {} : { sysKind: type },
|
|
7687
|
+
blockSeq
|
|
7688
|
+
});
|
|
7689
|
+
} else if (el.nodeName === "assistant") {
|
|
7690
|
+
const index = intAttr(el, "index");
|
|
7691
|
+
if (index !== void 0) {
|
|
7692
|
+
entries.push({
|
|
7693
|
+
kind: "assistant",
|
|
7694
|
+
lo: index,
|
|
7695
|
+
hi: index,
|
|
7696
|
+
text,
|
|
7697
|
+
blockSeq
|
|
7698
|
+
});
|
|
7699
|
+
continue;
|
|
7700
|
+
}
|
|
7701
|
+
const start = intAttr(el, "start");
|
|
7702
|
+
const end = intAttr(el, "end");
|
|
7703
|
+
if (start !== void 0 && end !== void 0) entries.push({
|
|
7704
|
+
kind: "assistant",
|
|
7705
|
+
lo: start,
|
|
7706
|
+
hi: end,
|
|
7707
|
+
text,
|
|
7708
|
+
blockSeq
|
|
7709
|
+
});
|
|
7710
|
+
} else if (el.nodeName === "reasoning") entries.push({
|
|
7711
|
+
kind: "reasoning",
|
|
7712
|
+
text,
|
|
7713
|
+
blockSeq
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7716
|
+
if (entries.length === 0) return opaque();
|
|
7717
|
+
return entries;
|
|
7718
|
+
}
|
|
7719
|
+
/**
|
|
7720
|
+
* 反思视图:全部 <history> 块(historySection 收集,按表层顺序)的内条目投影为条目。
|
|
7721
|
+
* 要求区间为全部块内条目引用的最小 / 最大 index;块解析失败降级为不可定位遗留条目;
|
|
7722
|
+
* skipReasoning 时不含 <reasoning> 参考条目。
|
|
7723
|
+
*/
|
|
7724
|
+
function buildReflectView(blocks, options = {}) {
|
|
7725
|
+
const entries = [];
|
|
7726
|
+
for (const block of blocks) entries.push(...parseBlockEntries(block.text, block.seq));
|
|
7727
|
+
const filtered = options.skipReasoning === true ? entries.filter((entry) => entry.kind !== "reasoning") : entries;
|
|
7728
|
+
return {
|
|
7729
|
+
entries: filtered,
|
|
7730
|
+
...viewBounds(filtered)
|
|
7731
|
+
};
|
|
7732
|
+
}
|
|
7733
|
+
/**
|
|
7734
|
+
* 把一个视图条目构建为 XML 元素(文本经 DOM 文本节点自动转义;user 条目的注释
|
|
7735
|
+
* 输出为 XML 注释节点)。getHistory 输出与最终 <history> 块共用。
|
|
7736
|
+
*/
|
|
7737
|
+
function entryToElement(doc, entry) {
|
|
7738
|
+
if (entry.kind === "user") {
|
|
7739
|
+
const el = doc.createElement("user_message");
|
|
7740
|
+
if (entry.lo !== void 0) el.setAttribute("index", String(entry.lo));
|
|
7741
|
+
el.appendChild(doc.createTextNode(entry.text));
|
|
7742
|
+
for (const note of entry.notes ?? []) el.appendChild(doc.createComment(note));
|
|
7743
|
+
return el;
|
|
7744
|
+
}
|
|
7745
|
+
if (entry.kind === "sys") {
|
|
7746
|
+
const el = doc.createElement("sys");
|
|
7747
|
+
el.setAttribute("type", entry.sysKind ?? "");
|
|
7748
|
+
if (entry.lo !== void 0) el.setAttribute("index", String(entry.lo));
|
|
7749
|
+
el.appendChild(doc.createTextNode(""));
|
|
7750
|
+
return el;
|
|
7751
|
+
}
|
|
7752
|
+
if (entry.kind === "reasoning") {
|
|
7753
|
+
const el = doc.createElement("reasoning");
|
|
7754
|
+
el.appendChild(doc.createTextNode(entry.text));
|
|
7755
|
+
return el;
|
|
7756
|
+
}
|
|
7757
|
+
const el = doc.createElement("assistant");
|
|
7758
|
+
if (entry.lo !== void 0 && entry.hi !== void 0 && entry.lo === entry.hi) el.setAttribute("index", String(entry.lo));
|
|
7759
|
+
else if (entry.lo !== void 0 && entry.hi !== void 0) {
|
|
7760
|
+
el.setAttribute("start", String(entry.lo));
|
|
7761
|
+
el.setAttribute("end", String(entry.hi));
|
|
7762
|
+
}
|
|
7763
|
+
el.appendChild(doc.createTextNode(entry.text));
|
|
7764
|
+
return el;
|
|
7765
|
+
}
|
|
7766
|
+
/**
|
|
7767
|
+
* 渲染条目序列为 XML 文本(无 <history> 包裹,条目逐行拼接)——getHistory 的输出形式。
|
|
7768
|
+
*/
|
|
7769
|
+
function renderEntriesXml(entries) {
|
|
7770
|
+
if (entries.length === 0) return "";
|
|
7771
|
+
const doc = newQuietParser().parseFromString("<root />", "text/xml");
|
|
7808
7772
|
const serializer = new import_lib.XMLSerializer();
|
|
7809
|
-
return
|
|
7773
|
+
return entries.map((entry) => serializer.serializeToString(entryToElement(doc, entry))).join("\n");
|
|
7774
|
+
}
|
|
7775
|
+
/** 压缩会话的工具状态:视图 + 替换记录 + skill 二次确认标记 + 完成标记。 */
|
|
7776
|
+
var CompressionState = class CompressionState {
|
|
7777
|
+
view;
|
|
7778
|
+
replacements = [];
|
|
7779
|
+
challengedSkills = /* @__PURE__ */ new Set();
|
|
7780
|
+
_completed = false;
|
|
7781
|
+
/**
|
|
7782
|
+
* @param view 压缩视图(观察或反思),工具校验与最终块构建的数据源。
|
|
7783
|
+
*/
|
|
7784
|
+
constructor(view) {
|
|
7785
|
+
this.view = view;
|
|
7786
|
+
}
|
|
7787
|
+
/** 已成功应用的替换次数(0 表示空提交)。 */
|
|
7788
|
+
get replacementCount() {
|
|
7789
|
+
return this.replacements.length;
|
|
7790
|
+
}
|
|
7791
|
+
/** 是否已调用 completeCompression。 */
|
|
7792
|
+
get completed() {
|
|
7793
|
+
return this._completed;
|
|
7794
|
+
}
|
|
7795
|
+
/**
|
|
7796
|
+
* 执行一次压缩工具调用:解析 JSON 参数并按工具名分发。
|
|
7797
|
+
* 未知工具名或参数非法 JSON 返回错误结果,不抛出。
|
|
7798
|
+
*/
|
|
7799
|
+
executeCall(name, rawArgs) {
|
|
7800
|
+
if (name === "getHistory" || name === "compressHistory" || name === "completeCompression") {
|
|
7801
|
+
let args;
|
|
7802
|
+
if (typeof rawArgs === "string" && rawArgs.trim() === "") args = {};
|
|
7803
|
+
else try {
|
|
7804
|
+
args = JSON.parse(rawArgs);
|
|
7805
|
+
} catch {
|
|
7806
|
+
return {
|
|
7807
|
+
text: `参数不是合法的 JSON:${rawArgs.slice(0, 200)}`,
|
|
7808
|
+
isError: true
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
const record = args ?? {};
|
|
7812
|
+
if (name === "getHistory") return this.getHistory(record);
|
|
7813
|
+
if (name === "compressHistory") return this.compressHistory(record);
|
|
7814
|
+
return this.complete();
|
|
7815
|
+
}
|
|
7816
|
+
return {
|
|
7817
|
+
text: `未知工具 ${name}(可用:getHistory / compressHistory / completeCompression)`,
|
|
7818
|
+
isError: true
|
|
7819
|
+
};
|
|
7820
|
+
}
|
|
7821
|
+
/** 解析可选数值参数(缺省 undefined;非有限数返回 NaN)。 */
|
|
7822
|
+
optionalNumber(value) {
|
|
7823
|
+
if (value === void 0 || value === null) return void 0;
|
|
7824
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return NaN;
|
|
7825
|
+
return Math.floor(value);
|
|
7826
|
+
}
|
|
7827
|
+
/**
|
|
7828
|
+
* getHistory:返回 [start..end] 区间内的条目(压缩视图,无 <history> 包裹)。
|
|
7829
|
+
* start/end 缺省为要求区间首尾;越界或 start > end 返回错误。区间切入已有压缩
|
|
7830
|
+
* 块(反思视图)时返回该块全部条目。
|
|
7831
|
+
*/
|
|
7832
|
+
getHistory(args) {
|
|
7833
|
+
const { minIndex, maxIndex } = this.view;
|
|
7834
|
+
if (minIndex === void 0 || maxIndex === void 0) return {
|
|
7835
|
+
text: "当前压缩区间没有可定位的条目",
|
|
7836
|
+
isError: true
|
|
7837
|
+
};
|
|
7838
|
+
const startRaw = this.optionalNumber(args.start);
|
|
7839
|
+
const endRaw = this.optionalNumber(args.end);
|
|
7840
|
+
if (startRaw !== void 0 && Number.isNaN(startRaw)) return {
|
|
7841
|
+
text: "start 必须是数字",
|
|
7842
|
+
isError: true
|
|
7843
|
+
};
|
|
7844
|
+
if (endRaw !== void 0 && Number.isNaN(endRaw)) return {
|
|
7845
|
+
text: "end 必须是数字",
|
|
7846
|
+
isError: true
|
|
7847
|
+
};
|
|
7848
|
+
const start = startRaw ?? minIndex;
|
|
7849
|
+
const end = endRaw ?? maxIndex;
|
|
7850
|
+
if (start < minIndex || start > maxIndex) return {
|
|
7851
|
+
text: `start ${start} 越界(要求区间 [${minIndex}..${maxIndex}])`,
|
|
7852
|
+
isError: true
|
|
7853
|
+
};
|
|
7854
|
+
if (end < minIndex || end > maxIndex) return {
|
|
7855
|
+
text: `end ${end} 越界(要求区间 [${minIndex}..${maxIndex}])`,
|
|
7856
|
+
isError: true
|
|
7857
|
+
};
|
|
7858
|
+
if (start > end) return {
|
|
7859
|
+
text: `start ${start} 不能大于 end ${end}`,
|
|
7860
|
+
isError: true
|
|
7861
|
+
};
|
|
7862
|
+
const intersects = (entry) => entry.lo !== void 0 && entry.hi !== void 0 && entry.lo <= end && entry.hi >= start;
|
|
7863
|
+
const expandedBlocks = /* @__PURE__ */ new Set();
|
|
7864
|
+
for (const entry of this.view.entries) if (entry.blockSeq !== void 0 && intersects(entry)) expandedBlocks.add(entry.blockSeq);
|
|
7865
|
+
const selected = this.view.entries.filter((entry) => entry.blockSeq !== void 0 ? expandedBlocks.has(entry.blockSeq) : intersects(entry));
|
|
7866
|
+
if (selected.length === 0) return {
|
|
7867
|
+
text: `区间 [${start}..${end}] 没有条目`,
|
|
7868
|
+
isError: false
|
|
7869
|
+
};
|
|
7870
|
+
return {
|
|
7871
|
+
text: `<!-- 完整消息区间 [${start}..${end}],共 ${selected.length} 条 -->\n${renderEntriesXml(selected)}`,
|
|
7872
|
+
isError: false
|
|
7873
|
+
};
|
|
7874
|
+
}
|
|
7875
|
+
/** 区间的简短描述(单条 index / 区间 start..end)。 */
|
|
7876
|
+
static spanLabel(lo, hi) {
|
|
7877
|
+
return lo === hi ? `完整消息 index ${lo}` : `完整消息区间 [${lo}..${hi}]`;
|
|
7878
|
+
}
|
|
7879
|
+
/**
|
|
7880
|
+
* compressHistory:把 index 单条或 start..end 连续区间的 assistant 条目替换为
|
|
7881
|
+
* content 摘要。校验失败返回错误结果(不应用);skill 块首次被覆盖返回要求
|
|
7882
|
+
* 重新思考的错误结果(不应用,标记已挑战);通过后记录替换并覆盖被完全包含
|
|
7883
|
+
* 的旧替换。
|
|
7884
|
+
*/
|
|
7885
|
+
compressHistory(args) {
|
|
7886
|
+
const { minIndex, maxIndex } = this.view;
|
|
7887
|
+
if (minIndex === void 0 || maxIndex === void 0) return {
|
|
7888
|
+
text: "当前压缩区间没有可定位的条目",
|
|
7889
|
+
isError: true
|
|
7890
|
+
};
|
|
7891
|
+
const content = args.content;
|
|
7892
|
+
if (typeof content !== "string" || content.trim() === "") return {
|
|
7893
|
+
text: "content 必须是非空摘要文本",
|
|
7894
|
+
isError: true
|
|
7895
|
+
};
|
|
7896
|
+
const hasIndex = args.index !== void 0 && args.index !== null;
|
|
7897
|
+
const hasStart = args.start !== void 0 && args.start !== null;
|
|
7898
|
+
const hasEnd = args.end !== void 0 && args.end !== null;
|
|
7899
|
+
if (hasIndex && (hasStart || hasEnd)) return {
|
|
7900
|
+
text: "index 与 start/end 不能同时提供",
|
|
7901
|
+
isError: true
|
|
7902
|
+
};
|
|
7903
|
+
if (hasStart !== hasEnd) return {
|
|
7904
|
+
text: "start 与 end 必须成对提供",
|
|
7905
|
+
isError: true
|
|
7906
|
+
};
|
|
7907
|
+
let lo;
|
|
7908
|
+
let hi;
|
|
7909
|
+
if (hasIndex) {
|
|
7910
|
+
const index = args.index;
|
|
7911
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) return {
|
|
7912
|
+
text: "index 必须是非负整数",
|
|
7913
|
+
isError: true
|
|
7914
|
+
};
|
|
7915
|
+
lo = index;
|
|
7916
|
+
hi = index;
|
|
7917
|
+
} else if (hasStart) {
|
|
7918
|
+
const start = args.start;
|
|
7919
|
+
const end = args.end;
|
|
7920
|
+
if (typeof start !== "number" || typeof end !== "number" || !Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < 0) return {
|
|
7921
|
+
text: "start 与 end 必须是非负整数",
|
|
7922
|
+
isError: true
|
|
7923
|
+
};
|
|
7924
|
+
if (start > end) return {
|
|
7925
|
+
text: `start ${start} 不能大于 end ${end}`,
|
|
7926
|
+
isError: true
|
|
7927
|
+
};
|
|
7928
|
+
lo = start;
|
|
7929
|
+
hi = end;
|
|
7930
|
+
} else return {
|
|
7931
|
+
text: "index 与 start/end 至少提供一个",
|
|
7932
|
+
isError: true
|
|
7933
|
+
};
|
|
7934
|
+
if (lo < minIndex || hi > maxIndex) return {
|
|
7935
|
+
text: `${CompressionState.spanLabel(lo, hi)} 超出要求的压缩区间 [${minIndex}..${maxIndex}]`,
|
|
7936
|
+
isError: true
|
|
7937
|
+
};
|
|
7938
|
+
let insideCount = 0;
|
|
7939
|
+
for (const entry of this.view.entries) {
|
|
7940
|
+
if (entry.lo === void 0 || entry.hi === void 0) continue;
|
|
7941
|
+
if (entry.lo > hi || entry.hi < lo) continue;
|
|
7942
|
+
if (entry.kind === "user") return {
|
|
7943
|
+
text: `${CompressionState.spanLabel(lo, hi)} 覆盖用户消息(index ${entry.lo}),用户消息不可压缩`,
|
|
7944
|
+
isError: true
|
|
7945
|
+
};
|
|
7946
|
+
if (entry.kind === "sys") return {
|
|
7947
|
+
text: `${CompressionState.spanLabel(lo, hi)} 覆盖系统消息(index ${entry.lo}),系统消息不可压缩`,
|
|
7948
|
+
isError: true
|
|
7949
|
+
};
|
|
7950
|
+
if (entry.kind === "assistant" && !(entry.lo >= lo && entry.hi <= hi)) return {
|
|
7951
|
+
text: `${CompressionState.spanLabel(lo, hi)} 与 assistant 条目 [${entry.lo}..${entry.hi}] 部分重叠,需完整包含或不相交`,
|
|
7952
|
+
isError: true
|
|
7953
|
+
};
|
|
7954
|
+
if (entry.kind === "assistant") insideCount += 1;
|
|
7955
|
+
}
|
|
7956
|
+
if (insideCount === 0) return {
|
|
7957
|
+
text: `${CompressionState.spanLabel(lo, hi)} 内没有可压缩的 assistant 条目`,
|
|
7958
|
+
isError: true
|
|
7959
|
+
};
|
|
7960
|
+
for (const rep of this.replacements) if (rep.lo <= hi && rep.hi >= lo && !(rep.lo >= lo && rep.hi <= hi)) return {
|
|
7961
|
+
text: `${CompressionState.spanLabel(lo, hi)} 与已有替换区间 [${rep.lo}..${rep.hi}] 部分重叠,只能完全包含或不相交`,
|
|
7962
|
+
isError: true
|
|
7963
|
+
};
|
|
7964
|
+
const unchallenged = [];
|
|
7965
|
+
for (const entry of this.view.entries) {
|
|
7966
|
+
if (entry.kind !== "assistant" || entry.toolName !== "skill") continue;
|
|
7967
|
+
if (entry.lo === void 0 || entry.lo < lo || entry.lo > hi) continue;
|
|
7968
|
+
if (!this.challengedSkills.has(entry.lo)) unchallenged.push(entry.lo);
|
|
7969
|
+
}
|
|
7970
|
+
if (unchallenged.length > 0) {
|
|
7971
|
+
for (const index of unchallenged) this.challengedSkills.add(index);
|
|
7972
|
+
return {
|
|
7973
|
+
text: `${CompressionState.spanLabel(lo, hi)} 包含 skill 加载(完整消息 index ${unchallenged.join("、")})。请重新思考该 skill 是否确定与后续任务无关:确定不相关时再次调用 compressHistory 压缩该区间;不确定或相关时不要压缩该区间,保持原样即可`,
|
|
7974
|
+
isError: true
|
|
7975
|
+
};
|
|
7976
|
+
}
|
|
7977
|
+
for (let i = this.replacements.length - 1; i >= 0; i -= 1) {
|
|
7978
|
+
const rep = this.replacements[i];
|
|
7979
|
+
if (rep === void 0) continue;
|
|
7980
|
+
if (rep.lo >= lo && rep.hi <= hi) this.replacements.splice(i, 1);
|
|
7981
|
+
}
|
|
7982
|
+
this.replacements.push({
|
|
7983
|
+
lo,
|
|
7984
|
+
hi,
|
|
7985
|
+
content
|
|
7986
|
+
});
|
|
7987
|
+
return {
|
|
7988
|
+
text: `已压缩${CompressionState.spanLabel(lo, hi)}(${insideCount} 条 assistant 条目)`,
|
|
7989
|
+
isError: false
|
|
7990
|
+
};
|
|
7991
|
+
}
|
|
7992
|
+
/** completeCompression:标记压缩完成(调用后压缩会话立即停止)。 */
|
|
7993
|
+
complete() {
|
|
7994
|
+
this._completed = true;
|
|
7995
|
+
return {
|
|
7996
|
+
text: "压缩完成",
|
|
7997
|
+
isError: false
|
|
7998
|
+
};
|
|
7999
|
+
}
|
|
8000
|
+
/**
|
|
8001
|
+
* 构建最终 <history> 块:按 index 顺序合并视图条目与替换记录——user / sys 条目
|
|
8002
|
+
* 原样、被替换区间生成 <assistant index|start end> 摘要条目(content 转义嵌入)、
|
|
8003
|
+
* 未替换 assistant 条目原样保留、reasoning 不进产物;块首为格式说明注释,开标签
|
|
8004
|
+
* 携带 tip 属性。产物为合法 XML,无需校验。
|
|
8005
|
+
*/
|
|
8006
|
+
buildFinalBlock() {
|
|
8007
|
+
const doc = new import_lib.DOMParser({ onError: () => {} }).parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
8008
|
+
const root = doc.documentElement;
|
|
8009
|
+
if (!root) return "";
|
|
8010
|
+
const coveredBy = (entry) => entry.lo !== void 0 && entry.hi !== void 0 && this.replacements.some((rep) => entry.lo !== void 0 && entry.hi !== void 0 && entry.lo >= rep.lo && entry.hi <= rep.hi);
|
|
8011
|
+
const reps = [...this.replacements].sort((a, b) => a.lo - b.lo);
|
|
8012
|
+
let nextRep = 0;
|
|
8013
|
+
const emitReplacement = (rep) => {
|
|
8014
|
+
const el = doc.createElement("assistant");
|
|
8015
|
+
if (rep.lo === rep.hi) el.setAttribute("index", String(rep.lo));
|
|
8016
|
+
else {
|
|
8017
|
+
el.setAttribute("start", String(rep.lo));
|
|
8018
|
+
el.setAttribute("end", String(rep.hi));
|
|
8019
|
+
}
|
|
8020
|
+
el.appendChild(doc.createTextNode(rep.content));
|
|
8021
|
+
root.appendChild(el);
|
|
8022
|
+
};
|
|
8023
|
+
const flushReplacementsBefore = (lo) => {
|
|
8024
|
+
while (nextRep < reps.length) {
|
|
8025
|
+
const rep = reps[nextRep];
|
|
8026
|
+
if (rep === void 0) break;
|
|
8027
|
+
if (lo !== void 0 && rep.lo >= lo) break;
|
|
8028
|
+
emitReplacement(rep);
|
|
8029
|
+
nextRep += 1;
|
|
8030
|
+
}
|
|
8031
|
+
};
|
|
8032
|
+
for (const entry of this.view.entries) {
|
|
8033
|
+
if (entry.kind === "reasoning") continue;
|
|
8034
|
+
if (coveredBy(entry)) continue;
|
|
8035
|
+
flushReplacementsBefore(entry.lo);
|
|
8036
|
+
root.appendChild(entryToElement(doc, entry));
|
|
8037
|
+
}
|
|
8038
|
+
flushReplacementsBefore(void 0);
|
|
8039
|
+
const serializer = new import_lib.XMLSerializer();
|
|
8040
|
+
const inner = Array.from(root.childNodes).map((node) => serializer.serializeToString(node)).join("\n");
|
|
8041
|
+
return `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${inner}\n</${HISTORY_TAG}>`;
|
|
8042
|
+
}
|
|
8043
|
+
};
|
|
8044
|
+
/** 压缩会话工具的 wire 定义(GenerateOptions.tools)。 */
|
|
8045
|
+
const COMPRESSION_TOOL_SCHEMAS = [
|
|
8046
|
+
{
|
|
8047
|
+
name: "getHistory",
|
|
8048
|
+
description: "查看压缩区间内的历史条目。返回压缩视图:已被压缩的内容以摘要条目呈现,区间切入已压缩块时返回该块全部条目。start/end 缺省为要求区间的第一个/最后一个完整消息 index,必须在要求区间内。",
|
|
8049
|
+
parameters: {
|
|
8050
|
+
type: "object",
|
|
8051
|
+
properties: {
|
|
8052
|
+
start: {
|
|
8053
|
+
type: "number",
|
|
8054
|
+
description: "区间起始完整消息 index(缺省为要求区间第一个)"
|
|
8055
|
+
},
|
|
8056
|
+
end: {
|
|
8057
|
+
type: "number",
|
|
8058
|
+
description: "区间结束完整消息 index(缺省为要求区间最后一个)"
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
}
|
|
8062
|
+
},
|
|
8063
|
+
{
|
|
8064
|
+
name: "compressHistory",
|
|
8065
|
+
description: "把 index 单条或 start..end 连续区间的 assistant 类条目(模型输出文本、工具调用)替换为 content 摘要。index 与 start/end 二选一,start 与 end 成对提供且 start==end 等同 index。区间不得覆盖用户消息或系统消息,不得与已有替换区间部分重叠(完全包含则覆盖)。用户消息与系统消息保持原样,无需处理。",
|
|
8066
|
+
parameters: {
|
|
8067
|
+
type: "object",
|
|
8068
|
+
properties: {
|
|
8069
|
+
index: {
|
|
8070
|
+
type: "number",
|
|
8071
|
+
description: "单条完整消息 index"
|
|
8072
|
+
},
|
|
8073
|
+
start: {
|
|
8074
|
+
type: "number",
|
|
8075
|
+
description: "区间起始完整消息 index(与 end 成对提供)"
|
|
8076
|
+
},
|
|
8077
|
+
end: {
|
|
8078
|
+
type: "number",
|
|
8079
|
+
description: "区间结束完整消息 index(与 start 成对提供)"
|
|
8080
|
+
},
|
|
8081
|
+
content: {
|
|
8082
|
+
type: "string",
|
|
8083
|
+
description: "替换后的摘要文本(纯文本)"
|
|
8084
|
+
}
|
|
8085
|
+
},
|
|
8086
|
+
required: ["content"]
|
|
8087
|
+
}
|
|
8088
|
+
},
|
|
8089
|
+
{
|
|
8090
|
+
name: "completeCompression",
|
|
8091
|
+
description: "全部压缩完成后调用,立即结束压缩会话。未压缩的条目将原样保留,允许不压缩任何内容直接完成。",
|
|
8092
|
+
parameters: {
|
|
8093
|
+
type: "object",
|
|
8094
|
+
properties: {}
|
|
8095
|
+
}
|
|
8096
|
+
}
|
|
8097
|
+
];
|
|
8098
|
+
//#endregion
|
|
8099
|
+
//#region src/rate-limit.ts
|
|
8100
|
+
/**
|
|
8101
|
+
* 全局限流门(插件进程级共享状态):任一摘要请求遇 429 后进入冷却期,
|
|
8102
|
+
* 此后所有摘要请求发出前先等待到「最近一次 429 + rateLimitWaitMs」之后。
|
|
8103
|
+
* 等待期间 signal 中止则立即放弃;等待期间的新 429 顺延冷却期。
|
|
8104
|
+
* 导出 isRateLimitError / noteRateLimit / gateRateLimit / RATE_LIMIT_WAIT_MS_DEFAULT /
|
|
8105
|
+
* resetRateLimitGate。
|
|
8106
|
+
*/
|
|
8107
|
+
/** 最近一次 429 限流的时间戳(ms;null = 未遇过限流,门直接放行)。 */
|
|
8108
|
+
let lastRateLimitAt = null;
|
|
8109
|
+
/** 判定错误信息是否为 429 限流(匹配 429 或 rate limit,大小写不敏感)。 */
|
|
8110
|
+
function isRateLimitError(message) {
|
|
8111
|
+
return /\b429\b|rate[\s_-]?limit/i.test(message);
|
|
8112
|
+
}
|
|
8113
|
+
/** 记录一次 429 限流:把冷却期起点更新为当前时间。 */
|
|
8114
|
+
function noteRateLimit() {
|
|
8115
|
+
lastRateLimitAt = Date.now();
|
|
8116
|
+
}
|
|
8117
|
+
/** 可中止延时:等待满 ms 返回 true;等待期间 signal 中止返回 false。 */
|
|
8118
|
+
function delay(ms, signal) {
|
|
8119
|
+
return new Promise((resolve) => {
|
|
8120
|
+
if (signal?.aborted) {
|
|
8121
|
+
resolve(false);
|
|
8122
|
+
return;
|
|
8123
|
+
}
|
|
8124
|
+
const timer = setTimeout(() => {
|
|
8125
|
+
cleanup();
|
|
8126
|
+
resolve(true);
|
|
8127
|
+
}, ms);
|
|
8128
|
+
const onAbort = () => {
|
|
8129
|
+
cleanup();
|
|
8130
|
+
resolve(false);
|
|
8131
|
+
};
|
|
8132
|
+
const cleanup = () => {
|
|
8133
|
+
clearTimeout(timer);
|
|
8134
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8135
|
+
};
|
|
8136
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8137
|
+
});
|
|
8138
|
+
}
|
|
8139
|
+
/**
|
|
8140
|
+
* 限流等待门:处于 429 冷却期时等待到期限再放行(返回 true);
|
|
8141
|
+
* 等待期间 signal 中止返回 false。未遇过限流或冷却期已过立即放行。waitMs ≤ 0 视为不限流。
|
|
8142
|
+
*/
|
|
8143
|
+
async function gateRateLimit(waitMs, signal) {
|
|
8144
|
+
if (waitMs <= 0) return true;
|
|
8145
|
+
while (lastRateLimitAt !== null) {
|
|
8146
|
+
const remaining = lastRateLimitAt + waitMs - Date.now();
|
|
8147
|
+
if (remaining <= 0) return true;
|
|
8148
|
+
if (!await delay(remaining, signal)) return false;
|
|
8149
|
+
}
|
|
8150
|
+
return true;
|
|
8151
|
+
}
|
|
8152
|
+
//#endregion
|
|
8153
|
+
//#region src/compress-loop.ts
|
|
8154
|
+
/**
|
|
8155
|
+
* 工具驱动的压缩循环:以新会话方式直连 ctx.llm.stream(),模型通过 getHistory /
|
|
8156
|
+
* compressHistory / completeCompression 三个工具完成压缩,替代直出 <history> 块。
|
|
8157
|
+
* 导出 runCompressionLoop / buildCompressionPrompt / buildCompressionTaskText /
|
|
8158
|
+
* CompressionLoopOptions / CompressionOutcome / COMPRESSION_NUDGE_TEXT。
|
|
8159
|
+
*
|
|
8160
|
+
* - 首条 user 消息仅含压缩指令与 start/end 区间(buildCompressionTaskText),不含
|
|
8161
|
+
* 历史消息内容;共享压缩提示词作为 system
|
|
8162
|
+
* - 每轮请求携带压缩工具 schemas(purpose='compaction',maxTokens 沿用配置);流经
|
|
8163
|
+
* BlockAssembler 组装为 assistant 消息,工具执行结果以 tool-result 消息回填
|
|
8164
|
+
* - completeCompression 调用后立即停止(同轮后续工具调用不再执行),最终 <history>
|
|
8165
|
+
* 块由 CompressionState 构建,全程无需整块校验
|
|
8166
|
+
* - 模型输出纯文本(无工具调用)时追加提醒消息继续,连续 2 轮仍无工具调用判失败
|
|
8167
|
+
* - 429 限流走全局限流等待门(gateRateLimit / noteRateLimit);其余请求级错误依赖
|
|
8168
|
+
* dsh 运行时重试,插件不做整体重试,错误直接判失败
|
|
8169
|
+
* - signal 中止标记 aborted;token usage 汇总全部轮次
|
|
8170
|
+
* - 成功与失败均把循环消息组原样落盘为子会话(recordCompressionSession),成功记
|
|
8171
|
+
* 录 sessionId 于日志,失败记录作为诊断子会话 id 向上传播
|
|
8172
|
+
*/
|
|
8173
|
+
/**
|
|
8174
|
+
* 共享压缩提示词(观察/反思同一套):完整消息定义、工具语义、压缩要求、skill 规则
|
|
8175
|
+
* 与提交方式。作为压缩会话的 system;skipReasoning=true 时省略 <reasoning> 说明行。
|
|
8176
|
+
*/
|
|
8177
|
+
function buildCompressionPrompt(skipReasoning) {
|
|
8178
|
+
return [
|
|
8179
|
+
"压缩历史消息为摘要。你应当通过工具查看、压缩并完成提交。",
|
|
8180
|
+
"",
|
|
8181
|
+
`【完整消息定义】${COMPLETE_MESSAGE_DEFINITION}`,
|
|
8182
|
+
"",
|
|
8183
|
+
"【工具】",
|
|
8184
|
+
"- getHistory(option?: {start?, end?}):查看压缩区间内的历史条目。start/end 缺省为要求区间的第一个/最后一个完整消息 index,必须在要求区间内。返回压缩视图:已压缩内容以摘要条目呈现,区间切入已压缩块时返回整块,不带 <history> 包裹。",
|
|
8185
|
+
"- compressHistory(option?: {index?, start?, end?, content}):把 index 单条或 start..end 连续区间的 assistant 类条目替换为 content 摘要(纯文本)。index 与 start/end 二选一,start==end 等同 index。区间不得覆盖用户消息或系统消息;与已有替换区间部分重叠会被拒绝,完全包含则覆盖。",
|
|
8186
|
+
"- completeCompression():全部压缩完成后调用,立即结束。",
|
|
8187
|
+
"",
|
|
8188
|
+
"【压缩要求】",
|
|
8189
|
+
"- 先用 getHistory 查看区间内容,再划分模块分批压缩;用户消息与系统消息不可压缩、保持原样,无需处理。",
|
|
8190
|
+
...skipReasoning ? [] : ["- <reasoning> 仅作压缩参考,不进产物。"],
|
|
8191
|
+
"- 将具有关联性的 assistant 消息按内在逻辑连贯性划分为连续模块,聚合为区间压缩(start..end):content 描述模块的目的、行为与结果;涉及的具体文件保留在内容中,多个前缀相同的路径合并简写。",
|
|
8192
|
+
"- 单条重要的完整消息以 index 单独压缩,内容不受限制。",
|
|
8193
|
+
"- 摘要粒度越往后越细:靠近末尾(最近)的完整消息保留更多细节(关键文件、改动与结论),开头(较早)的完整消息可适当从简。",
|
|
8194
|
+
"- 加载的 skill 属于关键信息(通过工具名为 skill 的调用识别)。不能判断该 skill 是否与后续任务相关时,不要压缩它:首次尝试压缩 skill 块会被要求再次思考,确定不相关才继续,其余情况保持原样。",
|
|
8195
|
+
"- 未压缩的条目将原样保留;宁可保留也不要强行压缩不确定的内容。",
|
|
8196
|
+
"- 全部完成后调用 completeCompression 结束;不要输出与工具调用无关的文本。"
|
|
8197
|
+
].join("\n");
|
|
7810
8198
|
}
|
|
7811
|
-
/**
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
break;
|
|
7822
|
-
case "usage":
|
|
7823
|
-
this._usage = chunk.usage;
|
|
7824
|
-
break;
|
|
7825
|
-
case "finish": this._finish = chunk.reason;
|
|
7826
|
-
}
|
|
7827
|
-
}
|
|
7828
|
-
/** 拼接后的文本输出。 */
|
|
7829
|
-
get text() {
|
|
7830
|
-
return this.textBuf;
|
|
7831
|
-
}
|
|
7832
|
-
/** 摘要 token usage(无则 undefined)。 */
|
|
7833
|
-
get usage() {
|
|
7834
|
-
return this._usage;
|
|
7835
|
-
}
|
|
7836
|
-
/** 终止原因(流未给出 finish 时视为 stop)。 */
|
|
7837
|
-
get finish() {
|
|
7838
|
-
return this._finish ?? { kind: "stop" };
|
|
7839
|
-
}
|
|
7840
|
-
};
|
|
7841
|
-
/** 构造插件自产 user 消息(摘要调用的输入消息;id 为品牌类型 MessageId)。 */
|
|
8199
|
+
/**
|
|
8200
|
+
* 构建压缩会话首条 user 消息文本(压缩指令 + start/end 区间,不含历史消息内容)。
|
|
8201
|
+
*/
|
|
8202
|
+
function buildCompressionTaskText(phase, start, end) {
|
|
8203
|
+
if (phase === "reflect") return `合并压缩全部 <history> 块,完整消息区间 [${start}..${end}]:用 getHistory 查看已有块条目,用 compressHistory 重新压缩合并 assistant 条目,完成后调用 completeCompression。`;
|
|
8204
|
+
return `压缩完整消息区间 [${start}..${end}]:用 getHistory 查看条目,用 compressHistory 分批压缩 assistant 条目,完成后调用 completeCompression。`;
|
|
8205
|
+
}
|
|
8206
|
+
/** 模型输出纯文本(无工具调用)时追加的提醒消息文本。 */
|
|
8207
|
+
const COMPRESSION_NUDGE_TEXT = "请通过工具执行压缩:用 getHistory 查看区间条目,用 compressHistory 压缩 assistant 条目,完成后调用 completeCompression。不要输出与工具调用无关的文本。";
|
|
8208
|
+
/** 构造插件自产 user 消息(压缩指令/提醒;id 为品牌类型 MessageId)。 */
|
|
7842
8209
|
function makePluginUserMessage(text) {
|
|
7843
8210
|
return {
|
|
7844
8211
|
id: uuid(),
|
|
@@ -7853,351 +8220,277 @@ function makePluginUserMessage(text) {
|
|
|
7853
8220
|
}
|
|
7854
8221
|
};
|
|
7855
8222
|
}
|
|
8223
|
+
/** 把工具执行结果封装为 tool-result 消息(回填到压缩会话)。 */
|
|
8224
|
+
function toolResultOf(call, result) {
|
|
8225
|
+
return createToolResultMessage({
|
|
8226
|
+
callId: call.id,
|
|
8227
|
+
content: [{
|
|
8228
|
+
type: "text",
|
|
8229
|
+
text: result.text
|
|
8230
|
+
}],
|
|
8231
|
+
isError: result.isError
|
|
8232
|
+
});
|
|
8233
|
+
}
|
|
7856
8234
|
/**
|
|
7857
|
-
*
|
|
7858
|
-
*
|
|
8235
|
+
* 把本轮已组装的部分 assistant 输出计入会话记录(error 终态时的诊断价值)。
|
|
8236
|
+
* 无任何文本/工具调用块(空流)时不追加。
|
|
7859
8237
|
*/
|
|
7860
|
-
function
|
|
7861
|
-
|
|
8238
|
+
function pushPartialAssistant(assembler, messages, target) {
|
|
8239
|
+
const blocks = assembler.blocks();
|
|
8240
|
+
if (blocks.length === 0) return;
|
|
8241
|
+
if (blocks.every((block) => block.type === "text" && block.text.trim() === "")) return;
|
|
8242
|
+
messages.push(assembler.message({
|
|
8243
|
+
kind: "model",
|
|
7862
8244
|
provider: target.provider,
|
|
7863
|
-
model: target.model
|
|
7864
|
-
|
|
7865
|
-
sessionId: session.id,
|
|
7866
|
-
purpose: "compaction",
|
|
7867
|
-
...signal === void 0 ? {} : { signal },
|
|
7868
|
-
system: instruction,
|
|
7869
|
-
messages: [makePluginUserMessage(contextText ?? "")]
|
|
7870
|
-
};
|
|
8245
|
+
model: target.model
|
|
8246
|
+
}));
|
|
7871
8247
|
}
|
|
7872
|
-
/**
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
8248
|
+
/** 累加两份 token usage(可选字段任一存在即求和保留)。 */
|
|
8249
|
+
function addUsage(total, add) {
|
|
8250
|
+
if (total === void 0) return { ...add };
|
|
8251
|
+
const sum = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
|
|
8252
|
+
const usage = {
|
|
8253
|
+
inputTokens: total.inputTokens + add.inputTokens,
|
|
8254
|
+
outputTokens: total.outputTokens + add.outputTokens
|
|
8255
|
+
};
|
|
8256
|
+
const cacheRead = sum(total.cacheReadTokens, add.cacheReadTokens);
|
|
8257
|
+
const cacheWrite = sum(total.cacheWriteTokens, add.cacheWriteTokens);
|
|
8258
|
+
const reasoning = sum(total.reasoningTokens, add.reasoningTokens);
|
|
8259
|
+
if (cacheRead !== void 0) usage.cacheReadTokens = cacheRead;
|
|
8260
|
+
if (cacheWrite !== void 0) usage.cacheWriteTokens = cacheWrite;
|
|
8261
|
+
if (reasoning !== void 0) usage.reasoningTokens = reasoning;
|
|
8262
|
+
return usage;
|
|
7881
8263
|
}
|
|
7882
|
-
/**
|
|
7883
|
-
function
|
|
7884
|
-
|
|
7885
|
-
if (raw === null || raw === "") return void 0;
|
|
7886
|
-
if (!/^\d+$/.test(raw)) return void 0;
|
|
7887
|
-
return Number(raw);
|
|
8264
|
+
/** 是否为 429 限流失败(异常消息或终态 failure 载荷)。 */
|
|
8265
|
+
function isRateLimitFailure(message, status) {
|
|
8266
|
+
return status === 429 || isRateLimitError(message);
|
|
7888
8267
|
}
|
|
7889
8268
|
/**
|
|
7890
|
-
*
|
|
7891
|
-
*
|
|
7892
|
-
* 出现 <reasoning> 元素时标记 hasReasoning(产物不允许)。
|
|
8269
|
+
* 运行工具驱动的压缩循环:直到模型调用 completeCompression(成功)、连续 2 轮无
|
|
8270
|
+
* 工具调用、请求级错误或 signal 中止(失败)。成功与失败均落盘循环会话记录。
|
|
7893
8271
|
*/
|
|
7894
|
-
function
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
const
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
const
|
|
7911
|
-
|
|
7912
|
-
|
|
8272
|
+
async function runCompressionLoop(ctx, session, options) {
|
|
8273
|
+
const logger = makeLogger(ctx, options.debug);
|
|
8274
|
+
const state = new CompressionState(options.view);
|
|
8275
|
+
const messages = [makePluginUserMessage(options.taskText)];
|
|
8276
|
+
let rounds = 0;
|
|
8277
|
+
let nudges = 0;
|
|
8278
|
+
let usage;
|
|
8279
|
+
const recordSessionId = async (success) => recordCompressionSession(ctx, session, {
|
|
8280
|
+
phase: options.phase,
|
|
8281
|
+
target: options.target,
|
|
8282
|
+
messages,
|
|
8283
|
+
rounds,
|
|
8284
|
+
success,
|
|
8285
|
+
debug: options.debug
|
|
8286
|
+
});
|
|
8287
|
+
const failWith = async (error, aborted) => {
|
|
8288
|
+
const id = await recordSessionId(false);
|
|
8289
|
+
return {
|
|
8290
|
+
ok: false,
|
|
8291
|
+
error,
|
|
8292
|
+
aborted,
|
|
8293
|
+
...id === void 0 ? {} : { recordSessionId: id }
|
|
8294
|
+
};
|
|
8295
|
+
};
|
|
8296
|
+
logger.step(`压缩循环开始(${options.phase === "reflect" ? "反思" : "观察"},provider ${options.target.provider},model ${options.target.model},maxTokens ${options.maxTokens === void 0 ? "未设置" : String(options.maxTokens)})`);
|
|
8297
|
+
for (;;) {
|
|
8298
|
+
if (options.signal?.aborted) {
|
|
8299
|
+
logger.warn("压缩循环中止(signal 已中止),放弃本次压缩");
|
|
8300
|
+
return await failWith(COMPACTION_ABORTED_ERROR, true);
|
|
8301
|
+
}
|
|
8302
|
+
if (!await gateRateLimit(options.rateLimitWaitMs, options.signal)) {
|
|
8303
|
+
logger.warn("压缩循环中止(限流等待被 signal 中止),放弃本次压缩");
|
|
8304
|
+
return await failWith(COMPACTION_ABORTED_ERROR, true);
|
|
8305
|
+
}
|
|
8306
|
+
const requestOptions = {
|
|
8307
|
+
provider: options.target.provider,
|
|
8308
|
+
model: options.target.model,
|
|
8309
|
+
...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
|
|
8310
|
+
sessionId: session.id,
|
|
8311
|
+
purpose: "compaction",
|
|
8312
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
8313
|
+
system: buildCompressionPrompt(options.skipReasoning),
|
|
8314
|
+
messages: [...messages],
|
|
8315
|
+
tools: COMPRESSION_TOOL_SCHEMAS
|
|
8316
|
+
};
|
|
8317
|
+
const assembler = new BlockAssembler();
|
|
8318
|
+
try {
|
|
8319
|
+
for await (const chunk of ctx.llm.stream(requestOptions)) assembler.push(chunk);
|
|
8320
|
+
} catch (error) {
|
|
8321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8322
|
+
if (isRateLimitFailure(message)) {
|
|
8323
|
+
noteRateLimit();
|
|
8324
|
+
logger.warn(`压缩循环触发限流(429),等待 ${options.rateLimitWaitMs}ms 后重试`);
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
logger.warn(`压缩循环请求失败: ${message}`);
|
|
8328
|
+
return await failWith(message, false);
|
|
8329
|
+
}
|
|
8330
|
+
const finish = assembler.finish;
|
|
8331
|
+
if (finish.kind === "aborted") {
|
|
8332
|
+
logger.warn("压缩循环中止(流以 aborted 结束),放弃本次压缩");
|
|
8333
|
+
return await failWith(COMPACTION_ABORTED_ERROR, true);
|
|
8334
|
+
}
|
|
8335
|
+
if (finish.kind === "error") {
|
|
8336
|
+
const failureMessage = typeof finish.failure?.message === "string" && finish.failure.message !== "" ? finish.failure.message : "流以 error 终态结束(无失败详情)";
|
|
8337
|
+
if (isRateLimitFailure(failureMessage, typeof finish.failure?.status === "number" ? finish.failure.status : void 0)) {
|
|
8338
|
+
noteRateLimit();
|
|
8339
|
+
logger.warn(`压缩循环触发限流(429),等待 ${options.rateLimitWaitMs}ms 后重试`);
|
|
8340
|
+
continue;
|
|
8341
|
+
}
|
|
8342
|
+
logger.warn(`压缩循环请求失败: ${failureMessage}`);
|
|
8343
|
+
pushPartialAssistant(assembler, messages, options.target);
|
|
8344
|
+
return await failWith(failureMessage, false);
|
|
8345
|
+
}
|
|
8346
|
+
rounds += 1;
|
|
8347
|
+
if (assembler.usage !== void 0) usage = addUsage(usage, assembler.usage);
|
|
8348
|
+
const assistantMessage = assembler.message({
|
|
8349
|
+
kind: "model",
|
|
8350
|
+
provider: options.target.provider,
|
|
8351
|
+
model: options.target.model
|
|
8352
|
+
});
|
|
8353
|
+
messages.push(assistantMessage);
|
|
8354
|
+
const calls = assembler.blocks().filter((block) => block.type === "tool-call");
|
|
8355
|
+
if (calls.length === 0) {
|
|
8356
|
+
nudges += 1;
|
|
8357
|
+
if (nudges >= 2) {
|
|
8358
|
+
const error = "模型连续 2 轮未调用压缩工具,放弃本次压缩";
|
|
8359
|
+
logger.warn(error);
|
|
8360
|
+
return await failWith(error, false);
|
|
8361
|
+
}
|
|
8362
|
+
logger.warn("模型未调用压缩工具(输出纯文本),追加提醒后继续");
|
|
8363
|
+
messages.push(makePluginUserMessage(COMPRESSION_NUDGE_TEXT));
|
|
7913
8364
|
continue;
|
|
7914
8365
|
}
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
} else if (tag === "sys") {
|
|
7923
|
-
const index = intAttr(el, "index");
|
|
7924
|
-
if (index === void 0) return null;
|
|
7925
|
-
entries.push({
|
|
7926
|
-
kind: "sys",
|
|
7927
|
-
index
|
|
7928
|
-
});
|
|
7929
|
-
} else if (tag === "assistant") {
|
|
7930
|
-
const index = intAttr(el, "index");
|
|
7931
|
-
if (index !== void 0) entries.push({
|
|
7932
|
-
kind: "assistant",
|
|
7933
|
-
index
|
|
7934
|
-
});
|
|
7935
|
-
else {
|
|
7936
|
-
const start = intAttr(el, "start");
|
|
7937
|
-
const end = intAttr(el, "end");
|
|
7938
|
-
if (start === void 0 || end === void 0) return null;
|
|
7939
|
-
entries.push({
|
|
7940
|
-
kind: "assistant",
|
|
7941
|
-
start,
|
|
7942
|
-
end
|
|
7943
|
-
});
|
|
8366
|
+
nudges = 0;
|
|
8367
|
+
let completed = false;
|
|
8368
|
+
for (const call of calls) {
|
|
8369
|
+
if (call.name === "completeCompression") {
|
|
8370
|
+
messages.push(toolResultOf(call, state.complete()));
|
|
8371
|
+
completed = true;
|
|
8372
|
+
break;
|
|
7944
8373
|
}
|
|
7945
|
-
|
|
8374
|
+
messages.push(toolResultOf(call, state.executeCall(call.name, call.arguments)));
|
|
8375
|
+
}
|
|
8376
|
+
if (completed) {
|
|
8377
|
+
const text = state.buildFinalBlock();
|
|
8378
|
+
if (state.replacementCount === 0) logger.warn("压缩完成但未执行任何压缩替换(空提交)");
|
|
8379
|
+
logger.info(`压缩循环完成(${rounds} 轮,${state.replacementCount} 次压缩替换,输出 ${text.length} 字符)`);
|
|
8380
|
+
const id = await recordSessionId(true);
|
|
8381
|
+
return {
|
|
8382
|
+
ok: true,
|
|
8383
|
+
text,
|
|
8384
|
+
rounds,
|
|
8385
|
+
...usage === void 0 ? {} : { usage },
|
|
8386
|
+
...id === void 0 ? {} : { recordSessionId: id }
|
|
8387
|
+
};
|
|
8388
|
+
}
|
|
7946
8389
|
}
|
|
7947
|
-
return {
|
|
7948
|
-
entries,
|
|
7949
|
-
hasReasoning
|
|
7950
|
-
};
|
|
7951
8390
|
}
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
8391
|
+
//#endregion
|
|
8392
|
+
//#region src/om-event.ts
|
|
8393
|
+
/** om 信封 text 前缀:标识该 feedback/record 记录由本插件写入。 */
|
|
8394
|
+
const OM_EVENT_PREFIX = "om:1:";
|
|
8395
|
+
/** 校验载荷字段与类别匹配(运行时守卫,保证返回类型的诚实性)。 */
|
|
8396
|
+
function isValidPayload(kind, data) {
|
|
8397
|
+
switch (kind) {
|
|
8398
|
+
case "om/warning": return typeof data.problem === "string" && typeof data.message === "string";
|
|
8399
|
+
case "om/observe-pending": return typeof data.triggerMessageIndex === "number" && Number.isSafeInteger(data.triggerMessageIndex);
|
|
8400
|
+
case "om/observe-invalidate": return typeof data.pendingSeq === "number" && Number.isSafeInteger(data.pendingSeq);
|
|
7962
8401
|
}
|
|
7963
|
-
return out;
|
|
7964
8402
|
}
|
|
7965
8403
|
/**
|
|
7966
|
-
*
|
|
7967
|
-
*
|
|
8404
|
+
* 解码一条会话事件为 om 私有事件:仅识别 feedback/record 中带 om 信封前缀的
|
|
8405
|
+
* text;前缀缺失、JSON 非法、kind 未知或载荷字段缺失时返回 undefined。
|
|
7968
8406
|
*/
|
|
7969
|
-
function
|
|
7970
|
-
if (
|
|
7971
|
-
const
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
hi
|
|
7979
|
-
});
|
|
7980
|
-
}
|
|
7981
|
-
for (let i = 1; i < ranges.length; i += 1) {
|
|
7982
|
-
const prev = ranges[i - 1];
|
|
7983
|
-
const curr = ranges[i];
|
|
7984
|
-
if (prev === void 0 || curr === void 0) return null;
|
|
7985
|
-
if (curr.lo !== prev.hi + 1) return null;
|
|
8407
|
+
function readOmEvent(event) {
|
|
8408
|
+
if (event === void 0 || event === null || event.type !== "feedback/record") return void 0;
|
|
8409
|
+
const text = event.data?.text;
|
|
8410
|
+
if (typeof text !== "string" || !text.startsWith("om:1:")) return void 0;
|
|
8411
|
+
let envelope;
|
|
8412
|
+
try {
|
|
8413
|
+
envelope = JSON.parse(text.slice(5));
|
|
8414
|
+
} catch {
|
|
8415
|
+
return;
|
|
7986
8416
|
}
|
|
7987
|
-
|
|
7988
|
-
const
|
|
7989
|
-
if (
|
|
8417
|
+
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) return void 0;
|
|
8418
|
+
const { kind, ...rest } = envelope;
|
|
8419
|
+
if (typeof kind !== "string") return void 0;
|
|
8420
|
+
if (![
|
|
8421
|
+
"om/warning",
|
|
8422
|
+
"om/observe-pending",
|
|
8423
|
+
"om/observe-invalidate"
|
|
8424
|
+
].includes(kind)) return void 0;
|
|
8425
|
+
const omKind = kind;
|
|
8426
|
+
if (!isValidPayload(omKind, rest)) return void 0;
|
|
7990
8427
|
return {
|
|
7991
|
-
|
|
7992
|
-
|
|
8428
|
+
kind: omKind,
|
|
8429
|
+
data: rest,
|
|
8430
|
+
seq: event.seq
|
|
7993
8431
|
};
|
|
7994
8432
|
}
|
|
7995
|
-
/**
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
const attrs = {};
|
|
8002
|
-
for (const m of raw.matchAll(/([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g)) {
|
|
8003
|
-
const key = m[1] ?? "";
|
|
8004
|
-
const value = m[3] ?? m[4] ?? m[5] ?? "";
|
|
8005
|
-
if (key !== "") attrs[key] = value;
|
|
8006
|
-
}
|
|
8007
|
-
return attrs;
|
|
8433
|
+
/** 编码一条 om 私有事件为 feedback/record 的 text 信封。 */
|
|
8434
|
+
function encodeOmEvent(kind, data) {
|
|
8435
|
+
return `${OM_EVENT_PREFIX}${JSON.stringify({
|
|
8436
|
+
kind,
|
|
8437
|
+
...data
|
|
8438
|
+
})}`;
|
|
8008
8439
|
}
|
|
8009
|
-
/**
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
*/
|
|
8013
|
-
function decodeXmlEntities(text) {
|
|
8014
|
-
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
8440
|
+
/** 追加一条 om 私有事件(借用 feedback/record,log-only,不进 surface),返回事件 seq。 */
|
|
8441
|
+
function appendOmEvent(session, kind, data) {
|
|
8442
|
+
return session.append("feedback/record", { text: encodeOmEvent(kind, data) }).seq;
|
|
8015
8443
|
}
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
*/
|
|
8022
|
-
function rebuildHistoryBlock(inner) {
|
|
8023
|
-
const entries = [];
|
|
8024
|
-
const stack = [];
|
|
8025
|
-
for (const m of inner.matchAll(HISTORY_ENTRY_TOKEN_RE)) {
|
|
8026
|
-
const tag = m[2] ?? "";
|
|
8027
|
-
if (m[1] === "/") for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
8028
|
-
const open = stack[i];
|
|
8029
|
-
if (open?.tag === tag) {
|
|
8030
|
-
entries.push({
|
|
8031
|
-
tag,
|
|
8032
|
-
attrs: open.attrs,
|
|
8033
|
-
content: inner.slice(open.contentFrom, m.index)
|
|
8034
|
-
});
|
|
8035
|
-
stack.splice(i, 1);
|
|
8036
|
-
break;
|
|
8037
|
-
}
|
|
8038
|
-
}
|
|
8039
|
-
else if (m[4] === "/") entries.push({
|
|
8040
|
-
tag,
|
|
8041
|
-
attrs: parseTagAttrs(m[3] ?? ""),
|
|
8042
|
-
content: ""
|
|
8043
|
-
});
|
|
8044
|
-
else stack.push({
|
|
8045
|
-
tag,
|
|
8046
|
-
attrs: parseTagAttrs(m[3] ?? ""),
|
|
8047
|
-
contentFrom: m.index + m[0].length
|
|
8048
|
-
});
|
|
8444
|
+
function findOmEvents(session, kind) {
|
|
8445
|
+
const result = [];
|
|
8446
|
+
for (const event of session.events) {
|
|
8447
|
+
const om = readOmEvent(event);
|
|
8448
|
+
if (om !== void 0 && (kind === void 0 || om.kind === kind)) result.push(om);
|
|
8049
8449
|
}
|
|
8050
|
-
|
|
8051
|
-
tag: open.tag,
|
|
8052
|
-
attrs: open.attrs,
|
|
8053
|
-
content: inner.slice(open.contentFrom)
|
|
8054
|
-
});
|
|
8055
|
-
if (entries.length === 0) return null;
|
|
8056
|
-
const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
8057
|
-
const root = doc.documentElement;
|
|
8058
|
-
if (!root) return null;
|
|
8059
|
-
for (const e of entries) {
|
|
8060
|
-
const el = doc.createElement(e.tag);
|
|
8061
|
-
for (const [key, value] of Object.entries(e.attrs)) el.setAttribute(key, decodeXmlEntities(value));
|
|
8062
|
-
el.appendChild(doc.createTextNode(decodeXmlEntities(e.content)));
|
|
8063
|
-
root.appendChild(el);
|
|
8064
|
-
}
|
|
8065
|
-
return new import_lib.XMLSerializer().serializeToString(root);
|
|
8450
|
+
return result;
|
|
8066
8451
|
}
|
|
8452
|
+
//#endregion
|
|
8453
|
+
//#region src/degrade.ts
|
|
8067
8454
|
/**
|
|
8068
|
-
*
|
|
8069
|
-
*
|
|
8070
|
-
*
|
|
8071
|
-
*
|
|
8072
|
-
*
|
|
8073
|
-
*
|
|
8455
|
+
* 降级报告:压缩流程中的挂载失败统一出口。
|
|
8456
|
+
* 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
|
|
8457
|
+
*
|
|
8458
|
+
* - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
|
|
8459
|
+
* 并向会话日志追加 log-only 的 om 警告事件(借用 feedback/record 的 om 信封,
|
|
8460
|
+
* 见 om-event.ts;客户端渲染为「功能降级」警告行);
|
|
8461
|
+
* 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
|
|
8462
|
+
* - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
|
|
8463
|
+
* - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
|
|
8074
8464
|
*/
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
if (openMatch === null || close === -1 || close < open) return { error: "输出中找不到完整的 <history> 块(缺少 <history> 开标签或 </history> 闭标签)" };
|
|
8081
|
-
const inner = raw.slice(open + openMatch[0].length, close);
|
|
8082
|
-
if (inner.trim().length < 10) return { error: "输出中 <history> 块内容过短(少于 10 字符)" };
|
|
8083
|
-
const block = raw.slice(open, close + closeTag.length);
|
|
8084
|
-
const candidate = parseHistoryBlock(block) === null ? rebuildHistoryBlock(inner) : block;
|
|
8085
|
-
if (candidate === null) return { error: "输出不是合法的 <history> 块(XML 结构非法,按条目模糊提取也未找到有效条目)" };
|
|
8086
|
-
const parsed = parseHistoryBlock(candidate);
|
|
8087
|
-
if (parsed === null) return { error: "输出不是合法的 <history> 块(条目结构非法:属性缺失或包含未定义元素)" };
|
|
8088
|
-
if (parsed.hasReasoning) return { error: "输出包含 <reasoning> 条目(产物不允许携带思考过程)" };
|
|
8089
|
-
const span = historyContinuity(parsed.entries);
|
|
8090
|
-
if (span === null) return { error: "输出条目 index/start/end 不连续(跳号、重叠或乱序)" };
|
|
8091
|
-
if (expected?.start !== void 0 && span.start !== expected.start) return { error: `输出覆盖区间从 ${span.start} 开始,与期望起始 ${expected.start} 不一致` };
|
|
8092
|
-
if (expected?.end !== void 0 && span.end !== expected.end) return { error: `输出覆盖区间止于 ${span.end},与期望结束 ${expected.end} 不一致` };
|
|
8093
|
-
const candidateInner = candidate.slice(candidate.indexOf(">") + 1, candidate.length - closeTag.length).trim();
|
|
8094
|
-
return { log: `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}>` };
|
|
8095
|
-
}
|
|
8465
|
+
/** 各降级问题面向用户的简短说明(om/warning 信封载荷 message;客户端警告行直接展示)。 */
|
|
8466
|
+
const DEGRADE_PROBLEMS = {
|
|
8467
|
+
"systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
|
|
8468
|
+
"tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
|
|
8469
|
+
};
|
|
8096
8470
|
/**
|
|
8097
|
-
*
|
|
8098
|
-
*
|
|
8099
|
-
*
|
|
8100
|
-
*
|
|
8101
|
-
* 调用)不落盘。失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)记录日志并重试;
|
|
8102
|
-
* 全部尝试耗尽返回失败结果(携带最后一次尝试的实际报错/具体问题与其诊断子会话 id)。
|
|
8103
|
-
* signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
|
|
8104
|
-
* 限流等待门。
|
|
8471
|
+
* 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
|
|
8472
|
+
* 出现时,向会话日志追加 log-only om 警告事件(客户端渲染警告行,每会话最多
|
|
8473
|
+
* 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
|
|
8474
|
+
* 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
|
|
8105
8475
|
*/
|
|
8106
|
-
|
|
8107
|
-
const
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
};
|
|
8120
|
-
}
|
|
8121
|
-
const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
|
|
8122
|
-
if (!await gateRateLimit(rateLimitWaitMs, signal)) {
|
|
8123
|
-
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次压缩`);
|
|
8124
|
-
return {
|
|
8125
|
-
ok: false,
|
|
8126
|
-
error: COMPACTION_ABORTED_ERROR,
|
|
8127
|
-
aborted: true,
|
|
8128
|
-
...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
|
|
8129
|
-
};
|
|
8130
|
-
}
|
|
8131
|
-
logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
|
|
8132
|
-
const prompt = `${instruction}\n\n${contextText ?? ""}`;
|
|
8133
|
-
const collector = new StreamCollector();
|
|
8134
|
-
let streamCompleted = false;
|
|
8135
|
-
/** 本次尝试的诊断子会话 id(调用完成后立即落盘;落盘失败时缺失)。 */
|
|
8136
|
-
const logAttempt = async () => recordCompactionAttempt(ctx, session, {
|
|
8137
|
-
phase: options?.phase,
|
|
8138
|
-
target,
|
|
8139
|
-
attempt: {
|
|
8140
|
-
prompt,
|
|
8141
|
-
rawOutput: collector.text
|
|
8142
|
-
},
|
|
8143
|
-
attemptNo: attempt,
|
|
8144
|
-
debug
|
|
8476
|
+
function reportDegrade(session, logger, problem) {
|
|
8477
|
+
const message = DEGRADE_PROBLEMS[problem];
|
|
8478
|
+
let first = true;
|
|
8479
|
+
try {
|
|
8480
|
+
first = !findOmEvents(session, "om/warning").some((om) => om.data.problem === problem);
|
|
8481
|
+
} catch {}
|
|
8482
|
+
if (!first) return;
|
|
8483
|
+
console.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
8484
|
+
logger.warn(message);
|
|
8485
|
+
try {
|
|
8486
|
+
appendOmEvent(session, "om/warning", {
|
|
8487
|
+
problem,
|
|
8488
|
+
message
|
|
8145
8489
|
});
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
streamCompleted = true;
|
|
8150
|
-
const diagnosticSessionId = await logAttempt();
|
|
8151
|
-
const extracted = extractSummaryDetailed(collector.text, options?.expected);
|
|
8152
|
-
const finish = collector.finish;
|
|
8153
|
-
if (finish.kind !== "stop") {
|
|
8154
|
-
lastFailure = {
|
|
8155
|
-
reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)`,
|
|
8156
|
-
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8157
|
-
};
|
|
8158
|
-
logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8159
|
-
continue;
|
|
8160
|
-
}
|
|
8161
|
-
if ("error" in extracted) {
|
|
8162
|
-
lastFailure = {
|
|
8163
|
-
reason: extracted.error,
|
|
8164
|
-
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8165
|
-
};
|
|
8166
|
-
logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8167
|
-
continue;
|
|
8168
|
-
}
|
|
8169
|
-
const text = extracted.log;
|
|
8170
|
-
const usage = collector.usage;
|
|
8171
|
-
logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + `,子会话 ${diagnosticSessionId ?? "未落盘"})`);
|
|
8172
|
-
return {
|
|
8173
|
-
ok: true,
|
|
8174
|
-
text,
|
|
8175
|
-
attemptCount: attempt,
|
|
8176
|
-
...usage === void 0 ? {} : { usage },
|
|
8177
|
-
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8178
|
-
};
|
|
8179
|
-
} catch (error) {
|
|
8180
|
-
const diagnosticSessionId = streamCompleted ? void 0 : await logAttempt();
|
|
8181
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
8182
|
-
if (isRateLimitError(message)) {
|
|
8183
|
-
noteRateLimit();
|
|
8184
|
-
logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
|
|
8185
|
-
}
|
|
8186
|
-
lastFailure = {
|
|
8187
|
-
error: message,
|
|
8188
|
-
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8189
|
-
};
|
|
8190
|
-
logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8191
|
-
}
|
|
8490
|
+
} catch (error) {
|
|
8491
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
8492
|
+
logger.warn(`om 警告事件追加失败: ${text}`);
|
|
8192
8493
|
}
|
|
8193
|
-
const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
|
|
8194
|
-
logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次,最后错误:${lastError}),拒绝放行本轮 step`);
|
|
8195
|
-
return {
|
|
8196
|
-
ok: false,
|
|
8197
|
-
error: lastError,
|
|
8198
|
-
aborted: false,
|
|
8199
|
-
...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
|
|
8200
|
-
};
|
|
8201
8494
|
}
|
|
8202
8495
|
//#endregion
|
|
8203
8496
|
//#region src/compress.ts
|
|
@@ -8206,21 +8499,27 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
8206
8499
|
* 导出 estimateTextTokens / isPairBalancedAfter / computeCompressRange / historySection /
|
|
8207
8500
|
* findObservePending / reflectPass / observePass / maybeCompress。
|
|
8208
8501
|
*
|
|
8209
|
-
* -
|
|
8210
|
-
*
|
|
8502
|
+
* - 摘要生成走工具驱动的压缩循环(compress-loop.ts runCompressionLoop):首条 user
|
|
8503
|
+
* 消息仅含压缩指令与 start/end 区间,模型经 getHistory / compressHistory /
|
|
8504
|
+
* completeCompression 工具完成压缩;最终 <history> 块由插件从视图与替换记录构建,
|
|
8505
|
+
* 全程无需整块校验
|
|
8506
|
+
* - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,把全部块条目作为
|
|
8507
|
+
* 压缩视图重新压缩合并(视图无条目时跳过,如全部为不可解析的历史遗留块)
|
|
8211
8508
|
* - 观察(触发 → 待定 → 延迟执行):净压力(上下文压力 − 已压缩块 token 合计 − 系统提示词
|
|
8212
8509
|
* token 估算 − 工具定义 token 估算)首次 ≥ observeThresholdTokens 时记录待定标记
|
|
8213
8510
|
* (触发点 = 当时的最后一条完整消息 index),本次不压缩;待定后新增完整消息数 ≥
|
|
8214
|
-
* tailMessageCount
|
|
8215
|
-
*
|
|
8511
|
+
* tailMessageCount 时,把压缩边界至触发点的全部消息作为压缩视图替换为新 <history>
|
|
8512
|
+
* 块(旧块保留),等待期间的新消息成为下一轮未压缩尾部(延迟窗口内压力允许
|
|
8216
8513
|
* 短暂超阈值);tailMessageCount=0 时触发当轮直接执行(不落待定标记)
|
|
8217
8514
|
* - 待定标记以 log-only om 信封事件(借用 feedback/record,kind 为 om/observe-pending /
|
|
8218
8515
|
* om/observe-invalidate,见 om-event.ts)持久化在会话日志中(重启后从日志恢复);
|
|
8219
|
-
*
|
|
8516
|
+
* 压缩失败保留待定,下个 pre-step 直接重试执行
|
|
8220
8517
|
* - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
|
|
8221
8518
|
* - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
|
|
8222
|
-
* -
|
|
8223
|
-
*
|
|
8519
|
+
* - 压缩循环最终失败(连续无工具调用 / 请求级错误)时 pass 返回失败结果(携带最后一次
|
|
8520
|
+
* 错误),压缩流程向上传播,pre-step 据此拒绝本 step 中断当前 turn;signal 中止标记
|
|
8521
|
+
* aborted(不中断);成功与失败均落盘压缩会话记录子会话(失败时其 id 随结果传播为
|
|
8522
|
+
* 诊断子会话 id)
|
|
8224
8523
|
* - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
|
|
8225
8524
|
* 失败补 end(error,实际报错 + 诊断子会话 sessionId);替换消息 source 标记插件标识供 UI 认领
|
|
8226
8525
|
* - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
|
|
@@ -8234,16 +8533,20 @@ function estimateTextTokens(text) {
|
|
|
8234
8533
|
return Math.ceil(text.length / 4);
|
|
8235
8534
|
}
|
|
8236
8535
|
/**
|
|
8237
|
-
*
|
|
8238
|
-
*
|
|
8536
|
+
* 运行压缩循环并归一化结果:失败统一追加 compaction/end(error)(携带诊断子会话 id),
|
|
8537
|
+
* 成功记会话记录日志;返回循环结果(成功时由调用方提交)。
|
|
8239
8538
|
*/
|
|
8240
|
-
function
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8539
|
+
async function runPassLoop(ctx, session, lifecycle, options, logger, phase) {
|
|
8540
|
+
const outcome = await runCompressionLoop(ctx, session, options);
|
|
8541
|
+
if (!outcome.ok) {
|
|
8542
|
+
logger.warn(`${phase === "reflect" ? "反思" : "观察"}:压缩循环失败(${outcome.error}),诊断子会话 ${outcome.recordSessionId ?? "未落盘"},追加 compaction/end(error)`);
|
|
8543
|
+
try {
|
|
8544
|
+
appendCompactionEnd(session, lifecycle, outcome.error, outcome.recordSessionId);
|
|
8545
|
+
} catch {}
|
|
8546
|
+
return outcome;
|
|
8245
8547
|
}
|
|
8246
|
-
|
|
8548
|
+
if (outcome.recordSessionId !== void 0) logger.step(`压缩会话记录子会话 ${outcome.recordSessionId}`);
|
|
8549
|
+
return outcome;
|
|
8247
8550
|
}
|
|
8248
8551
|
/**
|
|
8249
8552
|
* 判定消息是否为本插件的压缩日志消息并提取日志文本(按 source 标记判断,兼容旧宿主
|
|
@@ -8273,14 +8576,6 @@ function historyInnerText(text) {
|
|
|
8273
8576
|
return text.slice(gt + 1, close).trim();
|
|
8274
8577
|
}
|
|
8275
8578
|
/**
|
|
8276
|
-
* 反思输入拼合:全部块内文(historyInnerText 去掉开标签属性,块首格式说明注释剥离)
|
|
8277
|
-
* 按序合并进单个 <history> 块。正文条目内出现的属性/注释同名串原样保留。
|
|
8278
|
-
*/
|
|
8279
|
-
function mergeHistoryBlocks(blocks) {
|
|
8280
|
-
const inner = blocks.map((block) => stripLeadingFormatNote(historyInnerText(block.text))).join("\n");
|
|
8281
|
-
return `<${HISTORY_TAG}>\n${inner}\n</${HISTORY_TAG}>`;
|
|
8282
|
-
}
|
|
8283
|
-
/**
|
|
8284
8579
|
* 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡:按表层顺序折叠未闭合的
|
|
8285
8580
|
* 工具调用数,处理到 seq 后计数为 0 即平衡(防止把 tool-call 与其结果切到两侧)。
|
|
8286
8581
|
*/
|
|
@@ -8462,9 +8757,10 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
|
|
|
8462
8757
|
});
|
|
8463
8758
|
}
|
|
8464
8759
|
/**
|
|
8465
|
-
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens
|
|
8466
|
-
*
|
|
8467
|
-
*
|
|
8760
|
+
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,把全部块内条目作为
|
|
8761
|
+
* 压缩视图送入工具压缩循环,整个块区段合并替换为一条更紧凑的摘要。视图无条目(全部
|
|
8762
|
+
* 为不可解析的历史遗留块)时跳过。失败不产生部分替换;最终失败返回失败结果
|
|
8763
|
+
* (error = 最后一次错误)。
|
|
8468
8764
|
*/
|
|
8469
8765
|
async function reflectPass(ctx, agent, config, target, signal) {
|
|
8470
8766
|
const session = agent.session;
|
|
@@ -8489,43 +8785,41 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8489
8785
|
logger.step("反思:块区段缺失,跳过");
|
|
8490
8786
|
return { failed: false };
|
|
8491
8787
|
}
|
|
8788
|
+
const view = buildReflectView(blocks, { skipReasoning: config.compressSkipReasoning });
|
|
8789
|
+
if (view.minIndex === void 0 || view.maxIndex === void 0) {
|
|
8790
|
+
logger.step("反思:块内无可定位条目(历史遗留块),跳过");
|
|
8791
|
+
return { failed: false };
|
|
8792
|
+
}
|
|
8492
8793
|
const blockSeqs = blocks.map((block) => block.seq);
|
|
8493
|
-
const instruction = buildHistoryPrompt(config.compressSkipReasoning);
|
|
8494
|
-
const contextText = mergeHistoryBlocks(blocks);
|
|
8495
|
-
const expectedEnd = reflectExpectedEnd(contextText);
|
|
8496
8794
|
const lifecycle = {
|
|
8497
8795
|
compactionId: newCompactionId(),
|
|
8498
8796
|
turn: openTurnOf(session)
|
|
8499
8797
|
};
|
|
8500
8798
|
try {
|
|
8501
|
-
logger.step("反思:追加 compaction/start
|
|
8799
|
+
logger.step("反思:追加 compaction/start(压缩循环前开启压缩中提示)");
|
|
8502
8800
|
appendCompactionStart(session, lifecycle, "reflect");
|
|
8503
8801
|
} catch (error) {
|
|
8504
8802
|
const message = error instanceof Error ? error.message : String(error);
|
|
8505
8803
|
logger.warn(`反思压缩启动失败: ${message}`);
|
|
8506
8804
|
return { failed: false };
|
|
8507
8805
|
}
|
|
8508
|
-
const summaryResult = await
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8806
|
+
const summaryResult = await runPassLoop(ctx, session, lifecycle, {
|
|
8807
|
+
view,
|
|
8808
|
+
phase: "reflect",
|
|
8809
|
+
taskText: buildCompressionTaskText("reflect", view.minIndex, view.maxIndex),
|
|
8810
|
+
target,
|
|
8811
|
+
maxTokens: config.compressMaxTokens,
|
|
8514
8812
|
rateLimitWaitMs: config.rateLimitWaitMs,
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
aborted: summaryResult.aborted,
|
|
8526
|
-
...summaryResult.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.diagnosticSessionId }
|
|
8527
|
-
};
|
|
8528
|
-
}
|
|
8813
|
+
skipReasoning: config.compressSkipReasoning,
|
|
8814
|
+
debug: config.debug,
|
|
8815
|
+
...signal === void 0 ? {} : { signal }
|
|
8816
|
+
}, logger, "reflect");
|
|
8817
|
+
if (!summaryResult.ok) return {
|
|
8818
|
+
failed: true,
|
|
8819
|
+
error: summaryResult.error,
|
|
8820
|
+
aborted: summaryResult.aborted,
|
|
8821
|
+
...summaryResult.recordSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.recordSessionId }
|
|
8822
|
+
};
|
|
8529
8823
|
const report = summaryResult.text;
|
|
8530
8824
|
try {
|
|
8531
8825
|
logger.step("反思提交:追加 compaction/summary(影子价格认领)");
|
|
@@ -8542,7 +8836,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8542
8836
|
provider: target.provider,
|
|
8543
8837
|
model: target.model,
|
|
8544
8838
|
maxTokens: config.compressMaxTokens,
|
|
8545
|
-
attemptCount: summaryResult.
|
|
8839
|
+
attemptCount: summaryResult.rounds,
|
|
8546
8840
|
...summaryResult.usage === void 0 ? {} : { usage: summaryResult.usage }
|
|
8547
8841
|
});
|
|
8548
8842
|
logger.step("反思提交:替换整个 <history> 块区段为合并摘要");
|
|
@@ -8552,7 +8846,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8552
8846
|
end: last.seq
|
|
8553
8847
|
}, lifecycle.compactionId);
|
|
8554
8848
|
logger.step("反思提交:追加 compaction/end");
|
|
8555
|
-
appendCompactionEnd(session, lifecycle, void 0, summaryResult.
|
|
8849
|
+
appendCompactionEnd(session, lifecycle, void 0, summaryResult.recordSessionId);
|
|
8556
8850
|
logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
|
|
8557
8851
|
return { failed: false };
|
|
8558
8852
|
} catch (error) {
|
|
@@ -8627,9 +8921,9 @@ function measurePressureTokens(ctx, session, logger) {
|
|
|
8627
8921
|
* observeThresholdTokens 时记录待定标记(触发点 = 当时的最后一条完整消息 index),本次
|
|
8628
8922
|
* 不压缩(tailMessageCount=0 当轮直接执行,不落待定标记);已有待定标记时按新增完整
|
|
8629
8923
|
* 消息数 ≥ tailMessageCount 决定执行,压缩区间截至触发点(新增消息成为新未压缩尾部,
|
|
8630
|
-
*
|
|
8631
|
-
* 失败保留待定,下个 pre-step
|
|
8632
|
-
* (error =
|
|
8924
|
+
* 延迟窗口内压力允许短暂超阈值)。执行成功(或无可行区间)后写待定失效标记;压缩
|
|
8925
|
+
* 失败保留待定,下个 pre-step 直接重试执行。最终失败返回失败结果
|
|
8926
|
+
* (error = 最后一次错误)。
|
|
8633
8927
|
*/
|
|
8634
8928
|
async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
8635
8929
|
const session = agent.session;
|
|
@@ -8706,52 +9000,48 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
|
8706
9000
|
return { failed: false };
|
|
8707
9001
|
}
|
|
8708
9002
|
const shadowedSet = new Set(replaceSeqs);
|
|
8709
|
-
|
|
8710
|
-
if (inRangeCms.length === 0) {
|
|
9003
|
+
if (indexCompleteMessages(session).filter((cm) => cm.seqs.every((seq) => shadowedSet.has(seq))).length === 0) {
|
|
8711
9004
|
logger.step("观察:区间内无完整消息,清除待定标记视为完成");
|
|
8712
9005
|
clearPending();
|
|
8713
9006
|
return { failed: false };
|
|
8714
9007
|
}
|
|
8715
|
-
const
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
9008
|
+
const view = buildObserveView(session, replaceSeqs, { skipReasoning: config.compressSkipReasoning });
|
|
9009
|
+
if (view.minIndex === void 0 || view.maxIndex === void 0) {
|
|
9010
|
+
logger.step("观察:区间内无可定位条目,清除待定标记视为完成");
|
|
9011
|
+
clearPending();
|
|
9012
|
+
return { failed: false };
|
|
9013
|
+
}
|
|
9014
|
+
logger.step(`观察:保留旧块 ${blocks.length} 条,替换 [${replaceStart}..${range.end}](${replaceSeqs.length} 个表层节点,压缩至${triggerNote}),视图区间 ${view.minIndex}..${view.maxIndex}`);
|
|
8720
9015
|
const lifecycle = {
|
|
8721
9016
|
compactionId: newCompactionId(),
|
|
8722
9017
|
turn: openTurnOf(session)
|
|
8723
9018
|
};
|
|
8724
9019
|
try {
|
|
8725
|
-
logger.step("观察:追加 compaction/start
|
|
9020
|
+
logger.step("观察:追加 compaction/start(压缩循环前开启压缩中提示)");
|
|
8726
9021
|
appendCompactionStart(session, lifecycle, "observe");
|
|
8727
9022
|
} catch (error) {
|
|
8728
9023
|
const message = error instanceof Error ? error.message : String(error);
|
|
8729
9024
|
logger.warn(`观察压缩启动失败: ${message}`);
|
|
8730
9025
|
return { failed: false };
|
|
8731
9026
|
}
|
|
8732
|
-
const summaryResult = await
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
9027
|
+
const summaryResult = await runPassLoop(ctx, session, lifecycle, {
|
|
9028
|
+
view,
|
|
9029
|
+
phase: "observe",
|
|
9030
|
+
taskText: buildCompressionTaskText("observe", view.minIndex, view.maxIndex),
|
|
9031
|
+
target,
|
|
9032
|
+
maxTokens: config.compressMaxTokens,
|
|
8738
9033
|
rateLimitWaitMs: config.rateLimitWaitMs,
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
aborted: summaryResult.aborted,
|
|
8750
|
-
...summaryResult.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.diagnosticSessionId }
|
|
8751
|
-
};
|
|
8752
|
-
}
|
|
9034
|
+
skipReasoning: config.compressSkipReasoning,
|
|
9035
|
+
debug: config.debug,
|
|
9036
|
+
...signal === void 0 ? {} : { signal }
|
|
9037
|
+
}, logger, "observe");
|
|
9038
|
+
if (!summaryResult.ok) return {
|
|
9039
|
+
failed: true,
|
|
9040
|
+
error: summaryResult.error,
|
|
9041
|
+
aborted: summaryResult.aborted,
|
|
9042
|
+
...summaryResult.recordSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.recordSessionId }
|
|
9043
|
+
};
|
|
8753
9044
|
const report = summaryResult.text;
|
|
8754
|
-
const attemptCount = summaryResult.attemptCount - 1;
|
|
8755
9045
|
const usage = summaryResult.usage;
|
|
8756
9046
|
const shadowedTokenCount = replaceSeqs.reduce((total, seq) => {
|
|
8757
9047
|
const event = session.events[seq];
|
|
@@ -8784,7 +9074,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
|
8784
9074
|
provider: target.provider,
|
|
8785
9075
|
model: target.model,
|
|
8786
9076
|
maxTokens: config.compressMaxTokens,
|
|
8787
|
-
attemptCount,
|
|
9077
|
+
attemptCount: summaryResult.rounds,
|
|
8788
9078
|
...usage === void 0 ? {} : { usage }
|
|
8789
9079
|
});
|
|
8790
9080
|
logger.step("观察提交:替换被压缩新消息区间为 <history>(旧块保留)");
|
|
@@ -8794,7 +9084,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
|
8794
9084
|
end: range.end
|
|
8795
9085
|
}, lifecycle.compactionId);
|
|
8796
9086
|
logger.step("观察提交:追加 compaction/end");
|
|
8797
|
-
appendCompactionEnd(session, lifecycle, void 0, summaryResult.
|
|
9087
|
+
appendCompactionEnd(session, lifecycle, void 0, summaryResult.recordSessionId);
|
|
8798
9088
|
clearPending();
|
|
8799
9089
|
logger.info(`观察压缩完成(替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens,压缩至${triggerNote})`);
|
|
8800
9090
|
return { failed: false };
|
|
@@ -9051,7 +9341,6 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
9051
9341
|
compressMaxTokens: void 0,
|
|
9052
9342
|
rateLimitWaitMs: 6e4,
|
|
9053
9343
|
tailMessageCount: 5,
|
|
9054
|
-
compressRetryCount: 5,
|
|
9055
9344
|
compressSkipReasoning: true,
|
|
9056
9345
|
omEnabled: true,
|
|
9057
9346
|
debug: false,
|
|
@@ -9065,8 +9354,7 @@ const NUMBER_KEYS = [
|
|
|
9065
9354
|
["reflectThresholdTokens", true],
|
|
9066
9355
|
["compressMaxTokens", true],
|
|
9067
9356
|
["rateLimitWaitMs", true],
|
|
9068
|
-
["tailMessageCount", true]
|
|
9069
|
-
["compressRetryCount", true]
|
|
9357
|
+
["tailMessageCount", true]
|
|
9070
9358
|
];
|
|
9071
9359
|
/** 归一化原始配置输入:缺省 / null / 空串 / 非对象视为空对象。 */
|
|
9072
9360
|
function normalizeConfigInput(raw) {
|
|
@@ -9455,10 +9743,12 @@ function buildSemanticRecallTool(options) {
|
|
|
9455
9743
|
/**
|
|
9456
9744
|
* dsh-plugin-om 入口(tsdown 打包入口):导出 name / inject / apply。
|
|
9457
9745
|
* apply 注册 recall / recall-semantic 工具,并接线 agent/pre-step 自动压缩
|
|
9458
|
-
*
|
|
9459
|
-
*
|
|
9460
|
-
*
|
|
9461
|
-
*
|
|
9746
|
+
* (先反思后观察,仅主会话生效)。压缩走工具驱动的压缩循环(模型经 getHistory /
|
|
9747
|
+
* compressHistory / completeCompression 工具完成,见 compress-loop.ts);最终失败
|
|
9748
|
+
* (连续无工具调用 / 请求级错误)时拒绝本 step 中断当前 turn(signal 中止除外),
|
|
9749
|
+
* 主会话日志记录失败原因与诊断子会话 sessionId(完整循环会话由 compaction-log.ts
|
|
9750
|
+
* 落盘为子会话)。压缩与检索的实现见 compress.ts / compress-loop.ts /
|
|
9751
|
+
* compaction-log.ts / recall.ts / semantic-recall.ts。
|
|
9462
9752
|
*/
|
|
9463
9753
|
/** 插件名(Loader 识别入口的稳定标识)。 */
|
|
9464
9754
|
const name = "dsh-plugin-om";
|