dsh-plugin-om 0.0.15 → 0.0.17

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/dist/index.mjs CHANGED
@@ -19,8 +19,15 @@ const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
19
19
  /**
20
20
  * 「完整消息」定义(提示词 / recall 工具描述 / 块顶注释共用同一字符串)。
21
21
  * 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
22
+ * 用户消息指 user_message 中 source.kind 为 user 的部分;其余部分(宿主注入的上下文等)
23
+ * 为系统消息,同样是一条完整消息。
22
24
  */
23
- const COMPLETE_MESSAGE_DEFINITION = "`用户消息`,`模型输出文本`和`具有result的toolcall`被视作`完整消息`。首条`完整消息`的index是0,后续的index递增。";
25
+ const COMPLETE_MESSAGE_DEFINITION = "`完整消息`被定义为`用户消息`,`系统消息`,`模型输出文本`和`具有result的toolcall`的集合。首条`完整消息`的index是0,后续的index递增。";
26
+ /** 判定 user/message 的 source 是否为本插件自产(压缩日志机制消息;含兼容旧宿主 checkpoint 标记 'compact')。这类消息不占完整消息 index。 */
27
+ function isPluginOwnedSource(source) {
28
+ if (source?.kind !== "plugin") return false;
29
+ return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
30
+ }
24
31
  //#endregion
25
32
  //#region src/utils.ts
26
33
  /** 判断值是否为普通对象:typeof object 且非 null 且非数组(类型收窄用)。 */
@@ -42,6 +49,15 @@ function blocksToText(blocks) {
42
49
  for (const block of blocks) if (isRecord(block) && block.type === "text" && typeof block.text === "string") out.push(block.text);
43
50
  return out.join("");
44
51
  }
52
+ /** 消息内容的字符数:递归计入 text 块与 tool-result 内嵌文本(与宿主 token 估算口径一致)。 */
53
+ function textCharCount(message) {
54
+ if (!message || !Array.isArray(message.content)) return 0;
55
+ /** 累计字符数。 */
56
+ let total = 0;
57
+ for (const block of message.content) if (block.type === "text") total += block.text.length;
58
+ else if (block.type === "tool-result") total += blocksToText(block.content).length;
59
+ return total;
60
+ }
45
61
  /** 面向 recall 的完整消息呈现:text 原样;tool-call 展开(参数=代码);tool-result 取文本。 */
46
62
  function renderMessageText(message) {
47
63
  if (!message || !Array.isArray(message.content)) return "";
@@ -79,7 +95,22 @@ function routedTarget(session) {
79
95
  //#endregion
80
96
  //#region src/log-index.ts
81
97
  /**
82
- * 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(三类,见文件头)。
98
+ * 会话日志索引:完整消息索引(index 定位完整消息,recall 与摘要共用同一套编号),
99
+ * 以及表层节点定位辅助。事件日志仅追加(被遮蔽的事件仍可读,recall 依赖此性质)。
100
+ *
101
+ * 完整消息是摘要日志与 recall 共用的定位单位,分四类:
102
+ * - user:用户消息(user/message 中 source.kind === 'user')占一条;
103
+ * - sys:系统消息(user/message 中其余 source.kind 的部分,如宿主注入的上下文)
104
+ * 占一条;压缩日志中以 <sys type="KIND" index="N"> 空块表示(内容不进入输入);
105
+ * - assistant:模型输出文本(assistant/message 中的文本块)占一条;
106
+ * - toolcall:单个工具调用及其结果占一条(同一条 AI 消息里的文本与工具调用拆开,
107
+ * tool/result 按 source.callId 匹配其 tool-call;未匹配的 result 独立成条,防御)。
108
+ * 本插件自产的压缩日志消息(source.kind === 'plugin' 且 plugin 为本插件标识)不占 index。
109
+ * index 从 0 起、按日志顺序递增、只追加不重排 → 会话内全局稳定,
110
+ * 压缩后旧摘要条目引用的 index 仍然有效。
111
+ */
112
+ /**
113
+ * 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(四类,见文件头)。
83
114
  * 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
84
115
  * 插件在 agent/pre-step 触发压缩(日志 call-result 完备),因此压缩时不存在未闭合的
85
116
  * tool-call;索引对未闭合调用不设特殊处理。
@@ -96,12 +127,20 @@ function indexCompleteMessages(session) {
96
127
  const event = events[seq];
97
128
  if (!event) continue;
98
129
  if (event.type === "user/message") {
99
- if (event.data.source?.kind === "plugin") continue;
100
- cms.push({
130
+ /** 事件 source(区分用户消息与宿主注入/插件消息)。 */
131
+ const source = event.data.source;
132
+ if (isPluginOwnedSource(source)) continue;
133
+ if (source?.kind === "user") cms.push({
101
134
  index: cms.length,
102
135
  type: "user",
103
136
  seqs: [seq]
104
137
  });
138
+ else cms.push({
139
+ index: cms.length,
140
+ type: "sys",
141
+ seqs: [seq],
142
+ ...source?.kind === void 0 ? {} : { kind: source.kind }
143
+ });
105
144
  } else if (event.type === "assistant/message") {
106
145
  /** 助手消息(含文本与 tool-call 块)。 */
107
146
  const message = event.data.message;
@@ -153,13 +192,13 @@ function indexCompleteMessages(session) {
153
192
  }
154
193
  /**
155
194
  * 渲染一条完整消息的文本(recall 输出 / new 模式输入共用):
156
- * - user:消息原文;
195
+ * - user / sys:消息原文;
157
196
  * - assistant:仅文本块;
158
197
  * - toolcall:调用块(工具名 + 参数)+ 结果文本(pruner 裁剪超大结果)。
159
198
  */
160
199
  function renderCompleteMessage(session, cm, pruner) {
161
- if (cm.type === "user") {
162
- /** 用户消息事件(seq 缺失则无法渲染)。 */
200
+ if (cm.type === "user" || cm.type === "sys") {
201
+ /** 用户/系统消息事件(seq 缺失则无法渲染)。 */
163
202
  const seq = cm.seqs[0];
164
203
  const event = seq === void 0 ? void 0 : session.events[seq];
165
204
  /** 派生的消息对象。 */
@@ -7382,13 +7421,14 @@ function buildHistoryPrompt() {
7382
7421
  "- <history> 是历史消息的记录块:输入与输出都是合法的 <history> 块,块内条目是「模型消息 + index」的表达形式。",
7383
7422
  `- 完整消息:${COMPLETE_MESSAGE_DEFINITION}`,
7384
7423
  "- <user_message index=\"N\">:用户消息条目(N 为该条完整消息的 index;文本原样,图片/文件等以注释补充)。",
7385
- "- <system-reminder>:可能出现在 <user_message> 内的提醒块(宿主注入),视为真实元素;压缩产物中保留其块结构与内容原样(标签不转义)。",
7424
+ "- <sys type=\"(kind)\" index=\"N\">:系统消息条目(user_message 中非 kind:user 的部分,如宿主注入的上下文),块中为空,内容不进入压缩输入。",
7386
7425
  "- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
7387
7426
  "- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
7388
7427
  "- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
7389
7428
  "",
7390
7429
  "【压缩要求】",
7391
7430
  "- 完整保留用户消息:<user_message> 条目逐条保留原文,不概括、不省略。",
7431
+ "- 完整保留系统消息:<sys> 条目逐条原样保留(type 与 index 不变),块中为空,不概括、不省略、不填充内容。",
7392
7432
  "- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
7393
7433
  "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块:块内描述模块的目的、行为与结果;涉及的具体文件保留在模块内容中,多个前缀相同的路径合并简写。",
7394
7434
  "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现,内容不受限制。",
@@ -7400,6 +7440,7 @@ function buildHistoryPrompt() {
7400
7440
  "<user_message index=\"(index)\">",
7401
7441
  "(user 消息原文)",
7402
7442
  "</user_message>",
7443
+ "<sys type=\"(kind)\" index=\"(index)\"></sys>",
7403
7444
  "<assistant start=\"(起始 index)\" end=\"(结束 index)\">",
7404
7445
  "(模块的目的、行为与结果摘要)",
7405
7446
  "</assistant>",
@@ -7411,73 +7452,6 @@ function buildHistoryPrompt() {
7411
7452
  "【数据源】下方的 <history> 消息记录是本次要压缩的全部消息;压缩结果作为一个新的 <history> 块输出。"
7412
7453
  ].join("\n");
7413
7454
  }
7414
- /** 合法 <system-reminder> 块匹配:完整开闭标签对(非贪婪最短闭合;不匹配缺闭标签的残缺文本)。 */
7415
- const SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
7416
- /**
7417
- * 解析 system-reminder 块(合法性判定):块可被 XML 解析且根元素为 <system-reminder> 时
7418
- * 视为合法块并返回其根元素;否则(缺闭标签 / 内容含非法字符等)返回 null,按普通文本处理。
7419
- */
7420
- function parseSystemReminder(block) {
7421
- try {
7422
- /** 根元素(必须为 system-reminder)。 */
7423
- const root = new import_lib.DOMParser().parseFromString(block, "text/xml").documentElement;
7424
- return root && root.nodeName === "system-reminder" ? root : null;
7425
- } catch {
7426
- return null;
7427
- }
7428
- }
7429
- /**
7430
- * 把解析出的 system-reminder 元素按原样复制进目标文档(标签与内容均不转义):
7431
- * xmldom 无 importNode,手动递归复制元素 / 文本 / CDATA / 注释节点。
7432
- */
7433
- function copySystemReminder(doc, source) {
7434
- /** 目标元素(同标签名)。 */
7435
- const el = doc.createElement(source.nodeName);
7436
- for (let i = 0; i < source.attributes.length; i += 1) {
7437
- /** 源属性(名称 + 值原样复制)。 */
7438
- const attr = source.attributes[i];
7439
- if (attr) el.setAttribute(attr.name, attr.value);
7440
- }
7441
- for (const child of Array.from(source.childNodes)) if (child.nodeType === 1) el.appendChild(copySystemReminder(doc, child));
7442
- else if (child.nodeType === 3) el.appendChild(doc.createTextNode(child.nodeValue ?? ""));
7443
- else if (child.nodeType === 4) el.appendChild(doc.createCDATASection(child.nodeValue ?? ""));
7444
- else if (child.nodeType === 8) el.appendChild(doc.createComment(child.nodeValue ?? ""));
7445
- return el;
7446
- }
7447
- /**
7448
- * 追加用户文本到 user_message 元素:合法 <system-reminder> 块整块原样插入(不转义),
7449
- * 其余文本按普通文本 XML 转义(createTextNode)。
7450
- */
7451
- function appendUserText(doc, el, text) {
7452
- /** 已消费的文本位置。 */
7453
- let last = 0;
7454
- for (const m of text.matchAll(SYSTEM_REMINDER_RE)) {
7455
- /** 匹配起点(缺省 0)。 */
7456
- const start = m.index ?? 0;
7457
- /** 块前的普通文本(转义)。 */
7458
- if (start > last) el.appendChild(doc.createTextNode(text.slice(last, start)));
7459
- /** 块原文。 */
7460
- const block = m[0];
7461
- /** 合法判定(可被 XML 解析)。 */
7462
- const parsed = parseSystemReminder(block);
7463
- if (parsed !== null) el.appendChild(copySystemReminder(doc, parsed));
7464
- else el.appendChild(doc.createTextNode(block));
7465
- /** 消费位置推进到块尾。 */
7466
- last = start + block.length;
7467
- }
7468
- /** 末尾剩余普通文本(转义)。 */
7469
- if (last < text.length) el.appendChild(doc.createTextNode(text.slice(last)));
7470
- }
7471
- /**
7472
- * 提取文本中全部合法的 <system-reminder> 块(完整开闭标签对且内容可被 XML 解析);
7473
- * 供渲染(原样插入)与未压缩消息测量(不计入)共用;不合法部分按普通文本处理。
7474
- */
7475
- function legitimateSystemReminderBlocks(text) {
7476
- /** 合法块缓冲区。 */
7477
- const out = [];
7478
- for (const m of text.matchAll(SYSTEM_REMINDER_RE)) if (parseSystemReminder(m[0]) !== null) out.push(m[0]);
7479
- return out;
7480
- }
7481
7455
  /** 渲染用户消息条目(DOM 元素):文本块原样;图片/文件等非文本块以注释补充(说明传入了什么)。 */
7482
7456
  function renderUserEntry(doc, session, cm) {
7483
7457
  /** 用户消息事件(seq 缺失则无法渲染)。 */
@@ -7492,7 +7466,7 @@ function renderUserEntry(doc, session, cm) {
7492
7466
  /** 是否存在可输出内容(文本或注释)。 */
7493
7467
  let hasContent = false;
7494
7468
  for (const block of message.content) if (block.type === "text") {
7495
- appendUserText(doc, el, String(block.text));
7469
+ el.appendChild(doc.createTextNode(String(block.text)));
7496
7470
  hasContent = true;
7497
7471
  } else if (block.type === "image") {
7498
7472
  /** 图片附件元数据(名称 / 媒体类型 / 尺寸 / 字节数)。 */
@@ -7512,11 +7486,12 @@ function renderUserEntry(doc, session, cm) {
7512
7486
  }
7513
7487
  /**
7514
7488
  * 渲染完整消息记录(观察输入,new 模式):输出一个合法的 <history> 块——
7515
- * - user → <user_message index="N">(文本原样,其中合法 <system-reminder> 块整块原样保留、不转义;图片/文件注释);
7489
+ * - user → <user_message index="N">(文本原样;图片/文件注释);
7490
+ * - sys → <sys type="KIND" index="N"></sys>(系统消息空块,内容不进入压缩输入);
7516
7491
  * - 每条 assistant 消息的 reasoning → <reasoning>(参考条目,产物中没有);
7517
7492
  * - assistant / toolcall → <assistant index="N">(模型输出文本 / toolcall&result 原样)。
7518
7493
  * 文本经 XML 序列化自动转义(用户输入 / 文本 / reasoning 中的特殊字符不破坏 XML 合法性)。
7519
- * 仅渲染 seqs 全部落在给定集合内的完整消息(插件自产消息天然不占位)。
7494
+ * 仅渲染 seqs 全部落在给定集合内的完整消息(本插件自产消息天然不占位)。
7520
7495
  */
7521
7496
  function renderMessages(session, seqs) {
7522
7497
  /** 遮蔽 seq 集合。 */
@@ -7542,6 +7517,15 @@ function renderMessages(session, seqs) {
7542
7517
  const entries = [];
7543
7518
  for (const cm of indexCompleteMessages(session)) {
7544
7519
  if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
7520
+ if (cm.type === "sys") {
7521
+ /** sys 元素(type=source.kind,index=完整消息序号)。 */
7522
+ const sysEl = doc.createElement("sys");
7523
+ sysEl.setAttribute("type", cm.kind ?? "");
7524
+ sysEl.setAttribute("index", String(cm.index));
7525
+ sysEl.appendChild(doc.createTextNode(""));
7526
+ entries.push(sysEl);
7527
+ continue;
7528
+ }
7545
7529
  if (cm.type === "user") {
7546
7530
  /** 用户消息条目(无文本且无注释则跳过)。 */
7547
7531
  const rendered = renderUserEntry(doc, session, cm);
@@ -7640,7 +7624,7 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
7640
7624
  };
7641
7625
  }
7642
7626
  /** 产出日志后插入首个 <history> 后的格式说明(XML 注释,完整消息定义 + 条目标签语义)。 */
7643
- const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <user_message index="N"> 与 <assistant index="N"> 表示单条完整消息,<assistant start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index -->`;
7627
+ const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
7644
7628
  /** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
7645
7629
  function intAttr(el, name) {
7646
7630
  /** 属性原始值(缺失为 null)。 */
@@ -7653,7 +7637,7 @@ function intAttr(el, name) {
7653
7637
  * 用 XML 解析器解析一个 <history> 块(结构合法性校验):
7654
7638
  * - 非法 XML(标签不匹配 / 未闭合 / 多根等)→ null;
7655
7639
  * - 根节点必须是 <history>;
7656
- * - 顶层只允许 user_message / assistant 条目元素(其余元素类型视为不合法);
7640
+ * - 顶层只允许 user_message / assistant / sys 条目元素(其余元素类型视为不合法);
7657
7641
  * - 出现 <reasoning> 元素时标记 hasReasoning(产物不允许)。
7658
7642
  * 条目按文档顺序提取(index / start / end 属性缺失或不合法 → 不合法)。
7659
7643
  */
@@ -7694,6 +7678,14 @@ function parseHistoryBlock(xml) {
7694
7678
  kind: "user",
7695
7679
  index
7696
7680
  });
7681
+ } else if (tag === "sys") {
7682
+ /** index 属性(缺失/非法 → 不合法)。 */
7683
+ const index = intAttr(el, "index");
7684
+ if (index === void 0) return null;
7685
+ entries.push({
7686
+ kind: "sys",
7687
+ index
7688
+ });
7697
7689
  } else if (tag === "assistant") {
7698
7690
  /** 单条 index 或模块 start..end(二选一)。 */
7699
7691
  const index = intAttr(el, "index");
@@ -7918,10 +7910,9 @@ function reflectExpectedEnd(contextText) {
7918
7910
  */
7919
7911
  function historyTextOf(event) {
7920
7912
  if (event?.type !== "user/message") return void 0;
7921
- /** 消息 source(插件自产消息的标记)。 */
7913
+ /** 消息 source(本插件自产消息的标记;含兼容旧宿主 checkpoint 标记)。 */
7922
7914
  const source = event.data.source;
7923
- if (source?.kind !== "plugin") return void 0;
7924
- if (source.plugin !== "compact" && source.plugin !== "dsh-plugin-om") return void 0;
7915
+ if (!isPluginOwnedSource(source)) return void 0;
7925
7916
  /** 消息纯文本。 */
7926
7917
  const text = blocksToText(event.data.content);
7927
7918
  /** 首个 <history 出现位置(无则非压缩日志)。 */
@@ -7932,26 +7923,22 @@ function historyTextOf(event) {
7932
7923
  if (!rest.startsWith(">") && !rest.startsWith(" tip=")) return void 0;
7933
7924
  return text;
7934
7925
  }
7935
- /**
7936
- * 消息文本中合法 <system-reminder> 块的 token 估算(4 字符 ≈ 1 token,与 estimateTextTokens
7937
- * 一致):宿主注入的提醒不计入未压缩消息,避免推高观察阈值触发压缩。
7938
- */
7939
- function systemReminderTokens(message) {
7940
- /** 消息内容块(非记录或非数组则无文本可计)。 */
7941
- const content = isRecord(message) && Array.isArray(message.content) ? message.content : null;
7942
- if (!content) return 0;
7943
- /** token 合计。 */
7944
- let total = 0;
7945
- for (const block of content) {
7946
- if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
7947
- for (const sr of legitimateSystemReminderBlocks(block.text)) total += estimateTextTokens(sr);
7948
- }
7949
- return total;
7926
+ /** 提取 <history> 块内文(去掉开/闭标签与首尾空白;非块文本原样返回)。 */
7927
+ function historyInnerText(text) {
7928
+ /** 闭标签。 */
7929
+ const closeTag = `</${HISTORY_TAG}>`;
7930
+ const close = text.lastIndexOf(closeTag);
7931
+ if (close === -1) return text;
7932
+ const open = text.indexOf(`<${HISTORY_TAG}`);
7933
+ if (open === -1) return text;
7934
+ const gt = text.indexOf(">", open);
7935
+ if (gt === -1 || gt >= close) return text;
7936
+ return text.slice(gt + 1, close).trim();
7950
7937
  }
7951
7938
  /**
7952
7939
  * 未压缩消息 token 估算:最后一个 <history> 块之后的表层节点合计
7953
7940
  * (其前含自身视为已压缩,不计入观察阈值衡量对象;无压缩日志时计全部)。
7954
- * 用户消息中合法 <system-reminder> 块(宿主注入的提醒)的 token 扣除,不计入阈值。
7941
+ * 系统消息(user/message 中非 kind:user 的部分,如宿主注入的上下文)不计入阈值。
7955
7942
  */
7956
7943
  function measureUncompressedTokens(session, meter) {
7957
7944
  /** 压缩边界(最后一个 history 块的 seq;无则 undefined)。 */
@@ -7969,9 +7956,10 @@ function measureUncompressedTokens(session, meter) {
7969
7956
  const event = session.events[seq];
7970
7957
  const message = event ? session.deriveEventMessage(event) : null;
7971
7958
  if (!message) continue;
7972
- /** 消息 token 估算(用户消息扣除其中合法 system-reminder 块,宿主提醒不计入未压缩消息)。 */
7973
- const tokens = event?.type === "user/message" ? Math.max(0, meter.estimateMessage(message) - systemReminderTokens(message)) : meter.estimateMessage(message);
7974
- total += tokens;
7959
+ if (event?.type === "user/message") {
7960
+ if (event.data.source?.kind !== "user") continue;
7961
+ }
7962
+ total += meter.estimateMessage(message);
7975
7963
  }
7976
7964
  return total;
7977
7965
  }
@@ -8078,7 +8066,8 @@ function appendCompactionStart(session, lifecycle) {
8078
8066
  * summary 为完整合并后的 <history> 内文;usage 由摘要调用提取(无则省略)。
8079
8067
  */
8080
8068
  function appendCompactionSummary(session, data) {
8081
- return session.append("compaction/summary", {
8069
+ /** summary 事件载荷(宿主类型 + 插件扩展 shadowedCharCount;宿主 append 不做 schema 剥离,扩展字段原样持久化)。 */
8070
+ const payload = {
8082
8071
  compactionId: data.lifecycle.compactionId,
8083
8072
  summary: [{
8084
8073
  type: "text",
@@ -8087,11 +8076,13 @@ function appendCompactionSummary(session, data) {
8087
8076
  shadowedRange: data.shadowedRange,
8088
8077
  shadowedSeqs: data.shadowedSeqs,
8089
8078
  shadowedTokenCount: data.shadowedTokenCount,
8079
+ shadowedCharCount: data.shadowedCharCount,
8090
8080
  provider: data.provider,
8091
8081
  model: data.model,
8092
8082
  maxTokens: data.maxTokens,
8093
8083
  ...data.usage === void 0 ? {} : { usage: data.usage }
8094
- }).seq;
8084
+ };
8085
+ return session.append("compaction/summary", payload).seq;
8095
8086
  }
8096
8087
  /** 追加 compaction/end(log-only,结束生命周期;error 记录失败原因)。 */
8097
8088
  function appendCompactionEnd(session, lifecycle, error) {
@@ -8144,6 +8135,8 @@ async function reflectPass(ctx, agent, config, window, target, signal) {
8144
8135
  const threshold = Math.floor(window * config.historyMergeRatio);
8145
8136
  /** 全部块 token 估算合计(摘要总长)。 */
8146
8137
  const tokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
8138
+ /** 被压缩块区段的字符数合计(压缩前内文长度;UI 标题统计用,与观察路径一致只计内容不计标签)。 */
8139
+ const shadowedCharCount = blocks.reduce((total, block) => total + historyInnerText(block.text).length, 0);
8147
8140
  if (tokens < threshold) {
8148
8141
  logger.step(`反思:摘要 ${tokens} tokens < 阈值 ${threshold},跳过`);
8149
8142
  return;
@@ -8197,6 +8190,7 @@ async function reflectPass(ctx, agent, config, window, target, signal) {
8197
8190
  },
8198
8191
  shadowedSeqs: blockSeqs,
8199
8192
  shadowedTokenCount: tokens,
8193
+ shadowedCharCount,
8200
8194
  provider: target.provider,
8201
8195
  model: target.model,
8202
8196
  maxTokens: config.compressMaxTokens,
@@ -8290,6 +8284,14 @@ async function observePass(ctx, agent, config, window, tailCount, target, signal
8290
8284
  const message = event ? session.deriveEventMessage(event) : null;
8291
8285
  return total + (message ? ctx.tokenMeter.estimateMessage(message) : 0);
8292
8286
  }, 0);
8287
+ /** 被替换消息的字符数合计(压缩前文本长度;UI 标题统计用,递归计入 tool-result 内嵌文本)。 */
8288
+ const shadowedCharCount = replaceSeqs.reduce((total, seq) => {
8289
+ /** 当前表层事件。 */
8290
+ const event = session.events[seq];
8291
+ /** 事件对应的消息(取文本块字符数)。 */
8292
+ const message = event ? session.deriveEventMessage(event) : null;
8293
+ return total + (message ? textCharCount(message) : 0);
8294
+ }, 0);
8293
8295
  /** 本次压缩生命周期(compactionId + 当前轮次;摘要成功后才写入日志)。 */
8294
8296
  const lifecycle = {
8295
8297
  compactionId: newCompactionId(),
@@ -8309,6 +8311,7 @@ async function observePass(ctx, agent, config, window, tailCount, target, signal
8309
8311
  },
8310
8312
  shadowedSeqs: replaceSeqs,
8311
8313
  shadowedTokenCount,
8314
+ shadowedCharCount,
8312
8315
  provider: target.provider,
8313
8316
  model: target.model,
8314
8317
  maxTokens: config.compressMaxTokens,
@@ -8684,7 +8687,8 @@ function resolveConfig(raw) {
8684
8687
  //#region src/recall.ts
8685
8688
  /**
8686
8689
  * recall 工具:按「完整消息」序号(index)回看原始会话(start/end 为完整消息 index,
8687
- * offset 为相对 start 的完整消息步数)。完整消息即 `用户消息`,`模型输出文本`和`具有result的
8690
+ * offset 为相对 start 的完整消息步数)。完整消息即 `用户消息`(user_message 中 kind 为 user
8691
+ * 的部分)、`系统消息`(user_message 中其余 kind 的部分)、`模型输出文本`和`具有result的
8688
8692
  * toolcall`;首条完整消息的 index 为 0,后续递增、会话内全局稳定,与摘要日志条目同一套编号。
8689
8693
  * recall 自身不设输出上限:超大的工具结果由 tool-result-pruner 裁剪(pruneContent),
8690
8694
  * 输出 token 由 pruner 配置控制。
@@ -8718,20 +8722,20 @@ function parseRecallArgs(raw) {
8718
8722
  function buildRecallTool(getPruner) {
8719
8723
  return {
8720
8724
  name: "recall",
8721
- description: "根据完整消息序号(index),回看指定区间的过往消息。`用户消息`,`模型输出文本`和`具有result的toolcall`被视作`完整消息`。首条`完整消息`的index是0,后续的index递增。start 必须传入,是区间的基准;end 和 offset 二选一:end 指定另一个边界(含两端,与 start 的位置关系不影响结果),offset 指定区间包含的完整消息数量(正数向后、负数向前)。",
8725
+ description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以精确查询完整消息。用index指定一个区间,返回区间内所有完整消息的内容。`,
8722
8726
  parameters: {
8723
8727
  start: {
8724
8728
  type: "number",
8725
- description: "完整消息序号(index),区间的基准边界",
8729
+ description: "完整消息序号(index),作为基准边界,和end或offset配合,指定区间",
8726
8730
  required: true
8727
8731
  },
8728
8732
  end: {
8729
8733
  type: "number",
8730
- description: "与 offset 互斥,指定区间的另一个边界(含)。与 start 的位置关系不影响结果。"
8734
+ description: "与 offset 互斥,指定区间的另一个边界。"
8731
8735
  },
8732
8736
  offset: {
8733
8737
  type: "number",
8734
- description: "与 end 互斥,指定区间包含的完整消息数量。传入正数查看 start 之后的若干条,负数则是之前的。"
8738
+ description: "与 end 互斥。相对 start 的步数:正数向后、负数向前。"
8735
8739
  }
8736
8740
  },
8737
8741
  output: {
@@ -8795,7 +8799,8 @@ function buildRecallTool(getPruner) {
8795
8799
  //#region src/semantic-recall.ts
8796
8800
  /**
8797
8801
  * recall-semantic 工具:按自然语言 query 对会话全部完整消息(含被压缩/遮蔽的)做语义
8798
- * 检索,返回最匹配的完整消息与匹配说明。完整消息即 `用户消息`,`模型输出文本`和`具有result的
8802
+ * 检索,返回最匹配的完整消息与匹配说明。完整消息即 `用户消息`(user_message 中 kind 为 user
8803
+ * 的部分)、`系统消息`(user_message 中其余 kind 的部分)、`模型输出文本`和`具有result的
8799
8804
  * toolcall`;首条完整消息的 index 为 0,后续递增、会话内全局稳定。
8800
8805
  *
8801
8806
  * - 参数:query 必填;top_k(默认 3,1-10);start/end/offset 限定检索区间
@@ -8898,7 +8903,7 @@ function matchExplanation(query, text, score) {
8898
8903
  return `相似度 ${score.toFixed(3)}${keywords}`;
8899
8904
  }
8900
8905
  /** 模型未就绪时返回给模型的文案(告知即可;下载完成后无需另行通知,直接再次调用)。 */
8901
- const SEMANTIC_MODEL_NOT_READY_MESSAGE = "语义检索暂不可用:本地嵌入模型尚未就绪(正在后台下载约 113MB;下载完成后无需提示,直接再次调用本工具即可。若下载失败,下次调用会自动重试)。可稍后重试,或先用 recall 工具按完整消息 index 精确检索。";
8906
+ const SEMANTIC_MODEL_NOT_READY_MESSAGE = "语义检索暂不可用,本地向量模型尚未就绪。";
8902
8907
  /** 构建 recall-semantic 工具定义(embedder/modelStatus 可注入,测试传替身;缺省用本地模型)。 */
8903
8908
  function buildSemanticRecallTool(options) {
8904
8909
  /** pruner 获取器(可选,裁剪超大工具结果)。 */
@@ -8907,7 +8912,7 @@ function buildSemanticRecallTool(options) {
8907
8912
  const embed = options?.embedder ?? ((texts) => getEmbedder().then((fn) => fn(texts)));
8908
8913
  return {
8909
8914
  name: "recall-semantic",
8910
- description: "按语义(自然语言含义)在会话全部完整消息中检索:`用户消息`,`模型输出文本`和`具有result的toolcall`被视作`完整消息`。首条`完整消息`的index是0,后续的index递增。用一句话描述你要找的内容(可混用中英文与代码术语)。返回最匹配的若干条完整消息、index 与匹配说明;如需精确定位某个 index 周边的消息,请再用 recall 工具。",
8915
+ description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以按自然语言含义,检索最符合的完整消息。默认在全消息范围搜索,可以指定区间。`,
8911
8916
  parameters: {
8912
8917
  query: {
8913
8918
  type: "string",
@@ -8920,15 +8925,15 @@ function buildSemanticRecallTool(options) {
8920
8925
  },
8921
8926
  start: {
8922
8927
  type: "number",
8923
- description: "完整消息序号(index),可选。限定检索区间:以该条为基准边界(意义同 recall)。缺省检索全部消息。区间不合法(越界)时自动回退全量检索并在结果中说明。"
8928
+ description: "完整消息序号(index),可选。作为基准边界,和end或offset配合,指定搜索区间。"
8924
8929
  },
8925
8930
  end: {
8926
8931
  type: "number",
8927
- description: " offset 互斥,限定区间的另一个边界(含;意义同 recall)。"
8932
+ description: "必须和start配合使用,与 offset 互斥,指定搜索区间的另一个边界。"
8928
8933
  },
8929
8934
  offset: {
8930
8935
  type: "number",
8931
- description: " end 互斥。相对 start 的步数:正数向后、负数向前(意义同 recall)。"
8936
+ description: "必须和start配合使用,与 end 互斥。相对 start 的步数:正数向后、负数向前。"
8932
8937
  }
8933
8938
  },
8934
8939
  output: {
@@ -1,16 +1,3 @@
1
- /**
2
- * 会话日志索引:完整消息索引(index 定位完整消息,recall 与摘要共用同一套编号),
3
- * 以及表层节点定位辅助。事件日志仅追加(被遮蔽的事件仍可读,recall 依赖此性质)。
4
- *
5
- * 完整消息是摘要日志与 recall 共用的定位单位,分三类:
6
- * - user:用户消息(user/message)占一条;
7
- * - assistant:模型输出文本(assistant/message 中的文本块)占一条;
8
- * - toolcall:单个工具调用及其结果占一条(同一条 AI 消息里的文本与工具调用拆开,
9
- * tool/result 按 source.callId 匹配其 tool-call;未匹配的 result 独立成条,防御)。
10
- * 插件自产消息(<history>、运行时快照等 source.kind === 'plugin')不占 index。
11
- * index 从 0 起、按日志顺序递增、只追加不重排 → 会话内全局稳定,
12
- * 压缩后旧摘要条目引用的 index 仍然有效。
13
- */
14
1
  import type { CompleteMessage, MessageIndex, Session, SessionEvent } from './types.ts';
15
2
  /** 工具结果裁剪器结构(tool-result-pruner;超大结果渲染前裁剪)。 */
16
3
  export type PrunerLike = {
@@ -26,7 +13,7 @@ export declare function messageIdOfEvent(event: SessionEvent | undefined): strin
26
13
  /** 消息索引:按日志顺序列出全部消息事件,并按 message_id 定位其在消息序列中的下标。 */
27
14
  export declare function indexMessages(session: Session): MessageIndex;
28
15
  /**
29
- * 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(三类,见文件头)。
16
+ * 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(四类,见文件头)。
30
17
  * 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
31
18
  * 插件在 agent/pre-step 触发压缩(日志 call-result 完备),因此压缩时不存在未闭合的
32
19
  * tool-call;索引对未闭合调用不设特殊处理。
@@ -34,7 +21,7 @@ export declare function indexMessages(session: Session): MessageIndex;
34
21
  export declare function indexCompleteMessages(session: Session): CompleteMessage[];
35
22
  /**
36
23
  * 渲染一条完整消息的文本(recall 输出 / new 模式输入共用):
37
- * - user:消息原文;
24
+ * - user / sys:消息原文;
38
25
  * - assistant:仅文本块;
39
26
  * - toolcall:调用块(工具名 + 参数)+ 结果文本(pruner 裁剪超大结果)。
40
27
  */
@@ -1 +1 @@
1
- {"version":3,"file":"log-index.d.ts","sourceRoot":"","sources":["../src/log-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EAEZ,OAAO,EACP,YAAY,EACb,MAAM,YAAY,CAAC;AAGpB,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IAAE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,CAAC;AAE7F;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpF;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CA0B5D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CAkEzE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,MAAM,CAgER"}
1
+ {"version":3,"file":"log-index.d.ts","sourceRoot":"","sources":["../src/log-index.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EAEZ,OAAO,EACP,YAAY,EACb,MAAM,YAAY,CAAC;AAGpB,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IAAE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,CAAC;AAE7F;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpF;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CA0B5D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CA2EzE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,MAAM,CAgER"}
package/dist/recall.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * recall 工具:按「完整消息」序号(index)回看原始会话(start/end 为完整消息 index,
3
- * offset 为相对 start 的完整消息步数)。完整消息即 `用户消息`,`模型输出文本`和`具有result的
3
+ * offset 为相对 start 的完整消息步数)。完整消息即 `用户消息`(user_message 中 kind 为 user
4
+ * 的部分)、`系统消息`(user_message 中其余 kind 的部分)、`模型输出文本`和`具有result的
4
5
  * toolcall`;首条完整消息的 index 为 0,后续递增、会话内全局稳定,与摘要日志条目同一套编号。
5
6
  * recall 自身不设输出上限:超大的工具结果由 tool-result-pruner 裁剪(pruneContent),
6
7
  * 输出 token 由 pruner 配置控制。
@@ -1 +1 @@
1
- {"version":3,"file":"recall.d.ts","sourceRoot":"","sources":["../src/recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,YAAY,CAAC;AAGjE;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;iBAQzB,CAAC;AAEL,4CAA4C;AAC5C,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAYxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,cAAc,CAsFzE"}
1
+ {"version":3,"file":"recall.d.ts","sourceRoot":"","sources":["../src/recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,YAAY,CAAC;AAGjE;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;iBAQzB,CAAC;AAEL,4CAA4C;AAC5C,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAYxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,cAAc,CAoFzE"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * recall-semantic 工具:按自然语言 query 对会话全部完整消息(含被压缩/遮蔽的)做语义
3
- * 检索,返回最匹配的完整消息与匹配说明。完整消息即 `用户消息`,`模型输出文本`和`具有result的
3
+ * 检索,返回最匹配的完整消息与匹配说明。完整消息即 `用户消息`(user_message 中 kind 为 user
4
+ * 的部分)、`系统消息`(user_message 中其余 kind 的部分)、`模型输出文本`和`具有result的
4
5
  * toolcall`;首条完整消息的 index 为 0,后续递增、会话内全局稳定。
5
6
  *
6
7
  * - 参数:query 必填;top_k(默认 3,1-10);start/end/offset 限定检索区间
@@ -45,7 +46,7 @@ export declare function tokenize(text: string): Set<string>;
45
46
  /** 命中消息的匹配说明行:相似度 + 与 query 共有的关键词(最多 8 个)。 */
46
47
  export declare function matchExplanation(query: string, text: string, score: number): string;
47
48
  /** 模型未就绪时返回给模型的文案(告知即可;下载完成后无需另行通知,直接再次调用)。 */
48
- export declare const SEMANTIC_MODEL_NOT_READY_MESSAGE = "\u8BED\u4E49\u68C0\u7D22\u6682\u4E0D\u53EF\u7528\uFF1A\u672C\u5730\u5D4C\u5165\u6A21\u578B\u5C1A\u672A\u5C31\u7EEA\uFF08\u6B63\u5728\u540E\u53F0\u4E0B\u8F7D\u7EA6 113MB\uFF1B\u4E0B\u8F7D\u5B8C\u6210\u540E\u65E0\u9700\u63D0\u793A\uFF0C\u76F4\u63A5\u518D\u6B21\u8C03\u7528\u672C\u5DE5\u5177\u5373\u53EF\u3002\u82E5\u4E0B\u8F7D\u5931\u8D25\uFF0C\u4E0B\u6B21\u8C03\u7528\u4F1A\u81EA\u52A8\u91CD\u8BD5\uFF09\u3002\u53EF\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u5148\u7528 recall \u5DE5\u5177\u6309\u5B8C\u6574\u6D88\u606F index \u7CBE\u786E\u68C0\u7D22\u3002";
49
+ export declare const SEMANTIC_MODEL_NOT_READY_MESSAGE = "\u8BED\u4E49\u68C0\u7D22\u6682\u4E0D\u53EF\u7528\uFF0C\u672C\u5730\u5411\u91CF\u6A21\u578B\u5C1A\u672A\u5C31\u7EEA\u3002";
49
50
  /** 构建 recall-semantic 工具定义(embedder/modelStatus 可注入,测试传替身;缺省用本地模型)。 */
50
51
  export declare function buildSemanticRecallTool(options?: {
51
52
  getPruner?: () => unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-recall.d.ts","sourceRoot":"","sources":["../src/semantic-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAoB,KAAK,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/F,OAAO,KAAK,EAAmB,cAAc,EAAkB,MAAM,YAAY,CAAC;AAGlF,qEAAqE;AACrE,eAAO,MAAM,wBAAwB;;;;;;iBAUjC,CAAC;AAEL,6DAA6D;AAC7D,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,+CAA+C;AAC/C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAWxE;AAED,6DAA6D;AAC7D,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC,GACzD,WAAW,CA2Bb;AAED,uCAAuC;AACvC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOlD;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAcnF;AAED,+CAA+C;AAC/C,eAAO,MAAM,gCAAgC,0iBACoE,CAAC;AAElH,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE;IAChD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxD,GAAG,cAAc,CAuHjB"}
1
+ {"version":3,"file":"semantic-recall.d.ts","sourceRoot":"","sources":["../src/semantic-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAoB,KAAK,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/F,OAAO,KAAK,EAAmB,cAAc,EAAkB,MAAM,YAAY,CAAC;AAGlF,qEAAqE;AACrE,eAAO,MAAM,wBAAwB;;;;;;iBAUjC,CAAC;AAEL,6DAA6D;AAC7D,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,+CAA+C;AAC/C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAWxE;AAED,6DAA6D;AAC7D,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC,GACzD,WAAW,CA2Bb;AAED,uCAAuC;AACvC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOlD;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAcnF;AAED,+CAA+C;AAC/C,eAAO,MAAM,gCAAgC,6HAAyB,CAAC;AAEvE,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE;IAChD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxD,GAAG,cAAc,CAqHjB"}
@@ -5,9 +5,10 @@
5
5
  * 输入与输出都是合法的 <history> 块(模型消息 + index 的表达形式),先定义块、要求压缩、
6
6
  * 再给出数据源(下方 <history> 消息记录)。
7
7
  *
8
- * 观察输入由 renderMessages 渲染为 <history> 块:用户消息文本原样(其中合法
9
- * <system-reminder> 块整块原样保留、不转义;图片/文件以注释补充)、
10
- * <reasoning> 参考条目(产物中没有)、assistant 文本与 toolcall&result 原样。
8
+ * 观察输入由 renderMessages 渲染为 <history> 块:用户消息文本原样(图片/文件以
9
+ * 注释补充)、系统消息(user_message 中非 kind:user 的部分)渲染为
10
+ * <sys type="KIND" index="N"> 空块(内容不进入输入)、<reasoning> 参考条目
11
+ * (产物中没有)、assistant 文本与 toolcall&result 原样。
11
12
  * 输出经 extractSummaryLog 校验:合法 <history> 块、不含 <reasoning>、index/start/end 连续
12
13
  * (与预期覆盖区间一致),失败按 config.compressRetryCount 重试。
13
14
  * token usage 从流式响应的 usage chunk 提取,归入主会话记录。仅主会话生效(index.ts 守卫)。
@@ -20,18 +21,14 @@ import { type RoutedTarget } from './utils.ts';
20
21
  * index/start/end 连续)、输出格式(一个合法 <history> 块,无 reasoning)、数据源说明。
21
22
  */
22
23
  export declare function buildHistoryPrompt(): string;
23
- /**
24
- * 提取文本中全部合法的 <system-reminder> 块(完整开闭标签对且内容可被 XML 解析);
25
- * 供渲染(原样插入)与未压缩消息测量(不计入)共用;不合法部分按普通文本处理。
26
- */
27
- export declare function legitimateSystemReminderBlocks(text: string): string[];
28
24
  /**
29
25
  * 渲染完整消息记录(观察输入,new 模式):输出一个合法的 <history> 块——
30
- * - user → <user_message index="N">(文本原样,其中合法 <system-reminder> 块整块原样保留、不转义;图片/文件注释);
26
+ * - user → <user_message index="N">(文本原样;图片/文件注释);
27
+ * - sys → <sys type="KIND" index="N"></sys>(系统消息空块,内容不进入压缩输入);
31
28
  * - 每条 assistant 消息的 reasoning → <reasoning>(参考条目,产物中没有);
32
29
  * - assistant / toolcall → <assistant index="N">(模型输出文本 / toolcall&result 原样)。
33
30
  * 文本经 XML 序列化自动转义(用户输入 / 文本 / reasoning 中的特殊字符不破坏 XML 合法性)。
34
- * 仅渲染 seqs 全部落在给定集合内的完整消息(插件自产消息天然不占位)。
31
+ * 仅渲染 seqs 全部落在给定集合内的完整消息(本插件自产消息天然不占位)。
35
32
  */
36
33
  export declare function renderMessages(session: Session, seqs: readonly number[]): string;
37
34
  /** 摘要调用结果:文本 + 可选 token usage(摘要请求自身消耗,随 compaction/summary 归入主会话记录)。 */
@@ -42,10 +39,10 @@ export type SummarySubagentResult = {
42
39
  /** 日志最小有效长度:<history> 中间内容小于该长度视为不合法(C 段校验)。 */
43
40
  export declare const MIN_HISTORY_LENGTH = 10;
44
41
  /** 产出日志后插入首个 <history> 后的格式说明(XML 注释,完整消息定义 + 条目标签语义)。 */
45
- export declare const HISTORY_FORMAT_NOTE = "<!-- \u5B8C\u6574\u6D88\u606F\uFF1A`\u7528\u6237\u6D88\u606F`\uFF0C`\u6A21\u578B\u8F93\u51FA\u6587\u672C`\u548C`\u5177\u6709result\u7684toolcall`\u88AB\u89C6\u4F5C`\u5B8C\u6574\u6D88\u606F`\u3002\u9996\u6761`\u5B8C\u6574\u6D88\u606F`\u7684index\u662F0\uFF0C\u540E\u7EED\u7684index\u9012\u589E\u3002 <user_message index=\"N\"> \u4E0E <assistant index=\"N\"> \u8868\u793A\u5355\u6761\u5B8C\u6574\u6D88\u606F\uFF0C<assistant start=\"A\" end=\"B\"> \u8868\u793A\u8FDE\u7EED\u6A21\u5757\uFF0Cstart/end \u662F\u9996\u5C3E\u5B8C\u6574\u6D88\u606F\u7684 index -->";
42
+ export declare const HISTORY_FORMAT_NOTE = "<!-- \u5B8C\u6574\u6D88\u606F\uFF1A`\u5B8C\u6574\u6D88\u606F`\u88AB\u5B9A\u4E49\u4E3A`\u7528\u6237\u6D88\u606F`\uFF0C`\u7CFB\u7EDF\u6D88\u606F`\uFF0C`\u6A21\u578B\u8F93\u51FA\u6587\u672C`\u548C`\u5177\u6709result\u7684toolcall`\u7684\u96C6\u5408\u3002\u9996\u6761`\u5B8C\u6574\u6D88\u606F`\u7684index\u662F0\uFF0C\u540E\u7EED\u7684index\u9012\u589E\u3002 <TAG index=\"N\">\u8868\u793A\u5355\u6761\u5B8C\u6574\u6D88\u606F\uFF0C<TAG start=\"A\" end=\"B\"> \u8868\u793A\u8FDE\u7EED\u6A21\u5757\uFF0Cstart/end \u662F\u9996\u5C3E\u5B8C\u6574\u6D88\u606F\u7684 index\uFF1B<sys type=\"KIND\" index=\"N\"> \u8868\u793A\u88AB\u538B\u7F29\u7684\u7CFB\u7EDF\u6D88\u606F\uFF0C\u5757\u4E2D\u4E3A\u7A7A -->";
46
43
  /** history 块内条目(单条 index 或模块 start/end)。 */
47
44
  export type HistoryEntryRange = {
48
- kind: 'user' | 'assistant';
45
+ kind: 'user' | 'assistant' | 'sys';
49
46
  index?: number;
50
47
  start?: number;
51
48
  end?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"summarize.d.ts","sourceRoot":"","sources":["../src/summarize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,OAAO,KAAK,EAAE,KAAK,EAAmB,OAAO,EAAE,OAAO,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,KAAK,YAAY,EAAQ,MAAM,YAAY,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAoC3C;AA4ED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrE;AAyCD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CA2DhF;AAED,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB,CAAC;AA8EF,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,gjBAAyK,CAAC;AAE1M,4CAA4C;AAC5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AA8EF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAYrE;AACD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,iBAAiB,EAAE,GAC3B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAuBvC;AAED,sCAAsC;AACtC,MAAM,MAAM,sBAAsB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAiC/F;AACD,iFAAiF;AACjF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAK/C;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAA;CAAE,GACpE,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CA+EvC"}
1
+ {"version":3,"file":"summarize.d.ts","sourceRoot":"","sources":["../src/summarize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,OAAO,KAAK,EAAE,KAAK,EAAmB,OAAO,EAAE,OAAO,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,KAAK,YAAY,EAAQ,MAAM,YAAY,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAsC3C;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAqEhF;AAED,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB,CAAC;AA8EF,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,yrBAA6K,CAAC;AAE9M,4CAA4C;AAC5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,KAAK,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAoFF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAYrE;AACD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,iBAAiB,EAAE,GAC3B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAuBvC;AAED,sCAAsC;AACtC,MAAM,MAAM,sBAAsB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAiC/F;AACD,iFAAiF;AACjF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAK/C;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAA;CAAE,GACpE,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CA+EvC"}