dsh-plugin-om 0.0.26 → 0.0.28
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 +67 -64
- package/dist/client/OmWarningCard.d.ts +2 -2
- package/dist/client/definition.d.ts +8 -6
- package/dist/client/definition.d.ts.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client.js +47 -7
- package/dist/client.js.map +1 -1
- package/dist/compaction-log.d.ts +21 -21
- package/dist/compaction-log.d.ts.map +1 -1
- package/dist/compress.d.ts +7 -7
- package/dist/compress.d.ts.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/degrade.d.ts +3 -3
- package/dist/degrade.d.ts.map +1 -1
- package/dist/index.mjs +202 -116
- package/dist/om-event.d.ts +65 -0
- package/dist/om-event.d.ts.map +1 -0
- package/dist/summarize.d.ts +24 -13
- package/dist/summarize.d.ts.map +1 -1
- package/dist/types.d.ts +10 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -39,30 +39,87 @@ function isPluginOwnedSource(source) {
|
|
|
39
39
|
return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
|
|
40
40
|
}
|
|
41
41
|
//#endregion
|
|
42
|
+
//#region src/om-event.ts
|
|
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
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 解码一条会话事件为 om 私有事件:仅识别 feedback/record 中带 om 信封前缀的
|
|
55
|
+
* text;前缀缺失、JSON 非法、kind 未知或载荷字段缺失时返回 undefined。
|
|
56
|
+
*/
|
|
57
|
+
function readOmEvent(event) {
|
|
58
|
+
if (event === void 0 || event === null || event.type !== "feedback/record") return void 0;
|
|
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;
|
|
77
|
+
return {
|
|
78
|
+
kind: omKind,
|
|
79
|
+
data: rest,
|
|
80
|
+
seq: event.seq
|
|
81
|
+
};
|
|
82
|
+
}
|
|
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
|
|
42
103
|
//#region src/degrade.ts
|
|
43
104
|
/**
|
|
44
105
|
* 降级报告:压缩流程中的挂载失败统一出口。
|
|
45
106
|
* 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
|
|
46
107
|
*
|
|
47
108
|
* - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
|
|
48
|
-
* 并向会话日志追加 log-only 的
|
|
109
|
+
* 并向会话日志追加 log-only 的 om 警告事件(借用 feedback/record 的 om 信封,
|
|
110
|
+
* 见 om-event.ts;客户端渲染为「功能降级」警告行);
|
|
49
111
|
* 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
|
|
50
112
|
* - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
|
|
51
113
|
* - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
|
|
52
114
|
*/
|
|
53
|
-
/** 各降级问题面向用户的简短说明(om/warning
|
|
115
|
+
/** 各降级问题面向用户的简短说明(om/warning 信封载荷 message;客户端警告行直接展示)。 */
|
|
54
116
|
const DEGRADE_PROBLEMS = {
|
|
55
117
|
"systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
|
|
56
118
|
"tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
|
|
57
119
|
};
|
|
58
|
-
/** 判定事件是否为指定 problem 的已有 om/warning 记录(会话内去重依据)。 */
|
|
59
|
-
function isWarningFor(event, problem) {
|
|
60
|
-
if (event.type !== "om/warning") return false;
|
|
61
|
-
return event.data?.problem === problem;
|
|
62
|
-
}
|
|
63
120
|
/**
|
|
64
121
|
* 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
|
|
65
|
-
* 出现时,向会话日志追加 log-only
|
|
122
|
+
* 出现时,向会话日志追加 log-only om 警告事件(客户端渲染警告行,每会话最多
|
|
66
123
|
* 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
|
|
67
124
|
* 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
|
|
68
125
|
*/
|
|
@@ -70,19 +127,19 @@ function reportDegrade(session, logger, problem) {
|
|
|
70
127
|
const message = DEGRADE_PROBLEMS[problem];
|
|
71
128
|
let first = true;
|
|
72
129
|
try {
|
|
73
|
-
first = !session.
|
|
130
|
+
first = !findOmEvents(session, "om/warning").some((om) => om.data.problem === problem);
|
|
74
131
|
} catch {}
|
|
75
132
|
if (!first) return;
|
|
76
133
|
console.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
77
134
|
logger.warn(message);
|
|
78
135
|
try {
|
|
79
|
-
session
|
|
136
|
+
appendOmEvent(session, "om/warning", {
|
|
80
137
|
problem,
|
|
81
138
|
message
|
|
82
139
|
});
|
|
83
140
|
} catch (error) {
|
|
84
141
|
const text = error instanceof Error ? error.message : String(error);
|
|
85
|
-
logger.warn(`om
|
|
142
|
+
logger.warn(`om 警告事件追加失败: ${text}`);
|
|
86
143
|
}
|
|
87
144
|
}
|
|
88
145
|
//#endregion
|
|
@@ -7482,9 +7539,9 @@ function phaseLabel(phase) {
|
|
|
7482
7539
|
if (phase === "reflect") return "反思";
|
|
7483
7540
|
return "压缩";
|
|
7484
7541
|
}
|
|
7485
|
-
/** 诊断子会话 label
|
|
7486
|
-
function compactionLogLabel(phase,
|
|
7487
|
-
return `OM
|
|
7542
|
+
/** 诊断子会话 label:含压缩阶段与尝试序号。 */
|
|
7543
|
+
function compactionLogLabel(phase, attemptNo) {
|
|
7544
|
+
return `OM 压缩日志(${phaseLabel(phase)} · 第 ${attemptNo} 次尝试)`;
|
|
7488
7545
|
}
|
|
7489
7546
|
/** 追加一次尝试的「提示词 → 原始输出」消息组(surfaceOp append;id 为品牌类型,session.append 运行时校验)。 */
|
|
7490
7547
|
function appendAttemptMessages(child, attempt, step, target) {
|
|
@@ -7521,14 +7578,13 @@ function appendAttemptMessages(child, attempt, step, target) {
|
|
|
7521
7578
|
}, { surfaceOp: "append" });
|
|
7522
7579
|
}
|
|
7523
7580
|
/**
|
|
7524
|
-
*
|
|
7525
|
-
*
|
|
7526
|
-
*
|
|
7527
|
-
*
|
|
7528
|
-
*
|
|
7529
|
-
* undefined(不影响压缩失败流程)。
|
|
7581
|
+
* 把一次摘要尝试落盘为诊断子会话:ctx.sessions.create 创建子会话(header origin
|
|
7582
|
+
* 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、cwd 继承主会话),
|
|
7583
|
+
* 追加 one-shot descriptor(provider om-compaction-log,label 含压缩阶段与尝试序号),
|
|
7584
|
+
* 原样追加「提示词 + 原始输出」消息组,flush 持久化检查点,返回子会话 id。落盘自身
|
|
7585
|
+
* 绝不抛错:任何失败仅 logger.warn 并返回 undefined(不影响压缩流程)。
|
|
7530
7586
|
*/
|
|
7531
|
-
async function
|
|
7587
|
+
async function recordCompactionAttempt(ctx, parentSession, options) {
|
|
7532
7588
|
const logger = makeLogger(ctx, options.debug);
|
|
7533
7589
|
try {
|
|
7534
7590
|
const header = parentSession.header;
|
|
@@ -7542,23 +7598,19 @@ async function recordCompactionFailure(ctx, parentSession, options) {
|
|
|
7542
7598
|
version: SUBAGENT_DESCRIPTOR_VERSION,
|
|
7543
7599
|
mode: "one-shot",
|
|
7544
7600
|
provider: COMPACTION_LOG_PROVIDER,
|
|
7545
|
-
label: compactionLogLabel(options.phase, options.
|
|
7601
|
+
label: compactionLogLabel(options.phase, options.attemptNo)
|
|
7546
7602
|
});
|
|
7547
|
-
|
|
7548
|
-
const attempt = options.attempts[i];
|
|
7549
|
-
if (attempt === void 0) continue;
|
|
7550
|
-
appendAttemptMessages(child, attempt, i + 1, options.target);
|
|
7551
|
-
}
|
|
7603
|
+
appendAttemptMessages(child, options.attempt, 1, options.target);
|
|
7552
7604
|
try {
|
|
7553
7605
|
await ctx.sessions.flush(child);
|
|
7554
7606
|
} catch (error) {
|
|
7555
7607
|
const message = error instanceof Error ? error.message : String(error);
|
|
7556
|
-
logger.warn(
|
|
7608
|
+
logger.warn(`压缩日志子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
|
|
7557
7609
|
}
|
|
7558
7610
|
return child.id;
|
|
7559
7611
|
} catch (error) {
|
|
7560
7612
|
const message = error instanceof Error ? error.message : String(error);
|
|
7561
|
-
logger.warn(
|
|
7613
|
+
logger.warn(`压缩日志子会话落盘失败(第 ${options.attemptNo} 次尝试): ${message}`);
|
|
7562
7614
|
return;
|
|
7563
7615
|
}
|
|
7564
7616
|
}
|
|
@@ -7621,8 +7673,10 @@ async function gateRateLimit(waitMs, signal) {
|
|
|
7621
7673
|
/**
|
|
7622
7674
|
* 共享压缩提示词(观察/反思同一套):定义 history 块(模型消息 + index 的表达形式)、
|
|
7623
7675
|
* 完整消息定义、压缩要求、输出格式与数据源说明。
|
|
7676
|
+
* skipReasoning=true(默认,与 compressSkipReasoning 默认一致)时压缩输入不含
|
|
7677
|
+
* <reasoning> 参考条目,提示词相应省略 <reasoning> 的说明两行。
|
|
7624
7678
|
*/
|
|
7625
|
-
function buildHistoryPrompt() {
|
|
7679
|
+
function buildHistoryPrompt(skipReasoning = true) {
|
|
7626
7680
|
return [
|
|
7627
7681
|
"压缩 <history> 消息记录。你应当输出**单个**合法的 <history> 块。",
|
|
7628
7682
|
"",
|
|
@@ -7630,17 +7684,18 @@ function buildHistoryPrompt() {
|
|
|
7630
7684
|
"- <history> 是历史消息的记录块。",
|
|
7631
7685
|
"- <user_message index=\"N\">:用户消息条目。",
|
|
7632
7686
|
"- <sys type=\"(kind)\" index=\"N\">:系统消息条目。",
|
|
7633
|
-
"- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
|
|
7687
|
+
...skipReasoning ? [] : ["- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。"],
|
|
7634
7688
|
"- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
|
|
7635
7689
|
"- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
|
|
7636
7690
|
"",
|
|
7637
7691
|
"【压缩要求】",
|
|
7638
7692
|
"- <user_message> <sys> 条目从输入中逐条保留,不做任何处理。",
|
|
7639
|
-
"- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
|
|
7640
|
-
"- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\">
|
|
7641
|
-
"- 单条重要的完整消息以 <assistant index=\"\">
|
|
7693
|
+
...skipReasoning ? [] : ["- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。"],
|
|
7694
|
+
"- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块",
|
|
7695
|
+
"- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现",
|
|
7696
|
+
"- 压缩后的 <assistant> 块内,应当描述**行为逻辑**,强调关键的**结论、产出和任务**;涉及到的具体文件保留完整路径",
|
|
7642
7697
|
"- 加载的 skill 属于**关键信息**:应当产出独立块且不过多省略。",
|
|
7643
|
-
"-
|
|
7698
|
+
"- 压缩后的消息,区间边界与输入的消息必须完全相同,内部 index/start/end 必须连续,相邻区间的左右界必须相邻,",
|
|
7644
7699
|
"",
|
|
7645
7700
|
"【摘要粒度】",
|
|
7646
7701
|
"- 越往后越细:靠近末尾(最近)的完整消息保留更多细节(关键文件、改动与结论),开头(较早)的完整消息可适当从简。",
|
|
@@ -7697,14 +7752,15 @@ function renderUserEntry(doc, session, cm) {
|
|
|
7697
7752
|
}
|
|
7698
7753
|
/**
|
|
7699
7754
|
* 渲染完整消息记录(观察输入):输出一个合法的 <history> 块——
|
|
7700
|
-
* user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、
|
|
7701
|
-
*
|
|
7755
|
+
* user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、
|
|
7756
|
+
* assistant/toolcall → <assistant>(原样文本);skipReasoning=false 时另把
|
|
7757
|
+
* assistant 的 reasoning → <reasoning>(参考条目)。
|
|
7702
7758
|
* 文本经 XML 序列化自动转义;仅渲染 seqs 全部落在给定集合内的完整消息。
|
|
7703
7759
|
*/
|
|
7704
|
-
function renderMessages(session, seqs) {
|
|
7760
|
+
function renderMessages(session, seqs, skipReasoning = true) {
|
|
7705
7761
|
const shadowed = new Set(seqs);
|
|
7706
7762
|
const reasoningBySeq = /* @__PURE__ */ new Map();
|
|
7707
|
-
for (const seq of seqs) {
|
|
7763
|
+
if (!skipReasoning) for (const seq of seqs) {
|
|
7708
7764
|
const event = session.events[seq];
|
|
7709
7765
|
if (event?.type !== "assistant/message") continue;
|
|
7710
7766
|
const message = event.data.message;
|
|
@@ -7733,7 +7789,7 @@ function renderMessages(session, seqs) {
|
|
|
7733
7789
|
} else {
|
|
7734
7790
|
const callSeq = cm.seqs[0];
|
|
7735
7791
|
const reasonings = callSeq === void 0 ? void 0 : reasoningBySeq.get(callSeq);
|
|
7736
|
-
if (callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
|
|
7792
|
+
if (!skipReasoning && callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
|
|
7737
7793
|
emittedReasoning.add(callSeq);
|
|
7738
7794
|
for (const text of reasonings) {
|
|
7739
7795
|
const re = doc.createElement("reasoning");
|
|
@@ -7815,6 +7871,14 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
|
|
|
7815
7871
|
}
|
|
7816
7872
|
/** 产出日志后插入首个 <history> 后的格式说明(XML 注释,完整消息定义 + 条目标签语义)。 */
|
|
7817
7873
|
const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
|
|
7874
|
+
/**
|
|
7875
|
+
* 剥离 <history> 块内文块首的格式说明注释(HISTORY_FORMAT_NOTE 整体精确匹配,仅块首
|
|
7876
|
+
* 一处);正文条目内出现的同名注释串不动。非块首或不匹配时原样返回。
|
|
7877
|
+
*/
|
|
7878
|
+
function stripLeadingFormatNote(inner) {
|
|
7879
|
+
if (!inner.startsWith(HISTORY_FORMAT_NOTE)) return inner;
|
|
7880
|
+
return inner.slice(HISTORY_FORMAT_NOTE.length).replace(/^\s+/, "");
|
|
7881
|
+
}
|
|
7818
7882
|
/** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
|
|
7819
7883
|
function intAttr(el, name) {
|
|
7820
7884
|
const raw = el.getAttribute(name);
|
|
@@ -7886,7 +7950,7 @@ function parseHistoryBlock(xml) {
|
|
|
7886
7950
|
};
|
|
7887
7951
|
}
|
|
7888
7952
|
/**
|
|
7889
|
-
* 解析文本中全部 <history>
|
|
7953
|
+
* 解析文本中全部 <history> 块内的条目(逐块解析提取,兼容多块拼接文本)。
|
|
7890
7954
|
* 非法块跳过;仅提取不校验顺序(连续性由 historyContinuity 校验)。
|
|
7891
7955
|
*/
|
|
7892
7956
|
function parseHistoryEntries(text) {
|
|
@@ -8031,95 +8095,109 @@ function extractSummaryDetailed(raw, expected) {
|
|
|
8031
8095
|
}
|
|
8032
8096
|
/**
|
|
8033
8097
|
* 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
|
|
8034
|
-
*
|
|
8035
|
-
*
|
|
8036
|
-
*
|
|
8037
|
-
*
|
|
8038
|
-
*
|
|
8039
|
-
*
|
|
8098
|
+
* 每次实际发出的 LLM 调用(无论成功、校验不通过、非 stop 结束还是异常)完成后,
|
|
8099
|
+
* 立即把该次尝试的完整提示词与模型原始输出原样落盘为诊断子会话(phase 标注观察
|
|
8100
|
+
* 或反思),子会话 id 写入该次尝试的主会话日志;请求发出前被中止的尝试(无实际
|
|
8101
|
+
* 调用)不落盘。失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)记录日志并重试;
|
|
8102
|
+
* 全部尝试耗尽返回失败结果(携带最后一次尝试的实际报错/具体问题与其诊断子会话 id)。
|
|
8103
|
+
* signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
|
|
8104
|
+
* 限流等待门。
|
|
8040
8105
|
*/
|
|
8041
8106
|
async function runSummarySubagent(ctx, agent, instruction, contextText, maxTokens, target, debug, signal, options) {
|
|
8042
8107
|
const session = agent.session;
|
|
8043
8108
|
const logger = makeLogger(ctx, debug);
|
|
8044
8109
|
const maxAttempts = options?.maxAttempts ?? 11;
|
|
8045
8110
|
let lastFailure = {};
|
|
8046
|
-
/** 逐尝试的完整记录(提示词 + 原始输出原样收集,仅最终失败时落盘)。 */
|
|
8047
|
-
const attempts = [];
|
|
8048
|
-
/** 最终失败的统一出口:落盘诊断子会话并携带其 id 返回。 */
|
|
8049
|
-
const finishFailure = async (error, aborted) => {
|
|
8050
|
-
const diagnosticSessionId = await recordCompactionFailure(ctx, session, {
|
|
8051
|
-
phase: options?.phase,
|
|
8052
|
-
target,
|
|
8053
|
-
attempts,
|
|
8054
|
-
debug
|
|
8055
|
-
});
|
|
8056
|
-
return {
|
|
8057
|
-
ok: false,
|
|
8058
|
-
error,
|
|
8059
|
-
aborted,
|
|
8060
|
-
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8061
|
-
};
|
|
8062
|
-
};
|
|
8063
8111
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
8064
8112
|
if (signal?.aborted) {
|
|
8065
8113
|
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次压缩`);
|
|
8066
|
-
return
|
|
8114
|
+
return {
|
|
8115
|
+
ok: false,
|
|
8116
|
+
error: COMPACTION_ABORTED_ERROR,
|
|
8117
|
+
aborted: true,
|
|
8118
|
+
...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
|
|
8119
|
+
};
|
|
8067
8120
|
}
|
|
8068
8121
|
const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
|
|
8069
8122
|
if (!await gateRateLimit(rateLimitWaitMs, signal)) {
|
|
8070
8123
|
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次压缩`);
|
|
8071
|
-
return
|
|
8124
|
+
return {
|
|
8125
|
+
ok: false,
|
|
8126
|
+
error: COMPACTION_ABORTED_ERROR,
|
|
8127
|
+
aborted: true,
|
|
8128
|
+
...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
|
|
8129
|
+
};
|
|
8072
8130
|
}
|
|
8073
8131
|
logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
|
|
8074
8132
|
const prompt = `${instruction}\n\n${contextText ?? ""}`;
|
|
8075
8133
|
const collector = new StreamCollector();
|
|
8076
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
|
|
8145
|
+
});
|
|
8077
8146
|
try {
|
|
8078
8147
|
const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
|
|
8079
8148
|
for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
|
|
8080
8149
|
streamCompleted = true;
|
|
8081
|
-
|
|
8082
|
-
prompt,
|
|
8083
|
-
rawOutput: collector.text
|
|
8084
|
-
});
|
|
8150
|
+
const diagnosticSessionId = await logAttempt();
|
|
8085
8151
|
const extracted = extractSummaryDetailed(collector.text, options?.expected);
|
|
8086
8152
|
const finish = collector.finish;
|
|
8087
8153
|
if (finish.kind !== "stop") {
|
|
8088
|
-
lastFailure = {
|
|
8089
|
-
|
|
8154
|
+
lastFailure = {
|
|
8155
|
+
reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)`,
|
|
8156
|
+
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8157
|
+
};
|
|
8158
|
+
logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8090
8159
|
continue;
|
|
8091
8160
|
}
|
|
8092
8161
|
if ("error" in extracted) {
|
|
8093
|
-
lastFailure = {
|
|
8094
|
-
|
|
8162
|
+
lastFailure = {
|
|
8163
|
+
reason: extracted.error,
|
|
8164
|
+
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8165
|
+
};
|
|
8166
|
+
logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8095
8167
|
continue;
|
|
8096
8168
|
}
|
|
8097
8169
|
const text = extracted.log;
|
|
8098
8170
|
const usage = collector.usage;
|
|
8099
|
-
logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + "
|
|
8171
|
+
logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + `,子会话 ${diagnosticSessionId ?? "未落盘"})`);
|
|
8100
8172
|
return {
|
|
8101
8173
|
ok: true,
|
|
8102
8174
|
text,
|
|
8103
8175
|
attemptCount: attempt,
|
|
8104
|
-
...usage === void 0 ? {} : { usage }
|
|
8176
|
+
...usage === void 0 ? {} : { usage },
|
|
8177
|
+
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8105
8178
|
};
|
|
8106
8179
|
} catch (error) {
|
|
8107
|
-
|
|
8108
|
-
prompt,
|
|
8109
|
-
rawOutput: collector.text
|
|
8110
|
-
});
|
|
8180
|
+
const diagnosticSessionId = streamCompleted ? void 0 : await logAttempt();
|
|
8111
8181
|
const message = error instanceof Error ? error.message : String(error);
|
|
8112
8182
|
if (isRateLimitError(message)) {
|
|
8113
8183
|
noteRateLimit();
|
|
8114
8184
|
logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
|
|
8115
8185
|
}
|
|
8116
|
-
lastFailure = {
|
|
8117
|
-
|
|
8186
|
+
lastFailure = {
|
|
8187
|
+
error: message,
|
|
8188
|
+
...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
|
|
8189
|
+
};
|
|
8190
|
+
logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
8118
8191
|
}
|
|
8119
8192
|
}
|
|
8120
8193
|
const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
|
|
8121
8194
|
logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次,最后错误:${lastError}),拒绝放行本轮 step`);
|
|
8122
|
-
return
|
|
8195
|
+
return {
|
|
8196
|
+
ok: false,
|
|
8197
|
+
error: lastError,
|
|
8198
|
+
aborted: false,
|
|
8199
|
+
...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
|
|
8200
|
+
};
|
|
8123
8201
|
}
|
|
8124
8202
|
//#endregion
|
|
8125
8203
|
//#region src/compress.ts
|
|
@@ -8128,15 +8206,17 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
8128
8206
|
* 导出 estimateTextTokens / isPairBalancedAfter / computeCompressRange / historySection /
|
|
8129
8207
|
* findObservePending / reflectPass / observePass / maybeCompress。
|
|
8130
8208
|
*
|
|
8131
|
-
* - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens
|
|
8209
|
+
* - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,块内文拼合为单个
|
|
8210
|
+
* <history> 块输入摘要,合并为一条
|
|
8132
8211
|
* - 观察(触发 → 待定 → 延迟执行):净压力(上下文压力 − 已压缩块 token 合计 − 系统提示词
|
|
8133
8212
|
* token 估算 − 工具定义 token 估算)首次 ≥ observeThresholdTokens 时记录待定标记
|
|
8134
8213
|
* (触发点 = 当时的最后一条完整消息 index),本次不压缩;待定后新增完整消息数 ≥
|
|
8135
8214
|
* tailMessageCount 时,把压缩边界至触发点的全部消息摘要为新 <history> 块并精确替换
|
|
8136
8215
|
* 被压缩区间(旧块保留),等待期间的新消息成为下一轮未压缩尾部(延迟窗口内压力允许
|
|
8137
8216
|
* 短暂超阈值);tailMessageCount=0 时触发当轮直接执行(不落待定标记)
|
|
8138
|
-
* - 待定标记以 log-only om
|
|
8139
|
-
*
|
|
8217
|
+
* - 待定标记以 log-only om 信封事件(借用 feedback/record,kind 为 om/observe-pending /
|
|
8218
|
+
* om/observe-invalidate,见 om-event.ts)持久化在会话日志中(重启后从日志恢复);
|
|
8219
|
+
* 摘要失败保留待定,下个 pre-step 直接重试执行
|
|
8140
8220
|
* - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
|
|
8141
8221
|
* - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
|
|
8142
8222
|
* - 摘要尝试全部耗尽时 pass 返回失败结果(携带最后一次尝试的实际报错),压缩流程
|
|
@@ -8144,7 +8224,8 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
8144
8224
|
* - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
|
|
8145
8225
|
* 失败补 end(error,实际报错 + 诊断子会话 sessionId);替换消息 source 标记插件标识供 UI 认领
|
|
8146
8226
|
* - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
|
|
8147
|
-
* log-only om/
|
|
8227
|
+
* log-only om 警告事件(借用 feedback/record,客户端渲染功能降级警告行,每会话同一
|
|
8228
|
+
* 问题至多一次);
|
|
8148
8229
|
* 辅助估算的普通运行时报错仅记日志。降级与报错都不阻塞压缩(tokenMeter 压力数据
|
|
8149
8230
|
* 缺失时本轮跳过观察)
|
|
8150
8231
|
*/
|
|
@@ -8192,6 +8273,14 @@ function historyInnerText(text) {
|
|
|
8192
8273
|
return text.slice(gt + 1, close).trim();
|
|
8193
8274
|
}
|
|
8194
8275
|
/**
|
|
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
|
+
/**
|
|
8195
8284
|
* 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡:按表层顺序折叠未闭合的
|
|
8196
8285
|
* 工具调用数,处理到 seq 后计数为 0 即平衡(防止把 tool-call 与其结果切到两侧)。
|
|
8197
8286
|
*/
|
|
@@ -8266,40 +8355,35 @@ function historySection(session) {
|
|
|
8266
8355
|
};
|
|
8267
8356
|
}
|
|
8268
8357
|
/**
|
|
8269
|
-
* 查找当前活跃的观察压缩待定标记:按日志顺序取最后一条 om/observe-pending
|
|
8270
|
-
*
|
|
8271
|
-
*
|
|
8272
|
-
*
|
|
8358
|
+
* 查找当前活跃的观察压缩待定标记:按日志顺序取最后一条 om/observe-pending 信封事件,
|
|
8359
|
+
* 其后须无引用它的 om/observe-invalidate(已失效),且其后的压缩边界 seq 不大于标记
|
|
8360
|
+
* seq(边界后移说明标记期间已发生过压缩,标记过期——兜底「执行成功但失效标记未写出」
|
|
8361
|
+
* 的崩溃窗口)。无活跃标记返回 undefined。
|
|
8273
8362
|
*/
|
|
8274
8363
|
function findObservePending(session) {
|
|
8275
8364
|
let pending;
|
|
8276
8365
|
for (let seq = 0; seq < session.events.length; seq += 1) {
|
|
8277
8366
|
const event = session.events[seq];
|
|
8278
8367
|
if (!event) continue;
|
|
8279
|
-
|
|
8368
|
+
const om = readOmEvent(event);
|
|
8369
|
+
if (om?.kind === "om/observe-pending") pending = {
|
|
8280
8370
|
seq,
|
|
8281
|
-
triggerMessageIndex:
|
|
8371
|
+
triggerMessageIndex: om.data.triggerMessageIndex
|
|
8282
8372
|
};
|
|
8283
|
-
else if (
|
|
8373
|
+
else if (om?.kind === "om/observe-invalidate" && pending !== void 0 && om.data.pendingSeq === pending.seq) pending = void 0;
|
|
8284
8374
|
}
|
|
8285
8375
|
if (pending === void 0) return void 0;
|
|
8286
8376
|
const { boundarySeq } = historySection(session);
|
|
8287
8377
|
if (boundarySeq !== void 0 && boundarySeq > pending.seq) return void 0;
|
|
8288
8378
|
return pending;
|
|
8289
8379
|
}
|
|
8290
|
-
/** 追加观察压缩待定标记(log-only
|
|
8380
|
+
/** 追加观察压缩待定标记(log-only om 信封事件):记录触发点完整消息 index,返回事件 seq。 */
|
|
8291
8381
|
function appendObservePending(session, triggerMessageIndex) {
|
|
8292
|
-
return session
|
|
8293
|
-
key: "observe",
|
|
8294
|
-
triggerMessageIndex
|
|
8295
|
-
}).seq;
|
|
8382
|
+
return appendOmEvent(session, "om/observe-pending", { triggerMessageIndex });
|
|
8296
8383
|
}
|
|
8297
|
-
/** 追加观察压缩待定失效标记(log-only
|
|
8384
|
+
/** 追加观察压缩待定失效标记(log-only om 信封事件):声明指定 pending 已失效,返回事件 seq。 */
|
|
8298
8385
|
function appendObserveInvalidate(session, pendingSeq) {
|
|
8299
|
-
return session
|
|
8300
|
-
key: "observe",
|
|
8301
|
-
pendingSeq
|
|
8302
|
-
}).seq;
|
|
8386
|
+
return appendOmEvent(session, "om/observe-invalidate", { pendingSeq });
|
|
8303
8387
|
}
|
|
8304
8388
|
/** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null。 */
|
|
8305
8389
|
function openTurnOf(session) {
|
|
@@ -8346,7 +8430,7 @@ function appendCompactionSummary(session, data) {
|
|
|
8346
8430
|
}
|
|
8347
8431
|
/**
|
|
8348
8432
|
* 追加 compaction/end(log-only,结束生命周期;error 记录失败原因,
|
|
8349
|
-
* diagnosticSessionId
|
|
8433
|
+
* diagnosticSessionId 记录最后一次摘要尝试(无论成功或失败)的诊断子会话 id)。
|
|
8350
8434
|
*/
|
|
8351
8435
|
function appendCompactionEnd(session, lifecycle, error, diagnosticSessionId) {
|
|
8352
8436
|
const payload = {
|
|
@@ -8378,9 +8462,9 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
|
|
|
8378
8462
|
});
|
|
8379
8463
|
}
|
|
8380
8464
|
/**
|
|
8381
|
-
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens
|
|
8382
|
-
*
|
|
8383
|
-
*
|
|
8465
|
+
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,全部块内文拼合为
|
|
8466
|
+
* 单个 <history> 块送入摘要调用,整个块区段合并替换为一条更紧凑的摘要。失败不产生
|
|
8467
|
+
* 部分替换;摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
|
|
8384
8468
|
*/
|
|
8385
8469
|
async function reflectPass(ctx, agent, config, target, signal) {
|
|
8386
8470
|
const session = agent.session;
|
|
@@ -8406,8 +8490,8 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8406
8490
|
return { failed: false };
|
|
8407
8491
|
}
|
|
8408
8492
|
const blockSeqs = blocks.map((block) => block.seq);
|
|
8409
|
-
const instruction = buildHistoryPrompt();
|
|
8410
|
-
const contextText = blocks
|
|
8493
|
+
const instruction = buildHistoryPrompt(config.compressSkipReasoning);
|
|
8494
|
+
const contextText = mergeHistoryBlocks(blocks);
|
|
8411
8495
|
const expectedEnd = reflectExpectedEnd(contextText);
|
|
8412
8496
|
const lifecycle = {
|
|
8413
8497
|
compactionId: newCompactionId(),
|
|
@@ -8468,7 +8552,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8468
8552
|
end: last.seq
|
|
8469
8553
|
}, lifecycle.compactionId);
|
|
8470
8554
|
logger.step("反思提交:追加 compaction/end");
|
|
8471
|
-
appendCompactionEnd(session, lifecycle);
|
|
8555
|
+
appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
|
|
8472
8556
|
logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
|
|
8473
8557
|
return { failed: false };
|
|
8474
8558
|
} catch (error) {
|
|
@@ -8484,7 +8568,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8484
8568
|
* 估算系统提示词 tokens:按 agent 作用域组装并渲染系统提示词,按长度/4 启发式计。
|
|
8485
8569
|
* systemPrompt 服务经 ctx.get 容错读取(ctx 属性访问在服务未挂载时抛错);服务缺失
|
|
8486
8570
|
* 或组装/渲染失败时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
|
|
8487
|
-
* 服务缺失属挂载失败:console 外部 + om
|
|
8571
|
+
* 服务缺失属挂载失败:console 外部 + om 警告事件每会话报告一次;组装失败仅记日志。
|
|
8488
8572
|
*/
|
|
8489
8573
|
async function estimateSystemPromptTokens(ctx, agent, logger, signal) {
|
|
8490
8574
|
const systemPrompt = ctx.get("systemPrompt");
|
|
@@ -8524,7 +8608,7 @@ function estimateToolsTokens(session, logger) {
|
|
|
8524
8608
|
}
|
|
8525
8609
|
/**
|
|
8526
8610
|
* 读取上下文压力 tokens(tokenMeter.measure 的 totalTokens)。tokenMeter 调用异常时
|
|
8527
|
-
* 记日志并报告降级(console 外部 + om
|
|
8611
|
+
* 记日志并报告降级(console 外部 + om 警告事件每会话一次),返回 undefined——本轮
|
|
8528
8612
|
* 跳过观察压缩(无压力数据不触发),不阻塞 turn。
|
|
8529
8613
|
*/
|
|
8530
8614
|
function measurePressureTokens(ctx, session, logger) {
|
|
@@ -8631,8 +8715,8 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
|
8631
8715
|
const startIndex = inRangeCms[0]?.index ?? 0;
|
|
8632
8716
|
const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
|
|
8633
8717
|
logger.step(`观察:保留旧块 ${blocks.length} 条,替换 [${replaceStart}..${range.end}](${replaceSeqs.length} 个表层节点,压缩至${triggerNote}),新消息 index ${startIndex}..${endIndex}`);
|
|
8634
|
-
const instruction = buildHistoryPrompt();
|
|
8635
|
-
const contextText = renderMessages(session, replaceSeqs);
|
|
8718
|
+
const instruction = buildHistoryPrompt(config.compressSkipReasoning);
|
|
8719
|
+
const contextText = renderMessages(session, replaceSeqs, config.compressSkipReasoning);
|
|
8636
8720
|
const lifecycle = {
|
|
8637
8721
|
compactionId: newCompactionId(),
|
|
8638
8722
|
turn: openTurnOf(session)
|
|
@@ -8710,7 +8794,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
|
|
|
8710
8794
|
end: range.end
|
|
8711
8795
|
}, lifecycle.compactionId);
|
|
8712
8796
|
logger.step("观察提交:追加 compaction/end");
|
|
8713
|
-
appendCompactionEnd(session, lifecycle);
|
|
8797
|
+
appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
|
|
8714
8798
|
clearPending();
|
|
8715
8799
|
logger.info(`观察压缩完成(替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens,压缩至${triggerNote})`);
|
|
8716
8800
|
return { failed: false };
|
|
@@ -8968,6 +9052,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
8968
9052
|
rateLimitWaitMs: 6e4,
|
|
8969
9053
|
tailMessageCount: 5,
|
|
8970
9054
|
compressRetryCount: 5,
|
|
9055
|
+
compressSkipReasoning: true,
|
|
8971
9056
|
omEnabled: true,
|
|
8972
9057
|
debug: false,
|
|
8973
9058
|
recallEnabled: true,
|
|
@@ -9009,6 +9094,7 @@ function resolveConfig(raw) {
|
|
|
9009
9094
|
if (integer && !Number.isInteger(value)) continue;
|
|
9010
9095
|
config[key] = value;
|
|
9011
9096
|
}
|
|
9097
|
+
config.compressSkipReasoning = resolveBoolean(input.compressSkipReasoning, true);
|
|
9012
9098
|
config.omEnabled = resolveBoolean(input.omEnabled, true);
|
|
9013
9099
|
config.debug = resolveBoolean(input.debug, process.env.NODE_ENV !== "production");
|
|
9014
9100
|
config.recallEnabled = resolveBoolean(input.recallEnabled, true);
|