dsh-plugin-om 0.0.31 → 0.0.33

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
@@ -16,7 +16,8 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
16
16
  /**
17
17
  * 共享常量:插件级魔法字符串,集中定义避免散落各模块。
18
18
  * 导出 PLUGIN_LABEL / HISTORY_TAG / HISTORY_TIP / COMPLETE_MESSAGE_DEFINITION /
19
- * COMPACT_CHECKPOINT_PLUGIN / COMPACTION_ABORTED_ERROR / isPluginOwnedSource。
19
+ * HISTORY_FORMAT_NOTE / SKILL_TOOL_NAME / COMPACT_CHECKPOINT_PLUGIN /
20
+ * COMPACTION_ABORTED_ERROR / isPluginOwnedSource。
20
21
  */
21
22
  /** 插件标识:压缩消息 source.plugin 取值与日志前缀。 */
22
23
  const PLUGIN_LABEL = "dsh-plugin-om";
@@ -29,8 +30,12 @@ const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
29
30
  * 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
30
31
  */
31
32
  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"> 表示被压缩的系统消息,块中为空 -->`;
33
+ /**
34
+ * 最终 <history> 块内文块首的格式说明注释(XML 注释,完整消息定义 + 条目标签语义 + CDATA 约定)。
35
+ */
36
+ const HISTORY_FORMAT_NOTE = `<!-- ${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示多条连续消息,start/end 是首尾完整消息的 index;消息块的内容是用CDATA包裹的纯文本;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空;<skill_content name="S" index="N"> 表示未压缩的原始 skill -->`;
37
+ /** skill 工具名:toolcall 条目的工具名为该值时视为 skill 加载,<history> 块中以 <skill_content> 元素呈现。 */
38
+ const SKILL_TOOL_NAME = "skill";
34
39
  /**
35
40
  * 压缩因 signal 中止而放弃时 compaction/end 的 error 标识(服务端写入、客户端过滤):
36
41
  * 中止不是失败,客户端据此隐藏失败行(宿主不变量要求无 summary 的 end 必须带 error)。
@@ -134,13 +139,79 @@ function phaseLabel(phase) {
134
139
  function compressionRecordLabel(phase, rounds, success) {
135
140
  return `OM 压缩${success ? "会话记录" : "失败日志"}(${phaseLabel(phase)} · ${rounds} 轮)`;
136
141
  }
142
+ /** usage 各桶文案(缺失桶显示 -)。 */
143
+ function usageLine(usage) {
144
+ const field = (value) => value === void 0 ? "-" : String(value);
145
+ return `input ${field(usage.inputTokens)} / output ${field(usage.outputTokens)} / cacheRead ${field(usage.cacheReadTokens)} / cacheWrite ${field(usage.cacheWriteTokens)} / reasoning ${field(usage.reasoningTokens)} tokens`;
146
+ }
147
+ /** 汇总逐轮 usage(各桶任一轮存在即求和保留;全部轮次无 usage 时返回 undefined)。 */
148
+ function sumRoundUsage(rounds) {
149
+ let total;
150
+ for (const round of rounds) {
151
+ if (round.usage === void 0) continue;
152
+ if (total === void 0) {
153
+ total = { ...round.usage };
154
+ continue;
155
+ }
156
+ const sum = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
157
+ const merged = {
158
+ inputTokens: (total.inputTokens ?? 0) + (round.usage.inputTokens ?? 0),
159
+ outputTokens: (total.outputTokens ?? 0) + (round.usage.outputTokens ?? 0)
160
+ };
161
+ const cacheRead = sum(total.cacheReadTokens, round.usage.cacheReadTokens);
162
+ const cacheWrite = sum(total.cacheWriteTokens, round.usage.cacheWriteTokens);
163
+ const reasoning = sum(total.reasoningTokens, round.usage.reasoningTokens);
164
+ if (cacheRead !== void 0) merged.cacheReadTokens = cacheRead;
165
+ if (cacheWrite !== void 0) merged.cacheWriteTokens = cacheWrite;
166
+ if (reasoning !== void 0) merged.reasoningTokens = reasoning;
167
+ total = merged;
168
+ }
169
+ return total;
170
+ }
171
+ /**
172
+ * 格式化压缩循环统计为统计消息文本:起止时间(ISO)、总耗时、逐轮请求耗时与
173
+ * usage、usage 合计。随会话记录在子会话末尾落盘。
174
+ */
175
+ function formatCompressionStats(phase, success, stats) {
176
+ const lines = [
177
+ "【压缩统计】",
178
+ `阶段:${phaseLabel(phase)}|结果:${success ? "成功" : "失败"}`,
179
+ `开始:${new Date(stats.startedAt).toISOString()}`,
180
+ `结束:${new Date(stats.completedAt).toISOString()}`,
181
+ `总耗时:${stats.durationMs} ms`,
182
+ `请求轮数:${stats.rounds.length}`
183
+ ];
184
+ if (stats.rounds.length > 0) {
185
+ lines.push("逐轮:");
186
+ for (const round of stats.rounds) lines.push(`- 第 ${round.round} 轮:耗时 ${round.durationMs} ms${round.usage === void 0 ? "" : `,${usageLine(round.usage)}`}`);
187
+ }
188
+ const usage = sumRoundUsage(stats.rounds);
189
+ if (usage !== void 0) lines.push(`usage 合计:${usageLine(usage)}`);
190
+ return lines.join("\n");
191
+ }
192
+ /** 构造插件自产 user 消息(子会话统计消息;id 为品牌类型 MessageId)。 */
193
+ function makeStatsMessage(text) {
194
+ return {
195
+ id: uuid(),
196
+ role: "user",
197
+ content: [{
198
+ type: "text",
199
+ text
200
+ }],
201
+ source: {
202
+ kind: "plugin",
203
+ plugin: PLUGIN_LABEL
204
+ }
205
+ };
206
+ }
137
207
  /**
138
208
  * 把一次压缩工具循环的完整会话消息组落盘为子会话:ctx.sessions.create 创建子会话
139
209
  * (header origin 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、
140
210
  * cwd 继承主会话),追加 one-shot descriptor(provider om-compaction-log,label 含
141
211
  * 压缩阶段与轮数),逐消息原样追加(user 指令/提醒与 tool-result 为 user/message,
142
- * assistant 含 tool-call 块为 assistant/message),flush 持久化,返回子会话 id。
143
- * 成功与失败均调用;落盘自身绝不抛错,任何失败仅 logger.warn 并返回 undefined。
212
+ * assistant 含 tool-call 块为 assistant/message),末尾追加插件来源统计消息(起止
213
+ * 时间、总耗时、逐轮耗时与 usage),flush 持久化,返回子会话 id。成功与失败均调用;
214
+ * 落盘自身绝不抛错,任何失败仅 logger.warn 并返回 undefined。
144
215
  */
145
216
  async function recordCompressionSession(ctx, parentSession, options) {
146
217
  const logger = makeLogger(ctx, options.debug);
@@ -156,7 +227,7 @@ async function recordCompressionSession(ctx, parentSession, options) {
156
227
  version: SUBAGENT_DESCRIPTOR_VERSION,
157
228
  mode: "one-shot",
158
229
  provider: COMPACTION_LOG_PROVIDER,
159
- label: compressionRecordLabel(options.phase, options.rounds, options.success)
230
+ label: compressionRecordLabel(options.phase, options.stats.rounds.length, options.success)
160
231
  });
161
232
  let step = 0;
162
233
  for (const message of options.messages) {
@@ -168,6 +239,7 @@ async function recordCompressionSession(ctx, parentSession, options) {
168
239
  }, { surfaceOp: "append" });
169
240
  else child.append("user/message", message, { surfaceOp: "append" });
170
241
  }
242
+ child.append("user/message", makeStatsMessage(formatCompressionStats(options.phase, options.success, options.stats)), { surfaceOp: "append" });
171
243
  try {
172
244
  await ctx.sessions.flush(child);
173
245
  } catch (error) {
@@ -7316,8 +7388,9 @@ var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
7316
7388
  * 会话日志索引:完整消息索引与渲染。
7317
7389
  * 导出 indexCompleteMessages(完整消息四类折叠索引,recall 与摘要共用同一套编号)、
7318
7390
  * indexMessages / surfaceIndexOf / messageIdOfEvent(消息级定位辅助)、
7319
- * collectImageRefs / renderCompleteMessageParts / renderCompleteMessage(完整消息渲染
7320
- * 与图片附件收集)。事件日志仅追加(被遮蔽的事件仍可读,recall 依赖此性质)。
7391
+ * collectImageRefs / toolResultMessageOf / renderToolResultText / renderCompleteMessageParts /
7392
+ * renderCompleteMessage(完整消息渲染与图片附件收集)。事件日志仅追加(被遮蔽的事件仍可读,
7393
+ * recall 依赖此性质)。
7321
7394
  *
7322
7395
  * 完整消息分四类:user(用户消息)、sys(系统消息,压缩日志中以 <sys> 空块表示)、
7323
7396
  * assistant(模型输出文本)、toolcall(单个工具调用及其结果,result 按 callId 匹配并入)。
@@ -7418,7 +7491,28 @@ function collectImageRefs(content, out) {
7418
7491
  }
7419
7492
  }
7420
7493
  /**
7421
- * 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
7494
+ * 取 toolcall 完整消息的 tool/result 消息(超大结果经 pruner 裁剪):
7495
+ * 结果文本渲染与 <skill> 条目正文共用;非 toolcall 或无配对 result 时返回 undefined。
7496
+ */
7497
+ function toolResultMessageOf(session, cm, pruner) {
7498
+ if (cm.type !== "toolcall") return void 0;
7499
+ const resultSeq = cm.seqs[1];
7500
+ const resultEvent = resultSeq === void 0 ? void 0 : session.events[resultSeq];
7501
+ if (resultEvent?.type !== "tool/result") return void 0;
7502
+ const message = session.deriveEventMessage(resultEvent);
7503
+ if (!message) return void 0;
7504
+ const pruned = pruner?.pruneContent?.(message.content);
7505
+ return pruned ? {
7506
+ ...message,
7507
+ content: pruned
7508
+ } : message;
7509
+ }
7510
+ /** 渲染 toolcall 完整消息的工具返回文本(仅 result 内容;无配对 result 时为空串)。 */
7511
+ function renderToolResultText(session, cm, pruner) {
7512
+ const message = toolResultMessageOf(session, cm, pruner);
7513
+ return message ? renderMessageText(message) : "";
7514
+ }
7515
+ /** 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
7422
7516
  * user/sys 取消息原文,assistant 取文本块,toolcall 为调用块 + 结果文本
7423
7517
  * (pruner 裁剪超大结果);同时收集该条完整消息携带的图片附件(含 tool-result 嵌套,
7424
7518
  * pruner 裁剪掉的图片不收集)。
@@ -7465,19 +7559,10 @@ function renderCompleteMessageParts(session, cm, pruner) {
7465
7559
  if (call) parts.push(`[tool-call ${String(call.name ?? "")} id=${String(call.id ?? "")}]\n${safeJson(call.arguments)}`);
7466
7560
  }
7467
7561
  }
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) : "";
7562
+ const resultMessage = toolResultMessageOf(session, cm, pruner);
7563
+ if (resultMessage && Array.isArray(resultMessage.content)) {
7564
+ collectImageRefs(resultMessage.content, images);
7565
+ const text = renderMessageText(resultMessage);
7481
7566
  if (text.trim() !== "") parts.push(`[result]\n${text}`);
7482
7567
  }
7483
7568
  return {
@@ -7533,6 +7618,26 @@ function toolCallNameOf(session, cm) {
7533
7618
  for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) return String(block.name ?? "");
7534
7619
  }
7535
7620
  /**
7621
+ * 提取 skill 条目的 skill 名(tool-call 参数 JSON 的 name 字段)。非 skill 工具的
7622
+ * toolcall 返回 undefined;toolName 为 skill 但参数缺失或非法时返回空串。
7623
+ */
7624
+ function skillNameOf(session, cm) {
7625
+ if (toolCallNameOf(session, cm) !== "skill") return void 0;
7626
+ const seq = cm.seqs[0];
7627
+ const event = seq === void 0 ? void 0 : session.events[seq];
7628
+ if (event?.type !== "assistant/message") return "";
7629
+ const message = event.data.message;
7630
+ if (!message || !Array.isArray(message.content)) return "";
7631
+ for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) {
7632
+ try {
7633
+ const args = JSON.parse(String(block.arguments ?? ""));
7634
+ if (isRecord(args) && typeof args.name === "string") return args.name;
7635
+ } catch {}
7636
+ return "";
7637
+ }
7638
+ return "";
7639
+ }
7640
+ /**
7536
7641
  * 观察视图:被压缩区间(表层 seq 集合)内的完整消息投影为条目——
7537
7642
  * user → 原文(图片等非文本块为注释)、sys → 空条目、assistant/toolcall → 原文渲染,
7538
7643
  * reasoning 作为参考条目置于其所属 assistant 条目之前(每条 assistant 消息输出一次;
@@ -7590,15 +7695,18 @@ function buildObserveView(session, seqs, options = {}) {
7590
7695
  });
7591
7696
  }
7592
7697
  }
7593
- const text = renderCompleteMessage(session, cm);
7594
- if (text.trim() === "") continue;
7595
7698
  const toolName = cm.type === "toolcall" ? toolCallNameOf(session, cm) : void 0;
7699
+ const isSkill = toolName === SKILL_TOOL_NAME;
7700
+ const text = isSkill ? renderToolResultText(session, cm) : renderCompleteMessage(session, cm);
7701
+ if (text.trim() === "") continue;
7702
+ const skillName = isSkill ? skillNameOf(session, cm) : void 0;
7596
7703
  entries.push({
7597
7704
  kind: "assistant",
7598
7705
  lo: cm.index,
7599
7706
  hi: cm.index,
7600
7707
  text,
7601
- ...toolName === void 0 ? {} : { toolName }
7708
+ ...toolName === void 0 ? {} : { toolName },
7709
+ ...skillName === void 0 ? {} : { skillName }
7602
7710
  });
7603
7711
  }
7604
7712
  return {
@@ -7638,10 +7746,50 @@ function intAttr(el, name) {
7638
7746
  if (!/^\d+$/.test(raw)) return void 0;
7639
7747
  return Number(raw);
7640
7748
  }
7749
+ /** 读取元素第一个指定名子元素的正文(缺失返回 undefined)。 */
7750
+ function childText(el, tag) {
7751
+ const child = el.getElementsByTagName(tag)[0];
7752
+ return child === void 0 ? void 0 : child.textContent ?? "";
7753
+ }
7754
+ /**
7755
+ * 提取 skill 工具返回内容(原生 <skill_content> 包裹)中 <skill_resources> 与
7756
+ * <skill_instructions> 两段正文。非该包裹形态、两段缺失或两段之间存在其他内容时
7757
+ * 返回 undefined(调用方回退为整体 CDATA 原文)。
7758
+ */
7759
+ function skillContentSections(text) {
7760
+ const wrapper = /^[\s]*<skill_content\b[^>]*>([\s\S]*)<\/skill_content>[\s]*$/.exec(text);
7761
+ if (!wrapper) return void 0;
7762
+ const body = wrapper[1] ?? "";
7763
+ const match = /^([\s\S]*?)<skill_resources>([\s\S]*?)<\/skill_resources>([\s\S]*?)<skill_instructions>([\s\S]*?)<\/skill_instructions>([\s\S]*)$/.exec(body);
7764
+ if (!match) return void 0;
7765
+ const before = match[1] ?? "";
7766
+ const resources = match[2] ?? "";
7767
+ const between = match[3] ?? "";
7768
+ const instructions = match[4] ?? "";
7769
+ const after = match[5] ?? "";
7770
+ if (before.trim() !== "" || between.trim() !== "" || after.trim() !== "") return void 0;
7771
+ return {
7772
+ resources,
7773
+ instructions
7774
+ };
7775
+ }
7776
+ /**
7777
+ * 向元素追加 CDATA 正文:正文统一以 CDATA 包裹(逐字原样,不做实体转义)。
7778
+ * 正文含 ]]> 时拆为相邻多个 CDATA 段,拼接后逐字还原原文,对读取方透明。
7779
+ */
7780
+ function appendCdataText(doc, el, text) {
7781
+ const parts = text.split("]]>");
7782
+ for (let i = 0; i < parts.length; i += 1) if (i === 0) el.appendChild(doc.createCDATASection(parts[0] ?? ""));
7783
+ else {
7784
+ el.appendChild(doc.createCDATASection("]]"));
7785
+ el.appendChild(doc.createCDATASection(`>${parts[i] ?? ""}`));
7786
+ }
7787
+ }
7641
7788
  /**
7642
7789
  * 解析一个已有 <history> 块的内条目(反思视图):user_message / sys / assistant
7643
- * (index 单条或 start/end 区间)/ reasoning。整块无法解析或根非 <history> 时降级为
7644
- * 单条不可定位的历史遗留条目(text 为块内文原文,构建最终块时原样保留)。
7790
+ * (index 单条或 start/end 区间)/ skill_content(name 属性 + index 定位)/ reasoning。
7791
+ * 整块无法解析或根非 <history> 时降级为单条不可定位的历史遗留条目(text 为块内文原文,
7792
+ * 构建最终块时原样保留)。
7645
7793
  */
7646
7794
  function parseBlockEntries(blockText, blockSeq) {
7647
7795
  const opaque = () => [{
@@ -7707,6 +7855,23 @@ function parseBlockEntries(blockText, blockSeq) {
7707
7855
  text,
7708
7856
  blockSeq
7709
7857
  });
7858
+ } else if (el.nodeName === "skill_content") {
7859
+ const index = intAttr(el, "index");
7860
+ if (index !== void 0) {
7861
+ const name = el.getAttribute("name");
7862
+ const resources = childText(el, "skill_resources");
7863
+ const instructions = childText(el, "skill_instructions");
7864
+ const text = resources !== void 0 && instructions !== void 0 ? `<skill_content${name === null ? "" : ` name="${name}"`}><skill_resources>${resources}</skill_resources><skill_instructions>${instructions}</skill_instructions></skill_content>` : el.textContent ?? "";
7865
+ entries.push({
7866
+ kind: "assistant",
7867
+ lo: index,
7868
+ hi: index,
7869
+ text,
7870
+ toolName: SKILL_TOOL_NAME,
7871
+ ...name === null ? {} : { skillName: name },
7872
+ blockSeq
7873
+ });
7874
+ }
7710
7875
  } else if (el.nodeName === "reasoning") entries.push({
7711
7876
  kind: "reasoning",
7712
7877
  text,
@@ -7731,14 +7896,15 @@ function buildReflectView(blocks, options = {}) {
7731
7896
  };
7732
7897
  }
7733
7898
  /**
7734
- * 把一个视图条目构建为 XML 元素(文本经 DOM 文本节点自动转义;user 条目的注释
7735
- * 输出为 XML 注释节点)。getHistory 输出与最终 <history> 块共用。
7899
+ * 把一个视图条目构建为 XML 元素(正文统一以 CDATA 包裹,逐字原样;user 条目的注释
7900
+ * 输出为 XML 注释节点;sys 条目为无正文的空元素,自闭合输出)。getHistory 输出与
7901
+ * 最终 <history> 块共用。
7736
7902
  */
7737
7903
  function entryToElement(doc, entry) {
7738
7904
  if (entry.kind === "user") {
7739
7905
  const el = doc.createElement("user_message");
7740
7906
  if (entry.lo !== void 0) el.setAttribute("index", String(entry.lo));
7741
- el.appendChild(doc.createTextNode(entry.text));
7907
+ appendCdataText(doc, el, entry.text);
7742
7908
  for (const note of entry.notes ?? []) el.appendChild(doc.createComment(note));
7743
7909
  return el;
7744
7910
  }
@@ -7746,12 +7912,26 @@ function entryToElement(doc, entry) {
7746
7912
  const el = doc.createElement("sys");
7747
7913
  el.setAttribute("type", entry.sysKind ?? "");
7748
7914
  if (entry.lo !== void 0) el.setAttribute("index", String(entry.lo));
7749
- el.appendChild(doc.createTextNode(""));
7750
7915
  return el;
7751
7916
  }
7752
7917
  if (entry.kind === "reasoning") {
7753
7918
  const el = doc.createElement("reasoning");
7754
- el.appendChild(doc.createTextNode(entry.text));
7919
+ appendCdataText(doc, el, entry.text);
7920
+ return el;
7921
+ }
7922
+ if (entry.kind === "assistant" && entry.toolName === "skill") {
7923
+ const el = doc.createElement("skill_content");
7924
+ el.setAttribute("name", entry.skillName ?? "");
7925
+ if (entry.lo !== void 0 && entry.lo === entry.hi) el.setAttribute("index", String(entry.lo));
7926
+ const sections = skillContentSections(entry.text);
7927
+ if (sections) {
7928
+ const resources = doc.createElement("skill_resources");
7929
+ appendCdataText(doc, resources, sections.resources);
7930
+ el.appendChild(resources);
7931
+ const instructions = doc.createElement("skill_instructions");
7932
+ appendCdataText(doc, instructions, sections.instructions);
7933
+ el.appendChild(instructions);
7934
+ } else appendCdataText(doc, el, entry.text);
7755
7935
  return el;
7756
7936
  }
7757
7937
  const el = doc.createElement("assistant");
@@ -7760,7 +7940,7 @@ function entryToElement(doc, entry) {
7760
7940
  el.setAttribute("start", String(entry.lo));
7761
7941
  el.setAttribute("end", String(entry.hi));
7762
7942
  }
7763
- el.appendChild(doc.createTextNode(entry.text));
7943
+ appendCdataText(doc, el, entry.text);
7764
7944
  return el;
7765
7945
  }
7766
7946
  /**
@@ -7772,6 +7952,8 @@ function renderEntriesXml(entries) {
7772
7952
  const serializer = new import_lib.XMLSerializer();
7773
7953
  return entries.map((entry) => serializer.serializeToString(entryToElement(doc, entry))).join("\n");
7774
7954
  }
7955
+ //#endregion
7956
+ //#region src/compress-tools.ts
7775
7957
  /** 压缩会话的工具状态:视图 + 替换记录 + skill 二次确认标记 + 完成标记。 */
7776
7958
  var CompressionState = class CompressionState {
7777
7959
  view;
@@ -7893,6 +8075,10 @@ var CompressionState = class CompressionState {
7893
8075
  text: "content 必须是非空摘要文本",
7894
8076
  isError: true
7895
8077
  };
8078
+ if (content.includes("<![CDATA[")) return {
8079
+ text: "content 必须是纯文本摘要,不要包含 CDATA 包裹(<![CDATA[…]]>);CDATA 由插件在构建块时自动添加,请去除后重新提交",
8080
+ isError: true
8081
+ };
7896
8082
  const hasIndex = args.index !== void 0 && args.index !== null;
7897
8083
  const hasStart = args.start !== void 0 && args.start !== null;
7898
8084
  const hasEnd = args.end !== void 0 && args.end !== null;
@@ -7999,7 +8185,7 @@ var CompressionState = class CompressionState {
7999
8185
  }
8000
8186
  /**
8001
8187
  * 构建最终 <history> 块:按 index 顺序合并视图条目与替换记录——user / sys 条目
8002
- * 原样、被替换区间生成 <assistant index|start end> 摘要条目(content 转义嵌入)、
8188
+ * 原样、被替换区间生成 <assistant index|start end> 摘要条目(content 以 CDATA 包裹嵌入)、
8003
8189
  * 未替换 assistant 条目原样保留、reasoning 不进产物;块首为格式说明注释,开标签
8004
8190
  * 携带 tip 属性。产物为合法 XML,无需校验。
8005
8191
  */
@@ -8017,7 +8203,7 @@ var CompressionState = class CompressionState {
8017
8203
  el.setAttribute("start", String(rep.lo));
8018
8204
  el.setAttribute("end", String(rep.hi));
8019
8205
  }
8020
- el.appendChild(doc.createTextNode(rep.content));
8206
+ appendCdataText(doc, el, rep.content);
8021
8207
  root.appendChild(el);
8022
8208
  };
8023
8209
  const flushReplacementsBefore = (lo) => {
@@ -8080,7 +8266,7 @@ const COMPRESSION_TOOL_SCHEMAS = [
8080
8266
  },
8081
8267
  content: {
8082
8268
  type: "string",
8083
- description: "替换后的摘要文本(纯文本)"
8269
+ description: "替换后的摘要文本(纯文本,不要包含 CDATA 包裹,插件自动包裹)"
8084
8270
  }
8085
8271
  },
8086
8272
  required: ["content"]
@@ -8155,7 +8341,7 @@ async function gateRateLimit(waitMs, signal) {
8155
8341
  * 工具驱动的压缩循环:以新会话方式直连 ctx.llm.stream(),模型通过 getHistory /
8156
8342
  * compressHistory / completeCompression 三个工具完成压缩,替代直出 <history> 块。
8157
8343
  * 导出 runCompressionLoop / buildCompressionPrompt / buildCompressionTaskText /
8158
- * CompressionLoopOptions / CompressionOutcome / COMPRESSION_NUDGE_TEXT。
8344
+ * CompressionLoopOptions / CompressionOutcome / CompressionStats / COMPRESSION_NUDGE_TEXT。
8159
8345
  *
8160
8346
  * - 首条 user 消息仅含压缩指令与 start/end 区间(buildCompressionTaskText),不含
8161
8347
  * 历史消息内容;共享压缩提示词作为 system
@@ -8166,7 +8352,8 @@ async function gateRateLimit(waitMs, signal) {
8166
8352
  * - 模型输出纯文本(无工具调用)时追加提醒消息继续,连续 2 轮仍无工具调用判失败
8167
8353
  * - 429 限流走全局限流等待门(gateRateLimit / noteRateLimit);其余请求级错误依赖
8168
8354
  * dsh 运行时重试,插件不做整体重试,错误直接判失败
8169
- * - signal 中止标记 aborted;token usage 汇总全部轮次
8355
+ * - signal 中止标记 aborted;token usage 汇总全部轮次;统计循环起止时间戳、总耗时
8356
+ * 与逐轮请求统计(CompressionStats),随结果返回并随会话记录落盘
8170
8357
  * - 成功与失败均把循环消息组原样落盘为子会话(recordCompressionSession),成功记
8171
8358
  * 录 sessionId 于日志,失败记录作为诊断子会话 id 向上传播
8172
8359
  */
@@ -8182,7 +8369,7 @@ function buildCompressionPrompt(skipReasoning) {
8182
8369
  "",
8183
8370
  "【工具】",
8184
8371
  "- 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。区间不得覆盖用户消息或系统消息;与已有替换区间部分重叠会被拒绝,完全包含则覆盖。",
8372
+ "- compressHistory(option?: {index?, start?, end?, content}):把 index 单条或 start..end 连续区间的 assistant 类条目替换为 content 摘要(纯文本,不要包含 CDATA 包裹,插件自动包裹)。index 与 start/end 二选一,start==end 等同 index。区间不得覆盖用户消息或系统消息;与已有替换区间部分重叠会被拒绝,完全包含则覆盖。",
8186
8373
  "- completeCompression():全部压缩完成后调用,立即结束。",
8187
8374
  "",
8188
8375
  "【压缩要求】",
@@ -8192,7 +8379,8 @@ function buildCompressionPrompt(skipReasoning) {
8192
8379
  "- 单条重要的完整消息以 index 单独压缩",
8193
8380
  "- 压缩后的 assistant 消息内,应当描述**行为逻辑**,强调关键的**结论、产出和任务**;涉及到的具体文件保留完整路径",
8194
8381
  "- 摘要粒度越往后越细:靠近末尾(最近)的消息保留更多细节,开头(较早)的消息简写。",
8195
- "- 加载的 skill 属于关键信息。仅在你明确判断该 skill 与后续任务无关时,压缩它;如果该 skill 与后续任务相关,或者你无法判断,不要压缩,**保持原文**。",
8382
+ "- <history> 条目正文以 CDATA 包裹(空的 <sys> 条目为自闭合空元素,无正文),CDATA 内为逐字原样内容。",
8383
+ "- 加载的 skill 以 <skill_content name=\"…\" index=\"N\"> 条目呈现,内含 <skill_resources> 与 <skill_instructions> 两段,内文为其工具返回内容,属于关键信息。仅在你明确判断该 skill 与后续任务无关时,压缩它;如果该 skill 与后续任务相关,或者你无法判断,不要压缩,**保持原文**。",
8196
8384
  "- 未压缩的条目将原样保留;宁可保留也不要强行压缩不确定的内容。",
8197
8385
  "- 全部完成后调用 completeCompression 结束;不要输出与工具调用无关的文本。"
8198
8386
  ].join("\n");
@@ -8277,20 +8465,34 @@ async function runCompressionLoop(ctx, session, options) {
8277
8465
  let rounds = 0;
8278
8466
  let nudges = 0;
8279
8467
  let usage;
8280
- const recordSessionId = async (success) => recordCompressionSession(ctx, session, {
8468
+ const startedAt = Date.now();
8469
+ const roundStats = [];
8470
+ /** 汇总当前循环统计(completedAt 取调用时刻;rounds 数组为引用共享)。 */
8471
+ const buildStats = () => {
8472
+ const completedAt = Date.now();
8473
+ return {
8474
+ startedAt,
8475
+ completedAt,
8476
+ durationMs: completedAt - startedAt,
8477
+ rounds: roundStats
8478
+ };
8479
+ };
8480
+ const recordSessionId = async (success, stats) => recordCompressionSession(ctx, session, {
8281
8481
  phase: options.phase,
8282
8482
  target: options.target,
8283
8483
  messages,
8284
- rounds,
8484
+ stats,
8285
8485
  success,
8286
8486
  debug: options.debug
8287
8487
  });
8288
8488
  const failWith = async (error, aborted) => {
8289
- const id = await recordSessionId(false);
8489
+ const stats = buildStats();
8490
+ const id = await recordSessionId(false, stats);
8290
8491
  return {
8291
8492
  ok: false,
8292
8493
  error,
8293
8494
  aborted,
8495
+ stats,
8294
8496
  ...id === void 0 ? {} : { recordSessionId: id }
8295
8497
  };
8296
8498
  };
@@ -8304,6 +8506,7 @@ async function runCompressionLoop(ctx, session, options) {
8304
8506
  logger.warn("压缩循环中止(限流等待被 signal 中止),放弃本次压缩");
8305
8507
  return await failWith(COMPACTION_ABORTED_ERROR, true);
8306
8508
  }
8509
+ const roundStartedAt = Date.now();
8307
8510
  const requestOptions = {
8308
8511
  provider: options.target.provider,
8309
8512
  model: options.target.model,
@@ -8346,6 +8549,12 @@ async function runCompressionLoop(ctx, session, options) {
8346
8549
  }
8347
8550
  rounds += 1;
8348
8551
  if (assembler.usage !== void 0) usage = addUsage(usage, assembler.usage);
8552
+ roundStats.push({
8553
+ round: rounds,
8554
+ startedAt: roundStartedAt,
8555
+ durationMs: Date.now() - roundStartedAt,
8556
+ ...assembler.usage === void 0 ? {} : { usage: assembler.usage }
8557
+ });
8349
8558
  const assistantMessage = assembler.message({
8350
8559
  kind: "model",
8351
8560
  provider: options.target.provider,
@@ -8378,11 +8587,13 @@ async function runCompressionLoop(ctx, session, options) {
8378
8587
  const text = state.buildFinalBlock();
8379
8588
  if (state.replacementCount === 0) logger.warn("压缩完成但未执行任何压缩替换(空提交)");
8380
8589
  logger.info(`压缩循环完成(${rounds} 轮,${state.replacementCount} 次压缩替换,输出 ${text.length} 字符)`);
8381
- const id = await recordSessionId(true);
8590
+ const stats = buildStats();
8591
+ const id = await recordSessionId(true, stats);
8382
8592
  return {
8383
8593
  ok: true,
8384
8594
  text,
8385
8595
  rounds,
8596
+ stats,
8386
8597
  ...usage === void 0 ? {} : { usage },
8387
8598
  ...id === void 0 ? {} : { recordSessionId: id }
8388
8599
  };
@@ -8542,7 +8753,7 @@ async function runPassLoop(ctx, session, lifecycle, options, logger, phase) {
8542
8753
  if (!outcome.ok) {
8543
8754
  logger.warn(`${phase === "reflect" ? "反思" : "观察"}:压缩循环失败(${outcome.error}),诊断子会话 ${outcome.recordSessionId ?? "未落盘"},追加 compaction/end(error)`);
8544
8755
  try {
8545
- appendCompactionEnd(session, lifecycle, outcome.error, outcome.recordSessionId);
8756
+ appendCompactionEnd(session, lifecycle, outcome.error, outcome.recordSessionId, outcome.stats.durationMs);
8546
8757
  } catch {}
8547
8758
  return outcome;
8548
8759
  }
@@ -8703,7 +8914,8 @@ function appendCompactionStart(session, lifecycle, phase) {
8703
8914
  }
8704
8915
  /**
8705
8916
  * 追加 compaction/summary(log-only,承担影子价格认领:紧随其后的替换消息消费 claim)。
8706
- * summary 为完整合并后的 <history> 内文;usage 由摘要调用提取(无则省略)。
8917
+ * summary 为完整合并后的 <history> 内文;usage 由摘要调用提取(无则省略);stats 提供
8918
+ * 压缩循环计时(startedAt/completedAt/durationMs)。
8707
8919
  */
8708
8920
  function appendCompactionSummary(session, data) {
8709
8921
  const payload = {
@@ -8719,21 +8931,25 @@ function appendCompactionSummary(session, data) {
8719
8931
  provider: data.provider,
8720
8932
  model: data.model,
8721
8933
  ...data.maxTokens === void 0 ? {} : { maxTokens: data.maxTokens },
8722
- attemptCount: data.attemptCount,
8934
+ startedAt: data.stats.startedAt,
8935
+ completedAt: data.stats.completedAt,
8936
+ durationMs: data.stats.durationMs,
8723
8937
  ...data.usage === void 0 ? {} : { usage: data.usage }
8724
8938
  };
8725
8939
  return session.append("compaction/summary", payload).seq;
8726
8940
  }
8727
8941
  /**
8728
8942
  * 追加 compaction/end(log-only,结束生命周期;error 记录失败原因,
8729
- * diagnosticSessionId 记录最后一次摘要尝试(无论成功或失败)的诊断子会话 id)。
8943
+ * diagnosticSessionId 记录最后一次摘要尝试(无论成功或失败)的诊断子会话 id,
8944
+ * durationMs 记录失败路径的压缩循环耗时)。
8730
8945
  */
8731
- function appendCompactionEnd(session, lifecycle, error, diagnosticSessionId) {
8946
+ function appendCompactionEnd(session, lifecycle, error, diagnosticSessionId, durationMs) {
8732
8947
  const payload = {
8733
8948
  compactionId: lifecycle.compactionId,
8734
8949
  turn: lifecycle.turn,
8735
8950
  ...error === void 0 ? {} : { error },
8736
- ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8951
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId },
8952
+ ...durationMs === void 0 ? {} : { durationMs }
8737
8953
  };
8738
8954
  return session.append("compaction/end", payload).seq;
8739
8955
  }
@@ -8837,7 +9053,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8837
9053
  provider: target.provider,
8838
9054
  model: target.model,
8839
9055
  maxTokens: config.compressMaxTokens,
8840
- attemptCount: summaryResult.rounds,
9056
+ stats: summaryResult.stats,
8841
9057
  ...summaryResult.usage === void 0 ? {} : { usage: summaryResult.usage }
8842
9058
  });
8843
9059
  logger.step("反思提交:替换整个 <history> 块区段为合并摘要");
@@ -8854,7 +9070,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8854
9070
  const message = error instanceof Error ? error.message : String(error);
8855
9071
  logger.warn(`反思提交失败: ${message}`);
8856
9072
  try {
8857
- appendCompactionEnd(session, lifecycle, message);
9073
+ appendCompactionEnd(session, lifecycle, message, void 0, summaryResult.stats.durationMs);
8858
9074
  } catch {}
8859
9075
  return { failed: false };
8860
9076
  }
@@ -9075,7 +9291,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
9075
9291
  provider: target.provider,
9076
9292
  model: target.model,
9077
9293
  maxTokens: config.compressMaxTokens,
9078
- attemptCount: summaryResult.rounds,
9294
+ stats: summaryResult.stats,
9079
9295
  ...usage === void 0 ? {} : { usage }
9080
9296
  });
9081
9297
  logger.step("观察提交:替换被压缩新消息区间为 <history>(旧块保留)");
@@ -9093,7 +9309,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
9093
9309
  const message = error instanceof Error ? error.message : String(error);
9094
9310
  logger.warn(`观察压缩提交失败: ${message}`);
9095
9311
  try {
9096
- appendCompactionEnd(session, lifecycle, message);
9312
+ appendCompactionEnd(session, lifecycle, message, void 0, summaryResult.stats.durationMs);
9097
9313
  } catch {}
9098
9314
  return { failed: false };
9099
9315
  }
@@ -9337,11 +9553,11 @@ function cosineSimilarity(a, b) {
9337
9553
  */
9338
9554
  /** 默认配置(冻结对象,resolveConfig 合并的基底;debug 缺省值在解析时按 NODE_ENV 判定)。 */
9339
9555
  const DEFAULT_CONFIG = Object.freeze({
9340
- observeThresholdTokens: 45e3,
9341
- reflectThresholdTokens: 12e4,
9556
+ observeThresholdTokens: 35e3,
9557
+ reflectThresholdTokens: 4e4,
9342
9558
  compressMaxTokens: void 0,
9343
9559
  rateLimitWaitMs: 6e4,
9344
- tailMessageCount: 5,
9560
+ tailMessageCount: 20,
9345
9561
  compressSkipReasoning: true,
9346
9562
  omEnabled: true,
9347
9563
  debug: false,
@@ -1,5 +1,5 @@
1
1
  import type { ImageRefValue } from './recall-output.ts';
2
- import type { CompleteMessage, MessageIndex, Session, SessionEvent } from './types.ts';
2
+ import type { CompleteMessage, Message, MessageIndex, Session, SessionEvent } from './types.ts';
3
3
  /** 工具结果裁剪器结构(tool-result-pruner;超大结果渲染前裁剪)。 */
4
4
  export type PrunerLike = {
5
5
  pruneContent?: (blocks: readonly unknown[]) => unknown[] | null;
@@ -22,7 +22,13 @@ export declare function indexCompleteMessages(session: Session): CompleteMessage
22
22
  */
23
23
  export declare function collectImageRefs(content: unknown, out: ImageRefValue[]): void;
24
24
  /**
25
- * 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
25
+ * 取 toolcall 完整消息的 tool/result 消息(超大结果经 pruner 裁剪):
26
+ * 结果文本渲染与 <skill> 条目正文共用;非 toolcall 或无配对 result 时返回 undefined。
27
+ */
28
+ export declare function toolResultMessageOf(session: Session, cm: CompleteMessage, pruner?: PrunerLike): Message | undefined;
29
+ /** 渲染 toolcall 完整消息的工具返回文本(仅 result 内容;无配对 result 时为空串)。 */
30
+ export declare function renderToolResultText(session: Session, cm: CompleteMessage, pruner?: PrunerLike): string;
31
+ /** 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
26
32
  * user/sys 取消息原文,assistant 取文本块,toolcall 为调用块 + 结果文本
27
33
  * (pruner 裁剪超大结果);同时收集该条完整消息携带的图片附件(含 tool-result 嵌套,
28
34
  * pruner 裁剪掉的图片不收集)。
@@ -1 +1 @@
1
- {"version":3,"file":"log-index.d.ts","sourceRoot":"","sources":["../src/log-index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,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,oCAAoC;AACpC,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpF;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CAoB5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CA+DzE;AACD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,IAAI,CA+B7E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,CAuD3C;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,MAAM,CAER"}
1
+ {"version":3,"file":"log-index.d.ts","sourceRoot":"","sources":["../src/log-index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EACV,eAAe,EACf,OAAO,EACP,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,oCAAoC;AACpC,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpF;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CAoB5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CA+DzE;AACD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,IAAI,CA+B7E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,GAAG,SAAS,CASrB;AAED,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,CAiD3C;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,eAAe,EACnB,MAAM,CAAC,EAAE,UAAU,GAClB,MAAM,CAER"}