dsh-plugin-om 0.0.6 → 0.0.7
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 +39 -57
- package/dist/compress.d.ts +0 -6
- package/dist/compress.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +326 -302
- package/dist/log-index.d.ts +27 -2
- package/dist/log-index.d.ts.map +1 -1
- package/dist/recall.d.ts +12 -10
- package/dist/recall.d.ts.map +1 -1
- package/dist/semantic-recall.d.ts +16 -16
- package/dist/semantic-recall.d.ts.map +1 -1
- package/dist/summarize.d.ts +15 -16
- package/dist/summarize.d.ts.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -13,75 +13,6 @@ const HISTORY_TAG = "om-history";
|
|
|
13
13
|
/** 宿主压缩 checkpoint 标记的 plugin 名(dsh-compaction-basic 的 COMPACT_CHECKPOINT_MARKER.plugin)。 */
|
|
14
14
|
const COMPACT_CHECKPOINT_PLUGIN = "compact";
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/log-index.ts
|
|
17
|
-
/**
|
|
18
|
-
* 查找 seq 在表层节点序列中的下标(不在则返回 -1)。
|
|
19
|
-
* 表层节点按日志顺序排列,用于压缩边界定位与遮蔽范围计算。
|
|
20
|
-
*/
|
|
21
|
-
function surfaceIndexOf(nodes, seq) {
|
|
22
|
-
for (let i = 0; i < nodes.length; i += 1) if (nodes[i] === seq) return i;
|
|
23
|
-
return -1;
|
|
24
|
-
}
|
|
25
|
-
/** 提取一条消息事件的 message_id(user/assistant/tool-result 消息均有稳定 id;其余事件无)。 */
|
|
26
|
-
function messageIdOfEvent(event) {
|
|
27
|
-
if (!event) return void 0;
|
|
28
|
-
if (event.type === "user/message") return String(event.data.id ?? "");
|
|
29
|
-
if (event.type === "assistant/message") return String(event.data.message.id ?? "");
|
|
30
|
-
if (event.type === "tool/result") return String(event.data.message.id ?? "");
|
|
31
|
-
}
|
|
32
|
-
/** 消息索引:按日志顺序列出全部消息事件,并按 message_id 定位其在消息序列中的下标。 */
|
|
33
|
-
function indexMessages(session) {
|
|
34
|
-
/** 按日志顺序的消息事件列表。 */
|
|
35
|
-
const messages = [];
|
|
36
|
-
/** message_id → 序列下标映射。 */
|
|
37
|
-
const byId = /* @__PURE__ */ new Map();
|
|
38
|
-
/** 会话全部事件。 */
|
|
39
|
-
const events = session.events;
|
|
40
|
-
for (let seq = 0; seq < events.length; seq += 1) {
|
|
41
|
-
/** 当前待检查事件。 */
|
|
42
|
-
const event = events[seq];
|
|
43
|
-
if (!event) continue;
|
|
44
|
-
if (event.type !== "user/message" && event.type !== "assistant/message" && event.type !== "tool/result") continue;
|
|
45
|
-
/** 消息 id(缺失则跳过该事件)。 */
|
|
46
|
-
const id = messageIdOfEvent(event);
|
|
47
|
-
if (!id) continue;
|
|
48
|
-
/** 本条消息在消息序列中的下标。 */
|
|
49
|
-
const index = messages.length;
|
|
50
|
-
messages.push({
|
|
51
|
-
seq,
|
|
52
|
-
id,
|
|
53
|
-
type: event.type
|
|
54
|
-
});
|
|
55
|
-
byId.set(id, index);
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
messages,
|
|
59
|
-
byId
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/logger.ts
|
|
64
|
-
/**
|
|
65
|
-
* 日志辅助:步骤级(debug)日志默认仅 dev(非 production)输出,便于开发调试;
|
|
66
|
-
* 失败等关键日志始终输出(不受 debug 开关影响)。
|
|
67
|
-
* 开关配置键 debug:true 强制开启、false 强制关闭,缺省按 NODE_ENV !== 'production' 判定
|
|
68
|
-
* (默认值由 resolveConfig 解析,见 config.ts)。
|
|
69
|
-
*/
|
|
70
|
-
/** 构建插件日志门面:统一加 PLUGIN_LABEL 前缀,step 按 debug 开关过滤。 */
|
|
71
|
-
function makeLogger(ctx, debug) {
|
|
72
|
-
return {
|
|
73
|
-
step(message) {
|
|
74
|
-
if (debug) ctx.logger.debug(`${PLUGIN_LABEL}: ${message}`);
|
|
75
|
-
},
|
|
76
|
-
info(message) {
|
|
77
|
-
ctx.logger.info(`${PLUGIN_LABEL}: ${message}`);
|
|
78
|
-
},
|
|
79
|
-
warn(message) {
|
|
80
|
-
ctx.logger.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
//#endregion
|
|
85
16
|
//#region src/utils.ts
|
|
86
17
|
/** 判断值是否为普通对象:typeof object 且非 null 且非数组(类型收窄用)。 */
|
|
87
18
|
function isRecord(value) {
|
|
@@ -153,6 +84,174 @@ function routedTarget(session) {
|
|
|
153
84
|
} catch {}
|
|
154
85
|
}
|
|
155
86
|
//#endregion
|
|
87
|
+
//#region src/log-index.ts
|
|
88
|
+
/**
|
|
89
|
+
* 查找 seq 在表层节点序列中的下标(不在则返回 -1)。
|
|
90
|
+
* 表层节点按日志顺序排列,用于压缩边界定位与遮蔽范围计算。
|
|
91
|
+
*/
|
|
92
|
+
function surfaceIndexOf(nodes, seq) {
|
|
93
|
+
for (let i = 0; i < nodes.length; i += 1) if (nodes[i] === seq) return i;
|
|
94
|
+
return -1;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(三类,见文件头)。
|
|
98
|
+
* 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
|
|
99
|
+
*/
|
|
100
|
+
function indexCompleteMessages(session) {
|
|
101
|
+
/** 完整消息序列(index = 数组下标,0 起)。 */
|
|
102
|
+
const cms = [];
|
|
103
|
+
/** 等待结果的 toolcall 完整消息(callId → cm)。 */
|
|
104
|
+
const pending = /* @__PURE__ */ new Map();
|
|
105
|
+
/** 会话全部事件(仅追加)。 */
|
|
106
|
+
const events = session.events;
|
|
107
|
+
for (let seq = 0; seq < events.length; seq += 1) {
|
|
108
|
+
/** 当前待检查事件。 */
|
|
109
|
+
const event = events[seq];
|
|
110
|
+
if (!event) continue;
|
|
111
|
+
if (event.type === "user/message") {
|
|
112
|
+
if (event.data.source?.kind === "plugin") continue;
|
|
113
|
+
cms.push({
|
|
114
|
+
index: cms.length,
|
|
115
|
+
type: "user",
|
|
116
|
+
seqs: [seq]
|
|
117
|
+
});
|
|
118
|
+
} else if (event.type === "assistant/message") {
|
|
119
|
+
/** 助手消息(含文本与 tool-call 块)。 */
|
|
120
|
+
const message = event.data.message;
|
|
121
|
+
if (!message || !Array.isArray(message.content)) continue;
|
|
122
|
+
/** 是否存在文本块(有则先产出 assistant 条)。 */
|
|
123
|
+
let hasText = false;
|
|
124
|
+
for (const block of message.content) if (block.type === "text") {
|
|
125
|
+
hasText = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (hasText) cms.push({
|
|
129
|
+
index: cms.length,
|
|
130
|
+
type: "assistant",
|
|
131
|
+
seqs: [seq]
|
|
132
|
+
});
|
|
133
|
+
for (const block of message.content) {
|
|
134
|
+
if (block.type !== "tool-call") continue;
|
|
135
|
+
/** 工具调用 id(匹配 result 用)。 */
|
|
136
|
+
const callId = String(block.id ?? "");
|
|
137
|
+
/** 本调用对应的完整消息(结果随后并入)。 */
|
|
138
|
+
const cm = {
|
|
139
|
+
index: cms.length,
|
|
140
|
+
type: "toolcall",
|
|
141
|
+
seqs: [seq],
|
|
142
|
+
...callId === "" ? {} : { callId }
|
|
143
|
+
};
|
|
144
|
+
cms.push(cm);
|
|
145
|
+
if (callId !== "") pending.set(callId, cm);
|
|
146
|
+
}
|
|
147
|
+
} else if (event.type === "tool/result") {
|
|
148
|
+
/** 结果消息的 source(callId 关联调用)。 */
|
|
149
|
+
const source = event.data.message?.source;
|
|
150
|
+
/** 关联的调用 id。 */
|
|
151
|
+
const callId = String(source?.callId ?? "");
|
|
152
|
+
/** 匹配到的 toolcall 完整消息(无则独立成条)。 */
|
|
153
|
+
const cm = callId === "" ? void 0 : pending.get(callId);
|
|
154
|
+
if (cm) {
|
|
155
|
+
cm.seqs.push(seq);
|
|
156
|
+
pending.delete(callId);
|
|
157
|
+
} else cms.push({
|
|
158
|
+
index: cms.length,
|
|
159
|
+
type: "toolcall",
|
|
160
|
+
seqs: [seq],
|
|
161
|
+
...callId === "" ? {} : { callId }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return cms;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 渲染一条完整消息的文本(recall 输出 / new 模式输入共用):
|
|
169
|
+
* - user:消息原文;
|
|
170
|
+
* - assistant:仅文本块;
|
|
171
|
+
* - toolcall:调用块(工具名 + 参数)+ 结果文本(pruner 裁剪超大结果)。
|
|
172
|
+
*/
|
|
173
|
+
function renderCompleteMessage(session, cm, pruner) {
|
|
174
|
+
if (cm.type === "user") {
|
|
175
|
+
/** 用户消息事件(seq 缺失则无法渲染)。 */
|
|
176
|
+
const seq = cm.seqs[0];
|
|
177
|
+
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
178
|
+
/** 派生的消息对象。 */
|
|
179
|
+
const message = event ? session.deriveEventMessage(event) : null;
|
|
180
|
+
return message ? renderMessageText(message) : "";
|
|
181
|
+
}
|
|
182
|
+
if (cm.type === "assistant") {
|
|
183
|
+
/** 助手消息事件(seq 缺失则无法渲染)。 */
|
|
184
|
+
const seq = cm.seqs[0];
|
|
185
|
+
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
186
|
+
/** 派生的消息对象。 */
|
|
187
|
+
const message = event ? session.deriveEventMessage(event) : null;
|
|
188
|
+
if (!message || !Array.isArray(message.content)) return "";
|
|
189
|
+
/** 文本块拼接缓冲。 */
|
|
190
|
+
const texts = [];
|
|
191
|
+
for (const block of message.content) if (block.type === "text") texts.push(String(block.text));
|
|
192
|
+
return texts.join("\n");
|
|
193
|
+
}
|
|
194
|
+
/** 渲染缓冲。 */
|
|
195
|
+
const parts = [];
|
|
196
|
+
/** 承载调用块的 assistant 事件(seq 缺失则跳过调用块)。 */
|
|
197
|
+
const callSeq = cm.seqs[0];
|
|
198
|
+
const callEvent = callSeq === void 0 ? void 0 : session.events[callSeq];
|
|
199
|
+
if (callEvent?.type === "assistant/message") {
|
|
200
|
+
/** 助手消息对象(取对应调用块)。 */
|
|
201
|
+
const message = session.deriveEventMessage(callEvent);
|
|
202
|
+
if (message && Array.isArray(message.content)) {
|
|
203
|
+
/** 匹配的 tool-call 块。 */
|
|
204
|
+
let call;
|
|
205
|
+
for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) {
|
|
206
|
+
call = block;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (call) parts.push(`[tool-call ${String(call.name ?? "")} id=${String(call.id ?? "")}]\n${safeJson(call.arguments)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** 结果事件 seq(无则调用未闭合)。 */
|
|
213
|
+
const resultSeq = cm.seqs[1];
|
|
214
|
+
const resultEvent = resultSeq === void 0 ? void 0 : session.events[resultSeq];
|
|
215
|
+
if (resultEvent?.type === "tool/result") {
|
|
216
|
+
/** 结果消息对象(pruner 裁剪超大内容后渲染)。 */
|
|
217
|
+
let message = session.deriveEventMessage(resultEvent);
|
|
218
|
+
if (message && pruner?.pruneContent) {
|
|
219
|
+
/** 裁剪后的内容块。 */
|
|
220
|
+
const pruned = pruner.pruneContent(message.content);
|
|
221
|
+
if (pruned) message = {
|
|
222
|
+
...message,
|
|
223
|
+
content: pruned
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** 结果文本。 */
|
|
227
|
+
const text = message ? renderMessageText(message) : "";
|
|
228
|
+
if (text.trim() !== "") parts.push(`[result]\n${text}`);
|
|
229
|
+
}
|
|
230
|
+
return parts.join("\n");
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/logger.ts
|
|
234
|
+
/**
|
|
235
|
+
* 日志辅助:步骤级(debug)日志默认仅 dev(非 production)输出,便于开发调试;
|
|
236
|
+
* 失败等关键日志始终输出(不受 debug 开关影响)。
|
|
237
|
+
* 开关配置键 debug:true 强制开启、false 强制关闭,缺省按 NODE_ENV !== 'production' 判定
|
|
238
|
+
* (默认值由 resolveConfig 解析,见 config.ts)。
|
|
239
|
+
*/
|
|
240
|
+
/** 构建插件日志门面:统一加 PLUGIN_LABEL 前缀,step 按 debug 开关过滤。 */
|
|
241
|
+
function makeLogger(ctx, debug) {
|
|
242
|
+
return {
|
|
243
|
+
step(message) {
|
|
244
|
+
if (debug) ctx.logger.debug(`${PLUGIN_LABEL}: ${message}`);
|
|
245
|
+
},
|
|
246
|
+
info(message) {
|
|
247
|
+
ctx.logger.info(`${PLUGIN_LABEL}: ${message}`);
|
|
248
|
+
},
|
|
249
|
+
warn(message) {
|
|
250
|
+
ctx.logger.warn(`${PLUGIN_LABEL}: ${message}`);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
156
255
|
//#region src/summarize.ts
|
|
157
256
|
/** 观察者 persona:只针对未压缩消息产出观察日志,不用工具、不展示思考。 */
|
|
158
257
|
const OBSERVER_PERSONA = "你是 dsh-plugin-om 的上下文观察者(Observer,机制参考 Mastra Observational Memory):把会话中尚未压缩的消息压缩为一份观察日志。不用工具、不展示思考、不评价代码、不输出多余文字。";
|
|
@@ -161,14 +260,12 @@ const REFLECTOR_PERSONA = "你是 dsh-plugin-om 的上下文反思者(Reflecto
|
|
|
161
260
|
/**
|
|
162
261
|
* 构建观察指令主体:任务声明(fork:停止任务/禁止工具;new:说明总结日志)+ 模式相关
|
|
163
262
|
* 的上下文定位与压缩范围(fork:上方完整会话记录——尾部已在输入中实际截断,提示词不含
|
|
164
|
-
* 尾部规则;new:下方消息即压缩对象)+
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
263
|
+
* 尾部规则;new:下方消息即压缩对象)+ 完整消息与 index 定义(三类合并规则、起始编号)+
|
|
264
|
+
* 规则(用户消息完整保留原文 / AI 消息模块化压缩——工具调用按目的聚合——不限于 run_code /
|
|
265
|
+
* 倾向于新消息 / 中断标注 / 未完成写进度与下一步)+ 输出格式(合法 XML)+ 中断标记 +
|
|
266
|
+
* 追加说明。persona 由调用方拼接到指令开头。
|
|
168
267
|
*/
|
|
169
268
|
function buildObservePrompt(options) {
|
|
170
|
-
/** 对照表段落(无则标注「无」)。 */
|
|
171
|
-
const tableSection = options.table.length > 0 ? options.table : ["(无)"];
|
|
172
269
|
/** 中断标记段落(无则标注「无」)。 */
|
|
173
270
|
const interruptionSection = options.interruptions.length > 0 ? options.interruptions : ["(无)"];
|
|
174
271
|
return [
|
|
@@ -180,25 +277,30 @@ function buildObservePrompt(options) {
|
|
|
180
277
|
`如果消息记录里还没有 <${HISTORY_TAG}> 块,则除本指令外的全部消息都是压缩对象。`
|
|
181
278
|
] : ["下方的消息记录是本次要压缩的全部消息(上一个 <om-history> 块之后的新消息;不含旧压缩日志、不含尾部)。", "你的压缩结果会作为新的 <om-history> 块追加到已有压缩日志之后。"],
|
|
182
279
|
"",
|
|
280
|
+
"【完整消息与 index】",
|
|
281
|
+
"- 完整消息是压缩日志与 recall 共用的定位单位,每一条占一个 index(从 0 起、按会话顺序递增、全局稳定)。完整消息分三类:用户消息占一条;AI 文本占一条;工具调用及其结果占一条(每个 tool-call 与其 result 各一条,同一条 AI 消息里的文本与工具调用拆开)。",
|
|
282
|
+
`- 本次要压缩的新消息从 index ${options.startIndex} 开始编号,按日志顺序逐条递增;旧 <${HISTORY_TAG}> 中已有的 index 无需理会,也不要改动旧条目。`,
|
|
283
|
+
"",
|
|
183
284
|
"【规则】",
|
|
184
|
-
"- 用户消息完整保留原文,输出为 <user_message> 条目(
|
|
185
|
-
"- AI
|
|
285
|
+
"- 用户消息完整保留原文,输出为 <user_message index=\"N\"> 条目(N 为该条完整消息的 index,内容为消息原文,不概括、不省略)。",
|
|
286
|
+
"- AI 消息划分模块压缩(按模块聚合 assistant 的方式):所有工具调用(run_code 与其他工具同等对待,不限于 run_code)按调用目的聚合为一行 toolcall index:<该条完整消息的 index> purpose:<聚合目的> summary:<行为与结果摘要>;相邻的 AI 侧完整消息(文本与工具调用)聚合为一个 <assistant> 模块,start/end 标注模块覆盖的 index 区间(start 为首条 index、end 为末条 index);重要工具调用(关键决策、写文件等)单独用 <assistant index=\"N\"> 呈现。工具组内部细节(参数、完整输出)不保留,需要原文时用 recall 按 index 回看(start/end 定位)。",
|
|
287
|
+
"- 条目须按 index 顺序覆盖本次压缩的全部完整消息,不遗漏。",
|
|
186
288
|
"- 总结时倾向于新消息,旧消息一句话带过即可;新旧消息冲突时强调新消息,不修改旧日志条目。",
|
|
187
289
|
"- 若【中断标记】非空,在对应位置明确写出中断(例如「被用户打断,因此上一段工作未完成」),帮助后续理解用户为何再次输入消息、为何不延续之前的工作。",
|
|
188
290
|
"- 若当前工作看起来未完成(最后一次工具调用没有结果、或对话被中断/异常结束),在日志末尾说明当前进度与下一步要做什么。",
|
|
189
291
|
"【输出格式】只输出一个 <om-history> 包裹的合法 XML 日志块,不要解释、不要复述规则:",
|
|
190
292
|
`<${HISTORY_TAG}>`,
|
|
191
|
-
"<user_message
|
|
293
|
+
"<user_message index=\"(index)\">",
|
|
192
294
|
"(user 消息原文)",
|
|
193
295
|
"</user_message>",
|
|
194
|
-
"<assistant
|
|
195
|
-
"(压缩模块:toolcall
|
|
296
|
+
"<assistant start=\"(起始 index)\" end=\"(结束 index)\">",
|
|
297
|
+
"(压缩模块:toolcall 聚合行等,按模块聚合 assistant 的方式)",
|
|
298
|
+
"</assistant>",
|
|
299
|
+
"<assistant index=\"(index)\">",
|
|
300
|
+
"(重要调用单独条目)",
|
|
196
301
|
"</assistant>",
|
|
197
302
|
`</${HISTORY_TAG}>`,
|
|
198
303
|
"",
|
|
199
|
-
"【message_id 对照表】(按顺序对应消息记录中的未压缩消息,用于产出正确的 message_id)",
|
|
200
|
-
...tableSection,
|
|
201
|
-
"",
|
|
202
304
|
"【中断标记】",
|
|
203
305
|
...interruptionSection,
|
|
204
306
|
"",
|
|
@@ -214,17 +316,22 @@ function buildReflectPrompt(mode) {
|
|
|
214
316
|
...mode === "new" ? ["下方的消息记录包含当前的 <om-history> 压缩日志(最后一次 <om-history> 块)。", "只对这份压缩日志做精简合并;不要涉及日志之外的消息。"] : ["上方的消息记录是主会话的完整历史,其中包含当前的 <om-history> 压缩日志(最后一次 <om-history> 块)。", "只对这份压缩日志做精简合并;不要涉及日志之外的消息。"],
|
|
215
317
|
"",
|
|
216
318
|
"【规则】",
|
|
217
|
-
"-
|
|
218
|
-
"-
|
|
319
|
+
"- 完整消息是压缩日志的定位单位(用户消息 / AI 文本 / 工具调用及其结果各占一个 index,从 0 起、全局稳定);合并时单个完整消息的 index 不重新编号,被删除的条目 index 空缺。",
|
|
320
|
+
"- 用户消息保留要点与 index(格式:<user_message index=\"(index)\"> 要点 </user_message>);可省略的条目删除。",
|
|
321
|
+
"- toolcall 条目按调用目的进一步聚合,保留 index;不重要的条目 summary 写「(略)」。",
|
|
322
|
+
"- 相邻的 assistant 条目可合并为一个 <assistant> 模块,start/end 随合并后的区间变化。",
|
|
219
323
|
"- 保留中断说明与未完成说明(若原日志中有)。",
|
|
220
324
|
"- 过时事实丢弃,不逐字复制旧文本;新旧条目冲突时保留新条目。",
|
|
221
325
|
"【输出格式】只输出一个 <om-history> 包裹的合法 XML 日志块,不要解释、不要复述规则:",
|
|
222
326
|
`<${HISTORY_TAG}>`,
|
|
223
|
-
"<user_message
|
|
327
|
+
"<user_message index=\"(index)\">",
|
|
224
328
|
"(user 消息要点)",
|
|
225
329
|
"</user_message>",
|
|
226
|
-
"<assistant
|
|
227
|
-
"(压缩模块:toolcall
|
|
330
|
+
"<assistant start=\"(起始 index)\" end=\"(结束 index)\">",
|
|
331
|
+
"(压缩模块:toolcall 聚合行等,按模块聚合 assistant 的方式)",
|
|
332
|
+
"</assistant>",
|
|
333
|
+
"<assistant index=\"(index)\">",
|
|
334
|
+
"(重要调用单独条目)",
|
|
228
335
|
"</assistant>",
|
|
229
336
|
`</${HISTORY_TAG}>`,
|
|
230
337
|
"",
|
|
@@ -265,50 +372,25 @@ var StreamCollector = class {
|
|
|
265
372
|
}
|
|
266
373
|
};
|
|
267
374
|
/**
|
|
268
|
-
*
|
|
269
|
-
* -
|
|
270
|
-
* -
|
|
271
|
-
*
|
|
272
|
-
* 组内文本沿用 role 头 + message_id + 文本(tool-call 展开参数、tool-result 取文本)。
|
|
375
|
+
* 渲染完整消息记录(new 模式输入):按完整消息渲染为合法 XML,每条带绝对 index——
|
|
376
|
+
* - user → <user_message index="N">(原文)</user_message>;
|
|
377
|
+
* - assistant / toolcall → <assistant index="N">(文本 / 调用参数+结果)</assistant>。
|
|
378
|
+
* 仅渲染 seqs 全部落在给定集合内的完整消息(插件自产消息天然不占位)。
|
|
273
379
|
*/
|
|
274
380
|
function renderMessages(session, seqs) {
|
|
381
|
+
/** 遮蔽 seq 集合。 */
|
|
382
|
+
const shadowed = new Set(seqs);
|
|
275
383
|
/** 渲染段缓冲区。 */
|
|
276
384
|
const parts = [];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
/**
|
|
283
|
-
const
|
|
284
|
-
parts.push(
|
|
285
|
-
group = void 0;
|
|
286
|
-
};
|
|
287
|
-
for (const seq of seqs) {
|
|
288
|
-
/** 当前待渲染事件。 */
|
|
289
|
-
const event = session.events[seq];
|
|
290
|
-
if (!event) continue;
|
|
291
|
-
/** 消息 id(缺失则省略)。 */
|
|
292
|
-
const id = messageIdOfEvent(event);
|
|
293
|
-
/** 消息文本呈现。 */
|
|
294
|
-
const text = renderMessageText(session.deriveEventMessage(event));
|
|
295
|
-
if (event.type === "user/message") {
|
|
296
|
-
flush();
|
|
297
|
-
/** id 属性(缺失/空串则省略)。 */
|
|
298
|
-
const idAttr = id ? ` id="${id}"` : "";
|
|
299
|
-
parts.push(`<user_message${idAttr}>\n${text}\n</user_message>`);
|
|
300
|
-
} else if (event.type === "assistant/message" || event.type === "tool/result") {
|
|
301
|
-
if (!group) group = {
|
|
302
|
-
lines: [],
|
|
303
|
-
lastId: void 0
|
|
304
|
-
};
|
|
305
|
-
/** role 标签(tool/result 属 user 角色但标注为工具结果)。 */
|
|
306
|
-
const role = event.type === "assistant/message" ? "assistant" : "tool/result";
|
|
307
|
-
group.lines.push(`--- ${role}${id ? ` message_id=${id}` : ""} ---\n${text}`);
|
|
308
|
-
if (id) group.lastId = id;
|
|
309
|
-
}
|
|
385
|
+
for (const cm of indexCompleteMessages(session)) {
|
|
386
|
+
if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
|
|
387
|
+
/** 该条完整消息的文本(空内容跳过)。 */
|
|
388
|
+
const text = renderCompleteMessage(session, cm);
|
|
389
|
+
if (text.trim() === "") continue;
|
|
390
|
+
/** 标签(user 与 AI 侧分属两种条目)。 */
|
|
391
|
+
const tag = cm.type === "user" ? "user_message" : "assistant";
|
|
392
|
+
parts.push(`<${tag} index="${cm.index}">\n${text}\n</${tag}>`);
|
|
310
393
|
}
|
|
311
|
-
flush();
|
|
312
394
|
return parts.join("\n\n");
|
|
313
395
|
}
|
|
314
396
|
/** 构造插件自产 user 消息(指令或 new 模式的输入消息;id 为品牌类型 MessageId)。 */
|
|
@@ -360,7 +442,7 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, mode,
|
|
|
360
442
|
};
|
|
361
443
|
}
|
|
362
444
|
/** 产出日志后插入首个 <om-history> 后的格式说明(XML 注释,避免被误读为日志条目)。 */
|
|
363
|
-
const HISTORY_FORMAT_NOTE = "<!--
|
|
445
|
+
const HISTORY_FORMAT_NOTE = "<!-- 本日志以「完整消息」为定位单位:完整消息分三类(用户消息 / AI 文本 / 单个工具调用及其结果),各占一个 index(从 0 起、会话内全局稳定);<user_message index=\"N\"> 与 <assistant index=\"N\"> 定位单条完整消息,<assistant start=\"A\" end=\"B\"> 定位连续区间(含两端)。需要原文时用 recall(start, end) 按 index 回看。 -->";
|
|
364
446
|
/**
|
|
365
447
|
* 从 AI 摘要输出中提取合法日志(不信任 AI 的总结结果):
|
|
366
448
|
* - 取首个 <om-history> 到最后一个 </om-history>(含两个首尾)切为日志;
|
|
@@ -457,6 +539,8 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
457
539
|
* 表层长度-1-tailCount(尾部保留 config.tailMessageCount 条不压缩,作为摘要模型的
|
|
458
540
|
* 参考尾部),当前 turn 中已完备的消息同样可压缩;区间终点回退到 tool-call/result
|
|
459
541
|
* 配对平衡点(不切段)。
|
|
542
|
+
* 观察摘要的条目用「完整消息」index 定位(三类定义见 log-index.ts):新消息起始 index
|
|
543
|
+
* 由插件从日志计算后注入提示词,new 模式输入按完整消息渲染绝对 index。
|
|
460
544
|
* 仅主会话生效。
|
|
461
545
|
*/
|
|
462
546
|
/** 历史文本 token 估算:4 字符 ≈ 1 token(与宿主 dsh-token-meter 启发式一致)。 */
|
|
@@ -602,39 +686,6 @@ function extractHistoryText(session, shadowedSeqs) {
|
|
|
602
686
|
}
|
|
603
687
|
return found;
|
|
604
688
|
}
|
|
605
|
-
/**
|
|
606
|
-
* message_id 对照表:遮蔽区间内消息事件按表层顺序产出 id 行(插件自产 user/message
|
|
607
|
-
* 如运行时上下文快照与 <om-history> 不入表;观察摘要据此产出正确的 message_id)。
|
|
608
|
-
* 按表层顺序(shadowedSeqs)扫描:与 extractHistoryText 同理,seq 区间扫描会漏。
|
|
609
|
-
*/
|
|
610
|
-
function buildMessageIdTable(session, shadowedSeqs) {
|
|
611
|
-
/** 对照表行缓冲区。 */
|
|
612
|
-
const rows = [];
|
|
613
|
-
for (const seq of shadowedSeqs) {
|
|
614
|
-
/** 当前待检查事件。 */
|
|
615
|
-
const event = session.events[seq];
|
|
616
|
-
if (!event) continue;
|
|
617
|
-
if (event.type === "user/message") {
|
|
618
|
-
if (event.data.source?.kind === "plugin") continue;
|
|
619
|
-
/** 用户消息 id。 */
|
|
620
|
-
const id = messageIdOfEvent(event);
|
|
621
|
-
if (id) rows.push(`[user] message_id=${id}`);
|
|
622
|
-
} else if (event.type === "assistant/message") {
|
|
623
|
-
/** 助手消息 id。 */
|
|
624
|
-
const id = messageIdOfEvent(event);
|
|
625
|
-
if (id) rows.push(`[assistant] message_id=${id}`);
|
|
626
|
-
} else if (event.type === "tool/result") {
|
|
627
|
-
/** 结果消息 id。 */
|
|
628
|
-
const id = messageIdOfEvent(event);
|
|
629
|
-
if (id) {
|
|
630
|
-
/** 关联调用 id(供摘要模型按 callId 定位代码与结果)。 */
|
|
631
|
-
const callId = String(event.data.message.source.callId ?? "");
|
|
632
|
-
rows.push(`[tool/result callId=${callId}] message_id=${id}`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return rows;
|
|
637
|
-
}
|
|
638
689
|
/** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null(跨轮次场景)。 */
|
|
639
690
|
function openTurnOf(session) {
|
|
640
691
|
/** 折叠结果(turn/start 打开、turn/end 关闭)。 */
|
|
@@ -814,19 +865,21 @@ async function observePass(ctx, agent, config, window, tailCount, target, signal
|
|
|
814
865
|
const history = extractHistoryText(session, range.shadowedSeqs);
|
|
815
866
|
/** 中断标记行。 */
|
|
816
867
|
const interruptions = scanInterruptions(session, Math.min(...range.shadowedSeqs), range.lastEndSeq);
|
|
817
|
-
/** message_id 对照表行。 */
|
|
818
|
-
const table = buildMessageIdTable(session, range.shadowedSeqs);
|
|
819
868
|
const actualTailCount = [...session.surface.nodes].length - range.shadowedSeqs.length;
|
|
820
|
-
|
|
869
|
+
/** 遮蔽 seq 集合(计算新消息起始 index)。 */
|
|
870
|
+
const shadowedSet = new Set(range.shadowedSeqs);
|
|
871
|
+
/** 压缩区间内第一个完整消息的 index(新消息起始编号;区间内无完整消息则 0)。 */
|
|
872
|
+
const startIndex = indexCompleteMessages(session).find((cm) => cm.seqs.every((seq) => shadowedSet.has(seq)))?.index ?? 0;
|
|
873
|
+
logger.step(`观察:实际保留尾部 ${actualTailCount} 条(不压缩、不进日志),中断标记 ${interruptions.length} 条,新消息起始 index ${startIndex}`);
|
|
821
874
|
/** 观察指令(persona + 规则主体)。 */
|
|
822
875
|
const prompt = buildObservePrompt({
|
|
823
|
-
|
|
876
|
+
startIndex,
|
|
824
877
|
interruptions,
|
|
825
878
|
hasOldHistory: history !== void 0,
|
|
826
879
|
mode: config.summaryMode
|
|
827
880
|
});
|
|
828
881
|
/** 观察摘要结果(null 表示失败/跳过)。 */
|
|
829
|
-
const summaryResult = await runSummarySubagent(ctx, agent, `${OBSERVER_PERSONA}\n\n${prompt}`, config.summaryMode === "new" ? renderMessages(session, range.shadowedSeqs
|
|
882
|
+
const summaryResult = await runSummarySubagent(ctx, agent, `${OBSERVER_PERSONA}\n\n${prompt}`, config.summaryMode === "new" ? renderMessages(session, range.shadowedSeqs) : void 0, config.compressMaxTokens, config.summaryMode, actualTailCount, target, config.debug, signal);
|
|
830
883
|
if (summaryResult === null || summaryResult.text.trim().length === 0) {
|
|
831
884
|
logger.step("观察:摘要调用失败/无输出,不产生替换");
|
|
832
885
|
return;
|
|
@@ -1211,22 +1264,24 @@ function resolveConfig(raw) {
|
|
|
1211
1264
|
//#endregion
|
|
1212
1265
|
//#region src/recall.ts
|
|
1213
1266
|
/**
|
|
1214
|
-
* recall
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1267
|
+
* recall 工具:按「完整消息」序号(index)回看原始会话(start/end 为完整消息 index,
|
|
1268
|
+
* offset 为相对 start 的完整消息步数)。完整消息分三类(用户消息 / AI 文本 / 单个工具
|
|
1269
|
+
* 调用及其结果),各占一个 index(从 0 起、会话内全局稳定),与摘要日志条目同一套编号。
|
|
1270
|
+
* recall 自身不设输出上限:超大的工具结果由 tool-result-pruner 裁剪(pruneContent),
|
|
1271
|
+
* 输出 token 由 pruner 配置控制。
|
|
1217
1272
|
*
|
|
1218
|
-
* 参数由 zod schema(recallArgsSchema)在 execute 入口校验:
|
|
1219
|
-
*
|
|
1273
|
+
* 参数由 zod schema(recallArgsSchema)在 execute 入口校验:start 必填(number),
|
|
1274
|
+
* end/offset 至少提供一个;非法参数抛出可读错误。
|
|
1220
1275
|
*/
|
|
1221
1276
|
/**
|
|
1222
|
-
* recall 工具参数 schema:
|
|
1223
|
-
* (二者同时给出时
|
|
1277
|
+
* recall 工具参数 schema:start 必填(number);end 与 offset 至少提供一个
|
|
1278
|
+
* (二者同时给出时 end 优先,与 execute 语义一致);未知键自动剥离。
|
|
1224
1279
|
*/
|
|
1225
1280
|
const recallArgsSchema = z.object({
|
|
1226
|
-
|
|
1227
|
-
|
|
1281
|
+
start: z.number(),
|
|
1282
|
+
end: z.number().optional(),
|
|
1228
1283
|
offset: z.number().optional()
|
|
1229
|
-
}).refine((args) => args.
|
|
1284
|
+
}).refine((args) => args.end !== void 0 || args.offset !== void 0, { message: "end 与 offset 至少提供一个" });
|
|
1230
1285
|
/**
|
|
1231
1286
|
* 解析并校验 recall 调用参数:校验失败时抛出首个校验问题的可读消息
|
|
1232
1287
|
* (普通 Error 而非 ZodError,兼容 SDK 展示)。
|
|
@@ -1238,26 +1293,26 @@ function parseRecallArgs(raw) {
|
|
|
1238
1293
|
return result.data;
|
|
1239
1294
|
}
|
|
1240
1295
|
/**
|
|
1241
|
-
* 构建 recall 工具定义:按
|
|
1296
|
+
* 构建 recall 工具定义:按 start/end/offset 定位完整消息区间并渲染原始内容(标 index)。
|
|
1242
1297
|
* getPruner 返回 tool-result-pruner(可选),用于裁剪超大工具结果。
|
|
1243
1298
|
*/
|
|
1244
1299
|
function buildRecallTool(getPruner) {
|
|
1245
1300
|
return {
|
|
1246
1301
|
name: "recall",
|
|
1247
|
-
description: "
|
|
1302
|
+
description: "根据完整消息序号(index),回看指定区间的过往消息。完整消息是定位单位,分三类:用户消息、AI 文本、单个工具调用及其结果(每个 tool-call 与其 result 一条),各占一个 index(从 0 起、会话内全局稳定)。start 必须传入,是区间的基准;end 和 offset 二选一:end 指定另一个边界(含两端,与 start 的位置关系不影响结果),offset 指定区间包含的完整消息数量(正数向后、负数向前)。",
|
|
1248
1303
|
parameters: {
|
|
1249
|
-
|
|
1250
|
-
type: "
|
|
1251
|
-
description: "
|
|
1304
|
+
start: {
|
|
1305
|
+
type: "number",
|
|
1306
|
+
description: "完整消息序号(index),区间的基准边界",
|
|
1252
1307
|
required: true
|
|
1253
1308
|
},
|
|
1254
|
-
|
|
1255
|
-
type: "
|
|
1256
|
-
description: "
|
|
1309
|
+
end: {
|
|
1310
|
+
type: "number",
|
|
1311
|
+
description: "与 offset 互斥,指定区间的另一个边界(含)。与 start 的位置关系不影响结果。"
|
|
1257
1312
|
},
|
|
1258
1313
|
offset: {
|
|
1259
1314
|
type: "number",
|
|
1260
|
-
description: "与
|
|
1315
|
+
description: "与 end 互斥,指定区间包含的完整消息数量。传入正数查看 start 之后的若干条,负数则是之前的。"
|
|
1261
1316
|
}
|
|
1262
1317
|
},
|
|
1263
1318
|
output: {
|
|
@@ -1269,23 +1324,23 @@ function buildRecallTool(getPruner) {
|
|
|
1269
1324
|
},
|
|
1270
1325
|
async execute(args, exec) {
|
|
1271
1326
|
/** 解析并校验后的调用参数(不满足 schema 时抛出可读错误)。 */
|
|
1272
|
-
const {
|
|
1327
|
+
const { start, end, offset } = parseRecallArgs(args);
|
|
1273
1328
|
/** 当前会话(缺失则无法回看)。 */
|
|
1274
1329
|
const session = exec.agent?.session;
|
|
1275
1330
|
if (!session) return "会话异常";
|
|
1276
1331
|
if (!isMainSession(session)) return "recall 仅主会话可用";
|
|
1277
|
-
/**
|
|
1278
|
-
const
|
|
1279
|
-
/**
|
|
1280
|
-
const startIndex =
|
|
1281
|
-
if (startIndex
|
|
1282
|
-
/** 终点下标(
|
|
1332
|
+
/** 完整消息索引(index = 数组下标,0 起)。 */
|
|
1333
|
+
const cms = indexCompleteMessages(session);
|
|
1334
|
+
/** 起始下标(非整数 floor;越界返回提示)。 */
|
|
1335
|
+
const startIndex = Number.isFinite(start) ? Math.floor(start) : 0;
|
|
1336
|
+
if (startIndex < 0 || startIndex >= cms.length) return `start ${start} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1})`;
|
|
1337
|
+
/** 终点下标(end 优先,否则 startIndex + offset)。 */
|
|
1283
1338
|
let endIndex;
|
|
1284
|
-
if (
|
|
1285
|
-
/**
|
|
1286
|
-
const
|
|
1287
|
-
if (
|
|
1288
|
-
endIndex =
|
|
1339
|
+
if (end !== void 0) {
|
|
1340
|
+
/** end 取整(非整数 floor)。 */
|
|
1341
|
+
const e = Number.isFinite(end) ? Math.floor(end) : 0;
|
|
1342
|
+
if (e < 0 || e >= cms.length) return `end ${end} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1})`;
|
|
1343
|
+
endIndex = e;
|
|
1289
1344
|
} else {
|
|
1290
1345
|
/** offset 数值(refine 保证此分支 offset 已提供,schema 保证为 number)。 */
|
|
1291
1346
|
const raw = offset ?? 0;
|
|
@@ -1294,36 +1349,25 @@ function buildRecallTool(getPruner) {
|
|
|
1294
1349
|
/** 区间下界(钳制到 [0, len-1])。 */
|
|
1295
1350
|
const lo = Math.max(0, Math.min(startIndex, endIndex));
|
|
1296
1351
|
/** 区间上界(钳制到 [0, len-1])。 */
|
|
1297
|
-
const hi = Math.min(
|
|
1352
|
+
const hi = Math.min(cms.length - 1, Math.max(startIndex, endIndex));
|
|
1298
1353
|
/** tool-result-pruner(可选,裁剪超大工具结果)。 */
|
|
1299
1354
|
const pruner = getPruner?.();
|
|
1300
|
-
/**
|
|
1355
|
+
/** 渲染结果缓冲(每条完整消息一段,标 index + 类型)。 */
|
|
1301
1356
|
const parts = [];
|
|
1302
1357
|
for (let i = lo; i <= hi; i += 1) {
|
|
1303
|
-
/**
|
|
1304
|
-
const
|
|
1305
|
-
if (!
|
|
1306
|
-
/**
|
|
1307
|
-
const event = session.events[node.seq];
|
|
1308
|
-
if (!event) continue;
|
|
1309
|
-
/** 本条消息的呈现文本。 */
|
|
1358
|
+
/** 当前完整消息。 */
|
|
1359
|
+
const cm = cms[i];
|
|
1360
|
+
if (!cm) continue;
|
|
1361
|
+
/** 该条完整消息的呈现文本(pruner 裁剪超大结果;单条失败不影响整体)。 */
|
|
1310
1362
|
let text = "";
|
|
1311
1363
|
try {
|
|
1312
|
-
|
|
1313
|
-
let message = session.deriveEventMessage(event);
|
|
1314
|
-
if (message && event.type === "tool/result" && pruner?.pruneContent) {
|
|
1315
|
-
/** 裁剪后的内容块(返回 null 表示不裁剪)。 */
|
|
1316
|
-
const pruned = pruner.pruneContent(message.content);
|
|
1317
|
-
if (pruned) message = {
|
|
1318
|
-
...message,
|
|
1319
|
-
content: pruned
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
text = message ? renderMessageText(message) : "";
|
|
1364
|
+
text = renderCompleteMessage(session, cm, pruner);
|
|
1323
1365
|
} catch {}
|
|
1324
|
-
|
|
1366
|
+
/** 类型标注(toolcall 附调用 id,便于模型关联)。 */
|
|
1367
|
+
const callAttr = cm.type === "toolcall" && cm.callId ? ` callId=${cm.callId}` : "";
|
|
1368
|
+
parts.push(`-- [index ${cm.index}] ${cm.type}${callAttr} --\n${text}`);
|
|
1325
1369
|
}
|
|
1326
|
-
if (parts.length === 0) return "
|
|
1370
|
+
if (parts.length === 0) return "指定区间没有完整消息";
|
|
1327
1371
|
return parts.join("\n\n");
|
|
1328
1372
|
}
|
|
1329
1373
|
};
|
|
@@ -1331,15 +1375,15 @@ function buildRecallTool(getPruner) {
|
|
|
1331
1375
|
//#endregion
|
|
1332
1376
|
//#region src/semantic-recall.ts
|
|
1333
1377
|
/**
|
|
1334
|
-
* recall-semantic 工具:按自然语言 query
|
|
1335
|
-
*
|
|
1378
|
+
* recall-semantic 工具:按自然语言 query 对会话全部完整消息(含被压缩/遮蔽的)做语义
|
|
1379
|
+
* 检索,返回最匹配的完整消息与匹配说明。
|
|
1336
1380
|
*
|
|
1337
|
-
* - 参数:query 必填;top_k(默认 3,1-10);
|
|
1338
|
-
* (意义同 recall:
|
|
1339
|
-
* - 区间缺省(
|
|
1381
|
+
* - 参数:query 必填;top_k(默认 3,1-10);start/end/offset 限定检索区间
|
|
1382
|
+
* (意义同 recall:start 为基准边界,end 与 offset 二选一;end 优先)。
|
|
1383
|
+
* - 区间缺省(start 未提供)→ 检索全部消息;区间不合法(start/end 越界等)→
|
|
1340
1384
|
* 不报错,回退全量检索并在输出中明确告知(模型可见)。
|
|
1341
1385
|
* - 向量:本地 ONNX embedding(embedding.ts,懒加载 + 批量);相似度 = cosine。
|
|
1342
|
-
* - 输出:命中消息的
|
|
1386
|
+
* - 输出:命中消息的 index/类型(toolcall 附调用 id)+ 完整渲染文本 + 匹配说明(相似度、
|
|
1343
1387
|
* 命中的关键词);超大结果由 tool-result-pruner 裁剪(同 recall)。
|
|
1344
1388
|
* - 仅主会话可用(subagent 拒绝,与 recall 一致)。
|
|
1345
1389
|
*/
|
|
@@ -1347,8 +1391,8 @@ function buildRecallTool(getPruner) {
|
|
|
1347
1391
|
const semanticRecallArgsSchema = z.object({
|
|
1348
1392
|
query: z.string(),
|
|
1349
1393
|
top_k: z.number().int().min(1).max(10).optional(),
|
|
1350
|
-
|
|
1351
|
-
|
|
1394
|
+
start: z.number().optional(),
|
|
1395
|
+
end: z.number().optional(),
|
|
1352
1396
|
offset: z.number().optional()
|
|
1353
1397
|
}).refine((args) => args.query.trim().length > 0, { message: "query 不能为空" });
|
|
1354
1398
|
/** 解析并校验参数:失败时抛出可读错误(普通 Error 而非 ZodError)。 */
|
|
@@ -1359,36 +1403,35 @@ function parseSemanticRecallArgs(raw) {
|
|
|
1359
1403
|
return result.data;
|
|
1360
1404
|
}
|
|
1361
1405
|
/**
|
|
1362
|
-
*
|
|
1363
|
-
* -
|
|
1364
|
-
* -
|
|
1365
|
-
*
|
|
1406
|
+
* 解析检索区间(完整消息 index,含端点)。
|
|
1407
|
+
* - start 未提供 → 全量(fallback=false)。
|
|
1408
|
+
* - start 提供:end/offset 计算终点(语义同 recall,end 优先);
|
|
1409
|
+
* start/end 越界或区间为空 → fallback=true(全量 + 提示)。
|
|
1366
1410
|
*/
|
|
1367
|
-
function resolveSemanticRange(
|
|
1368
|
-
|
|
1369
|
-
if (total === 0) return {
|
|
1411
|
+
function resolveSemanticRange(total, args) {
|
|
1412
|
+
if (total <= 0) return {
|
|
1370
1413
|
lo: 0,
|
|
1371
1414
|
hi: -1,
|
|
1372
1415
|
fallback: false
|
|
1373
1416
|
};
|
|
1374
|
-
if (args.
|
|
1417
|
+
if (args.start === void 0) return {
|
|
1375
1418
|
lo: 0,
|
|
1376
1419
|
hi: total - 1,
|
|
1377
1420
|
fallback: false
|
|
1378
1421
|
};
|
|
1379
|
-
/**
|
|
1380
|
-
const start =
|
|
1381
|
-
if (start
|
|
1422
|
+
/** start 取整(非有限数按 -1 处理,走越界回退)。 */
|
|
1423
|
+
const start = Number.isFinite(args.start) ? Math.floor(args.start) : -1;
|
|
1424
|
+
if (start < 0 || start >= total) return {
|
|
1382
1425
|
lo: 0,
|
|
1383
1426
|
hi: total - 1,
|
|
1384
1427
|
fallback: true
|
|
1385
1428
|
};
|
|
1386
|
-
/**
|
|
1429
|
+
/** 终点 index(end 优先;否则 start + offset 取整)。 */
|
|
1387
1430
|
let end;
|
|
1388
|
-
if (args.
|
|
1389
|
-
/**
|
|
1390
|
-
const found =
|
|
1391
|
-
if (found
|
|
1431
|
+
if (args.end !== void 0) {
|
|
1432
|
+
/** end 取整(非有限数按 -1 处理,走越界回退)。 */
|
|
1433
|
+
const found = Number.isFinite(args.end) ? Math.floor(args.end) : -1;
|
|
1434
|
+
if (found < 0 || found >= total) return {
|
|
1392
1435
|
lo: 0,
|
|
1393
1436
|
hi: total - 1,
|
|
1394
1437
|
fallback: true
|
|
@@ -1435,7 +1478,7 @@ function matchExplanation(query, text, score) {
|
|
|
1435
1478
|
return `相似度 ${score.toFixed(3)}${keywords}`;
|
|
1436
1479
|
}
|
|
1437
1480
|
/** 模型未就绪时返回给模型的文案(告知即可;下载完成后无需另行通知,直接再次调用)。 */
|
|
1438
|
-
const SEMANTIC_MODEL_NOT_READY_MESSAGE = "语义检索暂不可用:本地嵌入模型尚未就绪(正在后台下载约 113MB;下载完成后无需提示,直接再次调用本工具即可。若下载失败,下次调用会自动重试)。可稍后重试,或先用 recall
|
|
1481
|
+
const SEMANTIC_MODEL_NOT_READY_MESSAGE = "语义检索暂不可用:本地嵌入模型尚未就绪(正在后台下载约 113MB;下载完成后无需提示,直接再次调用本工具即可。若下载失败,下次调用会自动重试)。可稍后重试,或先用 recall 工具按完整消息 index 精确检索。";
|
|
1439
1482
|
/** 构建 recall-semantic 工具定义(embedder/modelStatus 可注入,测试传替身;缺省用本地模型)。 */
|
|
1440
1483
|
function buildSemanticRecallTool(options) {
|
|
1441
1484
|
/** pruner 获取器(可选,裁剪超大工具结果)。 */
|
|
@@ -1444,7 +1487,7 @@ function buildSemanticRecallTool(options) {
|
|
|
1444
1487
|
const embed = options?.embedder ?? ((texts) => getEmbedder().then((fn) => fn(texts)));
|
|
1445
1488
|
return {
|
|
1446
1489
|
name: "recall-semantic",
|
|
1447
|
-
description: "
|
|
1490
|
+
description: "按语义(自然语言含义)在会话全部完整消息中检索:用一句话描述你要找的内容(可混用中英文与代码术语)。返回最匹配的若干条完整消息、index 与匹配说明;如需精确定位某个 index 周边的消息,请再用 recall 工具。",
|
|
1448
1491
|
parameters: {
|
|
1449
1492
|
query: {
|
|
1450
1493
|
type: "string",
|
|
@@ -1453,19 +1496,19 @@ function buildSemanticRecallTool(options) {
|
|
|
1453
1496
|
},
|
|
1454
1497
|
top_k: {
|
|
1455
1498
|
type: "number",
|
|
1456
|
-
description: "
|
|
1499
|
+
description: "返回最匹配的完整消息条数(1-10,默认 3)。"
|
|
1457
1500
|
},
|
|
1458
|
-
|
|
1459
|
-
type: "
|
|
1460
|
-
description: "
|
|
1501
|
+
start: {
|
|
1502
|
+
type: "number",
|
|
1503
|
+
description: "完整消息序号(index),可选。限定检索区间:以该条为基准边界(意义同 recall)。缺省检索全部消息。区间不合法(越界)时自动回退全量检索并在结果中说明。"
|
|
1461
1504
|
},
|
|
1462
|
-
|
|
1463
|
-
type: "
|
|
1464
|
-
description: "
|
|
1505
|
+
end: {
|
|
1506
|
+
type: "number",
|
|
1507
|
+
description: "与 offset 互斥,限定区间的另一个边界(含;意义同 recall)。"
|
|
1465
1508
|
},
|
|
1466
1509
|
offset: {
|
|
1467
1510
|
type: "number",
|
|
1468
|
-
description: "
|
|
1511
|
+
description: "与 end 互斥。相对 start 的步数:正数向后、负数向前(意义同 recall)。"
|
|
1469
1512
|
}
|
|
1470
1513
|
},
|
|
1471
1514
|
output: {
|
|
@@ -1477,39 +1520,34 @@ function buildSemanticRecallTool(options) {
|
|
|
1477
1520
|
},
|
|
1478
1521
|
async execute(args, exec) {
|
|
1479
1522
|
/** 解析并校验后的调用参数。 */
|
|
1480
|
-
const { query, top_k,
|
|
1523
|
+
const { query, top_k, start, end, offset } = parseSemanticRecallArgs(args);
|
|
1481
1524
|
/** 当前会话。 */
|
|
1482
1525
|
const session = exec.agent?.session;
|
|
1483
1526
|
if (!session) return "会话异常";
|
|
1484
1527
|
if (!isMainSession(session)) return "recall-semantic 仅主会话可用";
|
|
1485
1528
|
if ((await options?.modelStatus?.() ?? "ready") !== "ready") return SEMANTIC_MODEL_NOT_READY_MESSAGE;
|
|
1486
|
-
/**
|
|
1487
|
-
const
|
|
1488
|
-
if (
|
|
1489
|
-
/**
|
|
1490
|
-
const range = resolveSemanticRange(
|
|
1491
|
-
|
|
1492
|
-
|
|
1529
|
+
/** 完整消息索引(全部事件,含被压缩/遮蔽)。 */
|
|
1530
|
+
const cms = indexCompleteMessages(session);
|
|
1531
|
+
if (cms.length === 0) return "会话中没有可检索的消息";
|
|
1532
|
+
/** 检索区间(完整消息 index;区间不合法时回退全量并标记)。 */
|
|
1533
|
+
const range = resolveSemanticRange(cms.length, {
|
|
1534
|
+
start,
|
|
1535
|
+
end,
|
|
1493
1536
|
offset
|
|
1494
1537
|
});
|
|
1495
|
-
/**
|
|
1538
|
+
/** 候选完整消息渲染文本(区间内;跳过渲染失败/空文本)。 */
|
|
1496
1539
|
const candidates = [];
|
|
1497
1540
|
for (let i = range.lo; i <= range.hi; i += 1) {
|
|
1498
|
-
/**
|
|
1499
|
-
const
|
|
1500
|
-
if (!
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
if (!event) continue;
|
|
1504
|
-
/** 事件派生的消息对象。 */
|
|
1505
|
-
let message = null;
|
|
1541
|
+
/** 当前完整消息。 */
|
|
1542
|
+
const cm = cms[i];
|
|
1543
|
+
if (!cm) continue;
|
|
1544
|
+
/** 渲染文本(嵌入用完整文本,输出时再裁剪)。 */
|
|
1545
|
+
let text = "";
|
|
1506
1546
|
try {
|
|
1507
|
-
|
|
1547
|
+
text = renderCompleteMessage(session, cm);
|
|
1508
1548
|
} catch {}
|
|
1509
|
-
/** 渲染文本。 */
|
|
1510
|
-
const text = message ? renderMessageText(message) : "";
|
|
1511
1549
|
if (text.trim().length > 0) candidates.push({
|
|
1512
|
-
|
|
1550
|
+
cm,
|
|
1513
1551
|
text
|
|
1514
1552
|
});
|
|
1515
1553
|
}
|
|
@@ -1521,7 +1559,7 @@ function buildSemanticRecallTool(options) {
|
|
|
1521
1559
|
const vectors = await embed(candidates.map((c) => c.text));
|
|
1522
1560
|
/** 打分结果(相似度 + 候选)。 */
|
|
1523
1561
|
const scored = candidates.map((c, i) => ({
|
|
1524
|
-
|
|
1562
|
+
cm: c.cm,
|
|
1525
1563
|
text: c.text,
|
|
1526
1564
|
score: cosineSimilarity(queryVec, vectors[i] ?? /* @__PURE__ */ new Float32Array(0))
|
|
1527
1565
|
}));
|
|
@@ -1532,7 +1570,7 @@ function buildSemanticRecallTool(options) {
|
|
|
1532
1570
|
/** 结果缓冲。 */
|
|
1533
1571
|
const parts = [];
|
|
1534
1572
|
/** 范围描述(回退时明确告知模型)。 */
|
|
1535
|
-
const rangeNote = range.fallback ? "指定区间不合法(
|
|
1573
|
+
const rangeNote = range.fallback ? "指定区间不合法(start/end 越界等),已回退检索全部消息" : start === void 0 ? `检索全部消息(${candidates.length} 条可嵌入)` : `检索区间 [${range.lo}..${range.hi}](${candidates.length} 条可嵌入)`;
|
|
1536
1574
|
parts.push(`查询: ${query}`);
|
|
1537
1575
|
parts.push(rangeNote);
|
|
1538
1576
|
parts.push(`匹配 TOP-${hits.length}(共 ${scored.length} 条候选):`);
|
|
@@ -1540,30 +1578,16 @@ function buildSemanticRecallTool(options) {
|
|
|
1540
1578
|
/** 当前命中。 */
|
|
1541
1579
|
const hit = hits[i];
|
|
1542
1580
|
if (!hit) continue;
|
|
1543
|
-
/**
|
|
1544
|
-
const
|
|
1545
|
-
/**
|
|
1546
|
-
const id = event ? messageIdOfEvent(event) ?? "" : "";
|
|
1547
|
-
/** 命中消息文本(pruner 裁剪超大内容后渲染)。 */
|
|
1581
|
+
/** 类型标注(toolcall 附调用 id,与 recall 输出一致)。 */
|
|
1582
|
+
const callAttr = hit.cm.type === "toolcall" && hit.cm.callId ? ` callId=${hit.cm.callId}` : "";
|
|
1583
|
+
/** 输出文本(pruner 裁剪超大内容;裁剪失败保留嵌入用文本)。 */
|
|
1548
1584
|
let text = hit.text;
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
message = session.deriveEventMessage(event);
|
|
1556
|
-
} catch {}
|
|
1557
|
-
if (message) {
|
|
1558
|
-
/** 裁剪后的内容块。 */
|
|
1559
|
-
const pruned = pruner.pruneContent(message.content);
|
|
1560
|
-
if (pruned) text = renderMessageText({
|
|
1561
|
-
...message,
|
|
1562
|
-
content: pruned
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
parts.push(`-- [${i + 1}] seq ${hit.node.seq} ${hit.node.type} message_id=${id} — ${matchExplanation(query, hit.text, hit.score)} --`);
|
|
1585
|
+
try {
|
|
1586
|
+
/** tool-result-pruner(可选)。 */
|
|
1587
|
+
const pruner = getPruner();
|
|
1588
|
+
text = renderCompleteMessage(session, hit.cm, pruner);
|
|
1589
|
+
} catch {}
|
|
1590
|
+
parts.push(`-- [${i + 1}] index ${hit.cm.index} ${hit.cm.type}${callAttr} — ${matchExplanation(query, hit.text, hit.score)} --`);
|
|
1567
1591
|
parts.push(text);
|
|
1568
1592
|
}
|
|
1569
1593
|
return parts.join("\n\n");
|
|
@@ -1577,9 +1601,9 @@ function buildSemanticRecallTool(options) {
|
|
|
1577
1601
|
* 不依赖特定 tool mode(native / code / both 均可运行)。
|
|
1578
1602
|
*
|
|
1579
1603
|
* 模块:
|
|
1580
|
-
* - recall.ts recall({
|
|
1581
|
-
* - semantic-recall.ts recall-semantic({ query, top_k?,
|
|
1582
|
-
*
|
|
1604
|
+
* - recall.ts recall({ start, end?, offset? }) 工具:按完整消息 index 回看原始会话
|
|
1605
|
+
* - semantic-recall.ts recall-semantic({ query, top_k?, start?, end?, offset? }) 工具:
|
|
1606
|
+
* 按语义在全部完整消息(含被压缩/遮蔽)中检索,返回最匹配的完整消息与匹配说明
|
|
1583
1607
|
* (本地 ONNX embedding,模型随插件打包,懒加载)
|
|
1584
1608
|
* - compress.ts 自动压缩(OM 观察/反思两级阈值):pre-step 阻塞串行执行——
|
|
1585
1609
|
* 反思(摘要 ≥ 窗口 × historyMergeRatio 时摘要调用精简合并 <om-history>)、
|