dsh-plugin-om 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/client/OmCompactionCard.d.ts +1 -0
- package/dist/client/OmCompactionCard.d.ts.map +1 -1
- package/dist/client/OmWarningCard.d.ts +15 -0
- package/dist/client/OmWarningCard.d.ts.map +1 -0
- package/dist/client/definition.d.ts +27 -6
- package/dist/client/definition.d.ts.map +1 -1
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/locales.d.ts +4 -0
- package/dist/client/locales.d.ts.map +1 -1
- package/dist/client.js +151 -8
- package/dist/client.js.map +1 -1
- package/dist/compress.d.ts +22 -8
- package/dist/compress.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/degrade.d.ts +18 -0
- package/dist/degrade.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +276 -98
- package/dist/semantic-recall.d.ts.map +1 -1
- package/dist/summarize.d.ts +35 -11
- package/dist/summarize.d.ts.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
2
|
+
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
1
3
|
import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
@@ -11,7 +13,7 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
|
|
|
11
13
|
/**
|
|
12
14
|
* 共享常量:插件级魔法字符串,集中定义避免散落各模块。
|
|
13
15
|
* 导出 PLUGIN_LABEL / HISTORY_TAG / HISTORY_TIP / COMPLETE_MESSAGE_DEFINITION /
|
|
14
|
-
* COMPACT_CHECKPOINT_PLUGIN / isPluginOwnedSource。
|
|
16
|
+
* COMPACT_CHECKPOINT_PLUGIN / COMPACTION_ABORTED_ERROR / isPluginOwnedSource。
|
|
15
17
|
*/
|
|
16
18
|
/** 插件标识:压缩消息 source.plugin 取值与日志前缀。 */
|
|
17
19
|
const PLUGIN_LABEL = "dsh-plugin-om";
|
|
@@ -23,13 +25,65 @@ const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
|
|
|
23
25
|
* 「完整消息」定义(提示词 / 工具描述 / 块顶注释共用)。
|
|
24
26
|
* 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
|
|
25
27
|
*/
|
|
26
|
-
const COMPLETE_MESSAGE_DEFINITION = "
|
|
28
|
+
const COMPLETE_MESSAGE_DEFINITION = "`完整消息`指一条`用户消息`、`系统消息`、`模型输出文本`或`具有result的toolcall`;首条 index 为 0,按会话顺序递增。";
|
|
29
|
+
/**
|
|
30
|
+
* 压缩因 signal 中止而放弃时 compaction/end 的 error 标识(服务端写入、客户端过滤):
|
|
31
|
+
* 中止不是失败,客户端据此隐藏失败行(宿主不变量要求无 summary 的 end 必须带 error)。
|
|
32
|
+
*/
|
|
33
|
+
const COMPACTION_ABORTED_ERROR = "压缩已中止(signal aborted)";
|
|
27
34
|
/** 判定 user/message 的 source 是否为本插件自产(压缩日志消息;含旧宿主 checkpoint 标记兼容)。这类消息不占完整消息 index。 */
|
|
28
35
|
function isPluginOwnedSource(source) {
|
|
29
36
|
if (source?.kind !== "plugin") return false;
|
|
30
37
|
return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
|
|
31
38
|
}
|
|
32
39
|
//#endregion
|
|
40
|
+
//#region src/degrade.ts
|
|
41
|
+
/**
|
|
42
|
+
* 降级报告:压缩流程中的挂载失败统一出口。
|
|
43
|
+
* 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
|
|
44
|
+
*
|
|
45
|
+
* - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
|
|
46
|
+
* 并向会话日志追加 log-only 的 `om/warning` 事件(客户端渲染为「功能降级」警告行);
|
|
47
|
+
* 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
|
|
48
|
+
* - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
|
|
49
|
+
* - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
|
|
50
|
+
*/
|
|
51
|
+
/** 各降级问题面向用户的简短说明(om/warning 载荷 message;客户端警告行直接展示)。 */
|
|
52
|
+
const DEGRADE_PROBLEMS = {
|
|
53
|
+
"systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
|
|
54
|
+
"tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
|
|
55
|
+
};
|
|
56
|
+
/** 判定事件是否为指定 problem 的已有 om/warning 记录(会话内去重依据)。 */
|
|
57
|
+
function isWarningFor(event, problem) {
|
|
58
|
+
if (event.type !== "om/warning") return false;
|
|
59
|
+
return event.data?.problem === problem;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
|
|
63
|
+
* 出现时,向会话日志追加 log-only `om/warning` 事件(客户端渲染警告行,每会话最多
|
|
64
|
+
* 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
|
|
65
|
+
* 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
|
|
66
|
+
*/
|
|
67
|
+
function reportDegrade(session, logger, problem) {
|
|
68
|
+
const message = DEGRADE_PROBLEMS[problem];
|
|
69
|
+
let first = true;
|
|
70
|
+
try {
|
|
71
|
+
first = !session.events.some((event) => isWarningFor(event, problem));
|
|
72
|
+
} catch {}
|
|
73
|
+
if (!first) return;
|
|
74
|
+
console.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
75
|
+
logger.warn(message);
|
|
76
|
+
try {
|
|
77
|
+
session.append("om/warning", {
|
|
78
|
+
problem,
|
|
79
|
+
message
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
83
|
+
logger.warn(`om/warning 事件追加失败: ${text}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
33
87
|
//#region src/utils.ts
|
|
34
88
|
/** 判断值是否为普通对象(非 null、非数组)。 */
|
|
35
89
|
function isRecord(value) {
|
|
@@ -7474,27 +7528,24 @@ async function gateRateLimit(waitMs, signal) {
|
|
|
7474
7528
|
*/
|
|
7475
7529
|
function buildHistoryPrompt() {
|
|
7476
7530
|
return [
|
|
7477
|
-
"
|
|
7531
|
+
"压缩 <history> 消息记录。你应当输出**单个**合法的 <history> 块。",
|
|
7478
7532
|
"",
|
|
7479
7533
|
"【history 块定义】",
|
|
7480
|
-
"- <history>
|
|
7481
|
-
|
|
7482
|
-
"- <
|
|
7483
|
-
"- <sys type=\"(kind)\" index=\"N\">:系统消息条目(user_message 中非 kind:user 的部分,如宿主注入的上下文),块中为空,内容不进入压缩输入。",
|
|
7534
|
+
"- <history> 是历史消息的记录块。",
|
|
7535
|
+
"- <user_message index=\"N\">:用户消息条目。",
|
|
7536
|
+
"- <sys type=\"(kind)\" index=\"N\">:系统消息条目。",
|
|
7484
7537
|
"- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
|
|
7485
7538
|
"- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
|
|
7486
7539
|
"- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
|
|
7487
7540
|
"",
|
|
7488
7541
|
"【压缩要求】",
|
|
7489
|
-
"-
|
|
7490
|
-
"- 完整保留系统消息:<sys> 条目逐条原样保留(type 与 index 不变),块中为空,不概括、不省略、不填充内容。",
|
|
7542
|
+
"- <user_message> <sys> 条目从输入中逐条保留,不做任何处理。",
|
|
7491
7543
|
"- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
|
|
7492
7544
|
"- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块:块内描述模块的目的、行为与结果;涉及的具体文件保留在模块内容中,多个前缀相同的路径合并简写。",
|
|
7493
7545
|
"- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现,内容不受限制。",
|
|
7494
7546
|
"- 条目按 index 顺序覆盖本次压缩的全部完整消息:index/start/end 必须连续(区间内 index 连续、相邻条目相接),不跳号、不重叠、不遗漏。",
|
|
7495
|
-
"- 条目内容为合法 XML 文本:特殊字符保持转义形式(如 & → &、< → <),用户消息原文的转义形式原样保留,不要反转义。",
|
|
7496
7547
|
"",
|
|
7497
|
-
"
|
|
7548
|
+
"【输出格式】输出单个合法的 <history> 块,**不包含其他任何内容**:",
|
|
7498
7549
|
`<${HISTORY_TAG}>`,
|
|
7499
7550
|
"<user_message index=\"(index)\">",
|
|
7500
7551
|
"(user 消息原文)",
|
|
@@ -7511,6 +7562,13 @@ function buildHistoryPrompt() {
|
|
|
7511
7562
|
"【数据源】下方的 <history> 消息记录是本次要压缩的全部消息;压缩结果作为一个新的 <history> 块输出。"
|
|
7512
7563
|
].join("\n");
|
|
7513
7564
|
}
|
|
7565
|
+
/**
|
|
7566
|
+
* 静默 DOMParser:非致命解析问题不再走 xmldom 默认的 console.error 输出
|
|
7567
|
+
* (模型输出非法 XML 时避免刷 console),fatalError 仍抛 ParseError、解析语义不变。
|
|
7568
|
+
*/
|
|
7569
|
+
function newQuietParser() {
|
|
7570
|
+
return new import_lib.DOMParser({ onError: () => {} });
|
|
7571
|
+
}
|
|
7514
7572
|
/** 渲染用户消息条目(DOM 元素):文本块原样;图片/文件等非文本块以注释补充。 */
|
|
7515
7573
|
function renderUserEntry(doc, session, cm) {
|
|
7516
7574
|
const seq = cm.seqs[0];
|
|
@@ -7555,7 +7613,7 @@ function renderMessages(session, seqs) {
|
|
|
7555
7613
|
if (reasonings.length > 0) reasoningBySeq.set(seq, reasonings);
|
|
7556
7614
|
}
|
|
7557
7615
|
const emittedReasoning = /* @__PURE__ */ new Set();
|
|
7558
|
-
const doc =
|
|
7616
|
+
const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
7559
7617
|
const entries = [];
|
|
7560
7618
|
for (const cm of indexCompleteMessages(session)) {
|
|
7561
7619
|
if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
|
|
@@ -7646,7 +7704,7 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
|
|
|
7646
7704
|
return {
|
|
7647
7705
|
provider: target.provider,
|
|
7648
7706
|
model: target.model,
|
|
7649
|
-
maxTokens,
|
|
7707
|
+
...maxTokens === void 0 ? {} : { maxTokens },
|
|
7650
7708
|
sessionId: session.id,
|
|
7651
7709
|
purpose: "compaction",
|
|
7652
7710
|
...signal === void 0 ? {} : { signal },
|
|
@@ -7671,7 +7729,7 @@ function intAttr(el, name) {
|
|
|
7671
7729
|
function parseHistoryBlock(xml) {
|
|
7672
7730
|
let doc;
|
|
7673
7731
|
try {
|
|
7674
|
-
doc =
|
|
7732
|
+
doc = newQuietParser().parseFromString(xml, "text/xml");
|
|
7675
7733
|
} catch {
|
|
7676
7734
|
return null;
|
|
7677
7735
|
}
|
|
@@ -7830,7 +7888,7 @@ function rebuildHistoryBlock(inner) {
|
|
|
7830
7888
|
content: inner.slice(open.contentFrom)
|
|
7831
7889
|
});
|
|
7832
7890
|
if (entries.length === 0) return null;
|
|
7833
|
-
const doc =
|
|
7891
|
+
const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
7834
7892
|
const root = doc.documentElement;
|
|
7835
7893
|
if (!root) return null;
|
|
7836
7894
|
for (const e of entries) {
|
|
@@ -7844,37 +7902,39 @@ function rebuildHistoryBlock(inner) {
|
|
|
7844
7902
|
/**
|
|
7845
7903
|
* 从 AI 摘要输出中提取合法日志(不信任 AI 的总结结果):
|
|
7846
7904
|
* 取首个 <history> 开标签(允许带属性)到最后一个 </history> 切为候选块,
|
|
7847
|
-
*
|
|
7905
|
+
* 找不到或内容过短返回具体原因;候选块经 XML 结构校验,整块非法时按条目模糊
|
|
7848
7906
|
* 提取重建为合法块(不要求模型输出整体合法 XML);随后统一校验:无 reasoning、
|
|
7849
7907
|
* index 连续且与 expected 覆盖区间一致;通过后统一改写为带 tip 属性的开标签
|
|
7850
|
-
*
|
|
7908
|
+
* 并在块顶插入格式说明注释。所有失败原因均为说明性描述,不携带解析器原始报错。
|
|
7851
7909
|
*/
|
|
7852
|
-
function
|
|
7910
|
+
function extractSummaryDetailed(raw, expected) {
|
|
7853
7911
|
const closeTag = `</${HISTORY_TAG}>`;
|
|
7854
7912
|
const openMatch = HISTORY_OPEN_TAG_RE.exec(raw);
|
|
7855
7913
|
const open = openMatch?.index ?? -1;
|
|
7856
7914
|
const close = raw.lastIndexOf(closeTag);
|
|
7857
|
-
if (openMatch === null || close === -1 || close < open) return
|
|
7915
|
+
if (openMatch === null || close === -1 || close < open) return { error: "输出中找不到完整的 <history> 块(缺少 <history> 开标签或 </history> 闭标签)" };
|
|
7858
7916
|
const inner = raw.slice(open + openMatch[0].length, close);
|
|
7859
|
-
if (inner.trim().length < 10) return
|
|
7917
|
+
if (inner.trim().length < 10) return { error: "输出中 <history> 块内容过短(少于 10 字符)" };
|
|
7860
7918
|
const block = raw.slice(open, close + closeTag.length);
|
|
7861
7919
|
const candidate = parseHistoryBlock(block) === null ? rebuildHistoryBlock(inner) : block;
|
|
7862
|
-
if (candidate === null) return
|
|
7920
|
+
if (candidate === null) return { error: "输出不是合法的 <history> 块(XML 结构非法,按条目模糊提取也未找到有效条目)" };
|
|
7863
7921
|
const parsed = parseHistoryBlock(candidate);
|
|
7864
|
-
if (parsed === null) return
|
|
7865
|
-
if (parsed.hasReasoning) return
|
|
7922
|
+
if (parsed === null) return { error: "输出不是合法的 <history> 块(条目结构非法:属性缺失或包含未定义元素)" };
|
|
7923
|
+
if (parsed.hasReasoning) return { error: "输出包含 <reasoning> 条目(产物不允许携带思考过程)" };
|
|
7866
7924
|
const span = historyContinuity(parsed.entries);
|
|
7867
|
-
if (span === null) return
|
|
7868
|
-
if (expected?.start !== void 0 && span.start !== expected.start) return
|
|
7869
|
-
if (expected?.end !== void 0 && span.end !== expected.end) return
|
|
7925
|
+
if (span === null) return { error: "输出条目 index/start/end 不连续(跳号、重叠或乱序)" };
|
|
7926
|
+
if (expected?.start !== void 0 && span.start !== expected.start) return { error: `输出覆盖区间从 ${span.start} 开始,与期望起始 ${expected.start} 不一致` };
|
|
7927
|
+
if (expected?.end !== void 0 && span.end !== expected.end) return { error: `输出覆盖区间止于 ${span.end},与期望结束 ${expected.end} 不一致` };
|
|
7870
7928
|
const candidateInner = candidate.slice(candidate.indexOf(">") + 1, candidate.length - closeTag.length).trim();
|
|
7871
|
-
return `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}
|
|
7929
|
+
return { log: `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}>` };
|
|
7872
7930
|
}
|
|
7873
7931
|
/**
|
|
7874
7932
|
* 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
|
|
7875
|
-
* 失败(抛异常 / 空输出 / 非 stop 结束 /
|
|
7876
|
-
*
|
|
7877
|
-
*
|
|
7933
|
+
* 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,每次尝试的
|
|
7934
|
+
* 结果或报错始终写入日志(成功 info / 失败 warn,不受 debug 影响);全部尝试耗尽
|
|
7935
|
+
* 返回失败结果(携带最后一次尝试的实际报错/具体问题,不产生任何日志变更)。
|
|
7936
|
+
* signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
|
|
7937
|
+
* 限流等待门。
|
|
7878
7938
|
*/
|
|
7879
7939
|
async function runSummarySubagent(ctx, agent, instruction, contextText, maxTokens, target, debug, signal, options) {
|
|
7880
7940
|
const session = agent.session;
|
|
@@ -7883,30 +7943,44 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
7883
7943
|
let lastFailure = {};
|
|
7884
7944
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
7885
7945
|
if (signal?.aborted) {
|
|
7886
|
-
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal
|
|
7887
|
-
return
|
|
7946
|
+
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次压缩`);
|
|
7947
|
+
return {
|
|
7948
|
+
ok: false,
|
|
7949
|
+
error: COMPACTION_ABORTED_ERROR,
|
|
7950
|
+
aborted: true
|
|
7951
|
+
};
|
|
7888
7952
|
}
|
|
7889
7953
|
const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
|
|
7890
7954
|
if (!await gateRateLimit(rateLimitWaitMs, signal)) {
|
|
7891
|
-
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal
|
|
7892
|
-
return
|
|
7955
|
+
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次压缩`);
|
|
7956
|
+
return {
|
|
7957
|
+
ok: false,
|
|
7958
|
+
error: COMPACTION_ABORTED_ERROR,
|
|
7959
|
+
aborted: true
|
|
7960
|
+
};
|
|
7893
7961
|
}
|
|
7894
|
-
logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens})`);
|
|
7962
|
+
logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
|
|
7895
7963
|
try {
|
|
7896
7964
|
const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
|
|
7897
7965
|
const collector = new StreamCollector();
|
|
7898
7966
|
for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
|
|
7899
|
-
const
|
|
7967
|
+
const extracted = extractSummaryDetailed(collector.text, options?.expected);
|
|
7900
7968
|
const finish = collector.finish;
|
|
7901
|
-
if (finish.kind !== "stop"
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,忽略本次摘要"));
|
|
7969
|
+
if (finish.kind !== "stop") {
|
|
7970
|
+
lastFailure = { reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)` };
|
|
7971
|
+
logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
7905
7972
|
continue;
|
|
7906
7973
|
}
|
|
7974
|
+
if ("error" in extracted) {
|
|
7975
|
+
lastFailure = { reason: extracted.error };
|
|
7976
|
+
logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
7977
|
+
continue;
|
|
7978
|
+
}
|
|
7979
|
+
const text = extracted.log;
|
|
7907
7980
|
const usage = collector.usage;
|
|
7908
|
-
logger.
|
|
7981
|
+
logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + ")");
|
|
7909
7982
|
return {
|
|
7983
|
+
ok: true,
|
|
7910
7984
|
text,
|
|
7911
7985
|
attemptCount: attempt,
|
|
7912
7986
|
...usage === void 0 ? {} : { usage }
|
|
@@ -7918,11 +7992,16 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
7918
7992
|
logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
|
|
7919
7993
|
}
|
|
7920
7994
|
lastFailure = { error: message };
|
|
7921
|
-
logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : "
|
|
7995
|
+
logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
|
|
7922
7996
|
}
|
|
7923
7997
|
}
|
|
7924
|
-
|
|
7925
|
-
|
|
7998
|
+
const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
|
|
7999
|
+
logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次,最后错误:${lastError}),拒绝放行本轮 step`);
|
|
8000
|
+
return {
|
|
8001
|
+
ok: false,
|
|
8002
|
+
error: lastError,
|
|
8003
|
+
aborted: false
|
|
8004
|
+
};
|
|
7926
8005
|
}
|
|
7927
8006
|
//#endregion
|
|
7928
8007
|
//#region src/compress.ts
|
|
@@ -7932,12 +8011,19 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
7932
8011
|
* reflectPass / observePass / maybeCompress。
|
|
7933
8012
|
*
|
|
7934
8013
|
* - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要合并为一条
|
|
7935
|
-
* - 观察:净压力(上下文压力 − 已压缩块 token
|
|
8014
|
+
* - 观察:净压力(上下文压力 − 已压缩块 token 合计 − 系统提示词 token 估算 − 工具定义
|
|
8015
|
+
* token 估算)≥ observeThresholdTokens 时,
|
|
7936
8016
|
* 摘要未压缩消息为新 <history> 块并精确替换被压缩区间(旧块保留)
|
|
7937
8017
|
* - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
|
|
7938
8018
|
* - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
|
|
8019
|
+
* - 摘要尝试全部耗尽时 pass 返回失败结果(携带最后一次尝试的实际报错),压缩流程
|
|
8020
|
+
* 向上传播,pre-step 据此拒绝本 step 中断当前 turn;signal 中止标记 aborted(不中断)
|
|
7939
8021
|
* - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
|
|
7940
|
-
* 失败补 end(error);替换消息 source 标记插件标识供 UI 认领
|
|
8022
|
+
* 失败补 end(error,实际报错);替换消息 source 标记插件标识供 UI 认领
|
|
8023
|
+
* - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
|
|
8024
|
+
* log-only om/warning 事件(客户端渲染功能降级警告行,每会话同一问题至多一次);
|
|
8025
|
+
* 辅助估算的普通运行时报错仅记日志。降级与报错都不阻塞压缩(tokenMeter 压力数据
|
|
8026
|
+
* 缺失时本轮跳过观察)
|
|
7941
8027
|
*/
|
|
7942
8028
|
/** 历史文本 token 估算:4 字符 ≈ 1 token(与宿主 dsh-token-meter 启发式一致)。 */
|
|
7943
8029
|
function estimateTextTokens(text) {
|
|
@@ -8084,7 +8170,7 @@ function appendCompactionSummary(session, data) {
|
|
|
8084
8170
|
shadowedCharCount: data.shadowedCharCount,
|
|
8085
8171
|
provider: data.provider,
|
|
8086
8172
|
model: data.model,
|
|
8087
|
-
maxTokens: data.maxTokens,
|
|
8173
|
+
...data.maxTokens === void 0 ? {} : { maxTokens: data.maxTokens },
|
|
8088
8174
|
attemptCount: data.attemptCount,
|
|
8089
8175
|
...data.usage === void 0 ? {} : { usage: data.usage }
|
|
8090
8176
|
};
|
|
@@ -8120,7 +8206,8 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
|
|
|
8120
8206
|
}
|
|
8121
8207
|
/**
|
|
8122
8208
|
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要调用把整个块
|
|
8123
|
-
*
|
|
8209
|
+
* 区段合并替换为一条更紧凑的摘要。失败不产生部分替换;摘要尝试全部耗尽返回
|
|
8210
|
+
* 失败结果(error = 最后一次尝试的实际报错/具体问题)。
|
|
8124
8211
|
*/
|
|
8125
8212
|
async function reflectPass(ctx, agent, config, target, signal) {
|
|
8126
8213
|
const session = agent.session;
|
|
@@ -8129,21 +8216,21 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8129
8216
|
const { blocks } = historySection(session);
|
|
8130
8217
|
if (blocks.length === 0) {
|
|
8131
8218
|
logger.step("反思:无 <history> 压缩日志,跳过");
|
|
8132
|
-
return;
|
|
8219
|
+
return { failed: false };
|
|
8133
8220
|
}
|
|
8134
8221
|
const threshold = config.reflectThresholdTokens;
|
|
8135
8222
|
const tokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
|
|
8136
8223
|
const shadowedCharCount = blocks.reduce((total, block) => total + historyInnerText(block.text).length, 0);
|
|
8137
8224
|
if (tokens < threshold) {
|
|
8138
8225
|
logger.step(`反思:摘要 ${tokens} tokens < 阈值 ${threshold},跳过`);
|
|
8139
|
-
return;
|
|
8226
|
+
return { failed: false };
|
|
8140
8227
|
}
|
|
8141
8228
|
logger.step(`反思:摘要 ${tokens} tokens ≥ 阈值 ${threshold},触发精简合并(${blocks.length} 个块)`);
|
|
8142
8229
|
const first = blocks[0];
|
|
8143
8230
|
const last = blocks[blocks.length - 1];
|
|
8144
8231
|
if (first === void 0 || last === void 0) {
|
|
8145
8232
|
logger.step("反思:块区段缺失,跳过");
|
|
8146
|
-
return;
|
|
8233
|
+
return { failed: false };
|
|
8147
8234
|
}
|
|
8148
8235
|
const blockSeqs = blocks.map((block) => block.seq);
|
|
8149
8236
|
const instruction = buildHistoryPrompt();
|
|
@@ -8159,7 +8246,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8159
8246
|
} catch (error) {
|
|
8160
8247
|
const message = error instanceof Error ? error.message : String(error);
|
|
8161
8248
|
logger.warn(`反思压缩启动失败: ${message}`);
|
|
8162
|
-
return;
|
|
8249
|
+
return { failed: false };
|
|
8163
8250
|
}
|
|
8164
8251
|
const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
|
|
8165
8252
|
maxAttempts: config.compressRetryCount + 1,
|
|
@@ -8169,12 +8256,16 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8169
8256
|
},
|
|
8170
8257
|
rateLimitWaitMs: config.rateLimitWaitMs
|
|
8171
8258
|
});
|
|
8172
|
-
if (summaryResult
|
|
8173
|
-
logger.
|
|
8259
|
+
if (!summaryResult.ok) {
|
|
8260
|
+
logger.warn(`反思:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
|
|
8174
8261
|
try {
|
|
8175
|
-
appendCompactionEnd(session, lifecycle,
|
|
8262
|
+
appendCompactionEnd(session, lifecycle, summaryResult.error);
|
|
8176
8263
|
} catch {}
|
|
8177
|
-
return
|
|
8264
|
+
return {
|
|
8265
|
+
failed: true,
|
|
8266
|
+
error: summaryResult.error,
|
|
8267
|
+
aborted: summaryResult.aborted
|
|
8268
|
+
};
|
|
8178
8269
|
}
|
|
8179
8270
|
const report = summaryResult.text;
|
|
8180
8271
|
try {
|
|
@@ -8204,50 +8295,113 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8204
8295
|
logger.step("反思提交:追加 compaction/end");
|
|
8205
8296
|
appendCompactionEnd(session, lifecycle);
|
|
8206
8297
|
logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
|
|
8298
|
+
return { failed: false };
|
|
8207
8299
|
} catch (error) {
|
|
8208
8300
|
const message = error instanceof Error ? error.message : String(error);
|
|
8209
8301
|
logger.warn(`反思提交失败: ${message}`);
|
|
8210
8302
|
try {
|
|
8211
8303
|
appendCompactionEnd(session, lifecycle, message);
|
|
8212
8304
|
} catch {}
|
|
8305
|
+
return { failed: false };
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
/**
|
|
8309
|
+
* 估算系统提示词 tokens:按 agent 作用域组装并渲染系统提示词,按长度/4 启发式计。
|
|
8310
|
+
* systemPrompt 服务经 ctx.get 容错读取(ctx 属性访问在服务未挂载时抛错);服务缺失
|
|
8311
|
+
* 或组装/渲染失败时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
|
|
8312
|
+
* 服务缺失属挂载失败:console 外部 + om/warning 事件每会话报告一次;组装失败仅记日志。
|
|
8313
|
+
*/
|
|
8314
|
+
async function estimateSystemPromptTokens(ctx, agent, logger, signal) {
|
|
8315
|
+
const systemPrompt = ctx.get("systemPrompt");
|
|
8316
|
+
if (systemPrompt === void 0 || typeof systemPrompt.assemble !== "function") {
|
|
8317
|
+
reportDegrade(agent.session, logger, "systemPrompt-missing");
|
|
8318
|
+
return 0;
|
|
8319
|
+
}
|
|
8320
|
+
try {
|
|
8321
|
+
const agentCtx = agent.ctx;
|
|
8322
|
+
const scope = agentCtx === void 0 ? void 0 : scopeOf(agentCtx);
|
|
8323
|
+
const assembleContext = { agent };
|
|
8324
|
+
if (scope !== void 0) assembleContext.scope = scope;
|
|
8325
|
+
if (signal !== void 0) assembleContext.signal = signal;
|
|
8326
|
+
const assembly = await systemPrompt.assemble(assembleContext);
|
|
8327
|
+
return estimateTextTokens(renderPrompt(assembly));
|
|
8328
|
+
} catch (error) {
|
|
8329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8330
|
+
logger.warn(`系统提示词 tokens 估算失败,按 0 计: ${message}`);
|
|
8331
|
+
return 0;
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
/**
|
|
8335
|
+
* 估算请求工具定义 tokens:按会话请求头 tools(assembled tool schemas)JSON 序列化
|
|
8336
|
+
* 长度/4 启发式计(与宿主 dsh-token-meter 对工具 schema 的启发式一致)。请求头读取
|
|
8337
|
+
* 失败或缺 tools 时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
|
|
8338
|
+
*/
|
|
8339
|
+
function estimateToolsTokens(session, logger) {
|
|
8340
|
+
try {
|
|
8341
|
+
const header = session.requestHeader();
|
|
8342
|
+
if (header?.tools === void 0 || header.tools.length === 0) return 0;
|
|
8343
|
+
return estimateTextTokens(JSON.stringify(header.tools));
|
|
8344
|
+
} catch (error) {
|
|
8345
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8346
|
+
logger.warn(`工具定义 tokens 估算失败,按 0 计: ${message}`);
|
|
8347
|
+
return 0;
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
/**
|
|
8351
|
+
* 读取上下文压力 tokens(tokenMeter.measure 的 totalTokens)。tokenMeter 调用异常时
|
|
8352
|
+
* 记日志并报告降级(console 外部 + om/warning 每会话一次),返回 undefined——本轮
|
|
8353
|
+
* 跳过观察压缩(无压力数据不触发),不阻塞 turn。
|
|
8354
|
+
*/
|
|
8355
|
+
function measurePressureTokens(ctx, session, logger) {
|
|
8356
|
+
try {
|
|
8357
|
+
return ctx.tokenMeter.measure(session).totalTokens;
|
|
8358
|
+
} catch (error) {
|
|
8359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8360
|
+
logger.warn(`上下文压力读取失败(tokenMeter.measure 抛错),本轮跳过观察压缩: ${message}`);
|
|
8361
|
+
reportDegrade(session, logger, "tokenMeter-unavailable");
|
|
8362
|
+
return;
|
|
8213
8363
|
}
|
|
8214
8364
|
}
|
|
8215
8365
|
/**
|
|
8216
|
-
* 观察:净压力 tokens(上下文压力 − 已压缩 <history> 块 token
|
|
8217
|
-
* observeThresholdTokens
|
|
8218
|
-
*
|
|
8366
|
+
* 观察:净压力 tokens(上下文压力 − 已压缩 <history> 块 token 合计 − 系统提示词
|
|
8367
|
+
* token 估算 − 工具定义 token 估算)≥ observeThresholdTokens 时,摘要调用把未压缩
|
|
8368
|
+
* 消息压缩为观察日志,追加到旧摘要并替换被压缩消息区间。失败不产生部分替换;
|
|
8369
|
+
* 摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
|
|
8219
8370
|
*/
|
|
8220
8371
|
async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
8221
8372
|
const session = agent.session;
|
|
8222
8373
|
const logger = makeLogger(ctx, config.debug);
|
|
8223
8374
|
logger.step(`观察检查(观察阈值 ${config.observeThresholdTokens} tokens,尾部保留 ${tailCount} 条)`);
|
|
8224
8375
|
const threshold = config.observeThresholdTokens;
|
|
8225
|
-
const pressureTokens = ctx
|
|
8376
|
+
const pressureTokens = measurePressureTokens(ctx, session, logger);
|
|
8377
|
+
if (pressureTokens === void 0) return { failed: false };
|
|
8226
8378
|
const { blocks } = historySection(session);
|
|
8227
8379
|
const historyTokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
|
|
8228
|
-
const
|
|
8380
|
+
const systemTokens = await estimateSystemPromptTokens(ctx, agent, logger, signal);
|
|
8381
|
+
const toolsTokens = estimateToolsTokens(session, logger);
|
|
8382
|
+
const netTokens = pressureTokens - historyTokens - systemTokens - toolsTokens;
|
|
8229
8383
|
if (netTokens < threshold) {
|
|
8230
|
-
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})< 阈值 ${threshold},跳过`);
|
|
8231
|
-
return;
|
|
8384
|
+
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})< 阈值 ${threshold},跳过`);
|
|
8385
|
+
return { failed: false };
|
|
8232
8386
|
}
|
|
8233
|
-
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},触发压缩`);
|
|
8387
|
+
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})≥ 阈值 ${threshold},触发压缩`);
|
|
8234
8388
|
const range = computeCompressRange(session, tailCount);
|
|
8235
8389
|
if (!range) {
|
|
8236
8390
|
logger.step("观察:无可行压缩区间(边界后消息过短或配对无法平衡),跳过");
|
|
8237
|
-
return;
|
|
8391
|
+
return { failed: false };
|
|
8238
8392
|
}
|
|
8239
8393
|
logger.step(`观察:压缩区间 [${range.start}..${range.end}],遮蔽 ${range.shadowedSeqs.length} 个表层节点`);
|
|
8240
8394
|
const replaceSeqs = range.shadowedSeqs;
|
|
8241
8395
|
const replaceStart = replaceSeqs[0];
|
|
8242
8396
|
if (replaceStart === void 0) {
|
|
8243
8397
|
logger.step("观察:区间内无新消息(全部为压缩日志块),跳过");
|
|
8244
|
-
return;
|
|
8398
|
+
return { failed: false };
|
|
8245
8399
|
}
|
|
8246
8400
|
const shadowedSet = new Set(replaceSeqs);
|
|
8247
8401
|
const inRangeCms = indexCompleteMessages(session).filter((cm) => cm.seqs.every((seq) => shadowedSet.has(seq)));
|
|
8248
8402
|
if (inRangeCms.length === 0) {
|
|
8249
8403
|
logger.step("观察:区间内无完整消息,跳过");
|
|
8250
|
-
return;
|
|
8404
|
+
return { failed: false };
|
|
8251
8405
|
}
|
|
8252
8406
|
const startIndex = inRangeCms[0]?.index ?? 0;
|
|
8253
8407
|
const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
|
|
@@ -8264,7 +8418,7 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8264
8418
|
} catch (error) {
|
|
8265
8419
|
const message = error instanceof Error ? error.message : String(error);
|
|
8266
8420
|
logger.warn(`观察压缩启动失败: ${message}`);
|
|
8267
|
-
return;
|
|
8421
|
+
return { failed: false };
|
|
8268
8422
|
}
|
|
8269
8423
|
const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
|
|
8270
8424
|
maxAttempts: config.compressRetryCount + 1,
|
|
@@ -8274,12 +8428,16 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8274
8428
|
},
|
|
8275
8429
|
rateLimitWaitMs: config.rateLimitWaitMs
|
|
8276
8430
|
});
|
|
8277
|
-
if (summaryResult
|
|
8278
|
-
logger.
|
|
8431
|
+
if (!summaryResult.ok) {
|
|
8432
|
+
logger.warn(`观察:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
|
|
8279
8433
|
try {
|
|
8280
|
-
appendCompactionEnd(session, lifecycle,
|
|
8434
|
+
appendCompactionEnd(session, lifecycle, summaryResult.error);
|
|
8281
8435
|
} catch {}
|
|
8282
|
-
return
|
|
8436
|
+
return {
|
|
8437
|
+
failed: true,
|
|
8438
|
+
error: summaryResult.error,
|
|
8439
|
+
aborted: summaryResult.aborted
|
|
8440
|
+
};
|
|
8283
8441
|
}
|
|
8284
8442
|
const report = summaryResult.text;
|
|
8285
8443
|
const attemptCount = summaryResult.attemptCount - 1;
|
|
@@ -8287,7 +8445,13 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8287
8445
|
const shadowedTokenCount = replaceSeqs.reduce((total, seq) => {
|
|
8288
8446
|
const event = session.events[seq];
|
|
8289
8447
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
8290
|
-
|
|
8448
|
+
let tokens = 0;
|
|
8449
|
+
if (message) try {
|
|
8450
|
+
tokens = ctx.tokenMeter.estimateMessage(message);
|
|
8451
|
+
} catch {
|
|
8452
|
+
reportDegrade(session, logger, "tokenMeter-unavailable");
|
|
8453
|
+
}
|
|
8454
|
+
return total + tokens;
|
|
8291
8455
|
}, 0);
|
|
8292
8456
|
const shadowedCharCount = replaceSeqs.reduce((total, seq) => {
|
|
8293
8457
|
const event = session.events[seq];
|
|
@@ -8321,34 +8485,45 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8321
8485
|
logger.step("观察提交:追加 compaction/end");
|
|
8322
8486
|
appendCompactionEnd(session, lifecycle);
|
|
8323
8487
|
logger.info(`观察压缩完成(净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens)`);
|
|
8488
|
+
return { failed: false };
|
|
8324
8489
|
} catch (error) {
|
|
8325
8490
|
const message = error instanceof Error ? error.message : String(error);
|
|
8326
8491
|
logger.warn(`观察压缩提交失败: ${message}`);
|
|
8327
8492
|
try {
|
|
8328
8493
|
appendCompactionEnd(session, lifecycle, message);
|
|
8329
8494
|
} catch {}
|
|
8495
|
+
return { failed: false };
|
|
8330
8496
|
}
|
|
8331
8497
|
}
|
|
8332
|
-
/**
|
|
8498
|
+
/**
|
|
8499
|
+
* 压力检查 + 两级压缩入口:先反思后观察,pre-step 阻塞串行执行;仅主会话生效。
|
|
8500
|
+
* 反思摘要耗尽失败时直接返回失败结果(观察无需继续,本轮将被拒绝);观察失败同样
|
|
8501
|
+
* 传播失败结果,由 pre-step 拒绝本 step 中断当前 turn。
|
|
8502
|
+
*/
|
|
8333
8503
|
async function maybeCompress(ctx, agent, config, signal) {
|
|
8334
8504
|
const session = agent.session;
|
|
8335
8505
|
const logger = makeLogger(ctx, config.debug);
|
|
8336
8506
|
if (!config.omEnabled) {
|
|
8337
8507
|
logger.step("omEnabled=false,跳过压缩");
|
|
8338
|
-
return;
|
|
8508
|
+
return { failed: false };
|
|
8339
8509
|
}
|
|
8340
8510
|
const target = routedTarget(session);
|
|
8341
8511
|
if (target === void 0) {
|
|
8342
8512
|
logger.step("会话未路由(无 provider/model),跳过压缩");
|
|
8343
|
-
return;
|
|
8513
|
+
return { failed: false };
|
|
8344
8514
|
}
|
|
8345
8515
|
logger.step(`会话路由:provider ${target.provider},model ${target.model}`);
|
|
8346
8516
|
const tailCount = config.tailMessageCount;
|
|
8347
8517
|
logger.step("反思 pass 开始");
|
|
8348
|
-
await reflectPass(ctx, agent, config, target, signal);
|
|
8518
|
+
const reflect = await reflectPass(ctx, agent, config, target, signal);
|
|
8519
|
+
if (reflect.failed) {
|
|
8520
|
+
logger.step("反思 pass 失败(本轮将被拒绝),跳过观察 pass");
|
|
8521
|
+
return reflect;
|
|
8522
|
+
}
|
|
8349
8523
|
logger.step("反思 pass 结束,观察 pass 开始");
|
|
8350
|
-
await observePass(ctx, agent, config, tailCount, target, signal);
|
|
8524
|
+
const observe = await observePass(ctx, agent, config, tailCount, target, signal);
|
|
8351
8525
|
logger.step("观察 pass 结束,压缩流程完成");
|
|
8526
|
+
return observe;
|
|
8352
8527
|
}
|
|
8353
8528
|
//#endregion
|
|
8354
8529
|
//#region src/model-download.ts
|
|
@@ -8559,9 +8734,9 @@ function cosineSimilarity(a, b) {
|
|
|
8559
8734
|
*/
|
|
8560
8735
|
/** 默认配置(冻结对象,resolveConfig 合并的基底;debug 缺省值在解析时按 NODE_ENV 判定)。 */
|
|
8561
8736
|
const DEFAULT_CONFIG = Object.freeze({
|
|
8562
|
-
observeThresholdTokens:
|
|
8563
|
-
reflectThresholdTokens:
|
|
8564
|
-
compressMaxTokens:
|
|
8737
|
+
observeThresholdTokens: 45e3,
|
|
8738
|
+
reflectThresholdTokens: 12e4,
|
|
8739
|
+
compressMaxTokens: void 0,
|
|
8565
8740
|
rateLimitWaitMs: 6e4,
|
|
8566
8741
|
tailMessageCount: 5,
|
|
8567
8742
|
compressRetryCount: 5,
|
|
@@ -8720,9 +8895,9 @@ function renderRecallOutput(value) {
|
|
|
8720
8895
|
* 各字段描述经 .describe() 透传到 wire JSON Schema。
|
|
8721
8896
|
*/
|
|
8722
8897
|
const recallArgsSchema = z.object({
|
|
8723
|
-
start: z.number().describe("
|
|
8724
|
-
end: z.number().optional().describe("与 offset
|
|
8725
|
-
offset: z.number().optional().describe("与 end
|
|
8898
|
+
start: z.number().describe("完整消息 index,基准边界,与 end 或 offset 配合指定区间;end 与 offset 至少提供一个(同时给出时 end 优先)。"),
|
|
8899
|
+
end: z.number().optional().describe("与 offset 互斥,指定区间另一边界。"),
|
|
8900
|
+
offset: z.number().optional().describe("与 end 互斥,相对 start 的步数(正向后、负向前)。")
|
|
8726
8901
|
}).refine((args) => args.end !== void 0 || args.offset !== void 0, { message: "end 与 offset 至少提供一个" });
|
|
8727
8902
|
/** 解析并校验 recall 调用参数:失败时抛出首个校验问题的可读消息(普通 Error,兼容 SDK 展示)。 */
|
|
8728
8903
|
function parseRecallArgs(raw) {
|
|
@@ -8737,7 +8912,7 @@ function parseRecallArgs(raw) {
|
|
|
8737
8912
|
function buildRecallTool(getPruner) {
|
|
8738
8913
|
return {
|
|
8739
8914
|
name: "recall",
|
|
8740
|
-
description: `${COMPLETE_MESSAGE_DEFINITION}
|
|
8915
|
+
description: `${COMPLETE_MESSAGE_DEFINITION}按 index 区间精确返回区间内全部完整消息的内容。`,
|
|
8741
8916
|
parameters: parametersFromZod(recallArgsSchema),
|
|
8742
8917
|
output: {
|
|
8743
8918
|
schema: RECALL_OUTPUT_SCHEMA,
|
|
@@ -8804,11 +8979,11 @@ function buildRecallTool(getPruner) {
|
|
|
8804
8979
|
*/
|
|
8805
8980
|
/** recall-semantic 工具参数 schema:query 必填;top_k 默认 3(1-10);区间参数均可选。 */
|
|
8806
8981
|
const semanticRecallArgsSchema = z.object({
|
|
8807
|
-
query: z.string().describe("
|
|
8808
|
-
top_k: z.number().int().min(1).max(10).optional().describe("
|
|
8809
|
-
start: z.number().optional().describe("
|
|
8810
|
-
end: z.number().optional().describe("
|
|
8811
|
-
offset: z.number().optional().describe("
|
|
8982
|
+
query: z.string().describe("要找的内容的自然语言 query(中英文与代码术语皆可,如 \"修复 retry backoff 的逻辑\"),不能为空。"),
|
|
8983
|
+
top_k: z.number().int().min(1).max(10).optional().describe("返回最匹配条数(1-10,默认 3)。"),
|
|
8984
|
+
start: z.number().optional().describe("完整消息 index,基准边界,与 end/offset 配合指定搜索区间;缺省检索全部。"),
|
|
8985
|
+
end: z.number().optional().describe("须与 start 配合,与 offset 互斥,指定搜索区间另一边界。"),
|
|
8986
|
+
offset: z.number().optional().describe("须与 start 配合,与 end 互斥,相对 start 的步数(正向后、负向前)。")
|
|
8812
8987
|
}).refine((args) => args.query.trim().length > 0, { message: "query 不能为空" });
|
|
8813
8988
|
/** 解析并校验参数:失败时抛出可读错误(普通 Error 而非 ZodError)。 */
|
|
8814
8989
|
function parseSemanticRecallArgs(raw) {
|
|
@@ -8888,7 +9063,7 @@ function buildSemanticRecallTool(options) {
|
|
|
8888
9063
|
const embed = options?.embedder ?? ((texts) => getEmbedder().then((fn) => fn(texts)));
|
|
8889
9064
|
return {
|
|
8890
9065
|
name: "recall-semantic",
|
|
8891
|
-
description: `${COMPLETE_MESSAGE_DEFINITION}
|
|
9066
|
+
description: `${COMPLETE_MESSAGE_DEFINITION}按自然语言含义检索最符合的完整消息,默认全量搜索,可指定区间。注意:只匹配文本。`,
|
|
8892
9067
|
parameters: parametersFromZod(semanticRecallArgsSchema),
|
|
8893
9068
|
output: {
|
|
8894
9069
|
schema: RECALL_OUTPUT_SCHEMA,
|
|
@@ -8966,7 +9141,8 @@ function buildSemanticRecallTool(options) {
|
|
|
8966
9141
|
/**
|
|
8967
9142
|
* dsh-plugin-om 入口(tsdown 打包入口):导出 name / inject / apply。
|
|
8968
9143
|
* apply 注册 recall / recall-semantic 工具,并接线 agent/pre-step 自动压缩
|
|
8969
|
-
*
|
|
9144
|
+
* (先反思后观察,仅主会话生效)。压缩摘要尝试全部耗尽时拒绝本 step 中断当前
|
|
9145
|
+
* turn(signal 中止除外)。压缩与检索的实现见 compress.ts / recall.ts /
|
|
8970
9146
|
* semantic-recall.ts。
|
|
8971
9147
|
*/
|
|
8972
9148
|
/** 插件名(Loader 识别入口的稳定标识)。 */
|
|
@@ -8982,14 +9158,11 @@ const inject = [
|
|
|
8982
9158
|
function apply(ctx, config) {
|
|
8983
9159
|
const resolved = resolveConfig(config);
|
|
8984
9160
|
const logger = makeLogger(ctx, resolved.debug);
|
|
8985
|
-
logger.step(`apply 启动:observeThresholdTokens=${String(resolved.observeThresholdTokens)} reflectThresholdTokens=${String(resolved.reflectThresholdTokens)} compressMaxTokens=${String(resolved.compressMaxTokens)} tailMessageCount=${String(resolved.tailMessageCount)} omEnabled=${String(resolved.omEnabled)} debug=${String(resolved.debug)}`);
|
|
9161
|
+
logger.step(`apply 启动:observeThresholdTokens=${String(resolved.observeThresholdTokens)} reflectThresholdTokens=${String(resolved.reflectThresholdTokens)} compressMaxTokens=${resolved.compressMaxTokens === void 0 ? "未设置" : String(resolved.compressMaxTokens)} tailMessageCount=${String(resolved.tailMessageCount)} omEnabled=${String(resolved.omEnabled)} debug=${String(resolved.debug)}`);
|
|
8986
9162
|
if (resolved.recallEnabled) ctx.tools.register(buildRecallTool(() => ctx.get("toolResultPruner")));
|
|
8987
9163
|
if (resolved.semanticRecallEnabled) {
|
|
8988
9164
|
const warnModel = (message) => ctx.logger.warn(`dsh-plugin-om: ${message}`);
|
|
8989
|
-
const logModel = (message) =>
|
|
8990
|
-
console.log(message);
|
|
8991
|
-
logger.info(message);
|
|
8992
|
-
};
|
|
9165
|
+
const logModel = (message) => logger.info(message);
|
|
8993
9166
|
ensureModelReady(resolved.modelDir, warnModel, void 0, logModel);
|
|
8994
9167
|
ctx.tools.register(buildSemanticRecallTool({
|
|
8995
9168
|
getPruner: () => ctx.get("toolResultPruner"),
|
|
@@ -8998,16 +9171,21 @@ function apply(ctx, config) {
|
|
|
8998
9171
|
}));
|
|
8999
9172
|
}
|
|
9000
9173
|
ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
|
|
9174
|
+
let outcome = { failed: false };
|
|
9001
9175
|
try {
|
|
9002
9176
|
if (signal.aborted) logger.step("pre-step 已中止(signal aborted),跳过压缩");
|
|
9003
9177
|
else if (!isMainSession(agent.session)) logger.step("subagent 会话,跳过压缩(仅主会话生效)");
|
|
9004
9178
|
else {
|
|
9005
9179
|
logger.step(`pre-step 触发压缩(会话 ${agent.session.id})`);
|
|
9006
|
-
await maybeCompress(ctx, agent, resolved, signal);
|
|
9180
|
+
outcome = await maybeCompress(ctx, agent, resolved, signal);
|
|
9007
9181
|
}
|
|
9008
9182
|
} catch (error) {
|
|
9009
9183
|
logger.warn(`pre-step 处理失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
9010
9184
|
}
|
|
9185
|
+
if (outcome.failed && !outcome.aborted) {
|
|
9186
|
+
logger.warn(`上下文压缩失败,拒绝本 step 中断当前 turn:${outcome.error}`);
|
|
9187
|
+
return { kind: "reject" };
|
|
9188
|
+
}
|
|
9011
9189
|
return next();
|
|
9012
9190
|
});
|
|
9013
9191
|
}
|