@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.
Files changed (48) hide show
  1. package/README.md +118 -153
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +41 -0
  4. package/dist/processor-pieces/messageModifiers/chatHistoryMessageModifier.js +2 -4
  5. package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.d.ts +81 -0
  6. package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.js +316 -0
  7. package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.js +2 -19
  8. package/dist/processor-pieces/messageModifiers/directoryContextModifier.js +2 -6
  9. package/dist/processor-pieces/messageModifiers/environmentContextModifier.js +2 -7
  10. package/dist/processor-pieces/messageModifiers/ideContextModifier.js +2 -21
  11. package/dist/processor-pieces/messageModifiers/index.d.ts +3 -0
  12. package/dist/processor-pieces/messageModifiers/index.js +7 -1
  13. package/dist/processor-pieces/messageModifiers/toolInjectionModifier.js +2 -1
  14. package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.d.ts +34 -5
  15. package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.js +374 -89
  16. package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.d.ts +3 -0
  17. package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.js +50 -27
  18. package/dist/unified/agent/agent.d.ts +10 -0
  19. package/dist/unified/agent/agent.js +198 -10
  20. package/dist/unified/agent/codeboltAgent.d.ts +19 -100
  21. package/dist/unified/agent/codeboltAgent.js +209 -109
  22. package/dist/unified/base/agentStep.js +17 -17
  23. package/dist/unified/base/initialPromptGenerator.js +13 -31
  24. package/dist/unified/base/promptContext.d.ts +13 -0
  25. package/dist/unified/base/promptContext.js +213 -0
  26. package/dist/unified/base/responseExecutor.d.ts +7 -19
  27. package/dist/unified/base/responseExecutor.js +280 -259
  28. package/dist/unified/index.d.ts +9 -0
  29. package/dist/unified/index.js +20 -13
  30. package/dist/unified/services/CompressionCoordinator.d.ts +67 -0
  31. package/dist/unified/services/CompressionCoordinator.js +214 -0
  32. package/dist/unified/services/compaction/autoCompact.d.ts +52 -0
  33. package/dist/unified/services/compaction/autoCompact.js +294 -0
  34. package/dist/unified/services/compaction/compactionOrchestrator.d.ts +78 -0
  35. package/dist/unified/services/compaction/compactionOrchestrator.js +230 -0
  36. package/dist/unified/services/compaction/contextCollapse.d.ts +63 -0
  37. package/dist/unified/services/compaction/contextCollapse.js +291 -0
  38. package/dist/unified/services/compaction/microCompact.d.ts +34 -0
  39. package/dist/unified/services/compaction/microCompact.js +195 -0
  40. package/dist/unified/services/compaction/postCompactCleanup.d.ts +15 -0
  41. package/dist/unified/services/compaction/postCompactCleanup.js +37 -0
  42. package/dist/unified/services/compaction/reactiveCompact.d.ts +65 -0
  43. package/dist/unified/services/compaction/reactiveCompact.js +301 -0
  44. package/dist/unified/services/compaction/snipCompact.d.ts +31 -0
  45. package/dist/unified/services/compaction/snipCompact.js +124 -0
  46. package/dist/unified/services/compaction/types.d.ts +66 -0
  47. package/dist/unified/services/compaction/types.js +39 -0
  48. 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 the ~4 characters per token approximation
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 / 4);
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
- // Stop at a user message that isn't a tool response
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
- // 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;
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
- // Ensure we don't split too early
359
- if (splitIndex === 0) {
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 - find the next user message
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
- const truncated = content.length > 500 ? content.substring(0, 500) + '...' : content;
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. Analyze the following conversation history and create a concise state snapshot that preserves all critical information.
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
- Generate a state snapshot in the following XML format. Be extremely concise but preserve all critical technical details:
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
- <state_snapshot>
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
- <active_constraints>
396
- [List any user-specified rules, preferences, or constraints that must be maintained]
397
- </active_constraints>
469
+ <summary>
470
+ 1. PRIMARY REQUEST
471
+ [The user's original, complete request — use verbatim quotes where possible]
398
472
 
399
- <key_knowledge>
400
- [List crucial technical facts discovered: file structures, dependencies, API details, error patterns, etc.]
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
- <artifact_trail>
404
- [Track evolution of critical files/symbols that were modified or are important]
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
- <file_system_state>
408
- [Describe current relevant file system state: created files, modified files, directory structure if relevant]
409
- </file_system_state>
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
- <recent_actions>
412
- [Fact-based summary of tool calls and their outcomes - what was done, not how]
413
- </recent_actions>
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
- <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>
493
+ 6. PENDING TASKS
494
+ [Tasks that were mentioned but NOT yet completed. Be specific about what remains.]
419
495
 
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`;
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
- const compressedMessages = [
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
- const compressedMessages = [
767
+ let compressedMessages = [
563
768
  ...systemMessages.filter(sm => !preservedMessages.includes(sm)),
564
769
  {
565
770
  role: 'user',
566
- content: `[Conversation compressed. ${splitIndex} older messages summarized. Tool outputs may have been truncated.]`
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 compressedMessages = [
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: 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.'
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, _c;
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
- // 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);
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(messages);
989
+ result = await this.compressSimple(dedupedMessages);
718
990
  break;
719
991
  case 'smart':
720
- result = await this.compressSmart(messages);
992
+ result = await this.compressSmart(dedupedMessages);
721
993
  break;
722
994
  case 'summarize':
723
- result = await this.compressSummarize(messages);
995
+ result = await this.compressSummarize(dedupedMessages);
724
996
  break;
725
997
  default:
726
- result = await this.compressSmart(messages);
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[];