Agent/core/context-compress.ts
2026-08-06 22:17:39 +08:00

202 lines
5.5 KiB
TypeScript

// @ts-nocheck
const DEFAULT_THRESHOLD = 24000;
const DEFAULT_KEEP_RECENT = 6;
const TOOL_CONTENT_SOFT = 1800;
const TOOL_CONTENT_HARD = 800;
export function estimateTokens(messages, toolsCount = 0) {
let chars = 0;
for (const m of messages) {
if (typeof m.content === 'string') {
chars += m.content.length;
}
if (m.toolCalls) {
for (const tc of m.toolCalls) {
chars += JSON.stringify(tc).length;
}
}
chars += (m.role || '').length + 10;
}
chars += toolsCount * 2100;
return Math.ceil(chars / 3.5);
}
export function compressIfNeeded(messages, opts = {}) {
const threshold = opts.threshold || DEFAULT_THRESHOLD;
const keepRecent = opts.keepRecent || DEFAULT_KEEP_RECENT;
const toolsCount = opts.toolsCount || 0;
const originalTokens = estimateTokens(messages, toolsCount);
if (originalTokens <= threshold) {
return {
messages,
compressed: false,
originalTokens,
finalTokens: originalTokens,
hiddenCount: 0,
};
}
let working = slimToolMessages(messages, TOOL_CONTENT_SOFT);
let tokens = estimateTokens(working, toolsCount);
if (tokens <= threshold) {
return {
messages: working,
compressed: true,
originalTokens,
finalTokens: tokens,
hiddenCount: 0,
};
}
working = slimToolMessages(messages, TOOL_CONTENT_HARD);
tokens = estimateTokens(working, toolsCount);
if (tokens <= threshold) {
return {
messages: working,
compressed: true,
originalTokens,
finalTokens: tokens,
hiddenCount: 0,
};
}
const keepStart = findSafeKeepStart(working, keepRecent);
const last = working.slice(keepStart);
const lastUserIdx = (() => {
for (let i = working.length - 1; i >= 0; i--) {
if (working[i].role === 'user') return i;
}
return -1;
})();
const compressed = [];
if (lastUserIdx >= 0 && lastUserIdx < keepStart) {
compressed.push(working[lastUserIdx]);
}
const middleCount = lastUserIdx >= 0 && lastUserIdx < keepStart
? Math.max(0, keepStart - lastUserIdx - 1)
: Math.max(0, keepStart);
if (middleCount > 0) {
compressed.push({
role: 'system',
content: `[上下文已压缩] 已隐藏中间 ${middleCount} 条历史消息,只保留完整的最近工具调用组。请根据仍保留的工具结果回答用户;若信息不足请如实说明。`,
_compressed: true,
});
}
compressed.push(...last);
const finalTokens = estimateTokens(compressed, toolsCount);
return {
messages: compressed,
compressed: true,
originalTokens,
finalTokens,
hiddenCount: middleCount,
};
}
function findSafeKeepStart(messages, minKeep) {
if (!messages.length) return 0;
let start = Math.max(0, messages.length - minKeep);
while (start > 0 && messages[start].role === 'tool') {
start--;
}
if (messages[start]?.role === 'tool') {
while (start > 0 && messages[start].role === 'tool') start--;
}
return start;
}
function slimToolMessages(messages, maxLen) {
return messages.map(m => {
if (m.role !== 'tool') return m;
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
if (content.length <= maxLen) return m;
return {
...m,
content: slimToolContent(content, maxLen),
};
});
}
function slimToolContent(content, maxLen) {
try {
const parsed = JSON.parse(content);
const slim = slimToolObject(parsed);
const out = JSON.stringify(slim);
if (out.length <= maxLen) return out;
return out.slice(0, maxLen) + '…[已截断]';
} catch (_) {
return content.slice(0, maxLen) + '…[已截断]';
}
}
function slimToolObject(obj) {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.length > 3
? [...obj.slice(0, 3), `…另有 ${obj.length - 3} 条已省略`]
: obj;
}
const out = {};
for (const [k, v] of Object.entries(obj)) {
if (k === '_call' || k === 'guidance' || k === 'assistantGuidance' || k === 'ok' || k === 'error') {
out[k] = v;
continue;
}
if (k === 'result' && v && typeof v === 'object') {
out[k] = slimToolObject(v);
continue;
}
if (k === 'overview' || k === 'summary' || k === 'currentEntity' || k === 'lastQueryType') {
out[k] = typeof v === 'object' ? slimToolObject(v) : v;
continue;
}
if (Array.isArray(v)) {
if (v.length === 0) {
out[k] = v;
} else if (typeof v[0] !== 'object') {
out[k] = v.length > 8 ? [...v.slice(0, 8), `…+${v.length - 8}`] : v;
} else {
out[k] = {
_omitted: true,
count: v.length,
sample: slimToolObject(v[0]),
};
}
continue;
}
if (v && typeof v === 'object') {
out[k] = slimToolObject(v);
continue;
}
out[k] = v;
}
return out;
}