@codebolt/agent 5.0.7 → 5.0.9

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 (160) hide show
  1. package/dist/processor-pieces/base/basePreToolCallProcessor.js +5 -2
  2. package/dist/processor-pieces/messageModifiers/argumentProcessorModifier.d.ts +1 -1
  3. package/dist/processor-pieces/messageModifiers/argumentProcessorModifier.js +2 -2
  4. package/dist/processor-pieces/messageModifiers/atFileProcessorModifier.js +24 -34
  5. package/dist/processor-pieces/messageModifiers/chatRecordingModifier.d.ts +1 -1
  6. package/dist/processor-pieces/messageModifiers/chatRecordingModifier.js +2 -2
  7. package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.d.ts +1 -1
  8. package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.js +2 -2
  9. package/dist/processor-pieces/messageModifiers/directoryContextModifier.d.ts +1 -1
  10. package/dist/processor-pieces/messageModifiers/directoryContextModifier.js +8 -3
  11. package/dist/processor-pieces/messageModifiers/environmentContextModifier.d.ts +1 -1
  12. package/dist/processor-pieces/messageModifiers/environmentContextModifier.js +1 -1
  13. package/dist/processor-pieces/messageModifiers/ideContextModifier.js +114 -17
  14. package/dist/processor-pieces/messageModifiers/memoryImportModifier.d.ts +1 -1
  15. package/dist/processor-pieces/messageModifiers/memoryImportModifier.js +13 -5
  16. package/dist/processor-pieces/messageModifiers/toolInjectionModifier.d.ts +5 -0
  17. package/dist/processor-pieces/messageModifiers/toolInjectionModifier.js +4 -0
  18. package/dist/processor-pieces/postInferenceProcessors/loopDetectionModifier.js +24 -8
  19. package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.d.ts +202 -5
  20. package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.js +826 -19
  21. package/dist/processor-pieces/postToolCallProcessors/index.d.ts +1 -1
  22. package/dist/processor-pieces/postToolCallProcessors/index.js +2 -1
  23. package/dist/processor-pieces/postToolCallProcessors/shellProcessorModifier.js +7 -4
  24. package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.d.ts +1 -1
  25. package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.js +12 -16
  26. package/dist/processor-pieces/pretoolCallProcessors/toolParameterModifier.d.ts +2 -3
  27. package/dist/processor-pieces/pretoolCallProcessors/toolParameterModifier.js +1 -2
  28. package/dist/processor-pieces/utils/messageModifierHelper.js +5 -2
  29. package/dist/types/InternalTypes.d.ts +8 -6
  30. package/dist/unified/agent/agent.js +1 -2
  31. package/dist/unified/agent/codeboltAgent.d.ts +23 -2
  32. package/dist/unified/agent/codeboltAgent.js +55 -12
  33. package/dist/unified/agent/tools.d.ts +7 -11
  34. package/dist/unified/agent/tools.js +12 -3
  35. package/dist/unified/agent/workflow.js +38 -20
  36. package/dist/unified/agent/workflowSteps.d.ts +1 -1
  37. package/dist/unified/agent/workflowSteps.js +58 -23
  38. package/dist/unified/base/agentStep.js +20 -1
  39. package/dist/unified/base/initialPromptGenerator.js +3 -1
  40. package/dist/unified/base/responseExecutor.d.ts +3 -0
  41. package/dist/unified/base/responseExecutor.js +47 -21
  42. package/dist/unified/index.d.ts +1 -0
  43. package/dist/unified/index.js +4 -1
  44. package/dist/unified/services/LoopDetectionService.d.ts +44 -0
  45. package/dist/unified/services/LoopDetectionService.js +79 -0
  46. package/dist/unified/utils/utils.d.ts +20 -1
  47. package/dist/unified/utils/utils.js +8 -4
  48. package/package.json +5 -5
  49. package/dist/agent.d.ts +0 -86
  50. package/dist/agent.js +0 -324
  51. package/dist/builderpattern/agent.d.ts +0 -86
  52. package/dist/builderpattern/agent.js +0 -324
  53. package/dist/builderpattern/followupquestionbuilder.d.ts +0 -75
  54. package/dist/builderpattern/followupquestionbuilder.js +0 -197
  55. package/dist/builderpattern/index.d.ts +0 -14
  56. package/dist/builderpattern/index.js +0 -23
  57. package/dist/builderpattern/llmoutputhandler.d.ts +0 -102
  58. package/dist/builderpattern/llmoutputhandler.js +0 -452
  59. package/dist/builderpattern/promptbuilder.d.ts +0 -382
  60. package/dist/builderpattern/promptbuilder.js +0 -805
  61. package/dist/builderpattern/systemprompt.d.ts +0 -20
  62. package/dist/builderpattern/systemprompt.js +0 -48
  63. package/dist/builderpattern/taskInstruction.d.ts +0 -37
  64. package/dist/builderpattern/taskInstruction.js +0 -57
  65. package/dist/builderpattern/usermessage.d.ts +0 -70
  66. package/dist/builderpattern/usermessage.js +0 -123
  67. package/dist/composablepattern/agent.d.ts +0 -169
  68. package/dist/composablepattern/agent.js +0 -598
  69. package/dist/composablepattern/codebolt-storage.d.ts +0 -67
  70. package/dist/composablepattern/codebolt-storage.js +0 -320
  71. package/dist/composablepattern/document.d.ts +0 -149
  72. package/dist/composablepattern/document.js +0 -405
  73. package/dist/composablepattern/examples/codebolt-integration-example.d.ts +0 -12
  74. package/dist/composablepattern/examples/codebolt-integration-example.js +0 -218
  75. package/dist/composablepattern/examples/codebolt-storage-example.d.ts +0 -40
  76. package/dist/composablepattern/examples/codebolt-storage-example.js +0 -223
  77. package/dist/composablepattern/examples/custom-steps-example.d.ts +0 -20
  78. package/dist/composablepattern/examples/custom-steps-example.js +0 -455
  79. package/dist/composablepattern/examples/document-agent.d.ts +0 -17
  80. package/dist/composablepattern/examples/document-agent.js +0 -107
  81. package/dist/composablepattern/examples/simple-codebolt-integration.d.ts +0 -8
  82. package/dist/composablepattern/examples/simple-codebolt-integration.js +0 -164
  83. package/dist/composablepattern/examples/weather-agent.d.ts +0 -18
  84. package/dist/composablepattern/examples/weather-agent.js +0 -86
  85. package/dist/composablepattern/examples/workflow-example.d.ts +0 -12
  86. package/dist/composablepattern/examples/workflow-example.js +0 -463
  87. package/dist/composablepattern/index.d.ts +0 -63
  88. package/dist/composablepattern/index.js +0 -115
  89. package/dist/composablepattern/memory.d.ts +0 -89
  90. package/dist/composablepattern/memory.js +0 -141
  91. package/dist/composablepattern/tool.d.ts +0 -84
  92. package/dist/composablepattern/tool.js +0 -260
  93. package/dist/composablepattern/types.d.ts +0 -194
  94. package/dist/composablepattern/types.js +0 -6
  95. package/dist/composablepattern/user-context.d.ts +0 -214
  96. package/dist/composablepattern/user-context.js +0 -275
  97. package/dist/composablepattern/workflow.d.ts +0 -388
  98. package/dist/composablepattern/workflow.js +0 -562
  99. package/dist/followupquestionbuilder.d.ts +0 -75
  100. package/dist/followupquestionbuilder.js +0 -197
  101. package/dist/index.d.ts +0 -14
  102. package/dist/index.js +0 -23
  103. package/dist/llmoutputhandler.d.ts +0 -102
  104. package/dist/llmoutputhandler.js +0 -452
  105. package/dist/processor/agent/agentStep.d.ts +0 -49
  106. package/dist/processor/agent/agentStep.js +0 -241
  107. package/dist/processor/agent/toolExecutor.d.ts +0 -19
  108. package/dist/processor/agent/toolExecutor.js +0 -90
  109. package/dist/processor/index.d.ts +0 -7
  110. package/dist/processor/index.js +0 -36
  111. package/dist/processor/messageModifiers/baseMessageModifier.d.ts +0 -20
  112. package/dist/processor/messageModifiers/baseMessageModifier.js +0 -55
  113. package/dist/processor/processors/baseProcessor.d.ts +0 -14
  114. package/dist/processor/processors/baseProcessor.js +0 -29
  115. package/dist/processor/tools/baseTool.d.ts +0 -10
  116. package/dist/processor/tools/baseTool.js +0 -20
  117. package/dist/processor/tools/toolList.d.ts +0 -9
  118. package/dist/processor/tools/toolList.js +0 -22
  119. package/dist/processor/types/interfaces.d.ts +0 -94
  120. package/dist/processor/types/interfaces.js +0 -2
  121. package/dist/processor-pieces/messageModifiers/addToolsListMessageModifier.d.ts +0 -22
  122. package/dist/processor-pieces/messageModifiers/addToolsListMessageModifier.js +0 -144
  123. package/dist/processor-pieces/messageModifiers/baseContextMessageModifier.d.ts +0 -15
  124. package/dist/processor-pieces/messageModifiers/baseContextMessageModifier.js +0 -75
  125. package/dist/processor-pieces/messageModifiers/baseSystemInstructionMessageModifier.d.ts +0 -11
  126. package/dist/processor-pieces/messageModifiers/baseSystemInstructionMessageModifier.js +0 -46
  127. package/dist/processor-pieces/messageModifiers/handleUrlMessageModifier.d.ts +0 -12
  128. package/dist/processor-pieces/messageModifiers/handleUrlMessageModifier.js +0 -60
  129. package/dist/processor-pieces/messageModifiers/imageAttachmentMessageModifier.d.ts +0 -18
  130. package/dist/processor-pieces/messageModifiers/imageAttachmentMessageModifier.js +0 -222
  131. package/dist/processor-pieces/messageModifiers/workingDirectoryMessageModifier.d.ts +0 -22
  132. package/dist/processor-pieces/messageModifiers/workingDirectoryMessageModifier.js +0 -194
  133. package/dist/processor-pieces/processors/advancedLoopDetectionProcessor.d.ts +0 -41
  134. package/dist/processor-pieces/processors/advancedLoopDetectionProcessor.js +0 -197
  135. package/dist/processor-pieces/processors/chatCompressionProcessor.d.ts +0 -26
  136. package/dist/processor-pieces/processors/chatCompressionProcessor.js +0 -90
  137. package/dist/processor-pieces/processors/chatRecordingProcessor.d.ts +0 -81
  138. package/dist/processor-pieces/processors/chatRecordingProcessor.js +0 -329
  139. package/dist/processor-pieces/processors/contextManagementProcessor.d.ts +0 -40
  140. package/dist/processor-pieces/processors/contextManagementProcessor.js +0 -305
  141. package/dist/processor-pieces/processors/loopDetectionProcessor.d.ts +0 -30
  142. package/dist/processor-pieces/processors/loopDetectionProcessor.js +0 -137
  143. package/dist/processor-pieces/processors/responseValidationProcessor.d.ts +0 -30
  144. package/dist/processor-pieces/processors/responseValidationProcessor.js +0 -228
  145. package/dist/processor-pieces/processors/telemetryProcessor.d.ts +0 -99
  146. package/dist/processor-pieces/processors/telemetryProcessor.js +0 -311
  147. package/dist/processor-pieces/processors/tokenManagementProcessor.d.ts +0 -37
  148. package/dist/processor-pieces/processors/tokenManagementProcessor.js +0 -178
  149. package/dist/processor-pieces/processors/toolExecutionProcessor.d.ts +0 -43
  150. package/dist/processor-pieces/processors/toolExecutionProcessor.js +0 -207
  151. package/dist/processor-pieces/tools/fileTools.d.ts +0 -26
  152. package/dist/processor-pieces/tools/fileTools.js +0 -162
  153. package/dist/promptbuilder.d.ts +0 -382
  154. package/dist/promptbuilder.js +0 -805
  155. package/dist/systemprompt.d.ts +0 -20
  156. package/dist/systemprompt.js +0 -48
  157. package/dist/taskInstruction.d.ts +0 -37
  158. package/dist/taskInstruction.js +0 -57
  159. package/dist/usermessage.d.ts +0 -70
  160. package/dist/usermessage.js +0 -123
@@ -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
- maxMessages: 50,
14
- preserveSystemMessages: true,
15
- preserveLastUserMessages: 5,
16
- preserveLastToolResults: 10,
17
- compactStrategy: 'smart',
18
- ...options
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
- async modify(input) {
22
- // For now, pass through the nextPrompt unchanged since we need to understand
23
- // the ProcessedMessage structure better for compaction
24
- // TODO: Implement conversation compaction logic once we understand the message structure
25
- // Add metadata to track that this processor was applied
26
- const modifiedNextPrompt = {
27
- ...input.nextPrompt,
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
- ...input.nextPrompt.metadata,
30
- compactedBy: 'ConversationCompactorModifier',
31
- compactedAt: new Date().toISOString(),
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
- nextPrompt: modifiedNextPrompt,
37
- shouldExit: false
843
+ messages: result.compressedMessages,
844
+ metadata: result.metadata
38
845
  };
39
846
  }
40
847
  }