@codebolt/agent 6.0.0 → 6.1.2
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 +118 -153
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/processor-pieces/messageModifiers/chatHistoryMessageModifier.js +2 -4
- package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.d.ts +81 -0
- package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.js +316 -0
- package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.js +2 -19
- package/dist/processor-pieces/messageModifiers/directoryContextModifier.js +2 -6
- package/dist/processor-pieces/messageModifiers/environmentContextModifier.js +2 -7
- package/dist/processor-pieces/messageModifiers/ideContextModifier.js +2 -21
- package/dist/processor-pieces/messageModifiers/index.d.ts +3 -0
- package/dist/processor-pieces/messageModifiers/index.js +7 -1
- package/dist/processor-pieces/messageModifiers/toolInjectionModifier.js +2 -1
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.d.ts +34 -5
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.js +374 -89
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.d.ts +3 -0
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.js +50 -27
- package/dist/unified/agent/agent.d.ts +10 -0
- package/dist/unified/agent/agent.js +198 -10
- package/dist/unified/agent/codeboltAgent.d.ts +19 -100
- package/dist/unified/agent/codeboltAgent.js +209 -109
- package/dist/unified/base/agentStep.js +17 -17
- package/dist/unified/base/initialPromptGenerator.js +13 -31
- package/dist/unified/base/promptContext.d.ts +13 -0
- package/dist/unified/base/promptContext.js +213 -0
- package/dist/unified/base/responseExecutor.d.ts +7 -19
- package/dist/unified/base/responseExecutor.js +280 -259
- package/dist/unified/index.d.ts +9 -0
- package/dist/unified/index.js +20 -13
- package/dist/unified/services/CompressionCoordinator.d.ts +67 -0
- package/dist/unified/services/CompressionCoordinator.js +214 -0
- package/dist/unified/services/compaction/autoCompact.d.ts +52 -0
- package/dist/unified/services/compaction/autoCompact.js +294 -0
- package/dist/unified/services/compaction/compactionOrchestrator.d.ts +78 -0
- package/dist/unified/services/compaction/compactionOrchestrator.js +230 -0
- package/dist/unified/services/compaction/contextCollapse.d.ts +63 -0
- package/dist/unified/services/compaction/contextCollapse.js +291 -0
- package/dist/unified/services/compaction/microCompact.d.ts +34 -0
- package/dist/unified/services/compaction/microCompact.js +195 -0
- package/dist/unified/services/compaction/postCompactCleanup.d.ts +15 -0
- package/dist/unified/services/compaction/postCompactCleanup.js +37 -0
- package/dist/unified/services/compaction/reactiveCompact.d.ts +65 -0
- package/dist/unified/services/compaction/reactiveCompact.js +301 -0
- package/dist/unified/services/compaction/snipCompact.d.ts +31 -0
- package/dist/unified/services/compaction/snipCompact.js +124 -0
- package/dist/unified/services/compaction/types.d.ts +66 -0
- package/dist/unified/services/compaction/types.js +39 -0
- package/package.json +25 -29
|
@@ -111,6 +111,16 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
111
111
|
super();
|
|
112
112
|
this.failedCompressionAttempts = 0;
|
|
113
113
|
this.hasFailedCompression = false;
|
|
114
|
+
this.lastCompressionTokenCount = 0;
|
|
115
|
+
this.stepsSinceLastCompression = 0;
|
|
116
|
+
// ========================================================================
|
|
117
|
+
// Token Counting Utilities
|
|
118
|
+
// ========================================================================
|
|
119
|
+
/**
|
|
120
|
+
* Calibrated characters-per-token ratio.
|
|
121
|
+
* Starts at 4 (rough default) but gets refined when actual API usage data is available.
|
|
122
|
+
*/
|
|
123
|
+
this.charsPerToken = 4;
|
|
114
124
|
this.options = {
|
|
115
125
|
compressionTokenThreshold: (_a = options.compressionTokenThreshold) !== null && _a !== void 0 ? _a : DEFAULT_COMPRESSION_TOKEN_THRESHOLD,
|
|
116
126
|
preserveThreshold: (_b = options.preserveThreshold) !== null && _b !== void 0 ? _b : DEFAULT_COMPRESSION_PRESERVE_THRESHOLD,
|
|
@@ -125,17 +135,41 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
125
135
|
enableLogging: (_l = options.enableLogging) !== null && _l !== void 0 ? _l : false
|
|
126
136
|
};
|
|
127
137
|
}
|
|
128
|
-
// ========================================================================
|
|
129
|
-
// Token Counting Utilities
|
|
130
|
-
// ========================================================================
|
|
131
138
|
/**
|
|
132
139
|
* Estimates token count for a string
|
|
133
|
-
* Uses
|
|
140
|
+
* Uses calibrated chars-per-token ratio (refined by actual API usage when available)
|
|
134
141
|
*/
|
|
135
142
|
estimateTokens(text) {
|
|
136
143
|
if (!text)
|
|
137
144
|
return 0;
|
|
138
|
-
return Math.ceil(text.length /
|
|
145
|
+
return Math.ceil(text.length / this.charsPerToken);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Calibrates the chars-per-token ratio using actual API usage data.
|
|
149
|
+
* Called when rawLLMResponseMessage.usage is available to improve estimation accuracy.
|
|
150
|
+
*/
|
|
151
|
+
calibrateTokenEstimation(messages, usage) {
|
|
152
|
+
if (!(usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) || usage.prompt_tokens <= 0)
|
|
153
|
+
return;
|
|
154
|
+
// Calculate total characters in messages that were sent as the prompt
|
|
155
|
+
// (exclude the last assistant message which is the completion)
|
|
156
|
+
let promptChars = 0;
|
|
157
|
+
for (const msg of messages) {
|
|
158
|
+
if (!msg)
|
|
159
|
+
continue;
|
|
160
|
+
const content = typeof msg.content === 'string'
|
|
161
|
+
? msg.content
|
|
162
|
+
: (msg.content ? JSON.stringify(msg.content) : '');
|
|
163
|
+
promptChars += content.length;
|
|
164
|
+
}
|
|
165
|
+
if (promptChars > 0) {
|
|
166
|
+
// Calculate actual ratio, but clamp to reasonable bounds (2-6 chars per token)
|
|
167
|
+
const actualRatio = promptChars / usage.prompt_tokens;
|
|
168
|
+
this.charsPerToken = Math.max(2, Math.min(6, actualRatio));
|
|
169
|
+
if (this.options.enableLogging) {
|
|
170
|
+
console.log(`[ConversationCompactor] Calibrated chars/token ratio to ${this.charsPerToken.toFixed(2)} (from API usage: ${usage.prompt_tokens} tokens, ${promptChars} chars)`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
139
173
|
}
|
|
140
174
|
/**
|
|
141
175
|
* Counts tokens for an array of messages
|
|
@@ -152,18 +186,6 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
152
186
|
return total + this.estimateTokens(content) + 4;
|
|
153
187
|
}, 0);
|
|
154
188
|
}
|
|
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
189
|
/**
|
|
168
190
|
* Extracts text content from a message
|
|
169
191
|
*/
|
|
@@ -205,6 +227,41 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
205
227
|
}
|
|
206
228
|
return false;
|
|
207
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Finds the original user request (first non-system, non-tool user message)
|
|
232
|
+
*/
|
|
233
|
+
getOriginalUserMessage(messages) {
|
|
234
|
+
return messages.find(m => m.role === 'user' && !this.isToolResponseMessage(m)) || null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Inserts the original user message right after system messages if it's
|
|
238
|
+
* not already present in the preserved messages.
|
|
239
|
+
*/
|
|
240
|
+
ensureOriginalUserMessage(compressedMessages, originalMessages) {
|
|
241
|
+
var _a;
|
|
242
|
+
const originalUserMessage = this.getOriginalUserMessage(originalMessages);
|
|
243
|
+
if (!originalUserMessage)
|
|
244
|
+
return compressedMessages;
|
|
245
|
+
// Check if the original user message is already in compressed messages
|
|
246
|
+
if (compressedMessages.includes(originalUserMessage))
|
|
247
|
+
return compressedMessages;
|
|
248
|
+
// Find insertion point: right after the last system message (or at index 0)
|
|
249
|
+
let insertIndex = 0;
|
|
250
|
+
for (let i = 0; i < compressedMessages.length; i++) {
|
|
251
|
+
if (((_a = compressedMessages[i]) === null || _a === void 0 ? void 0 : _a.role) === 'system') {
|
|
252
|
+
insertIndex = i + 1;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const result = [...compressedMessages];
|
|
259
|
+
result.splice(insertIndex, 0, {
|
|
260
|
+
role: 'user',
|
|
261
|
+
content: `[Original user request]: ${this.getMessageContent(originalUserMessage)}`
|
|
262
|
+
});
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
208
265
|
/**
|
|
209
266
|
* Truncates content to a specified number of lines
|
|
210
267
|
* Keeps first half and last half of allowed lines for context
|
|
@@ -303,11 +360,24 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
303
360
|
index++;
|
|
304
361
|
continue;
|
|
305
362
|
}
|
|
306
|
-
//
|
|
363
|
+
// Never split right after an assistant with tool_calls
|
|
364
|
+
// (tool responses must follow)
|
|
365
|
+
if (index > 0) {
|
|
366
|
+
const prevMsg = messages[index - 1];
|
|
367
|
+
if ((prevMsg === null || prevMsg === void 0 ? void 0 : prevMsg.role) === 'assistant' && prevMsg.tool_calls && prevMsg.tool_calls.length > 0) {
|
|
368
|
+
index++;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Never split on a tool response message
|
|
373
|
+
if (msg.role === 'tool' || this.isToolResponseMessage(msg)) {
|
|
374
|
+
index++;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
// Safe to split at user message (non-tool) or assistant without pending tool calls
|
|
307
378
|
if (msg.role === 'user' && !this.isToolResponseMessage(msg)) {
|
|
308
379
|
break;
|
|
309
380
|
}
|
|
310
|
-
// Also stop at assistant message without pending tool calls
|
|
311
381
|
if (msg.role === 'assistant' && (!msg.tool_calls || msg.tool_calls.length === 0)) {
|
|
312
382
|
break;
|
|
313
383
|
}
|
|
@@ -337,29 +407,33 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
337
407
|
// Too few messages to compress
|
|
338
408
|
return 0;
|
|
339
409
|
}
|
|
340
|
-
//
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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;
|
|
410
|
+
// Identify turn boundaries (each turn starts with a user message)
|
|
411
|
+
const turnStartIndices = [];
|
|
412
|
+
for (let i = 0; i < processableMessages.length; i++) {
|
|
413
|
+
const msg = processableMessages[i];
|
|
414
|
+
if (msg && msg.role === 'user' && !this.isToolResponseMessage(msg)) {
|
|
415
|
+
turnStartIndices.push(i);
|
|
356
416
|
}
|
|
357
417
|
}
|
|
358
|
-
|
|
359
|
-
|
|
418
|
+
if (turnStartIndices.length <= this.options.minPreservedUserMessages) {
|
|
419
|
+
// Too few turns to compress
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
// Calculate how many turns to preserve based on preserveThreshold
|
|
423
|
+
const turnsToPreserve = Math.max(this.options.minPreservedUserMessages, Math.ceil(turnStartIndices.length * this.options.preserveThreshold));
|
|
424
|
+
// The split point is at the start of the first preserved turn
|
|
425
|
+
// We always keep the first user message (original task) separately via ensureOriginalUserMessage,
|
|
426
|
+
// so we split to keep the last N turns
|
|
427
|
+
const turnsToRemove = turnStartIndices.length - turnsToPreserve;
|
|
428
|
+
if (turnsToRemove <= 0) {
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
// Split at the boundary of the last turn to remove
|
|
432
|
+
let splitIndex = turnStartIndices[turnsToRemove];
|
|
433
|
+
if (splitIndex === undefined) {
|
|
360
434
|
splitIndex = Math.floor(processableMessages.length * (1 - this.options.preserveThreshold));
|
|
361
435
|
}
|
|
362
|
-
// Adjust to turn boundary
|
|
436
|
+
// Adjust to turn boundary for safety (handles tool response edge cases)
|
|
363
437
|
splitIndex = this.adjustToTurnBoundary(processableMessages, splitIndex);
|
|
364
438
|
// Map back to original messages array if we filtered system messages
|
|
365
439
|
if (this.options.preserveSystemMessages) {
|
|
@@ -377,51 +451,64 @@ class ConversationCompactorModifier extends basePostToolCallProcessor_1.BasePost
|
|
|
377
451
|
getCompressionPrompt(historyToCompress) {
|
|
378
452
|
const historyText = historyToCompress.map((msg, i) => {
|
|
379
453
|
const content = this.getMessageContent(msg);
|
|
380
|
-
|
|
381
|
-
return `[${i + 1}] ${msg.role}: ${truncated}`;
|
|
454
|
+
return `[${i + 1}] ${msg.role}: ${content}`;
|
|
382
455
|
}).join('\n\n');
|
|
383
|
-
return `You are a conversation compression assistant.
|
|
456
|
+
return `You are a conversation compression assistant. Your job is to create a detailed, structured summary that preserves ALL critical context so work can continue seamlessly.
|
|
384
457
|
|
|
385
458
|
CONVERSATION HISTORY TO COMPRESS:
|
|
386
459
|
${historyText}
|
|
387
460
|
|
|
388
|
-
|
|
461
|
+
First, analyze the conversation in a <thinking> block to identify what information is essential for continuing the work. Then produce the summary.
|
|
462
|
+
|
|
463
|
+
<thinking>
|
|
464
|
+
Analyze: What is the user's core task? What technical decisions were made? What files were read/modified? What problems were encountered and how were they solved? What is the current state of work?
|
|
465
|
+
</thinking>
|
|
389
466
|
|
|
390
|
-
|
|
391
|
-
<overall_goal>
|
|
392
|
-
[Describe the user's high-level objective in 1-2 sentences]
|
|
393
|
-
</overall_goal>
|
|
467
|
+
Then generate the summary with ALL of the following sections. If a section is not applicable, write "N/A" but do NOT omit the section.
|
|
394
468
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
469
|
+
<summary>
|
|
470
|
+
1. PRIMARY REQUEST
|
|
471
|
+
[The user's original, complete request — use verbatim quotes where possible]
|
|
398
472
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
</key_knowledge>
|
|
473
|
+
2. TASK EVOLUTION
|
|
474
|
+
[How the task evolved through the conversation. Include verbatim quotes of key user instructions or clarifications that changed the direction of work]
|
|
402
475
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
</artifact_trail>
|
|
476
|
+
3. KEY TECHNICAL CONCEPTS
|
|
477
|
+
[Technical details critical for continuing: architecture decisions, design patterns chosen, algorithms, data structures, API contracts, configuration values]
|
|
406
478
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
479
|
+
4. FILES AND CODE
|
|
480
|
+
[Every file that was read, created, or modified. For each file include:
|
|
481
|
+
- Full file path
|
|
482
|
+
- What was done (read/created/modified)
|
|
483
|
+
- Key content or changes made (include exact code snippets for critical changes)
|
|
484
|
+
- Current state of the file]
|
|
410
485
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
486
|
+
5. PROBLEM SOLVING
|
|
487
|
+
[Problems encountered and their solutions:
|
|
488
|
+
- Error messages (verbatim)
|
|
489
|
+
- Root causes identified
|
|
490
|
+
- Solutions applied
|
|
491
|
+
- Workarounds in place]
|
|
414
492
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
</task_state>
|
|
418
|
-
</state_snapshot>
|
|
493
|
+
6. PENDING TASKS
|
|
494
|
+
[Tasks that were mentioned but NOT yet completed. Be specific about what remains.]
|
|
419
495
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
496
|
+
7. CURRENT WORK STATE
|
|
497
|
+
[Exactly where the work left off — what was the last action taken and what was its result?]
|
|
498
|
+
|
|
499
|
+
8. NEXT STEP
|
|
500
|
+
[The single most logical next action to take to continue the work]
|
|
501
|
+
|
|
502
|
+
9. REQUIRED FILES
|
|
503
|
+
[List of file paths that the agent will need to re-read to continue working effectively. These are files whose contents were important context but will be lost after compression.]
|
|
504
|
+
</summary>
|
|
505
|
+
|
|
506
|
+
CRITICAL RULES:
|
|
507
|
+
- Preserve ALL file paths exactly as they appeared
|
|
508
|
+
- Preserve ALL code snippets, function names, variable names, error messages verbatim
|
|
509
|
+
- Include the FULL original user request, not a paraphrase
|
|
510
|
+
- Do not summarize away technical details — they are needed to continue work
|
|
511
|
+
- Focus on facts and specifics, not general descriptions`;
|
|
425
512
|
}
|
|
426
513
|
/**
|
|
427
514
|
* Generates a state snapshot summary using LLM
|
|
@@ -479,6 +566,122 @@ Keep the same XML format.`;
|
|
|
479
566
|
return summary;
|
|
480
567
|
}
|
|
481
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Extracts file paths from the "Required Files" section of a summary.
|
|
571
|
+
* Looks for section 9 (REQUIRED FILES) and extracts paths listed there.
|
|
572
|
+
*/
|
|
573
|
+
extractRequiredFiles(summary) {
|
|
574
|
+
const files = [];
|
|
575
|
+
// Match section "9. REQUIRED FILES" through the next section or end of summary
|
|
576
|
+
const sectionMatch = summary.match(/9\.\s*REQUIRED FILES\s*\n([\s\S]*?)(?=\n\d+\.\s|\n<\/summary>|$)/i);
|
|
577
|
+
if (!sectionMatch || !sectionMatch[1])
|
|
578
|
+
return files;
|
|
579
|
+
const sectionContent = sectionMatch[1].trim();
|
|
580
|
+
if (sectionContent === 'N/A' || sectionContent === 'None')
|
|
581
|
+
return files;
|
|
582
|
+
// Extract file paths: lines starting with -, *, or just paths starting with /
|
|
583
|
+
const lines = sectionContent.split('\n');
|
|
584
|
+
for (const line of lines) {
|
|
585
|
+
const trimmed = line.trim().replace(/^[-*•]\s*/, '');
|
|
586
|
+
// Match file paths (absolute or relative)
|
|
587
|
+
const pathMatch = trimmed.match(/^[`'"]*([^\s`'"]+\.[a-zA-Z0-9]+)[`'"]*/) ||
|
|
588
|
+
trimmed.match(/^[`'"]*([/~][^\s`'"]+)[`'"]*/) ||
|
|
589
|
+
trimmed.match(/^[`'"]*([a-zA-Z][^\s`'"]*\/[^\s`'"]+)[`'"]*$/);
|
|
590
|
+
if (pathMatch && pathMatch[1]) {
|
|
591
|
+
files.push(pathMatch[1]);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return files;
|
|
595
|
+
}
|
|
596
|
+
// ========================================================================
|
|
597
|
+
// File-Read Deduplication (Pre-compression)
|
|
598
|
+
// ========================================================================
|
|
599
|
+
/**
|
|
600
|
+
* Deduplicates repeated file reads in conversation history.
|
|
601
|
+
* When the same file is read multiple times, older reads are replaced with
|
|
602
|
+
* a short note, keeping only the latest read for each file path.
|
|
603
|
+
* This reduces token count before full compression kicks in.
|
|
604
|
+
*/
|
|
605
|
+
deduplicateFileReads(messages) {
|
|
606
|
+
var _a, _b;
|
|
607
|
+
// Track the last occurrence index of each file path
|
|
608
|
+
const fileReadIndices = new Map();
|
|
609
|
+
// Common patterns for file read tool results
|
|
610
|
+
const filePathPatterns = [
|
|
611
|
+
/(?:read_file|readFile|cat|file_read)\s*[:\-]?\s*(.+)/i,
|
|
612
|
+
/(?:Content of|Reading|File:)\s+['"`]?([^\s'"`]+)['"`]?/i,
|
|
613
|
+
/^(['"`]?\/[^\s'"`]+['"`]?)/m,
|
|
614
|
+
];
|
|
615
|
+
for (let i = 0; i < messages.length; i++) {
|
|
616
|
+
const msg = messages[i];
|
|
617
|
+
if (!msg || !this.isToolResponseMessage(msg))
|
|
618
|
+
continue;
|
|
619
|
+
const content = this.getMessageContent(msg);
|
|
620
|
+
if (!content)
|
|
621
|
+
continue;
|
|
622
|
+
// Try to extract a file path from the tool result
|
|
623
|
+
let filePath = null;
|
|
624
|
+
for (const pattern of filePathPatterns) {
|
|
625
|
+
const match = content.match(pattern);
|
|
626
|
+
if (match && match[1]) {
|
|
627
|
+
filePath = match[1].trim().replace(/['"`]/g, '');
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Also check the message name field (often contains the tool name + path)
|
|
632
|
+
if (!filePath && msg.name) {
|
|
633
|
+
const nameMatch = msg.name.match(/read[_-]?file/i);
|
|
634
|
+
if (nameMatch) {
|
|
635
|
+
// Path might be in tool_call arguments of the preceding assistant message
|
|
636
|
+
if (i > 0) {
|
|
637
|
+
const prevMsg = messages[i - 1];
|
|
638
|
+
if (prevMsg === null || prevMsg === void 0 ? void 0 : prevMsg.tool_calls) {
|
|
639
|
+
for (const tc of prevMsg.tool_calls) {
|
|
640
|
+
const args = typeof ((_a = tc.function) === null || _a === void 0 ? void 0 : _a.arguments) === 'string'
|
|
641
|
+
? tc.function.arguments
|
|
642
|
+
: JSON.stringify(((_b = tc.function) === null || _b === void 0 ? void 0 : _b.arguments) || '');
|
|
643
|
+
const pathMatch = args.match(/['"]?path['"]?\s*:\s*['"]([^'"]+)['"]/);
|
|
644
|
+
if (pathMatch && pathMatch[1]) {
|
|
645
|
+
filePath = pathMatch[1];
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (filePath) {
|
|
654
|
+
const indices = fileReadIndices.get(filePath) || [];
|
|
655
|
+
indices.push(i);
|
|
656
|
+
fileReadIndices.set(filePath, indices);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Replace older duplicate reads with a short note
|
|
660
|
+
let deduplicatedCount = 0;
|
|
661
|
+
const result = [...messages];
|
|
662
|
+
fileReadIndices.forEach((indices, filePath) => {
|
|
663
|
+
if (indices.length <= 1)
|
|
664
|
+
return;
|
|
665
|
+
// Keep the last read (most recent), replace older ones
|
|
666
|
+
for (let j = 0; j < indices.length - 1; j++) {
|
|
667
|
+
const idx = indices[j];
|
|
668
|
+
if (idx === undefined)
|
|
669
|
+
continue;
|
|
670
|
+
const msg = result[idx];
|
|
671
|
+
if (!msg)
|
|
672
|
+
continue;
|
|
673
|
+
result[idx] = {
|
|
674
|
+
...msg,
|
|
675
|
+
content: `[NOTE: Duplicate file read removed for "${filePath}". See latest read below.]`
|
|
676
|
+
};
|
|
677
|
+
deduplicatedCount++;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
if (this.options.enableLogging && deduplicatedCount > 0) {
|
|
681
|
+
console.log(`[ConversationCompactor] Deduplicated ${deduplicatedCount} file reads`);
|
|
682
|
+
}
|
|
683
|
+
return { messages: result, deduplicatedCount };
|
|
684
|
+
}
|
|
482
685
|
// ========================================================================
|
|
483
686
|
// Compression Strategies
|
|
484
687
|
// ========================================================================
|
|
@@ -504,14 +707,16 @@ Keep the same XML format.`;
|
|
|
504
707
|
// Keep messages after split point
|
|
505
708
|
const preservedMessages = messages.slice(splitIndex);
|
|
506
709
|
// Combine: system messages first, then placeholder, then preserved history
|
|
507
|
-
|
|
710
|
+
let compressedMessages = [
|
|
508
711
|
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
509
712
|
{
|
|
510
713
|
role: 'user',
|
|
511
|
-
content: `[Previous conversation context has been compressed. ${splitIndex} messages were removed to manage context length.]`
|
|
714
|
+
content: `[Previous conversation context has been compressed. ${splitIndex} messages were removed to manage context length.\nThe original user task has been retained above. Pay special attention to recent messages as they contain the most current state of work.\nIf you need the contents of any files that were previously read, please re-read them before making changes.]`
|
|
512
715
|
},
|
|
513
716
|
...preservedMessages
|
|
514
717
|
];
|
|
718
|
+
// Ensure original user request is preserved
|
|
719
|
+
compressedMessages = this.ensureOriginalUserMessage(compressedMessages, messages);
|
|
515
720
|
return {
|
|
516
721
|
compressedMessages,
|
|
517
722
|
metadata: {
|
|
@@ -559,14 +764,16 @@ Keep the same XML format.`;
|
|
|
559
764
|
? truncatedMessages.filter(m => m.role === 'system')
|
|
560
765
|
: [];
|
|
561
766
|
const preservedMessages = truncatedMessages.slice(splitIndex);
|
|
562
|
-
|
|
767
|
+
let compressedMessages = [
|
|
563
768
|
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
564
769
|
{
|
|
565
770
|
role: 'user',
|
|
566
|
-
content: `[Conversation compressed. ${splitIndex} older messages
|
|
771
|
+
content: `[Conversation compressed. ${splitIndex} older messages were removed and tool outputs may have been truncated.\nThe original user task has been retained above. Pay special attention to recent messages as they contain the most current state of work.\nIf you need the contents of any files that were previously read, please re-read them before making changes.]`
|
|
567
772
|
},
|
|
568
773
|
...preservedMessages
|
|
569
774
|
];
|
|
775
|
+
// Ensure original user request is preserved
|
|
776
|
+
compressedMessages = this.ensureOriginalUserMessage(compressedMessages, messages);
|
|
570
777
|
return {
|
|
571
778
|
compressedMessages,
|
|
572
779
|
metadata: {
|
|
@@ -613,19 +820,30 @@ Keep the same XML format.`;
|
|
|
613
820
|
if (!summary || summary.trim().length === 0) {
|
|
614
821
|
throw new Error('Empty summary generated');
|
|
615
822
|
}
|
|
616
|
-
// Build compressed message array
|
|
617
|
-
const
|
|
823
|
+
// Build compressed message array with continuation framing
|
|
824
|
+
const continuationWrappedSummary = `This session is being continued from a previous conversation that ran out of context. Below is a summary of the conversation so far.
|
|
825
|
+
|
|
826
|
+
${summary}
|
|
827
|
+
|
|
828
|
+
Please continue from where the previous conversation left off. Do not ask the user to re-explain what they already told you — the summary above contains all the context you need. If you need to read any files mentioned in the summary, do so before making changes.`;
|
|
829
|
+
let compressedMessages = [
|
|
618
830
|
...systemMessages.filter(sm => !preservedMessages.includes(sm)),
|
|
619
831
|
{
|
|
620
832
|
role: 'user',
|
|
621
|
-
content:
|
|
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.'
|
|
833
|
+
content: continuationWrappedSummary
|
|
626
834
|
},
|
|
627
835
|
...preservedMessages
|
|
628
836
|
];
|
|
837
|
+
// Ensure original user request is preserved
|
|
838
|
+
compressedMessages = this.ensureOriginalUserMessage(compressedMessages, messages);
|
|
839
|
+
// Parse "Required Files" from summary and inject a hint for the agent
|
|
840
|
+
const requiredFiles = this.extractRequiredFiles(summary);
|
|
841
|
+
if (requiredFiles.length > 0) {
|
|
842
|
+
compressedMessages.push({
|
|
843
|
+
role: 'user',
|
|
844
|
+
content: `[IMPORTANT: The following files were being actively worked on before context compression and their contents are no longer in context. You should re-read these files before making any changes:\n${requiredFiles.map((f) => `- ${f}`).join('\n')}]`
|
|
845
|
+
});
|
|
846
|
+
}
|
|
629
847
|
return {
|
|
630
848
|
compressedMessages,
|
|
631
849
|
metadata: {
|
|
@@ -647,7 +865,7 @@ Keep the same XML format.`;
|
|
|
647
865
|
// Main Entry Point
|
|
648
866
|
// ========================================================================
|
|
649
867
|
async modify(input) {
|
|
650
|
-
var _a, _b
|
|
868
|
+
var _a, _b;
|
|
651
869
|
const { nextPrompt, rawLLMResponseMessage, tokenLimit } = input;
|
|
652
870
|
try {
|
|
653
871
|
// Safety check: ensure messages array exists
|
|
@@ -664,11 +882,63 @@ Keep the same XML format.`;
|
|
|
664
882
|
// Get model token limit: prefer tokenLimit from LLM response, then lookup by model name, then fallback to config
|
|
665
883
|
const modelName = rawLLMResponseMessage === null || rawLLMResponseMessage === void 0 ? void 0 : rawLLMResponseMessage.model;
|
|
666
884
|
const modelTokenLimit = tokenLimit !== null && tokenLimit !== void 0 ? tokenLimit : (modelName ? getModelTokenLimit(modelName) : this.options.modelTokenLimit);
|
|
667
|
-
//
|
|
668
|
-
|
|
885
|
+
// Calibrate token estimation using actual API usage data when available.
|
|
886
|
+
// This improves the chars/4 approximation with real tokenizer data.
|
|
887
|
+
// Note: We still count the ACTUAL messages array (not usage.prompt_tokens directly)
|
|
888
|
+
// because the messages array has grown since the LLM call (tool results appended).
|
|
889
|
+
const usage = rawLLMResponseMessage === null || rawLLMResponseMessage === void 0 ? void 0 : rawLLMResponseMessage.usage;
|
|
890
|
+
if (usage) {
|
|
891
|
+
this.calibrateTokenEstimation(messages, usage);
|
|
892
|
+
}
|
|
893
|
+
const currentTokens = this.countMessageTokens(messages);
|
|
669
894
|
const threshold = modelTokenLimit * this.options.compressionTokenThreshold;
|
|
895
|
+
this.stepsSinceLastCompression++;
|
|
670
896
|
if (this.options.enableLogging) {
|
|
671
|
-
console.log(`[ConversationCompactor] Current tokens: ${currentTokens}, Threshold: ${threshold}`);
|
|
897
|
+
console.log(`[ConversationCompactor] Current tokens: ${currentTokens}, Threshold: ${threshold}, Steps since last compression: ${this.stepsSinceLastCompression}`);
|
|
898
|
+
}
|
|
899
|
+
// Skip if we just compressed recently and tokens haven't grown significantly
|
|
900
|
+
// This prevents re-compressing when the token estimate is still high right after compression
|
|
901
|
+
if (this.lastCompressionTokenCount > 0 && this.stepsSinceLastCompression <= 2) {
|
|
902
|
+
// Only re-compress if tokens have grown by at least 20% since last compression
|
|
903
|
+
const growthThreshold = this.lastCompressionTokenCount * 1.2;
|
|
904
|
+
if (currentTokens < growthThreshold) {
|
|
905
|
+
if (this.options.enableLogging) {
|
|
906
|
+
console.log(`[ConversationCompactor] Skipping - recently compressed (${this.stepsSinceLastCompression} steps ago), tokens: ${currentTokens}, last compressed to: ${this.lastCompressionTokenCount}`);
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
nextPrompt: {
|
|
910
|
+
...nextPrompt,
|
|
911
|
+
metadata: {
|
|
912
|
+
...nextPrompt.metadata,
|
|
913
|
+
compression: {
|
|
914
|
+
status: CompressionStatus.NOOP,
|
|
915
|
+
originalTokenCount: currentTokens,
|
|
916
|
+
newTokenCount: currentTokens,
|
|
917
|
+
messagesCompressed: 0,
|
|
918
|
+
messagesPreserved: messages.length,
|
|
919
|
+
toolOutputsTruncated: false,
|
|
920
|
+
failedAttempts: this.failedCompressionAttempts,
|
|
921
|
+
timestamp: new Date().toISOString(),
|
|
922
|
+
strategy: this.options.compactStrategy
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
shouldExit: false
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Also check metadata from previous compression (in case instance state was lost)
|
|
931
|
+
const prevCompression = (_b = nextPrompt.metadata) === null || _b === void 0 ? void 0 : _b['compression'];
|
|
932
|
+
if ((prevCompression === null || prevCompression === void 0 ? void 0 : prevCompression.status) === 'COMPRESSED' && prevCompression.newTokenCount) {
|
|
933
|
+
if (currentTokens < prevCompression.newTokenCount * 1.2) {
|
|
934
|
+
if (this.options.enableLogging) {
|
|
935
|
+
console.log(`[ConversationCompactor] Skipping - metadata indicates recent compression, tokens haven't grown significantly`);
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
nextPrompt,
|
|
939
|
+
shouldExit: false
|
|
940
|
+
};
|
|
941
|
+
}
|
|
672
942
|
}
|
|
673
943
|
// Check if compression is needed
|
|
674
944
|
if (currentTokens < threshold) {
|
|
@@ -710,22 +980,32 @@ Keep the same XML format.`;
|
|
|
710
980
|
shouldExit: false
|
|
711
981
|
};
|
|
712
982
|
}
|
|
983
|
+
// Pre-compression: deduplicate file reads to reduce token waste
|
|
984
|
+
const { messages: dedupedMessages } = this.deduplicateFileReads(messages);
|
|
713
985
|
// Apply compression based on strategy
|
|
714
986
|
let result;
|
|
715
987
|
switch (this.options.compactStrategy) {
|
|
716
988
|
case 'simple':
|
|
717
|
-
result = await this.compressSimple(
|
|
989
|
+
result = await this.compressSimple(dedupedMessages);
|
|
718
990
|
break;
|
|
719
991
|
case 'smart':
|
|
720
|
-
result = await this.compressSmart(
|
|
992
|
+
result = await this.compressSmart(dedupedMessages);
|
|
721
993
|
break;
|
|
722
994
|
case 'summarize':
|
|
723
|
-
result = await this.compressSummarize(
|
|
995
|
+
result = await this.compressSummarize(dedupedMessages);
|
|
724
996
|
break;
|
|
725
997
|
default:
|
|
726
|
-
result = await this.compressSmart(
|
|
998
|
+
result = await this.compressSmart(dedupedMessages);
|
|
727
999
|
}
|
|
728
1000
|
const newTokens = this.countMessageTokens(result.compressedMessages);
|
|
1001
|
+
// Ensure system message is preserved after compression
|
|
1002
|
+
const hasSystemMessage = result.compressedMessages.some(m => m.role === 'system');
|
|
1003
|
+
if (!hasSystemMessage) {
|
|
1004
|
+
const originalSystemMsg = messages.find(m => m.role === 'system');
|
|
1005
|
+
if (originalSystemMsg) {
|
|
1006
|
+
result.compressedMessages.unshift(originalSystemMsg);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
729
1009
|
// Verify compression was effective
|
|
730
1010
|
if (newTokens >= currentTokens) {
|
|
731
1011
|
this.failedCompressionAttempts++;
|
|
@@ -766,6 +1046,9 @@ Keep the same XML format.`;
|
|
|
766
1046
|
timestamp: new Date().toISOString(),
|
|
767
1047
|
strategy: this.options.compactStrategy
|
|
768
1048
|
};
|
|
1049
|
+
// Track compression state for cooldown logic
|
|
1050
|
+
this.lastCompressionTokenCount = newTokens;
|
|
1051
|
+
this.stepsSinceLastCompression = 0;
|
|
769
1052
|
if (this.options.enableLogging) {
|
|
770
1053
|
console.log(`[ConversationCompactor] Compression complete: ${currentTokens} -> ${newTokens} tokens (saved ${currentTokens - newTokens})`);
|
|
771
1054
|
}
|
|
@@ -821,6 +1104,8 @@ Keep the same XML format.`;
|
|
|
821
1104
|
resetCompressionState() {
|
|
822
1105
|
this.failedCompressionAttempts = 0;
|
|
823
1106
|
this.hasFailedCompression = false;
|
|
1107
|
+
this.lastCompressionTokenCount = 0;
|
|
1108
|
+
this.stepsSinceLastCompression = 0;
|
|
824
1109
|
}
|
|
825
1110
|
/**
|
|
826
1111
|
* Gets the current compression statistics
|
|
@@ -20,11 +20,14 @@ export interface ChatCompressionOptions {
|
|
|
20
20
|
contextPercentageThreshold?: number;
|
|
21
21
|
enableCompression?: boolean;
|
|
22
22
|
force?: boolean;
|
|
23
|
+
/** Model token limit used for threshold calculation. @default 128000 */
|
|
24
|
+
modelTokenLimit?: number;
|
|
23
25
|
}
|
|
24
26
|
export declare class ChatCompressionModifier extends BasePreInferenceProcessor {
|
|
25
27
|
private readonly options;
|
|
26
28
|
private hasFailedCompressionAttempt;
|
|
27
29
|
constructor(options?: ChatCompressionOptions);
|
|
30
|
+
private buildCompressionMetadata;
|
|
28
31
|
modify(_originalRequest: FlatUserMessage, createdMessage: ProcessedMessage): Promise<ProcessedMessage>;
|
|
29
32
|
tryCompressChat(messages: MessageObject[], force?: boolean): Promise<ChatCompressionInfo & {
|
|
30
33
|
compressedMessages?: MessageObject[];
|