小白龙 Bailongma - 初始提交
自主操作员与思考搭档系统。 包含 orchestrator-v2 多Agent编排层、后台意识引擎、记忆系统、ACUI 组件。
This commit is contained in:
149
orchestrator-v2/context-compressor.js
Normal file
149
orchestrator-v2/context-compressor.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ContextCompressor — automatic context window compression
|
||||
// Monitors session token usage and produces structured summaries
|
||||
// when token budgets are exceeded. Supports iterative compression
|
||||
// (compressed sessions can be further compressed).
|
||||
class ContextCompressor {
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.maxTokensBeforeCompress = config.maxTokensBeforeCompress || 20000;
|
||||
this.minCompressionGain = config.minCompressionGain || 0.1; // 10% minimum gain
|
||||
this.summaryTemplate = config.summaryTemplate || this._defaultTemplate();
|
||||
}
|
||||
|
||||
_defaultTemplate() {
|
||||
return {
|
||||
activeTask: '',
|
||||
goal: '',
|
||||
progress: [],
|
||||
decisions: [],
|
||||
blockers: [],
|
||||
openQuestions: [],
|
||||
keyFiles: [],
|
||||
remainingWork: [],
|
||||
notes: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Check if a session needs compression
|
||||
shouldCompress(sessionStore, sessionId) {
|
||||
var tokenCount = sessionStore.getTotalTokenCount(sessionId);
|
||||
return tokenCount > this.maxTokensBeforeCompress;
|
||||
}
|
||||
|
||||
// Compress a session: produce summary and mark as compressed
|
||||
async compress(sessionStore, sessionId, childSessionId) {
|
||||
var session = sessionStore.getSession(sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
var messages = sessionStore.getMessages(sessionId);
|
||||
var summary = this._buildSummary(session, messages);
|
||||
|
||||
// Mark current session as compressed
|
||||
sessionStore.compressSession(sessionId, summary.summary, childSessionId);
|
||||
|
||||
// If there was a previous compression, propagate context
|
||||
if (session.parent_id) {
|
||||
var parentChain = sessionStore.getSessionChain(sessionId);
|
||||
summary.compressionChain = parentChain.length;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Build a structured summary from session data and messages
|
||||
_buildSummary(session, messages) {
|
||||
var summary = JSON.parse(JSON.stringify(this.summaryTemplate));
|
||||
|
||||
// Extract from session metadata
|
||||
summary.activeTask = session.task || '';
|
||||
if (session.summary) {
|
||||
summary.notes = session.summary;
|
||||
}
|
||||
|
||||
// Extract from messages (last N messages for most recent context)
|
||||
var recentMsgs = messages.slice(-20);
|
||||
|
||||
for (var i = 0; i < recentMsgs.length; i++) {
|
||||
var msg = recentMsgs[i];
|
||||
var content = msg.content || '';
|
||||
|
||||
// Look for decision patterns
|
||||
var decisionMatch = content.match(/决定|选择|采用|使用|改用|确定|确认.*方案/i);
|
||||
if (decisionMatch) {
|
||||
var snippet = content.slice(Math.max(0, decisionMatch.index - 20), decisionMatch.index + 40);
|
||||
summary.decisions.push(snippet);
|
||||
}
|
||||
|
||||
// Look for blocker patterns
|
||||
var blockerMatch = content.match(/阻塞|卡住|问题|错误|失败|报错|无法|不能|不行/i);
|
||||
if (blockerMatch) {
|
||||
var snippet = content.slice(Math.max(0, blockerMatch.index - 20), blockerMatch.index + 40);
|
||||
summary.blockers.push(snippet);
|
||||
}
|
||||
|
||||
// Look for file references
|
||||
var fileMatch = content.match(/[A-Za-z]:\\[^\s,,。;;::!!??()()\[\]【】{}"']+/g);
|
||||
if (fileMatch) {
|
||||
for (var j = 0; j < fileMatch.length; j++) {
|
||||
if (summary.keyFiles.indexOf(fileMatch[j]) < 0) {
|
||||
summary.keyFiles.push(fileMatch[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for progress markers
|
||||
if (content.includes('完成') || content.includes('成功') || content.includes('通过')) {
|
||||
summary.progress.push(content.slice(0, 80));
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and trim
|
||||
summary.decisions = this._unique(summary.decisions).slice(0, 10);
|
||||
summary.blockers = this._unique(summary.blockers).slice(0, 10);
|
||||
summary.keyFiles = this._unique(summary.keyFiles).slice(0, 15);
|
||||
summary.progress = this._unique(summary.progress).slice(0, 10);
|
||||
|
||||
// Build condensed summary string
|
||||
var summaryStr = 'Task: ' + (summary.activeTask || 'N/A');
|
||||
if (summary.progress.length) summaryStr += ' | Progress: ' + summary.progress.length + ' items';
|
||||
if (summary.decisions.length) summaryStr += ' | Decisions: ' + summary.decisions.length;
|
||||
if (summary.blockers.length) summaryStr += ' | Blockers: ' + summary.blockers.length;
|
||||
if (summary.keyFiles.length) summaryStr += ' | Files: ' + summary.keyFiles.length;
|
||||
summary.summary = summaryStr;
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Estimate token count for a string (approx: 1 token ~= 2 CJK chars or 4 ASCII chars)
|
||||
estimateTokens(text) {
|
||||
if (!text) return 0;
|
||||
var cjkCount = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g) || []).length;
|
||||
var asciiCount = text.length - cjkCount;
|
||||
return Math.ceil(cjkCount / 1.5) + Math.ceil(asciiCount / 4);
|
||||
}
|
||||
|
||||
// Check compression effectiveness (anti-thrash)
|
||||
compressionGain(sessionStore, sessionId) {
|
||||
var before = sessionStore.getTotalTokenCount(sessionId);
|
||||
var session = sessionStore.getSession(sessionId);
|
||||
if (!before || !session) return 0;
|
||||
|
||||
var summaryLen = this.estimateTokens(session.summary || '');
|
||||
if (before === 0) return 0;
|
||||
|
||||
return (before - summaryLen) / before;
|
||||
}
|
||||
|
||||
_unique(arr) {
|
||||
var result = [];
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (result.indexOf(arr[i]) < 0) result.push(arr[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContextCompressor;
|
||||
Reference in New Issue
Block a user