@codebolt/agent 5.0.7 → 5.0.8
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/dist/processor-pieces/base/basePreToolCallProcessor.js +5 -2
- package/dist/processor-pieces/messageModifiers/argumentProcessorModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/argumentProcessorModifier.js +2 -2
- package/dist/processor-pieces/messageModifiers/atFileProcessorModifier.js +24 -34
- package/dist/processor-pieces/messageModifiers/chatRecordingModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/chatRecordingModifier.js +2 -2
- package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.js +2 -2
- package/dist/processor-pieces/messageModifiers/directoryContextModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/directoryContextModifier.js +8 -3
- package/dist/processor-pieces/messageModifiers/environmentContextModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/environmentContextModifier.js +1 -1
- package/dist/processor-pieces/messageModifiers/ideContextModifier.js +114 -17
- package/dist/processor-pieces/messageModifiers/memoryImportModifier.d.ts +1 -1
- package/dist/processor-pieces/messageModifiers/memoryImportModifier.js +13 -5
- package/dist/processor-pieces/messageModifiers/toolInjectionModifier.d.ts +5 -0
- package/dist/processor-pieces/messageModifiers/toolInjectionModifier.js +4 -0
- package/dist/processor-pieces/postInferenceProcessors/loopDetectionModifier.js +24 -8
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.d.ts +202 -5
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.js +826 -19
- package/dist/processor-pieces/postToolCallProcessors/index.d.ts +1 -1
- package/dist/processor-pieces/postToolCallProcessors/index.js +2 -1
- package/dist/processor-pieces/postToolCallProcessors/shellProcessorModifier.js +7 -4
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.d.ts +1 -1
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.js +12 -16
- package/dist/processor-pieces/pretoolCallProcessors/toolParameterModifier.d.ts +2 -3
- package/dist/processor-pieces/pretoolCallProcessors/toolParameterModifier.js +1 -2
- package/dist/processor-pieces/utils/messageModifierHelper.js +5 -2
- package/dist/types/InternalTypes.d.ts +8 -6
- package/dist/unified/agent/agent.js +1 -2
- package/dist/unified/agent/codeboltAgent.d.ts +23 -2
- package/dist/unified/agent/codeboltAgent.js +55 -12
- package/dist/unified/agent/tools.d.ts +7 -11
- package/dist/unified/agent/tools.js +12 -3
- package/dist/unified/agent/workflow.js +38 -20
- package/dist/unified/agent/workflowSteps.d.ts +1 -1
- package/dist/unified/agent/workflowSteps.js +58 -23
- package/dist/unified/base/agentStep.js +20 -1
- package/dist/unified/base/initialPromptGenerator.js +3 -1
- package/dist/unified/base/responseExecutor.d.ts +3 -0
- package/dist/unified/base/responseExecutor.js +40 -18
- package/dist/unified/index.d.ts +1 -0
- package/dist/unified/index.js +4 -1
- package/dist/unified/services/LoopDetectionService.d.ts +44 -0
- package/dist/unified/services/LoopDetectionService.js +79 -0
- package/dist/unified/utils/utils.d.ts +20 -1
- package/dist/unified/utils/utils.js +8 -4
- package/package.json +5 -5
|
@@ -2,39 +2,846 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Conversation Compactor Modifier
|
|
4
4
|
* Post-Tool Call processor for compacting conversation history to reduce context size
|
|
5
|
+
*
|
|
6
|
+
* Inspired by gemini-cli's ChatCompressionService approach:
|
|
7
|
+
* - Truncates large tool outputs
|
|
8
|
+
* - Summarizes old conversation history using LLM
|
|
9
|
+
* - Preserves recent context for continuity
|
|
5
10
|
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
6
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.ConversationCompactorModifier = void 0;
|
|
15
|
+
exports.ConversationCompactorModifier = exports.CompressionStatus = void 0;
|
|
8
16
|
const basePostToolCallProcessor_1 = require("../base/basePostToolCallProcessor");
|
|
17
|
+
const codeboltjs_1 = __importDefault(require("@codebolt/codeboltjs"));
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Constants (from gemini-cli)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/** Default threshold for compression - compress when exceeding 50% of model limit */
|
|
22
|
+
const DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5;
|
|
23
|
+
/** Fraction of recent history to preserve - keep last 30% */
|
|
24
|
+
const DEFAULT_COMPRESSION_PRESERVE_THRESHOLD = 0.3;
|
|
25
|
+
/** Maximum token budget for all tool outputs combined */
|
|
26
|
+
const DEFAULT_TOOL_RESPONSE_TOKEN_BUDGET = 50000;
|
|
27
|
+
/** Lines to keep when truncating tool outputs */
|
|
28
|
+
const DEFAULT_TRUNCATE_LINES = 30;
|
|
29
|
+
/** Default model token limit */
|
|
30
|
+
const DEFAULT_MODEL_TOKEN_LIMIT = 128000;
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Model Token Limits Lookup
|
|
33
|
+
// ============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Known model token limits
|
|
36
|
+
* Based on common LLM provider specifications
|
|
37
|
+
*/
|
|
38
|
+
const MODEL_TOKEN_LIMITS = {
|
|
39
|
+
// OpenAI models
|
|
40
|
+
'gpt-4': 8192,
|
|
41
|
+
'gpt-4-32k': 32768,
|
|
42
|
+
'gpt-4-turbo': 128000,
|
|
43
|
+
'gpt-4-turbo-preview': 128000,
|
|
44
|
+
'gpt-4o': 128000,
|
|
45
|
+
'gpt-4o-mini': 128000,
|
|
46
|
+
'gpt-3.5-turbo': 16385,
|
|
47
|
+
'gpt-3.5-turbo-16k': 16385,
|
|
48
|
+
// Anthropic models
|
|
49
|
+
'claude-3-opus': 200000,
|
|
50
|
+
'claude-3-sonnet': 200000,
|
|
51
|
+
'claude-3-haiku': 200000,
|
|
52
|
+
'claude-3-5-sonnet': 200000,
|
|
53
|
+
'claude-2': 100000,
|
|
54
|
+
'claude-2.1': 200000,
|
|
55
|
+
// Google models
|
|
56
|
+
'gemini-pro': 32768,
|
|
57
|
+
'gemini-1.5-pro': 1048576,
|
|
58
|
+
'gemini-1.5-flash': 1048576,
|
|
59
|
+
// Mistral models
|
|
60
|
+
'mistral-large': 32768,
|
|
61
|
+
'mistral-medium': 32768,
|
|
62
|
+
'mistral-small': 32768,
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Gets the token limit for a model
|
|
66
|
+
* Falls back to default if model not found
|
|
67
|
+
*/
|
|
68
|
+
function getModelTokenLimit(modelName) {
|
|
69
|
+
if (!modelName)
|
|
70
|
+
return DEFAULT_MODEL_TOKEN_LIMIT;
|
|
71
|
+
// Try exact match first
|
|
72
|
+
if (MODEL_TOKEN_LIMITS[modelName]) {
|
|
73
|
+
return MODEL_TOKEN_LIMITS[modelName];
|
|
74
|
+
}
|
|
75
|
+
// Try partial match (model names often have version suffixes)
|
|
76
|
+
const lowerModel = modelName.toLowerCase();
|
|
77
|
+
for (const [key, limit] of Object.entries(MODEL_TOKEN_LIMITS)) {
|
|
78
|
+
if (lowerModel.includes(key.toLowerCase()) || key.toLowerCase().includes(lowerModel)) {
|
|
79
|
+
return limit;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return DEFAULT_MODEL_TOKEN_LIMIT;
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Types and Interfaces
|
|
86
|
+
// ============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Compression Status Enum - tracks the result of compression attempts
|
|
89
|
+
*/
|
|
90
|
+
var CompressionStatus;
|
|
91
|
+
(function (CompressionStatus) {
|
|
92
|
+
/** Compression was successful */
|
|
93
|
+
CompressionStatus["COMPRESSED"] = "COMPRESSED";
|
|
94
|
+
/** Compression not needed - under threshold */
|
|
95
|
+
CompressionStatus["NOOP"] = "NOOP";
|
|
96
|
+
/** Compression failed - summary larger than original */
|
|
97
|
+
CompressionStatus["FAILED_INFLATED_TOKEN_COUNT"] = "FAILED_INFLATED_TOKEN_COUNT";
|
|
98
|
+
/** Compression failed - token counting error */
|
|
99
|
+
CompressionStatus["FAILED_TOKEN_COUNT_ERROR"] = "FAILED_TOKEN_COUNT_ERROR";
|
|
100
|
+
/** Compression failed - LLM summarization error */
|
|
101
|
+
CompressionStatus["FAILED_SUMMARIZATION_ERROR"] = "FAILED_SUMMARIZATION_ERROR";
|
|
102
|
+
/** Tool output truncation was applied (without full compression) */
|
|
103
|
+
CompressionStatus["TOOL_OUTPUT_TRUNCATED"] = "TOOL_OUTPUT_TRUNCATED";
|
|
104
|
+
})(CompressionStatus || (exports.CompressionStatus = CompressionStatus = {}));
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Main Class
|
|
107
|
+
// ============================================================================
|
|
9
108
|
class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePostToolCallProcessor {
|
|
10
109
|
constructor(options = {}) {
|
|
110
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
11
111
|
super();
|
|
112
|
+
this.failedCompressionAttempts = 0;
|
|
113
|
+
this.hasFailedCompression = false;
|
|
12
114
|
this.options = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
compactStrategy: 'smart',
|
|
18
|
-
|
|
115
|
+
compressionTokenThreshold: (_a = options.compressionTokenThreshold) !== null && _a !== void 0 ? _a : DEFAULT_COMPRESSION_TOKEN_THRESHOLD,
|
|
116
|
+
preserveThreshold: (_b = options.preserveThreshold) !== null && _b !== void 0 ? _b : DEFAULT_COMPRESSION_PRESERVE_THRESHOLD,
|
|
117
|
+
toolResponseTokenBudget: (_c = options.toolResponseTokenBudget) !== null && _c !== void 0 ? _c : DEFAULT_TOOL_RESPONSE_TOKEN_BUDGET,
|
|
118
|
+
truncateLines: (_d = options.truncateLines) !== null && _d !== void 0 ? _d : DEFAULT_TRUNCATE_LINES,
|
|
119
|
+
compactStrategy: (_e = options.compactStrategy) !== null && _e !== void 0 ? _e : 'smart',
|
|
120
|
+
preserveSystemMessages: (_f = options.preserveSystemMessages) !== null && _f !== void 0 ? _f : true,
|
|
121
|
+
minPreservedUserMessages: (_g = options.minPreservedUserMessages) !== null && _g !== void 0 ? _g : 3,
|
|
122
|
+
enableVerificationPass: (_h = options.enableVerificationPass) !== null && _h !== void 0 ? _h : false,
|
|
123
|
+
modelTokenLimit: (_j = options.modelTokenLimit) !== null && _j !== void 0 ? _j : DEFAULT_MODEL_TOKEN_LIMIT,
|
|
124
|
+
llmRole: (_k = options.llmRole) !== null && _k !== void 0 ? _k : 'summarizer',
|
|
125
|
+
enableLogging: (_l = options.enableLogging) !== null && _l !== void 0 ? _l : false
|
|
19
126
|
};
|
|
20
127
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
128
|
+
// ========================================================================
|
|
129
|
+
// Token Counting Utilities
|
|
130
|
+
// ========================================================================
|
|
131
|
+
/**
|
|
132
|
+
* Estimates token count for a string
|
|
133
|
+
* Uses the ~4 characters per token approximation
|
|
134
|
+
*/
|
|
135
|
+
estimateTokens(text) {
|
|
136
|
+
if (!text)
|
|
137
|
+
return 0;
|
|
138
|
+
return Math.ceil(text.length / 4);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Counts tokens for an array of messages
|
|
142
|
+
*/
|
|
143
|
+
countMessageTokens(messages) {
|
|
144
|
+
if (!messages || !Array.isArray(messages))
|
|
145
|
+
return 0;
|
|
146
|
+
return messages.reduce((total, msg) => {
|
|
147
|
+
if (!msg)
|
|
148
|
+
return total;
|
|
149
|
+
const content = !msg.content ? '' :
|
|
150
|
+
(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
|
|
151
|
+
// Add overhead for role and structure (~4 tokens per message)
|
|
152
|
+
return total + this.estimateTokens(content) + 4;
|
|
153
|
+
}, 0);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Gets content length from a message (handles string and array content)
|
|
157
|
+
*/
|
|
158
|
+
getMessageContentLength(message) {
|
|
159
|
+
if (!message || message.content === undefined || message.content === null) {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
if (typeof message.content === 'string') {
|
|
163
|
+
return message.content.length;
|
|
164
|
+
}
|
|
165
|
+
return JSON.stringify(message.content).length;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Extracts text content from a message
|
|
169
|
+
*/
|
|
170
|
+
getMessageContent(message) {
|
|
171
|
+
if (!message || message.content === undefined || message.content === null) {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
if (typeof message.content === 'string') {
|
|
175
|
+
return message.content;
|
|
176
|
+
}
|
|
177
|
+
// Extract text from content blocks
|
|
178
|
+
if (!Array.isArray(message.content)) {
|
|
179
|
+
return JSON.stringify(message.content);
|
|
180
|
+
}
|
|
181
|
+
return message.content
|
|
182
|
+
.filter(block => block && block.text)
|
|
183
|
+
.map(block => block.text)
|
|
184
|
+
.join('\n');
|
|
185
|
+
}
|
|
186
|
+
// ========================================================================
|
|
187
|
+
// Tool Output Truncation (Phase 1)
|
|
188
|
+
// ========================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a message is a function/tool response
|
|
191
|
+
*/
|
|
192
|
+
isToolResponseMessage(message) {
|
|
193
|
+
if (!message)
|
|
194
|
+
return false;
|
|
195
|
+
if (message.role === 'tool')
|
|
196
|
+
return true;
|
|
197
|
+
if (message.tool_call_id)
|
|
198
|
+
return true;
|
|
199
|
+
// Check for function response in content
|
|
200
|
+
if (typeof message.content === 'object' && message.content !== null) {
|
|
201
|
+
if (Array.isArray(message.content)) {
|
|
202
|
+
return message.content.some(block => block && (block.type === 'tool_result' ||
|
|
203
|
+
block.type === 'function_response'));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Truncates content to a specified number of lines
|
|
210
|
+
* Keeps first half and last half of allowed lines for context
|
|
211
|
+
*/
|
|
212
|
+
truncateToLines(content, maxLines) {
|
|
213
|
+
const lines = content.split('\n');
|
|
214
|
+
if (lines.length <= maxLines) {
|
|
215
|
+
return content;
|
|
216
|
+
}
|
|
217
|
+
// Keep first half and last half of allowed lines for context
|
|
218
|
+
const halfLines = Math.floor(maxLines / 2);
|
|
219
|
+
const firstPart = lines.slice(0, halfLines);
|
|
220
|
+
const lastPart = lines.slice(-halfLines);
|
|
221
|
+
const omittedCount = lines.length - maxLines;
|
|
222
|
+
return [
|
|
223
|
+
...firstPart,
|
|
224
|
+
`\n... [${omittedCount} lines truncated] ...\n`,
|
|
225
|
+
...lastPart
|
|
226
|
+
].join('\n');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Truncates large tool output messages to reduce token count
|
|
230
|
+
* Iterates backwards (prioritizing recent messages) following gemini-cli approach
|
|
231
|
+
*/
|
|
232
|
+
truncateLargeToolOutputs(messages) {
|
|
233
|
+
const result = [...messages];
|
|
234
|
+
let truncated = false;
|
|
235
|
+
let tokensSaved = 0;
|
|
236
|
+
// Calculate current tool response tokens and identify tool message indices
|
|
237
|
+
let toolResponseTokens = 0;
|
|
238
|
+
const toolMessageInfo = [];
|
|
239
|
+
for (let i = 0; i < result.length; i++) {
|
|
240
|
+
const msg = result[i];
|
|
241
|
+
if (msg && (msg.role === 'tool' || this.isToolResponseMessage(msg))) {
|
|
242
|
+
const content = this.getMessageContent(msg);
|
|
243
|
+
const tokens = this.estimateTokens(content);
|
|
244
|
+
toolMessageInfo.push({ index: i, tokens });
|
|
245
|
+
toolResponseTokens += tokens;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// If under budget, no truncation needed
|
|
249
|
+
if (toolResponseTokens <= this.options.toolResponseTokenBudget) {
|
|
250
|
+
return { messages: result, truncated: false, tokensSaved: 0 };
|
|
251
|
+
}
|
|
252
|
+
// Iterate from oldest to newest tool messages for truncation
|
|
253
|
+
// (We truncate older tool outputs first to preserve recent context)
|
|
254
|
+
for (const info of toolMessageInfo) {
|
|
255
|
+
if (toolResponseTokens <= this.options.toolResponseTokenBudget) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
const msg = result[info.index];
|
|
259
|
+
if (!msg)
|
|
260
|
+
continue;
|
|
261
|
+
const originalContent = this.getMessageContent(msg);
|
|
262
|
+
const originalTokens = this.estimateTokens(originalContent);
|
|
263
|
+
// Only truncate if content is substantial
|
|
264
|
+
if (originalTokens > 100) {
|
|
265
|
+
const truncatedContent = this.truncateToLines(originalContent, this.options.truncateLines);
|
|
266
|
+
const newTokens = this.estimateTokens(truncatedContent);
|
|
267
|
+
if (newTokens < originalTokens) {
|
|
268
|
+
// Update the message with truncated content
|
|
269
|
+
const updatedMsg = {
|
|
270
|
+
role: msg.role,
|
|
271
|
+
content: truncatedContent
|
|
272
|
+
};
|
|
273
|
+
if (msg.tool_call_id)
|
|
274
|
+
updatedMsg.tool_call_id = msg.tool_call_id;
|
|
275
|
+
if (msg.tool_calls)
|
|
276
|
+
updatedMsg.tool_calls = msg.tool_calls;
|
|
277
|
+
if (msg.name)
|
|
278
|
+
updatedMsg.name = msg.name;
|
|
279
|
+
result[info.index] = updatedMsg;
|
|
280
|
+
const saved = originalTokens - newTokens;
|
|
281
|
+
tokensSaved += saved;
|
|
282
|
+
toolResponseTokens -= saved;
|
|
283
|
+
truncated = true;
|
|
284
|
+
if (this.options.enableLogging) {
|
|
285
|
+
console.log(`[ConversationCompactor] Truncated tool output at index ${info.index}, saved ${saved} tokens`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { messages: result, truncated, tokensSaved };
|
|
291
|
+
}
|
|
292
|
+
// ========================================================================
|
|
293
|
+
// History Split Point Calculation (Phase 2)
|
|
294
|
+
// ========================================================================
|
|
295
|
+
/**
|
|
296
|
+
* Adjusts split index to the start of the next turn (user message)
|
|
297
|
+
* Ensures we don't split in the middle of a conversation turn
|
|
298
|
+
*/
|
|
299
|
+
adjustToTurnBoundary(messages, index) {
|
|
300
|
+
while (index < messages.length) {
|
|
301
|
+
const msg = messages[index];
|
|
302
|
+
if (!msg) {
|
|
303
|
+
index++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Stop at a user message that isn't a tool response
|
|
307
|
+
if (msg.role === 'user' && !this.isToolResponseMessage(msg)) {
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
// Also stop at assistant message without pending tool calls
|
|
311
|
+
if (msg.role === 'assistant' && (!msg.tool_calls || msg.tool_calls.length === 0)) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
index++;
|
|
315
|
+
}
|
|
316
|
+
return index;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Maps filtered array index back to original messages array
|
|
320
|
+
*/
|
|
321
|
+
mapToOriginalIndex(original, filtered, filteredIndex) {
|
|
322
|
+
if (filteredIndex >= filtered.length)
|
|
323
|
+
return original.length;
|
|
324
|
+
const targetMessage = filtered[filteredIndex];
|
|
325
|
+
return original.findIndex(msg => msg === targetMessage);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Finds the index to split history for compression
|
|
329
|
+
* Returns the index after which messages should be preserved
|
|
330
|
+
*/
|
|
331
|
+
findCompressionSplitIndex(messages) {
|
|
332
|
+
// Filter out system messages if preserving them
|
|
333
|
+
const processableMessages = this.options.preserveSystemMessages
|
|
334
|
+
? messages.filter(m => m.role !== 'system')
|
|
335
|
+
: messages;
|
|
336
|
+
if (processableMessages.length <= this.options.minPreservedUserMessages * 2) {
|
|
337
|
+
// Too few messages to compress
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
// Calculate content lengths for fraction-based splitting
|
|
341
|
+
const contentLengths = processableMessages.map(msg => this.getMessageContentLength(msg));
|
|
342
|
+
const totalLength = contentLengths.reduce((sum, len) => sum + len, 0);
|
|
343
|
+
// Find index after (1 - preserveThreshold) of content
|
|
344
|
+
// e.g., if preserveThreshold is 0.3, find index after 70% of content
|
|
345
|
+
const targetLength = totalLength * (1 - this.options.preserveThreshold);
|
|
346
|
+
let accumulatedLength = 0;
|
|
347
|
+
let splitIndex = 0;
|
|
348
|
+
for (let i = 0; i < contentLengths.length; i++) {
|
|
349
|
+
const len = contentLengths[i];
|
|
350
|
+
if (len !== undefined) {
|
|
351
|
+
accumulatedLength += len;
|
|
352
|
+
}
|
|
353
|
+
if (accumulatedLength >= targetLength) {
|
|
354
|
+
splitIndex = i;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Ensure we don't split too early
|
|
359
|
+
if (splitIndex === 0) {
|
|
360
|
+
splitIndex = Math.floor(processableMessages.length * (1 - this.options.preserveThreshold));
|
|
361
|
+
}
|
|
362
|
+
// Adjust to turn boundary - find the next user message
|
|
363
|
+
splitIndex = this.adjustToTurnBoundary(processableMessages, splitIndex);
|
|
364
|
+
// Map back to original messages array if we filtered system messages
|
|
365
|
+
if (this.options.preserveSystemMessages) {
|
|
366
|
+
return this.mapToOriginalIndex(messages, processableMessages, splitIndex);
|
|
367
|
+
}
|
|
368
|
+
return splitIndex;
|
|
369
|
+
}
|
|
370
|
+
// ========================================================================
|
|
371
|
+
// LLM Summarization (Phase 3 - 'summarize' strategy only)
|
|
372
|
+
// ========================================================================
|
|
373
|
+
/**
|
|
374
|
+
* Generates the compression prompt with state_snapshot XML format
|
|
375
|
+
* Based on gemini-cli's approach
|
|
376
|
+
*/
|
|
377
|
+
getCompressionPrompt(historyToCompress) {
|
|
378
|
+
const historyText = historyToCompress.map((msg, i) => {
|
|
379
|
+
const content = this.getMessageContent(msg);
|
|
380
|
+
const truncated = content.length > 500 ? content.substring(0, 500) + '...' : content;
|
|
381
|
+
return `[${i + 1}] ${msg.role}: ${truncated}`;
|
|
382
|
+
}).join('\n\n');
|
|
383
|
+
return `You are a conversation compression assistant. Analyze the following conversation history and create a concise state snapshot that preserves all critical information.
|
|
384
|
+
|
|
385
|
+
CONVERSATION HISTORY TO COMPRESS:
|
|
386
|
+
${historyText}
|
|
387
|
+
|
|
388
|
+
Generate a state snapshot in the following XML format. Be extremely concise but preserve all critical technical details:
|
|
389
|
+
|
|
390
|
+
<state_snapshot>
|
|
391
|
+
<overall_goal>
|
|
392
|
+
[Describe the user's high-level objective in 1-2 sentences]
|
|
393
|
+
</overall_goal>
|
|
394
|
+
|
|
395
|
+
<active_constraints>
|
|
396
|
+
[List any user-specified rules, preferences, or constraints that must be maintained]
|
|
397
|
+
</active_constraints>
|
|
398
|
+
|
|
399
|
+
<key_knowledge>
|
|
400
|
+
[List crucial technical facts discovered: file structures, dependencies, API details, error patterns, etc.]
|
|
401
|
+
</key_knowledge>
|
|
402
|
+
|
|
403
|
+
<artifact_trail>
|
|
404
|
+
[Track evolution of critical files/symbols that were modified or are important]
|
|
405
|
+
</artifact_trail>
|
|
406
|
+
|
|
407
|
+
<file_system_state>
|
|
408
|
+
[Describe current relevant file system state: created files, modified files, directory structure if relevant]
|
|
409
|
+
</file_system_state>
|
|
410
|
+
|
|
411
|
+
<recent_actions>
|
|
412
|
+
[Fact-based summary of tool calls and their outcomes - what was done, not how]
|
|
413
|
+
</recent_actions>
|
|
414
|
+
|
|
415
|
+
<task_state>
|
|
416
|
+
[Current state of the task: what's complete, what's pending, and the IMMEDIATE next step]
|
|
417
|
+
</task_state>
|
|
418
|
+
</state_snapshot>
|
|
419
|
+
|
|
420
|
+
IMPORTANT:
|
|
421
|
+
- Be extremely concise - aim for maximum information density
|
|
422
|
+
- Preserve technical accuracy - exact file paths, function names, error messages
|
|
423
|
+
- Focus on facts that affect future decisions
|
|
424
|
+
- Do not include conversational pleasantries or redundant information`;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Generates a state snapshot summary using LLM
|
|
428
|
+
*/
|
|
429
|
+
async generateStateSummary(historyToCompress) {
|
|
430
|
+
var _a;
|
|
431
|
+
try {
|
|
432
|
+
const prompt = this.getCompressionPrompt(historyToCompress);
|
|
433
|
+
const response = await codeboltjs_1.default.llm.inference({
|
|
434
|
+
messages: [
|
|
435
|
+
{ role: 'system', content: 'You are a precise conversation compression assistant.' },
|
|
436
|
+
{ role: 'user', content: prompt }
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
const summary = ((_a = response.completion) === null || _a === void 0 ? void 0 : _a.content) || '';
|
|
440
|
+
if (this.options.enableLogging) {
|
|
441
|
+
console.log(`[ConversationCompactor] Generated summary (${summary.length} chars)`);
|
|
442
|
+
}
|
|
443
|
+
return summary;
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
console.error('[ConversationCompactor] Error generating summary:', error);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Optional verification pass - asks LLM to verify and correct the summary
|
|
452
|
+
*/
|
|
453
|
+
async verifySummary(summary, originalHistory) {
|
|
454
|
+
var _a;
|
|
455
|
+
if (!this.options.enableVerificationPass) {
|
|
456
|
+
return summary;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const verificationPrompt = `Review this conversation state snapshot for accuracy and completeness.
|
|
460
|
+
|
|
461
|
+
STATE SNAPSHOT:
|
|
462
|
+
${summary}
|
|
463
|
+
|
|
464
|
+
ORIGINAL MESSAGES COUNT: ${originalHistory.length}
|
|
465
|
+
|
|
466
|
+
If the snapshot is accurate and complete, respond with just the original snapshot.
|
|
467
|
+
If there are inaccuracies or missing critical information, provide a corrected version.
|
|
468
|
+
Keep the same XML format.`;
|
|
469
|
+
const response = await codeboltjs_1.default.llm.inference({
|
|
470
|
+
messages: [
|
|
471
|
+
{ role: 'system', content: 'You are verifying a conversation compression for accuracy.' },
|
|
472
|
+
{ role: 'user', content: verificationPrompt }
|
|
473
|
+
]
|
|
474
|
+
});
|
|
475
|
+
return ((_a = response.completion) === null || _a === void 0 ? void 0 : _a.content) || summary;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
console.warn('[ConversationCompactor] Verification pass failed, using original summary');
|
|
479
|
+
return summary;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// ========================================================================
|
|
483
|
+
// Compression Strategies
|
|
484
|
+
// ========================================================================
|
|
485
|
+
/**
|
|
486
|
+
* Simple compression strategy - just removes old messages
|
|
487
|
+
*/
|
|
488
|
+
async compressSimple(messages) {
|
|
489
|
+
const splitIndex = this.findCompressionSplitIndex(messages);
|
|
490
|
+
if (splitIndex === 0) {
|
|
491
|
+
return {
|
|
492
|
+
compressedMessages: messages,
|
|
493
|
+
metadata: {
|
|
494
|
+
strategy: 'simple',
|
|
495
|
+
messagesCompressed: 0,
|
|
496
|
+
messagesPreserved: messages.length
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
// Preserve system messages
|
|
501
|
+
const systemMessages = this.options.preserveSystemMessages
|
|
502
|
+
? messages.filter(m => m.role === 'system')
|
|
503
|
+
: [];
|
|
504
|
+
// Keep messages after split point
|
|
505
|
+
const preservedMessages = messages.slice(splitIndex);
|
|
506
|
+
// Combine: system messages first, then placeholder, then preserved history
|
|
507
|
+
const compressedMessages = [
|
|
508
|
+
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
509
|
+
{
|
|
510
|
+
role: 'user',
|
|
511
|
+
content: `[Previous conversation context has been compressed. ${splitIndex} messages were removed to manage context length.]`
|
|
512
|
+
},
|
|
513
|
+
...preservedMessages
|
|
514
|
+
];
|
|
515
|
+
return {
|
|
516
|
+
compressedMessages,
|
|
517
|
+
metadata: {
|
|
518
|
+
strategy: 'simple',
|
|
519
|
+
messagesCompressed: splitIndex,
|
|
520
|
+
messagesPreserved: preservedMessages.length,
|
|
521
|
+
toolOutputsTruncated: false
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Smart compression strategy - truncates tool outputs + intelligent message removal
|
|
527
|
+
*/
|
|
528
|
+
async compressSmart(messages) {
|
|
529
|
+
// Phase 1: Truncate large tool outputs
|
|
530
|
+
const { messages: truncatedMessages, truncated } = this.truncateLargeToolOutputs(messages);
|
|
531
|
+
// Check if truncation alone was sufficient
|
|
532
|
+
const tokensAfterTruncation = this.countMessageTokens(truncatedMessages);
|
|
533
|
+
const threshold = this.options.modelTokenLimit * this.options.compressionTokenThreshold;
|
|
534
|
+
if (tokensAfterTruncation < threshold) {
|
|
535
|
+
return {
|
|
536
|
+
compressedMessages: truncatedMessages,
|
|
537
|
+
metadata: {
|
|
538
|
+
strategy: 'smart',
|
|
539
|
+
toolOutputsTruncated: truncated,
|
|
540
|
+
messagesCompressed: 0,
|
|
541
|
+
messagesPreserved: truncatedMessages.length
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
// Phase 2: Remove old messages
|
|
546
|
+
const splitIndex = this.findCompressionSplitIndex(truncatedMessages);
|
|
547
|
+
if (splitIndex === 0) {
|
|
548
|
+
return {
|
|
549
|
+
compressedMessages: truncatedMessages,
|
|
550
|
+
metadata: {
|
|
551
|
+
strategy: 'smart',
|
|
552
|
+
toolOutputsTruncated: truncated,
|
|
553
|
+
messagesCompressed: 0,
|
|
554
|
+
messagesPreserved: truncatedMessages.length
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const systemMessages = this.options.preserveSystemMessages
|
|
559
|
+
? truncatedMessages.filter(m => m.role === 'system')
|
|
560
|
+
: [];
|
|
561
|
+
const preservedMessages = truncatedMessages.slice(splitIndex);
|
|
562
|
+
const compressedMessages = [
|
|
563
|
+
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
564
|
+
{
|
|
565
|
+
role: 'user',
|
|
566
|
+
content: `[Conversation compressed. ${splitIndex} older messages summarized. Tool outputs may have been truncated.]`
|
|
567
|
+
},
|
|
568
|
+
...preservedMessages
|
|
569
|
+
];
|
|
570
|
+
return {
|
|
571
|
+
compressedMessages,
|
|
28
572
|
metadata: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
573
|
+
strategy: 'smart',
|
|
574
|
+
toolOutputsTruncated: truncated,
|
|
575
|
+
messagesCompressed: splitIndex,
|
|
576
|
+
messagesPreserved: preservedMessages.length
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Summarize compression strategy - uses LLM to create state snapshot
|
|
582
|
+
*/
|
|
583
|
+
async compressSummarize(messages) {
|
|
584
|
+
// Phase 1: Truncate large tool outputs
|
|
585
|
+
const { messages: truncatedMessages, truncated } = this.truncateLargeToolOutputs(messages);
|
|
586
|
+
// Phase 2: Find split point
|
|
587
|
+
const splitIndex = this.findCompressionSplitIndex(truncatedMessages);
|
|
588
|
+
if (splitIndex === 0) {
|
|
589
|
+
// Nothing to compress
|
|
590
|
+
return {
|
|
591
|
+
compressedMessages: truncatedMessages,
|
|
592
|
+
metadata: {
|
|
593
|
+
strategy: 'summarize',
|
|
594
|
+
toolOutputsTruncated: truncated,
|
|
595
|
+
messagesCompressed: 0,
|
|
596
|
+
messagesPreserved: truncatedMessages.length
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// Phase 3: Generate summary of messages to compress
|
|
601
|
+
const systemMessages = this.options.preserveSystemMessages
|
|
602
|
+
? truncatedMessages.filter(m => m.role === 'system')
|
|
603
|
+
: [];
|
|
604
|
+
const historyToCompress = truncatedMessages
|
|
605
|
+
.slice(0, splitIndex)
|
|
606
|
+
.filter(m => m.role !== 'system');
|
|
607
|
+
const preservedMessages = truncatedMessages.slice(splitIndex);
|
|
608
|
+
try {
|
|
609
|
+
// Generate LLM summary
|
|
610
|
+
let summary = await this.generateStateSummary(historyToCompress);
|
|
611
|
+
// Optional verification pass
|
|
612
|
+
summary = await this.verifySummary(summary, historyToCompress);
|
|
613
|
+
if (!summary || summary.trim().length === 0) {
|
|
614
|
+
throw new Error('Empty summary generated');
|
|
615
|
+
}
|
|
616
|
+
// Build compressed message array
|
|
617
|
+
const compressedMessages = [
|
|
618
|
+
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
619
|
+
{
|
|
620
|
+
role: 'user',
|
|
621
|
+
content: summary
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
role: 'assistant',
|
|
625
|
+
content: 'Understood. I have processed the conversation state snapshot and am ready to continue from where we left off.'
|
|
626
|
+
},
|
|
627
|
+
...preservedMessages
|
|
628
|
+
];
|
|
629
|
+
return {
|
|
630
|
+
compressedMessages,
|
|
631
|
+
metadata: {
|
|
632
|
+
strategy: 'summarize',
|
|
633
|
+
toolOutputsTruncated: truncated,
|
|
634
|
+
messagesCompressed: historyToCompress.length,
|
|
635
|
+
messagesPreserved: preservedMessages.length
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
// Fall back to smart compression if summarization fails
|
|
641
|
+
console.warn('[ConversationCompactor] Summarization failed, falling back to smart compression:', error);
|
|
642
|
+
this.failedCompressionAttempts++;
|
|
643
|
+
return this.compressSmart(messages);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// ========================================================================
|
|
647
|
+
// Main Entry Point
|
|
648
|
+
// ========================================================================
|
|
649
|
+
async modify(input) {
|
|
650
|
+
var _a, _b, _c;
|
|
651
|
+
const { nextPrompt, rawLLMResponseMessage, tokenLimit } = input;
|
|
652
|
+
try {
|
|
653
|
+
// Safety check: ensure messages array exists
|
|
654
|
+
if (!((_a = nextPrompt === null || nextPrompt === void 0 ? void 0 : nextPrompt.message) === null || _a === void 0 ? void 0 : _a.messages) || !Array.isArray(nextPrompt.message.messages)) {
|
|
655
|
+
if (this.options.enableLogging) {
|
|
656
|
+
console.warn('[ConversationCompactor] No messages array found, skipping compression');
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
nextPrompt,
|
|
660
|
+
shouldExit: false
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const messages = nextPrompt.message.messages;
|
|
664
|
+
// Get model token limit: prefer tokenLimit from LLM response, then lookup by model name, then fallback to config
|
|
665
|
+
const modelName = rawLLMResponseMessage === null || rawLLMResponseMessage === void 0 ? void 0 : rawLLMResponseMessage.model;
|
|
666
|
+
const modelTokenLimit = tokenLimit !== null && tokenLimit !== void 0 ? tokenLimit : (modelName ? getModelTokenLimit(modelName) : this.options.modelTokenLimit);
|
|
667
|
+
// Use actual token count from LLM response if available, otherwise estimate
|
|
668
|
+
const currentTokens = (_c = (_b = rawLLMResponseMessage === null || rawLLMResponseMessage === void 0 ? void 0 : rawLLMResponseMessage.usage) === null || _b === void 0 ? void 0 : _b.prompt_tokens) !== null && _c !== void 0 ? _c : this.countMessageTokens(messages);
|
|
669
|
+
const threshold = modelTokenLimit * this.options.compressionTokenThreshold;
|
|
670
|
+
if (this.options.enableLogging) {
|
|
671
|
+
console.log(`[ConversationCompactor] Current tokens: ${currentTokens}, Threshold: ${threshold}`);
|
|
672
|
+
}
|
|
673
|
+
// Check if compression is needed
|
|
674
|
+
if (currentTokens < threshold) {
|
|
675
|
+
return {
|
|
676
|
+
nextPrompt: {
|
|
677
|
+
...nextPrompt,
|
|
678
|
+
metadata: {
|
|
679
|
+
...nextPrompt.metadata,
|
|
680
|
+
compression: {
|
|
681
|
+
status: CompressionStatus.NOOP,
|
|
682
|
+
originalTokenCount: currentTokens,
|
|
683
|
+
newTokenCount: currentTokens,
|
|
684
|
+
messagesCompressed: 0,
|
|
685
|
+
messagesPreserved: messages.length,
|
|
686
|
+
toolOutputsTruncated: false,
|
|
687
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
688
|
+
timestamp: new Date().toISOString(),
|
|
689
|
+
strategy: this.options.compactStrategy
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
shouldExit: false
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
// Skip if we've had too many failed attempts
|
|
697
|
+
if (this.hasFailedCompression && this.failedCompressionAttempts >= 3) {
|
|
698
|
+
if (this.options.enableLogging) {
|
|
699
|
+
console.warn('[ConversationCompactor] Skipping - too many failed attempts');
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
nextPrompt: {
|
|
703
|
+
...nextPrompt,
|
|
704
|
+
metadata: {
|
|
705
|
+
...nextPrompt.metadata,
|
|
706
|
+
compressionSkipped: true,
|
|
707
|
+
compressionSkipReason: 'Too many failed attempts'
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
shouldExit: false
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
// Apply compression based on strategy
|
|
714
|
+
let result;
|
|
715
|
+
switch (this.options.compactStrategy) {
|
|
716
|
+
case 'simple':
|
|
717
|
+
result = await this.compressSimple(messages);
|
|
718
|
+
break;
|
|
719
|
+
case 'smart':
|
|
720
|
+
result = await this.compressSmart(messages);
|
|
721
|
+
break;
|
|
722
|
+
case 'summarize':
|
|
723
|
+
result = await this.compressSummarize(messages);
|
|
724
|
+
break;
|
|
725
|
+
default:
|
|
726
|
+
result = await this.compressSmart(messages);
|
|
727
|
+
}
|
|
728
|
+
const newTokens = this.countMessageTokens(result.compressedMessages);
|
|
729
|
+
// Verify compression was effective
|
|
730
|
+
if (newTokens >= currentTokens) {
|
|
731
|
+
this.failedCompressionAttempts++;
|
|
732
|
+
this.hasFailedCompression = true;
|
|
733
|
+
if (this.options.enableLogging) {
|
|
734
|
+
console.warn(`[ConversationCompactor] Compression inflated token count: ${currentTokens} -> ${newTokens}`);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
nextPrompt: {
|
|
738
|
+
...nextPrompt,
|
|
739
|
+
metadata: {
|
|
740
|
+
...nextPrompt.metadata,
|
|
741
|
+
compression: {
|
|
742
|
+
status: CompressionStatus.FAILED_INFLATED_TOKEN_COUNT,
|
|
743
|
+
originalTokenCount: currentTokens,
|
|
744
|
+
newTokenCount: newTokens,
|
|
745
|
+
messagesCompressed: 0,
|
|
746
|
+
messagesPreserved: messages.length,
|
|
747
|
+
toolOutputsTruncated: false,
|
|
748
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
749
|
+
timestamp: new Date().toISOString(),
|
|
750
|
+
strategy: this.options.compactStrategy
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
shouldExit: false
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// Build final compression metadata
|
|
758
|
+
const compressionMetadata = {
|
|
759
|
+
status: CompressionStatus.COMPRESSED,
|
|
760
|
+
originalTokenCount: currentTokens,
|
|
761
|
+
newTokenCount: newTokens,
|
|
762
|
+
messagesCompressed: result.metadata.messagesCompressed || 0,
|
|
763
|
+
messagesPreserved: result.metadata.messagesPreserved || 0,
|
|
764
|
+
toolOutputsTruncated: result.metadata.toolOutputsTruncated || false,
|
|
765
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
766
|
+
timestamp: new Date().toISOString(),
|
|
32
767
|
strategy: this.options.compactStrategy
|
|
768
|
+
};
|
|
769
|
+
if (this.options.enableLogging) {
|
|
770
|
+
console.log(`[ConversationCompactor] Compression complete: ${currentTokens} -> ${newTokens} tokens (saved ${currentTokens - newTokens})`);
|
|
33
771
|
}
|
|
772
|
+
return {
|
|
773
|
+
nextPrompt: {
|
|
774
|
+
message: {
|
|
775
|
+
...nextPrompt.message,
|
|
776
|
+
messages: result.compressedMessages
|
|
777
|
+
},
|
|
778
|
+
metadata: {
|
|
779
|
+
...nextPrompt.metadata,
|
|
780
|
+
compression: compressionMetadata,
|
|
781
|
+
compactedBy: 'ConversationCompactorModifier',
|
|
782
|
+
compactedAt: compressionMetadata.timestamp
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
shouldExit: false
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
console.error('[ConversationCompactor] Error during compression:', error);
|
|
790
|
+
this.failedCompressionAttempts++;
|
|
791
|
+
this.hasFailedCompression = true;
|
|
792
|
+
return {
|
|
793
|
+
nextPrompt: {
|
|
794
|
+
...nextPrompt,
|
|
795
|
+
metadata: {
|
|
796
|
+
...nextPrompt.metadata,
|
|
797
|
+
compression: {
|
|
798
|
+
status: CompressionStatus.FAILED_SUMMARIZATION_ERROR,
|
|
799
|
+
originalTokenCount: this.countMessageTokens(nextPrompt.message.messages),
|
|
800
|
+
newTokenCount: this.countMessageTokens(nextPrompt.message.messages),
|
|
801
|
+
messagesCompressed: 0,
|
|
802
|
+
messagesPreserved: nextPrompt.message.messages.length,
|
|
803
|
+
toolOutputsTruncated: false,
|
|
804
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
805
|
+
timestamp: new Date().toISOString(),
|
|
806
|
+
strategy: this.options.compactStrategy
|
|
807
|
+
},
|
|
808
|
+
compressionError: error instanceof Error ? error.message : 'Unknown error'
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
shouldExit: false
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// ========================================================================
|
|
816
|
+
// Public Utility Methods
|
|
817
|
+
// ========================================================================
|
|
818
|
+
/**
|
|
819
|
+
* Resets the compression state (useful for new conversations)
|
|
820
|
+
*/
|
|
821
|
+
resetCompressionState() {
|
|
822
|
+
this.failedCompressionAttempts = 0;
|
|
823
|
+
this.hasFailedCompression = false;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Gets the current compression statistics
|
|
827
|
+
*/
|
|
828
|
+
getCompressionStats() {
|
|
829
|
+
return {
|
|
830
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
831
|
+
hasFailedCompression: this.hasFailedCompression,
|
|
832
|
+
options: this.options
|
|
34
833
|
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Forces compression regardless of threshold (for testing or manual trigger)
|
|
837
|
+
*/
|
|
838
|
+
async forceCompress(messages) {
|
|
839
|
+
const result = await (this.options.compactStrategy === 'summarize'
|
|
840
|
+
? this.compressSummarize(messages)
|
|
841
|
+
: this.compressSmart(messages));
|
|
35
842
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
843
|
+
messages: result.compressedMessages,
|
|
844
|
+
metadata: result.metadata
|
|
38
845
|
};
|
|
39
846
|
}
|
|
40
847
|
}
|