sema-core 2.0.15 → 2.1.0
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 +1 -31
- package/dist/core/Conversation.d.ts.map +1 -1
- package/dist/core/Conversation.js +10 -7
- package/dist/core/Conversation.js.map +1 -1
- package/dist/core/RunTools.d.ts.map +1 -1
- package/dist/core/RunTools.js +43 -8
- package/dist/core/RunTools.js.map +1 -1
- package/dist/core/SemaCore.d.ts +6 -2
- package/dist/core/SemaCore.d.ts.map +1 -1
- package/dist/core/SemaCore.js +5 -2
- package/dist/core/SemaCore.js.map +1 -1
- package/dist/core/SemaEngine.d.ts.map +1 -1
- package/dist/core/SemaEngine.js +45 -13
- package/dist/core/SemaEngine.js.map +1 -1
- package/dist/core/SemaSession.d.ts +3 -11
- package/dist/core/SemaSession.d.ts.map +1 -1
- package/dist/core/SemaSession.js +2 -10
- package/dist/core/SemaSession.js.map +1 -1
- package/dist/core/SessionPool.d.ts.map +1 -1
- package/dist/core/SessionPool.js +3 -2
- package/dist/core/SessionPool.js.map +1 -1
- package/dist/events/types.d.ts +11 -0
- package/dist/events/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/ConfManager.d.ts +9 -1
- package/dist/manager/ConfManager.d.ts.map +1 -1
- package/dist/manager/ConfManager.js +25 -22
- package/dist/manager/ConfManager.js.map +1 -1
- package/dist/manager/ModelManager.d.ts +30 -2
- package/dist/manager/ModelManager.d.ts.map +1 -1
- package/dist/manager/ModelManager.js +103 -32
- package/dist/manager/ModelManager.js.map +1 -1
- package/dist/manager/PermissionManager.d.ts.map +1 -1
- package/dist/manager/PermissionManager.js +105 -41
- package/dist/manager/PermissionManager.js.map +1 -1
- package/dist/prompt/compact.d.ts +30 -1
- package/dist/prompt/compact.d.ts.map +1 -1
- package/dist/prompt/compact.js +75 -18
- package/dist/prompt/compact.js.map +1 -1
- package/dist/prompt/inputPredict.d.ts +2 -0
- package/dist/prompt/inputPredict.d.ts.map +1 -0
- package/dist/prompt/inputPredict.js +43 -0
- package/dist/prompt/inputPredict.js.map +1 -0
- package/dist/prompt/permission.d.ts +1 -1
- package/dist/prompt/permission.d.ts.map +1 -1
- package/dist/prompt/permission.js +49 -6
- package/dist/prompt/permission.js.map +1 -1
- package/dist/prompt/tools/fetchUrl.d.ts +1 -1
- package/dist/prompt/tools/fetchUrl.d.ts.map +1 -1
- package/dist/prompt/tools/fetchUrl.js +3 -0
- package/dist/prompt/tools/fetchUrl.js.map +1 -1
- package/dist/services/agents/agentsManager.d.ts.map +1 -1
- package/dist/services/agents/agentsManager.js +3 -2
- package/dist/services/agents/agentsManager.js.map +1 -1
- package/dist/services/agents/genSystemReminder.d.ts +10 -1
- package/dist/services/agents/genSystemReminder.d.ts.map +1 -1
- package/dist/services/agents/genSystemReminder.js +48 -2
- package/dist/services/agents/genSystemReminder.js.map +1 -1
- package/dist/services/api/adapt/openai.d.ts.map +1 -1
- package/dist/services/api/adapt/openai.js +25 -4
- package/dist/services/api/adapt/openai.js.map +1 -1
- package/dist/services/api/adapt/util.d.ts +15 -0
- package/dist/services/api/adapt/util.d.ts.map +1 -1
- package/dist/services/api/adapt/util.js +54 -2
- package/dist/services/api/adapt/util.js.map +1 -1
- package/dist/services/api/apiUtil.d.ts.map +1 -1
- package/dist/services/api/apiUtil.js +8 -7
- package/dist/services/api/apiUtil.js.map +1 -1
- package/dist/services/api/queryLLM.d.ts.map +1 -1
- package/dist/services/api/queryLLM.js +22 -16
- package/dist/services/api/queryLLM.js.map +1 -1
- package/dist/services/commands/commandsManager.d.ts.map +1 -1
- package/dist/services/commands/commandsManager.js +3 -2
- package/dist/services/commands/commandsManager.js.map +1 -1
- package/dist/services/commands/runCommand.d.ts.map +1 -1
- package/dist/services/commands/runCommand.js +17 -6
- package/dist/services/commands/runCommand.js.map +1 -1
- package/dist/services/hooks/hookTriggers.d.ts.map +1 -1
- package/dist/services/hooks/hookTriggers.js +3 -2
- package/dist/services/hooks/hookTriggers.js.map +1 -1
- package/dist/services/mcp/MCPClient.d.ts.map +1 -1
- package/dist/services/mcp/MCPClient.js +18 -8
- package/dist/services/mcp/MCPClient.js.map +1 -1
- package/dist/services/mcp/MCPManager.d.ts +27 -12
- package/dist/services/mcp/MCPManager.d.ts.map +1 -1
- package/dist/services/mcp/MCPManager.js +189 -75
- package/dist/services/mcp/MCPManager.js.map +1 -1
- package/dist/services/mcp/MCPToolAdapter.d.ts.map +1 -1
- package/dist/services/mcp/MCPToolAdapter.js +128 -22
- package/dist/services/mcp/MCPToolAdapter.js.map +1 -1
- package/dist/services/plugins/pluginsManager.d.ts.map +1 -1
- package/dist/services/plugins/pluginsManager.js +24 -11
- package/dist/services/plugins/pluginsManager.js.map +1 -1
- package/dist/services/skills/skillsManager.d.ts +18 -3
- package/dist/services/skills/skillsManager.d.ts.map +1 -1
- package/dist/services/skills/skillsManager.js +49 -4
- package/dist/services/skills/skillsManager.js.map +1 -1
- package/dist/tools/FetchUrl.d.ts.map +1 -1
- package/dist/tools/FetchUrl.js +10 -3
- package/dist/tools/FetchUrl.js.map +1 -1
- package/dist/tools/ListCrons.d.ts +1 -0
- package/dist/tools/ListCrons.d.ts.map +1 -1
- package/dist/tools/ListCrons.js +1 -0
- package/dist/tools/ListCrons.js.map +1 -1
- package/dist/tools/LoadTools.js +1 -1
- package/dist/tools/PeekBgJob.d.ts +2 -2
- package/dist/tools/PlanToAgent.d.ts.map +1 -1
- package/dist/tools/PlanToAgent.js +7 -4
- package/dist/tools/PlanToAgent.js.map +1 -1
- package/dist/tools/RunShell.d.ts.map +1 -1
- package/dist/tools/RunShell.js +4 -16
- package/dist/tools/RunShell.js.map +1 -1
- package/dist/tools/Skill.d.ts.map +1 -1
- package/dist/tools/Skill.js +3 -2
- package/dist/tools/Skill.js.map +1 -1
- package/dist/tools/base/Tool.d.ts +1 -0
- package/dist/tools/base/Tool.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +8 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/mcp.d.ts +7 -0
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/types/model.d.ts +3 -0
- package/dist/types/model.d.ts.map +1 -1
- package/dist/types/session.d.ts +6 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/settings.d.ts +3 -1
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/types/skill.d.ts +2 -0
- package/dist/types/skill.d.ts.map +1 -1
- package/dist/util/autoRunContext.d.ts +2 -0
- package/dist/util/autoRunContext.d.ts.map +1 -1
- package/dist/util/autoRunContext.js +2 -0
- package/dist/util/autoRunContext.js.map +1 -1
- package/dist/util/compact.d.ts +27 -10
- package/dist/util/compact.d.ts.map +1 -1
- package/dist/util/compact.js +403 -61
- package/dist/util/compact.js.map +1 -1
- package/dist/util/fetchUrl.d.ts +1 -0
- package/dist/util/fetchUrl.d.ts.map +1 -1
- package/dist/util/fetchUrl.js +34 -4
- package/dist/util/fetchUrl.js.map +1 -1
- package/dist/util/history.d.ts +1 -1
- package/dist/util/history.d.ts.map +1 -1
- package/dist/util/history.js +21 -5
- package/dist/util/history.js.map +1 -1
- package/dist/util/i18n/de.d.ts +92 -0
- package/dist/util/i18n/de.d.ts.map +1 -0
- package/dist/util/i18n/de.js +104 -0
- package/dist/util/i18n/de.js.map +1 -0
- package/dist/util/i18n/en.d.ts +92 -0
- package/dist/util/i18n/en.d.ts.map +1 -0
- package/dist/util/i18n/en.js +104 -0
- package/dist/util/i18n/en.js.map +1 -0
- package/dist/util/i18n/fr.d.ts +92 -0
- package/dist/util/i18n/fr.d.ts.map +1 -0
- package/dist/util/i18n/fr.js +104 -0
- package/dist/util/i18n/fr.js.map +1 -0
- package/dist/util/i18n/index.d.ts +7 -0
- package/dist/util/i18n/index.d.ts.map +1 -0
- package/dist/util/i18n/index.js +36 -0
- package/dist/util/i18n/index.js.map +1 -0
- package/dist/util/i18n/it.d.ts +92 -0
- package/dist/util/i18n/it.d.ts.map +1 -0
- package/dist/util/i18n/it.js +104 -0
- package/dist/util/i18n/it.js.map +1 -0
- package/dist/util/i18n/zh.d.ts +96 -0
- package/dist/util/i18n/zh.d.ts.map +1 -0
- package/dist/util/i18n/zh.js +107 -0
- package/dist/util/i18n/zh.js.map +1 -0
- package/dist/util/i18n.d.ts +98 -0
- package/dist/util/i18n.d.ts.map +1 -0
- package/dist/util/i18n.js +228 -0
- package/dist/util/i18n.js.map +1 -0
- package/dist/util/inputPredict.d.ts +15 -0
- package/dist/util/inputPredict.d.ts.map +1 -0
- package/dist/util/inputPredict.js +135 -0
- package/dist/util/inputPredict.js.map +1 -0
- package/dist/util/message.d.ts +1 -1
- package/dist/util/message.d.ts.map +1 -1
- package/dist/util/message.js +5 -2
- package/dist/util/message.js.map +1 -1
- package/dist/util/model.d.ts.map +1 -1
- package/dist/util/model.js +9 -3
- package/dist/util/model.js.map +1 -1
- package/dist/util/shell.d.ts.map +1 -1
- package/dist/util/shell.js +6 -1
- package/dist/util/shell.js.map +1 -1
- package/dist/util/shellSafety.d.ts +40 -4
- package/dist/util/shellSafety.d.ts.map +1 -1
- package/dist/util/shellSafety.js +545 -8
- package/dist/util/shellSafety.js.map +1 -1
- package/dist/util/tokens.d.ts +10 -2
- package/dist/util/tokens.d.ts.map +1 -1
- package/dist/util/tokens.js +19 -15
- package/dist/util/tokens.js.map +1 -1
- package/package.json +3 -1
package/dist/util/compact.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.NULL_TOOL = exports.__compactTestHooks = void 0;
|
|
|
4
4
|
exports.needsAutoCompact = needsAutoCompact;
|
|
5
5
|
exports.applyMicroCompact = applyMicroCompact;
|
|
6
6
|
exports.compactMessages = compactMessages;
|
|
7
|
+
exports.collectSkillActivations = collectSkillActivations;
|
|
7
8
|
exports.autoCompact = autoCompact;
|
|
8
9
|
const tokens_1 = require("./tokens");
|
|
9
10
|
const message_1 = require("./message");
|
|
@@ -15,6 +16,9 @@ const microcompact_1 = require("./microcompact");
|
|
|
15
16
|
const zod_1 = require("zod");
|
|
16
17
|
const tokens_2 = require("./tokens");
|
|
17
18
|
const compact_1 = require("../prompt/compact");
|
|
19
|
+
const genSystemReminder_1 = require("../services/agents/genSystemReminder");
|
|
20
|
+
const tool_1 = require("../prompt/tool");
|
|
21
|
+
const define_1 = require("../prompt/define");
|
|
18
22
|
const defaultCompactDependencies = {
|
|
19
23
|
queryLLM: queryLLM_1.queryLLM,
|
|
20
24
|
getModelManager: ModelManager_1.getModelManager,
|
|
@@ -35,6 +39,13 @@ exports.__compactTestHooks = {
|
|
|
35
39
|
* 提前触发压缩以避免接近token限制时的API调用失败
|
|
36
40
|
*/
|
|
37
41
|
const AUTO_COMPACT_THRESHOLD_RATIO = 0.75;
|
|
42
|
+
/**
|
|
43
|
+
* 自动压缩时原样保留的最近工具轮次数(一个轮次 = assistant(tool_use) + 紧随的 tool_result user 消息)。
|
|
44
|
+
* 最后一条 assistant 之后的未消费批次另行无条件保留,不计入此数。
|
|
45
|
+
*/
|
|
46
|
+
const COMPACT_KEEP_RECENT_TOOL_ROUNDS = 4;
|
|
47
|
+
// 图片块固定估算,与 microcompact 口径一致(勿按 base64 长度折算)
|
|
48
|
+
const IMAGE_BLOCK_TOKEN_ESTIMATE = 1500;
|
|
38
49
|
function getContextLimit(sessionId) {
|
|
39
50
|
try {
|
|
40
51
|
return compactDependencies.getModelManager().getModel('main', sessionId)?.contextLength ?? 128_000;
|
|
@@ -94,8 +105,9 @@ function truncateMessages(messages, targetTokenLimit) {
|
|
|
94
105
|
if (message.type === 'assistant' && 'usage' in message.message) {
|
|
95
106
|
const inputTokens = getInputTokensFromUsage(message.message.usage);
|
|
96
107
|
if (inputTokens >= tokensToRemove) {
|
|
97
|
-
//
|
|
98
|
-
|
|
108
|
+
// 从这条 assistant 开始保留:它的 tool_use 与下一条 user 消息里的 tool_result 配对,
|
|
109
|
+
// 若从 i + 1 开始会留下孤儿 tool_result,API 会拒绝
|
|
110
|
+
cutIndex = i;
|
|
99
111
|
break;
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -117,7 +129,7 @@ function truncateMessages(messages, targetTokenLimit) {
|
|
|
117
129
|
}
|
|
118
130
|
// 在开头添加截断提示消息
|
|
119
131
|
if (result.length < messages.length) {
|
|
120
|
-
const truncatedMessage = (0, message_1.buildUserMsg)(
|
|
132
|
+
const truncatedMessage = (0, message_1.buildUserMsg)(`${compact_1.CONTEXT_TRUNCATED_NOTICE_LEAD} ${messages.length - result.length} earlier messages removed. Recent conversation preserved.`);
|
|
121
133
|
result.unshift(truncatedMessage);
|
|
122
134
|
}
|
|
123
135
|
(0, log_1.logDebug)(`[Compact] Truncation result: kept ${result.length} of ${messages.length} messages`);
|
|
@@ -183,18 +195,29 @@ function emitCompactUsage(messagesBefore, messagesAfter, sessionId, mode, reason
|
|
|
183
195
|
}
|
|
184
196
|
}
|
|
185
197
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
198
|
+
* 估算当前上下文占用,口径为"即将发送的请求":
|
|
199
|
+
* 最近有效 usage 的 input(上次请求的上下文)+ 该次 output(本次响应已进入上下文)
|
|
200
|
+
* + 该 assistant 之后新增消息(工具结果、新用户输入)的估算。
|
|
201
|
+
* 只看 input 会滞后一轮,单轮新增多个大工具结果时可能撞限。
|
|
202
|
+
* 没有任何有效 usage 时(如全是合成消息的历史)按全部消息估算。
|
|
203
|
+
* 压缩判断与 compact:micro 事件统计共用此函数,保证口径一致。
|
|
204
|
+
*/
|
|
205
|
+
function estimateContextTokens(messages) {
|
|
206
|
+
const { inputTokens, outputTokens, index } = (0, tokens_1.countTokens)(messages);
|
|
207
|
+
return inputTokens + outputTokens + estimateMessagesTokens(messages.slice(index + 1));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 根据令牌使用量判断是否应触发自动压缩(口径见 estimateContextTokens)
|
|
188
211
|
*
|
|
189
|
-
* @param discountTokens 估算折扣:
|
|
190
|
-
* micro 清理的节省要到下一次响应才可见,期间用该折扣修正判断。默认 0
|
|
212
|
+
* @param discountTokens 估算折扣:usage 读的是上一次 API 响应,
|
|
213
|
+
* micro 清理的节省要到下一次响应才可见,期间用该折扣修正判断。默认 0。
|
|
214
|
+
* 传入的 messages 应是清理前的历史,否则 usage 下标之后被清理的块会在估算与折扣中重复扣减。
|
|
191
215
|
*/
|
|
192
216
|
function needsAutoCompact(messages, discountTokens = 0, sessionId) {
|
|
193
217
|
if (messages.length < 3)
|
|
194
218
|
return false;
|
|
195
|
-
const inputTokenCount = (0, tokens_1.countTokens)(messages).inputTokens;
|
|
196
219
|
const autoCompactThreshold = getContextLimit(sessionId) * AUTO_COMPACT_THRESHOLD_RATIO;
|
|
197
|
-
return (
|
|
220
|
+
return (estimateContextTokens(messages) - discountTokens) >= autoCompactThreshold;
|
|
198
221
|
}
|
|
199
222
|
/**
|
|
200
223
|
* Micro 压缩集成入口(全量摘要前的第一道防线)
|
|
@@ -211,10 +234,11 @@ function applyMicroCompact(messages, sessionId) {
|
|
|
211
234
|
if (!result.changed) {
|
|
212
235
|
return { messages, needFullCompact: true, changed: false };
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
// 在清理前的 messages 上判断再减本次节省:清理后的消息里 usage 下标之后的块已是占位符,
|
|
238
|
+
// 若在其上估算再减全部节省,这部分会被扣两次而低估占用
|
|
239
|
+
const stillOver = needsAutoCompact(messages, result.estimatedSavedTokens, sessionId);
|
|
240
|
+
// estimatedTokenAfter = 清理前的估算占用 − 估算节省,与 stillOver 的判断口径完全一致
|
|
241
|
+
const tokenBefore = estimateContextTokens(messages);
|
|
218
242
|
const microData = {
|
|
219
243
|
clearedCount: result.clearedCount,
|
|
220
244
|
estimatedSavedTokens: result.estimatedSavedTokens,
|
|
@@ -250,17 +274,28 @@ function applyMicroCompact(messages, sessionId) {
|
|
|
250
274
|
*/
|
|
251
275
|
async function compactMessages(messages, abortController, sessionId, options = {}) {
|
|
252
276
|
const allowTruncationFallback = options.allowTruncationFallback ?? true;
|
|
277
|
+
// 自动压缩路径只压历史的一段,事件口径需按整段上下文计算,由 autoCompact 自行发出
|
|
278
|
+
const emitUsageEvent = options.emitUsageEvent ?? true;
|
|
279
|
+
const emitUsage = (messagesAfter, mode, reason, error) => {
|
|
280
|
+
if (emitUsageEvent)
|
|
281
|
+
emitCompactUsage(messages, messagesAfter, sessionId, mode, reason, error);
|
|
282
|
+
};
|
|
253
283
|
if (messages.length < 2) {
|
|
254
284
|
return { kind: 'unchanged', messages };
|
|
255
285
|
}
|
|
256
286
|
try {
|
|
257
287
|
const summaryResult = await executeAutoCompact(messages, abortController, sessionId, options.customInstructions);
|
|
288
|
+
// 用户中断:适配层不抛错而是返回部分内容,不能当成摘要,也不能进有损的截断兜底
|
|
289
|
+
if (abortController.signal.aborted) {
|
|
290
|
+
(0, log_1.logInfo)('[Compact] Aborted by user, keeping messages unchanged');
|
|
291
|
+
return { kind: 'unchanged', messages };
|
|
292
|
+
}
|
|
258
293
|
if (summaryResult.kind === 'summary') {
|
|
259
|
-
|
|
294
|
+
emitUsage(summaryResult.messages, 'summary');
|
|
260
295
|
return summaryResult;
|
|
261
296
|
}
|
|
262
297
|
if (!allowTruncationFallback) {
|
|
263
|
-
|
|
298
|
+
emitUsage(null, 'failed', summaryResult.reason);
|
|
264
299
|
return {
|
|
265
300
|
kind: 'failed',
|
|
266
301
|
error: new Error(`Compact did not produce a valid summary: ${summaryResult.reason}`),
|
|
@@ -269,7 +304,7 @@ async function compactMessages(messages, abortController, sessionId, options = {
|
|
|
269
304
|
const contextLimit = getContextLimit(sessionId);
|
|
270
305
|
const targetLimit = contextLimit * 0.5; // 截断到50%容量
|
|
271
306
|
const truncatedMessages = truncateMessages(messages, targetLimit);
|
|
272
|
-
|
|
307
|
+
emitUsage(truncatedMessages, 'truncated', summaryResult.reason);
|
|
273
308
|
return {
|
|
274
309
|
kind: 'truncated',
|
|
275
310
|
messages: truncatedMessages,
|
|
@@ -277,8 +312,13 @@ async function compactMessages(messages, abortController, sessionId, options = {
|
|
|
277
312
|
};
|
|
278
313
|
}
|
|
279
314
|
catch (error) {
|
|
315
|
+
// 用户中断(SDK 层抛出中断异常的路径):同上,直接退出不降级
|
|
316
|
+
if (abortController.signal.aborted) {
|
|
317
|
+
(0, log_1.logInfo)('[Compact] Aborted by user, keeping messages unchanged');
|
|
318
|
+
return { kind: 'unchanged', messages };
|
|
319
|
+
}
|
|
280
320
|
if (!allowTruncationFallback) {
|
|
281
|
-
|
|
321
|
+
emitUsage(null, 'failed', 'COMPACT_ERROR', error);
|
|
282
322
|
return {
|
|
283
323
|
kind: 'failed',
|
|
284
324
|
error,
|
|
@@ -291,7 +331,7 @@ async function compactMessages(messages, abortController, sessionId, options = {
|
|
|
291
331
|
const targetLimit = contextLimit * 0.5; // 截断到50%容量
|
|
292
332
|
const truncatedMessages = truncateMessages(messages, targetLimit);
|
|
293
333
|
(0, log_1.logError)(`Successfully applied truncation fallback, reduced from ${messages.length} to ${truncatedMessages.length} messages`);
|
|
294
|
-
|
|
334
|
+
emitUsage(truncatedMessages, 'truncated', 'COMPACT_ERROR', error);
|
|
295
335
|
return {
|
|
296
336
|
kind: 'truncated',
|
|
297
337
|
messages: truncatedMessages,
|
|
@@ -301,7 +341,7 @@ async function compactMessages(messages, abortController, sessionId, options = {
|
|
|
301
341
|
catch (truncationError) {
|
|
302
342
|
// 如果连截断都失败,返回失败结果
|
|
303
343
|
(0, log_1.logError)(`Truncation fallback also failed: ${truncationError}`);
|
|
304
|
-
|
|
344
|
+
emitUsage(null, 'failed', 'COMPACT_ERROR', truncationError);
|
|
305
345
|
return {
|
|
306
346
|
kind: 'failed',
|
|
307
347
|
error: truncationError,
|
|
@@ -310,53 +350,355 @@ async function compactMessages(messages, abortController, sessionId, options = {
|
|
|
310
350
|
}
|
|
311
351
|
}
|
|
312
352
|
/**
|
|
313
|
-
*
|
|
353
|
+
* 扫描消息列表中 skill 工具的成功激活:tool_use(skill) 配对非报错 tool_result,
|
|
354
|
+
* 且同消息内有当时注入的 skill 全文 text 块。同名多次调用取最后一次。
|
|
355
|
+
* 全文从历史原样取:天然证明激活成功、参数已替换、与模型当时所见一致
|
|
356
|
+
* (microCompact 只清 tool_result 不碰 text 块,该块必然还在)。
|
|
357
|
+
*/
|
|
358
|
+
function collectSkillActivations(messages) {
|
|
359
|
+
// tool_use_id -> skill 名
|
|
360
|
+
const toolUseNames = new Map();
|
|
361
|
+
for (const msg of messages) {
|
|
362
|
+
if (msg.type !== 'assistant' || !Array.isArray(msg.message.content))
|
|
363
|
+
continue;
|
|
364
|
+
for (const block of msg.message.content) {
|
|
365
|
+
if (block.type === 'tool_use' && block.name === tool_1.TOOL_NAME_SKILL) {
|
|
366
|
+
const skillName = block.input?.skill;
|
|
367
|
+
if (skillName)
|
|
368
|
+
toolUseNames.set(block.id, skillName);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const byName = new Map();
|
|
373
|
+
for (const msg of messages) {
|
|
374
|
+
if (msg.type !== 'user' || !Array.isArray(msg.message.content))
|
|
375
|
+
continue;
|
|
376
|
+
const content = msg.message.content;
|
|
377
|
+
for (let i = 0; i < content.length; i++) {
|
|
378
|
+
const block = content[i];
|
|
379
|
+
// 上一次压缩注入的 skill 原文块:解析后接力,保证 skill 上下文可跨多次压缩存续;
|
|
380
|
+
// 同名后续真实激活按消息序覆盖接力内容
|
|
381
|
+
if (block.type === 'text' && block.text.startsWith(`${define_1.REMINDER_SYS_OPEN}\n${compact_1.SKILL_CONTEXT_NOTICE}`)) {
|
|
382
|
+
for (const activation of parseSkillContextBlock(block.text, msg.uuid)) {
|
|
383
|
+
byName.set(activation.name, activation);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (block.type !== 'tool_result')
|
|
388
|
+
continue;
|
|
389
|
+
const name = toolUseNames.get(block.tool_use_id);
|
|
390
|
+
if (!name || block.is_error === true)
|
|
391
|
+
continue;
|
|
392
|
+
// skill 全文(additionalBlocks)紧随 tool_result 之后;
|
|
393
|
+
// 跳过 reminder-sys 开头的块(注入的用户消息、hook 上下文等非 skill 正文)
|
|
394
|
+
for (let j = i + 1; j < content.length; j++) {
|
|
395
|
+
const next = content[j];
|
|
396
|
+
if (next.type === 'tool_result')
|
|
397
|
+
break;
|
|
398
|
+
if (next.type === 'text' && !next.text.startsWith(define_1.REMINDER_SYS_OPEN)) {
|
|
399
|
+
byName.set(name, { name, text: next.text, uuid: msg.uuid });
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return [...byName.values()];
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 解析压缩后注入的 skill 原文块(格式见 generatePostCompactReminders 的 sections 拼装),
|
|
409
|
+
* 还原为激活列表。skill 正文若恰好含 "### Skill: " 行会造成误切分,风险极低且
|
|
410
|
+
* 伪名称会被后续的存在性过滤剔除。
|
|
411
|
+
*/
|
|
412
|
+
function parseSkillContextBlock(text, uuid) {
|
|
413
|
+
let body = text;
|
|
414
|
+
if (body.startsWith(define_1.REMINDER_SYS_OPEN))
|
|
415
|
+
body = body.slice(define_1.REMINDER_SYS_OPEN.length);
|
|
416
|
+
if (body.endsWith(define_1.REMINDER_SYS_CLOSE))
|
|
417
|
+
body = body.slice(0, -define_1.REMINDER_SYS_CLOSE.length);
|
|
418
|
+
return body.split(/^### Skill: /m).slice(1).map(section => {
|
|
419
|
+
const newlineIdx = section.indexOf('\n');
|
|
420
|
+
const name = (newlineIdx === -1 ? section : section.slice(0, newlineIdx)).trim();
|
|
421
|
+
const skillText = (newlineIdx === -1 ? '' : section.slice(newlineIdx + 1)).trim();
|
|
422
|
+
return { name, text: skillText, uuid };
|
|
423
|
+
}).filter(a => a.name && a.text);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* 把 reminder 块插入首条 user 消息(压缩通知/截断通知)的通知文本之前:
|
|
427
|
+
* 阅读序为 [reminders..., 通知文本, 摘要(assistant)],通知文本紧邻其引出的摘要。
|
|
428
|
+
* 首条消息非 user 时原样返回(截断兜底未删任何消息的罕见场景),不做强行注入。
|
|
429
|
+
*/
|
|
430
|
+
function prependBlocksToLeadingUserMsg(messages, blocks) {
|
|
431
|
+
const first = messages[0];
|
|
432
|
+
if (!first || first.type !== 'user') {
|
|
433
|
+
return messages;
|
|
434
|
+
}
|
|
435
|
+
const content = first.message.content;
|
|
436
|
+
const contentBlocks = typeof content === 'string'
|
|
437
|
+
? [{ type: 'text', text: content }]
|
|
438
|
+
: [...content];
|
|
439
|
+
const nextFirst = {
|
|
440
|
+
...first,
|
|
441
|
+
message: { ...first.message, content: [...blocks, ...contentBlocks] },
|
|
442
|
+
};
|
|
443
|
+
return [nextFirst, ...messages.slice(1)];
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 按文本长度估算一组消息的 token 数(口径与 microcompact 一致)。
|
|
447
|
+
* 用于 compact:exec 事件里被压区与新前缀的差值计算,以及 needsAutoCompact 对
|
|
448
|
+
* 最近 usage 之后新增消息的估算。
|
|
449
|
+
*/
|
|
450
|
+
function estimateMessagesTokens(messages) {
|
|
451
|
+
let total = 0;
|
|
452
|
+
for (const msg of messages) {
|
|
453
|
+
const content = msg.message.content;
|
|
454
|
+
if (typeof content === 'string') {
|
|
455
|
+
total += (0, microcompact_1.estimateTokensFromText)(content);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (!Array.isArray(content))
|
|
459
|
+
continue;
|
|
460
|
+
for (const block of content) {
|
|
461
|
+
switch (block?.type) {
|
|
462
|
+
case 'text':
|
|
463
|
+
total += (0, microcompact_1.estimateTokensFromText)(block.text ?? '');
|
|
464
|
+
break;
|
|
465
|
+
case 'thinking':
|
|
466
|
+
total += (0, microcompact_1.estimateTokensFromText)(block.thinking ?? '');
|
|
467
|
+
break;
|
|
468
|
+
case 'tool_use':
|
|
469
|
+
total += (0, microcompact_1.estimateTokensFromText)(JSON.stringify(block.input ?? {}));
|
|
470
|
+
break;
|
|
471
|
+
case 'tool_result': {
|
|
472
|
+
const inner = block.content;
|
|
473
|
+
if (typeof inner === 'string') {
|
|
474
|
+
total += (0, microcompact_1.estimateTokensFromText)(inner);
|
|
475
|
+
}
|
|
476
|
+
else if (Array.isArray(inner)) {
|
|
477
|
+
for (const part of inner) {
|
|
478
|
+
if (part?.type === 'text')
|
|
479
|
+
total += (0, microcompact_1.estimateTokensFromText)(part.text ?? '');
|
|
480
|
+
else if (part?.type === 'image')
|
|
481
|
+
total += IMAGE_BLOCK_TOKEN_ESTIMATE;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case 'image':
|
|
487
|
+
total += IMAGE_BLOCK_TOKEN_ESTIMATE;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return total;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* 计算自动压缩的切点:messages.slice(0, cutIdx) 为被压区,其余为保留区。
|
|
314
496
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
497
|
+
* 切点只落在两类位置,二者都保证 tool_use/tool_result 不被拆散,
|
|
498
|
+
* 也保证 skill 激活的 tool_use 与其 tool_result/全文块在同一侧(collectSkillActivations 按区域配对):
|
|
499
|
+
* - 某个工具轮次的 assistant 消息:保留最后一条 assistant 起的未消费尾部,
|
|
500
|
+
* 再往前保留 COMPACT_KEEP_RECENT_TOOL_ROUNDS 个已消费轮次,切在最老一个保留轮次的 assistant 上;
|
|
501
|
+
* - 最后一条真实用户消息:它落在保留窗口内或紧贴切点时(本轮很短、或最新消息就是新查询),
|
|
502
|
+
* 切到它,用户原话原样保留,等价于旧行为。
|
|
321
503
|
*
|
|
322
|
-
*
|
|
504
|
+
* 旧行为整轮豁免:agent 单轮跑几十次工具时可压区几乎为空,再触发只能对上一份摘要复摘,
|
|
505
|
+
* 摘要越压越大而真实上下文单调增长。
|
|
323
506
|
*/
|
|
324
|
-
|
|
325
|
-
//
|
|
507
|
+
function findCompactCut(messages) {
|
|
508
|
+
// 从后往前找最后一条真实用户消息(首块非 tool_result)
|
|
326
509
|
let lastRealUserIdx = -1;
|
|
327
510
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
328
|
-
if (messages[i].type
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
511
|
+
if (messages[i].type !== 'user')
|
|
512
|
+
continue;
|
|
513
|
+
const content = messages[i].message.content;
|
|
514
|
+
const isToolResult = Array.isArray(content) &&
|
|
515
|
+
content.length > 0 &&
|
|
516
|
+
content[0]?.type === 'tool_result';
|
|
517
|
+
if (!isToolResult) {
|
|
518
|
+
lastRealUserIdx = i;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (lastRealUserIdx === -1) {
|
|
523
|
+
return { cutIdx: 0, lastRealUserIdx };
|
|
524
|
+
}
|
|
525
|
+
let lastAssistantIdx = -1;
|
|
526
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
527
|
+
if (messages[i].type === 'assistant') {
|
|
528
|
+
lastAssistantIdx = i;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// 未消费尾部(最后一条 assistant 起)无条件保留,再往前数 N 个 assistant 轮次
|
|
533
|
+
let cutIdx = lastAssistantIdx;
|
|
534
|
+
if (lastAssistantIdx > 0) {
|
|
535
|
+
let rounds = 0;
|
|
536
|
+
for (let i = lastAssistantIdx - 1; i >= 0 && rounds < COMPACT_KEEP_RECENT_TOOL_ROUNDS; i--) {
|
|
537
|
+
if (messages[i].type === 'assistant') {
|
|
538
|
+
rounds++;
|
|
539
|
+
cutIdx = i;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// 没有 assistant,或最后一条真实用户消息落在保留窗口内/紧贴切点:切到用户消息
|
|
544
|
+
if (lastAssistantIdx === -1 || lastRealUserIdx >= cutIdx - 1) {
|
|
545
|
+
cutIdx = lastRealUserIdx;
|
|
546
|
+
}
|
|
547
|
+
return { cutIdx, lastRealUserIdx };
|
|
548
|
+
}
|
|
549
|
+
// 回注用户指令原文的上限:超长粘贴不应抵消压缩收益
|
|
550
|
+
const MAX_VERBATIM_INSTRUCTION_CHARS = 4000;
|
|
551
|
+
/**
|
|
552
|
+
* 系统合成的 user 文本块,不是用户原话,不得当作指令回注:
|
|
553
|
+
* reminder-sys 注入、压缩摘要、截断通知、中断标记
|
|
554
|
+
*/
|
|
555
|
+
function isSyntheticUserText(text) {
|
|
556
|
+
return text.startsWith(define_1.REMINDER_SYS_OPEN) ||
|
|
557
|
+
text.startsWith(compact_1.COMPACT_SUMMARY_LEAD) ||
|
|
558
|
+
text.startsWith(compact_1.CONTEXT_TRUNCATED_NOTICE_LEAD) ||
|
|
559
|
+
text.trim() === message_1.REQ_INTERRUPT_MSG;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* 提取真实用户消息的正文(跳过系统合成块与非文本块),用于压缩后原样回注。
|
|
563
|
+
* 被压区开头若是上一次压缩的前缀消息,其中回注过的指令块(LATEST_USER_INSTRUCTION_NOTICE 起始)
|
|
564
|
+
* 原样接力,保证用户指令可跨多次压缩存续;摘要块本身跳过,避免摘要被当作指令滚雪球。
|
|
565
|
+
*/
|
|
566
|
+
function extractUserInstructionText(msg) {
|
|
567
|
+
const content = msg.message.content;
|
|
568
|
+
let text = '';
|
|
569
|
+
if (typeof content === 'string') {
|
|
570
|
+
text = isSyntheticUserText(content) ? '' : content.trim();
|
|
571
|
+
}
|
|
572
|
+
else if (Array.isArray(content)) {
|
|
573
|
+
const parts = [];
|
|
574
|
+
for (const block of content) {
|
|
575
|
+
if (block.type !== 'text')
|
|
576
|
+
continue;
|
|
577
|
+
if (block.text.startsWith(compact_1.LATEST_USER_INSTRUCTION_NOTICE)) {
|
|
578
|
+
// 上一次压缩回注的指令块:直接接力,不再拼接其他块
|
|
579
|
+
return block.text.slice(compact_1.LATEST_USER_INSTRUCTION_NOTICE.length).trim();
|
|
336
580
|
}
|
|
581
|
+
if (isSyntheticUserText(block.text))
|
|
582
|
+
continue;
|
|
583
|
+
const trimmed = block.text.trim();
|
|
584
|
+
if (trimmed)
|
|
585
|
+
parts.push(trimmed);
|
|
337
586
|
}
|
|
587
|
+
text = parts.join('\n\n');
|
|
588
|
+
}
|
|
589
|
+
if (text.length > MAX_VERBATIM_INSTRUCTION_CHARS) {
|
|
590
|
+
text = `${text.slice(0, MAX_VERBATIM_INSTRUCTION_CHARS)}\n[... truncated]`;
|
|
338
591
|
}
|
|
592
|
+
return text;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* 自动压缩路径的 compact:exec / conversation:usage 事件。
|
|
596
|
+
* 被压区只是整段上下文的一段,事件按整段口径计算:
|
|
597
|
+
* tokenBefore = 触发时的整段占用(含 system prompt 与工具定义,取自上次 API 响应的 usage),
|
|
598
|
+
* tokenCompact = tokenBefore − 被压区估算 + 新前缀估算。
|
|
599
|
+
*/
|
|
600
|
+
function emitAutoCompactUsage(tokenBefore, messagesToCompact, compactedMessages, sessionId, mode, reason) {
|
|
601
|
+
try {
|
|
602
|
+
const removed = estimateMessagesTokens(messagesToCompact);
|
|
603
|
+
const added = estimateMessagesTokens(compactedMessages);
|
|
604
|
+
const tokenCompact = Math.max(0, tokenBefore - removed + added);
|
|
605
|
+
if (added >= removed) {
|
|
606
|
+
(0, log_1.logWarn)(`[Compact] Compacted prefix (~${added} tokens) is not smaller than the compacted range (~${removed} tokens)`);
|
|
607
|
+
}
|
|
608
|
+
(0, log_1.logDebug)(`[Compact] Usage: before=${tokenBefore}, removed≈${removed}, added≈${added}, after≈${tokenCompact}`);
|
|
609
|
+
const compactExecData = {
|
|
610
|
+
tokenBefore,
|
|
611
|
+
tokenCompact,
|
|
612
|
+
compactRate: tokenBefore > 0 ? calculateCompactRate(tokenBefore, tokenCompact) : 0,
|
|
613
|
+
mode,
|
|
614
|
+
reason,
|
|
615
|
+
};
|
|
616
|
+
const eventBus = compactDependencies.getEventBus();
|
|
617
|
+
eventBus.emit('compact:exec', compactExecData, sessionId);
|
|
618
|
+
eventBus.emit('conversation:usage', {
|
|
619
|
+
usage: { useTokens: tokenCompact, maxTokens: getContextLimit(sessionId), promptTokens: tokenCompact },
|
|
620
|
+
}, sessionId);
|
|
621
|
+
}
|
|
622
|
+
catch (usageError) {
|
|
623
|
+
(0, log_1.logError)(`Failed to emit auto compact usage: ${usageError}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* 自动上下文压缩的主要入口函数
|
|
628
|
+
*
|
|
629
|
+
* 该函数在每次查询前被调用,用于检查对话是否已超出容量需要压缩。
|
|
630
|
+
* 切点由 findCompactCut 决定:保留最近几个工具轮次与未消费尾部,其余历史(含本轮更早的工具轮次)
|
|
631
|
+
* 交给摘要。这样可以保证:
|
|
632
|
+
* 1. tool_use / tool_result 的配对关系不被破坏(切点只落在轮次边界的 assistant 或真实用户消息上)
|
|
633
|
+
* 2. 压缩后前缀是单条 user 消息(与手动 /compact 同形),保留区无论以 user 还是 assistant 开头,
|
|
634
|
+
* 角色交替都合法(连续 user 由 prepareMessagesForApi 合并)
|
|
635
|
+
* 3. 最后一条真实用户消息被压进摘要时,原文回注,模型不丢失用户措辞
|
|
636
|
+
*
|
|
637
|
+
* 执行自动压缩(调用前应先通过 needsAutoCompact 判断是否需要压缩)
|
|
638
|
+
*/
|
|
639
|
+
async function autoCompact(messages, abortController, sessionId, options = {}) {
|
|
640
|
+
const { cutIdx, lastRealUserIdx } = findCompactCut(messages);
|
|
339
641
|
if (lastRealUserIdx === -1) {
|
|
340
642
|
// 没有找到真实用户消息,跳过压缩
|
|
341
643
|
return { changed: false, messages };
|
|
342
644
|
}
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
// 保留当前对话轮次(最后一条真实用户消息及之后的所有内容)
|
|
346
|
-
const messagesToKeep = messages.slice(lastRealUserIdx);
|
|
645
|
+
const messagesToCompact = messages.slice(0, cutIdx);
|
|
646
|
+
const messagesToKeep = messages.slice(cutIdx);
|
|
347
647
|
if (messagesToCompact.length < 2) {
|
|
348
648
|
// 历史消息太少,不值得压缩
|
|
349
649
|
return { changed: false, messages };
|
|
350
650
|
}
|
|
351
|
-
|
|
651
|
+
// 最后一条真实用户消息落入被压区(长工具轮次场景):压缩后原样回注
|
|
652
|
+
const compactedUserMsg = lastRealUserIdx < cutIdx ? messages[lastRealUserIdx] : null;
|
|
653
|
+
// 收集将被压掉的 skill 激活;保留区仍有同名激活的不补(原文还在)
|
|
654
|
+
let compactedSkills = collectSkillActivations(messagesToCompact);
|
|
655
|
+
if (compactedSkills.length > 0) {
|
|
656
|
+
const keptNames = new Set(collectSkillActivations(messagesToKeep).map(a => a.name));
|
|
657
|
+
compactedSkills = compactedSkills.filter(a => !keptNames.has(a.name));
|
|
658
|
+
}
|
|
659
|
+
(0, log_1.logDebug)(`[Compact] Cut at ${cutIdx}/${messages.length} (lastRealUser=${lastRealUserIdx}, userMsgCompacted=${compactedUserMsg !== null}), ` +
|
|
660
|
+
`skills to re-inject: [${compactedSkills.map(a => a.name).join(', ')}]`);
|
|
661
|
+
// 触发时的整段上下文占用(上次 API 响应的 usage,含本次响应输出),作为事件基准
|
|
662
|
+
const tokensBefore = (0, tokens_1.countTokens)(messages);
|
|
663
|
+
const tokenBefore = tokensBefore.inputTokens + tokensBefore.outputTokens;
|
|
664
|
+
const compactResult = await compactMessages(messagesToCompact, abortController, sessionId, { emitUsageEvent: false });
|
|
352
665
|
if (compactResult.kind === 'summary' || compactResult.kind === 'truncated') {
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
666
|
+
// 截断兜底会保留部分历史:激活消息 / 用户消息幸存的不补注,避免重复
|
|
667
|
+
let userMsgSurvived = false;
|
|
668
|
+
if (compactResult.kind === 'truncated') {
|
|
669
|
+
const survivedUuids = new Set(compactResult.messages.map(m => m.uuid));
|
|
670
|
+
compactedSkills = compactedSkills.filter(a => !survivedUuids.has(a.uuid));
|
|
671
|
+
userMsgSurvived = compactedUserMsg !== null && survivedUuids.has(compactedUserMsg.uuid);
|
|
672
|
+
}
|
|
673
|
+
// 压缩后重新注入被压掉的 skill 原文与 skills/rules reminder(原注入随历史被摘要替换而丢失)
|
|
674
|
+
const reminders = await (0, genSystemReminder_1.generatePostCompactReminders)(options.hasSkillTool ?? false, compactedSkills);
|
|
675
|
+
const instruction = compactedUserMsg && !userMsgSurvived ? extractUserInstructionText(compactedUserMsg) : '';
|
|
676
|
+
const instructionBlocks = instruction
|
|
677
|
+
? [{ type: 'text', text: `${compact_1.LATEST_USER_INSTRUCTION_NOTICE}\n\n${instruction}` }]
|
|
678
|
+
: [];
|
|
679
|
+
let compactedMessages;
|
|
680
|
+
if (compactResult.kind === 'summary') {
|
|
681
|
+
// 单条 user 前缀:[reminders..., 包装后的摘要, 用户指令原文]
|
|
682
|
+
compactedMessages = [(0, message_1.buildUserMsg)([
|
|
683
|
+
...reminders,
|
|
684
|
+
{ type: 'text', text: (0, compact_1.wrapCompactSummary)(compactResult.summary) },
|
|
685
|
+
...instructionBlocks,
|
|
686
|
+
])];
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// 截断兜底:前置拼进首条截断通知 user 消息,不新增消息
|
|
690
|
+
const extraBlocks = [...reminders, ...instructionBlocks];
|
|
691
|
+
compactedMessages = extraBlocks.length > 0
|
|
692
|
+
? prependBlocksToLeadingUserMsg(compactResult.messages, extraBlocks)
|
|
693
|
+
: compactResult.messages;
|
|
694
|
+
}
|
|
695
|
+
// 组合结果示例(长工具轮次场景):
|
|
696
|
+
// [prefix(user), assistant(tool_use), toolResult(user), ..., assistant(tool_use), toolResult(user)]
|
|
697
|
+
// 组合结果示例(短轮次 / 新查询场景):
|
|
698
|
+
// [prefix(user), lastRealUserMsg(user), ...] → 连续 user 由 prepareMessagesForApi 合并
|
|
699
|
+
const finalMessages = [...compactedMessages, ...messagesToKeep];
|
|
700
|
+
(0, log_1.logDebug)(`[Compact] Final messages count: ${finalMessages.length}, kept: ${messagesToKeep.length} messages`);
|
|
701
|
+
emitAutoCompactUsage(tokenBefore, messagesToCompact, compactedMessages, sessionId, compactResult.kind, compactResult.kind === 'truncated' ? compactResult.reason : undefined);
|
|
360
702
|
return {
|
|
361
703
|
changed: true,
|
|
362
704
|
messages: finalMessages,
|
|
@@ -398,12 +740,13 @@ exports.NULL_TOOL = {
|
|
|
398
740
|
async function executeAutoCompact(messages, abortController, sessionId, customInstructions) {
|
|
399
741
|
// 使用 null tool 作为占位,避免模型调用任何工具
|
|
400
742
|
const tools = [exports.NULL_TOOL];
|
|
401
|
-
// 将压缩指令作为 user message
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
743
|
+
// 将压缩指令作为 user message 追加到要压缩的历史对话后,再统一规范化:
|
|
744
|
+
// 被压区可能以 user(tool_result) 结尾(自动压缩切在轮次边界),规范化会把连续 user 合并
|
|
745
|
+
// 无自定义指示时 buildCompressionPrompt 返回 COMPRESSION_PROMPT 原文
|
|
746
|
+
const messagesWithPrompt = (0, message_1.prepareMessagesForApi)([
|
|
747
|
+
...messages,
|
|
405
748
|
(0, message_1.buildUserMsg)((0, compact_1.buildCompressionPrompt)(customInstructions))
|
|
406
|
-
];
|
|
749
|
+
]);
|
|
407
750
|
const summaryResponse = await compactDependencies.queryLLM(messagesWithPrompt, [
|
|
408
751
|
{
|
|
409
752
|
type: 'text',
|
|
@@ -432,10 +775,10 @@ async function executeAutoCompact(messages, abortController, sessionId, customIn
|
|
|
432
775
|
// 注意:新用户消息的添加由 checkAutoCompact 统一处理,这里不需要处理
|
|
433
776
|
// 重要:summaryResponse 的 usage 包含了整个压缩过程的 token 数(历史对话 + 压缩指令)
|
|
434
777
|
// 需要修正为压缩后消息的实际 token 数(压缩通知 + 摘要)
|
|
435
|
-
const compactNoticeMessage = (0, message_1.buildUserMsg)(
|
|
436
|
-
The conversation has been automatically compressed due to token limit. Below is a comprehensive summary.`);
|
|
778
|
+
const compactNoticeMessage = (0, message_1.buildUserMsg)(compact_1.COMPACT_RESUME_NOTICE);
|
|
437
779
|
// 修正 usage:压缩后的实际 token 数应该是压缩通知 + 摘要内容
|
|
438
|
-
// 估算:压缩通知约 30 tokens,摘要使用 completion_tokens
|
|
780
|
+
// 估算:压缩通知约 30 tokens,摘要使用 completion_tokens。
|
|
781
|
+
// countTokens 把 input + output 作为"该点的上下文占用",摘要只计入 input,output 置 0 避免双计
|
|
439
782
|
const originalUsage = summaryResponse.message.usage;
|
|
440
783
|
const estimatedNoticeTokens = 30;
|
|
441
784
|
const summaryTokens = originalUsage.completion_tokens || originalUsage.output_tokens || 0;
|
|
@@ -451,15 +794,14 @@ The conversation has been automatically compressed due to token limit. Below is
|
|
|
451
794
|
...originalUsage,
|
|
452
795
|
// 修正 input_tokens:压缩通知 + 摘要内容
|
|
453
796
|
input_tokens: correctedInputTokens,
|
|
454
|
-
|
|
455
|
-
output_tokens: summaryTokens,
|
|
797
|
+
output_tokens: 0,
|
|
456
798
|
// 如果是 OpenAI 格式,也要修正
|
|
457
799
|
prompt_tokens: correctedInputTokens,
|
|
458
|
-
completion_tokens:
|
|
800
|
+
completion_tokens: 0,
|
|
459
801
|
}
|
|
460
802
|
}
|
|
461
803
|
};
|
|
462
|
-
(0, log_1.logDebug)(`[Compact] Corrected summary usage: originalInput=${originalInputTokens}, originalOutput=${originalOutputTokens}, correctedInput=${correctedInputTokens}, correctedOutput
|
|
804
|
+
(0, log_1.logDebug)(`[Compact] Corrected summary usage: originalInput=${originalInputTokens}, originalOutput=${originalOutputTokens}, correctedInput=${correctedInputTokens}, correctedOutput=0`);
|
|
463
805
|
// 构建压缩后的消息列表(只包含压缩通知和摘要,不包含新用户消息)
|
|
464
806
|
const compactedMessages = [compactNoticeMessage, correctedSummaryMessage];
|
|
465
807
|
return {
|