@codebolt/agent 5.0.9 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +351 -258
- 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 +22 -30
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 3: Context Collapse
|
|
4
|
+
*
|
|
5
|
+
* Incrementally archives messages into a collapse store with granular summaries.
|
|
6
|
+
* Unlike full auto-compact which produces one monolithic summary, context collapse
|
|
7
|
+
* preserves granular per-range summaries so more context is retained.
|
|
8
|
+
*
|
|
9
|
+
* Key properties:
|
|
10
|
+
* - Commits at ~90% of context window
|
|
11
|
+
* - Blocks at ~95% (hard limit)
|
|
12
|
+
* - Archives messages incrementally (not all at once)
|
|
13
|
+
* - Read-time projection over full history
|
|
14
|
+
* - Recovery: drains staged collapses on 413 errors (cheap)
|
|
15
|
+
* - When enabled, suppresses auto-compact to avoid racing
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ContextCollapse = void 0;
|
|
19
|
+
const types_1 = require("./types");
|
|
20
|
+
/** Default commit threshold (fraction of context window) */
|
|
21
|
+
const DEFAULT_COMMIT_THRESHOLD = 0.9;
|
|
22
|
+
/** Default blocking threshold */
|
|
23
|
+
const DEFAULT_BLOCKING_THRESHOLD = 0.95;
|
|
24
|
+
/** Minimum tokens to archive per collapse commit */
|
|
25
|
+
const MIN_COLLAPSE_TOKENS = 2000;
|
|
26
|
+
class ContextCollapse {
|
|
27
|
+
constructor(options) {
|
|
28
|
+
var _a, _b, _c, _d, _e, _f;
|
|
29
|
+
this.name = 'collapse';
|
|
30
|
+
/** The collapse store: ordered list of collapsed ranges */
|
|
31
|
+
this.store = [];
|
|
32
|
+
/** Staged collapses waiting for API confirmation */
|
|
33
|
+
this.staged = [];
|
|
34
|
+
this.options = {
|
|
35
|
+
commitThreshold: (_a = options === null || options === void 0 ? void 0 : options.commitThreshold) !== null && _a !== void 0 ? _a : DEFAULT_COMMIT_THRESHOLD,
|
|
36
|
+
blockingThreshold: (_b = options === null || options === void 0 ? void 0 : options.blockingThreshold) !== null && _b !== void 0 ? _b : DEFAULT_BLOCKING_THRESHOLD,
|
|
37
|
+
modelTokenLimit: (_c = options === null || options === void 0 ? void 0 : options.modelTokenLimit) !== null && _c !== void 0 ? _c : 128000,
|
|
38
|
+
llmRole: (_d = options === null || options === void 0 ? void 0 : options.llmRole) !== null && _d !== void 0 ? _d : 'summarizer',
|
|
39
|
+
maxSummaries: (_e = options === null || options === void 0 ? void 0 : options.maxSummaries) !== null && _e !== void 0 ? _e : 20,
|
|
40
|
+
enableLogging: (_f = options === null || options === void 0 ? void 0 : options.enableLogging) !== null && _f !== void 0 ? _f : false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
shouldApply(ctx) {
|
|
44
|
+
if (!ctx.contextCollapseEnabled)
|
|
45
|
+
return false;
|
|
46
|
+
const estimator = new types_1.TokenEstimator();
|
|
47
|
+
const tokenCount = estimator.estimateForMessages(ctx.messages);
|
|
48
|
+
const threshold = this.options.commitThreshold * this.options.modelTokenLimit;
|
|
49
|
+
return tokenCount >= threshold;
|
|
50
|
+
}
|
|
51
|
+
async apply(ctx) {
|
|
52
|
+
if (!ctx.contextCollapseEnabled)
|
|
53
|
+
return ctx;
|
|
54
|
+
const estimator = new types_1.TokenEstimator();
|
|
55
|
+
const tokenCount = estimator.estimateForMessages(ctx.messages);
|
|
56
|
+
const commitThreshold = this.options.commitThreshold * this.options.modelTokenLimit;
|
|
57
|
+
if (tokenCount < commitThreshold) {
|
|
58
|
+
return ctx;
|
|
59
|
+
}
|
|
60
|
+
// Determine how many messages to archive
|
|
61
|
+
const messages = ctx.messages;
|
|
62
|
+
const targetTokens = commitThreshold * 0.8; // Target 80% of threshold after collapse
|
|
63
|
+
const currentTokens = tokenCount;
|
|
64
|
+
const tokensToArchive = currentTokens - targetTokens;
|
|
65
|
+
if (tokensToArchive < MIN_COLLAPSE_TOKENS) {
|
|
66
|
+
return ctx;
|
|
67
|
+
}
|
|
68
|
+
// Find the split point: archive from the beginning, after system messages
|
|
69
|
+
const splitIdx = this.findSplitPoint(messages, tokensToArchive, estimator);
|
|
70
|
+
if (splitIdx <= 0) {
|
|
71
|
+
return ctx;
|
|
72
|
+
}
|
|
73
|
+
// Archive the range
|
|
74
|
+
const toArchive = messages.slice(0, splitIdx);
|
|
75
|
+
const preserved = messages.slice(splitIdx);
|
|
76
|
+
// Generate granular summary
|
|
77
|
+
const summary = await this.generateGranularSummary(toArchive);
|
|
78
|
+
const entry = {
|
|
79
|
+
startIdx: 0,
|
|
80
|
+
endIdx: splitIdx,
|
|
81
|
+
summary,
|
|
82
|
+
originalTokens: estimator.estimateForMessages(toArchive),
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
staged: true,
|
|
85
|
+
};
|
|
86
|
+
// Add to staged (will be confirmed after successful API call)
|
|
87
|
+
this.staged.push(entry);
|
|
88
|
+
// Build projected view: summaries + preserved messages
|
|
89
|
+
const systemMessages = toArchive.filter(m => m.role === 'system');
|
|
90
|
+
const projectedMessages = [
|
|
91
|
+
...systemMessages,
|
|
92
|
+
...this.buildSummaryMessages(),
|
|
93
|
+
...preserved,
|
|
94
|
+
];
|
|
95
|
+
const tokensFreed = currentTokens - estimator.estimateForMessages(projectedMessages);
|
|
96
|
+
const boundary = {
|
|
97
|
+
layer: 'collapse',
|
|
98
|
+
tokensFreed: Math.max(0, tokensFreed),
|
|
99
|
+
messagesRemoved: toArchive.length - systemMessages.length,
|
|
100
|
+
timestamp: entry.timestamp,
|
|
101
|
+
};
|
|
102
|
+
if (this.options.enableLogging) {
|
|
103
|
+
console.log(`[ContextCollapse] Collapsed ${toArchive.length} messages (~${entry.originalTokens} tokens). Freed ~${tokensFreed} tokens.`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...ctx,
|
|
107
|
+
messages: projectedMessages,
|
|
108
|
+
compactionHistory: [...(ctx.compactionHistory || []), boundary],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Drain staged collapses on API 413 errors (recovery path).
|
|
113
|
+
* Confirms all staged entries into the permanent store.
|
|
114
|
+
*/
|
|
115
|
+
drainStaged() {
|
|
116
|
+
if (this.staged.length === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
const count = this.staged.length;
|
|
119
|
+
for (const entry of this.staged) {
|
|
120
|
+
entry.staged = false;
|
|
121
|
+
this.store.push(entry);
|
|
122
|
+
}
|
|
123
|
+
// Trim store to max size
|
|
124
|
+
if (this.store.length > this.options.maxSummaries) {
|
|
125
|
+
this.store = this.store.slice(-this.options.maxSummaries);
|
|
126
|
+
}
|
|
127
|
+
this.staged = [];
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Recovery from overflow: drain staged collapses and rebuild.
|
|
132
|
+
*/
|
|
133
|
+
recoverFromOverflow(messages) {
|
|
134
|
+
const committed = this.drainStaged();
|
|
135
|
+
if (committed === 0) {
|
|
136
|
+
return { messages, committed: 0 };
|
|
137
|
+
}
|
|
138
|
+
// Rebuild with confirmed summaries
|
|
139
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
140
|
+
const nonSystem = messages.filter(m => m.role !== 'system');
|
|
141
|
+
const rebuilt = [
|
|
142
|
+
...systemMessages,
|
|
143
|
+
...this.buildSummaryMessages(),
|
|
144
|
+
...nonSystem,
|
|
145
|
+
];
|
|
146
|
+
return { messages: rebuilt, committed };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if at blocking threshold.
|
|
150
|
+
*/
|
|
151
|
+
isAtBlockingLimit(messages) {
|
|
152
|
+
const estimator = new types_1.TokenEstimator();
|
|
153
|
+
const tokenCount = estimator.estimateForMessages(messages);
|
|
154
|
+
const blockingLimit = this.options.blockingThreshold * this.options.modelTokenLimit;
|
|
155
|
+
return tokenCount >= blockingLimit;
|
|
156
|
+
}
|
|
157
|
+
reset() {
|
|
158
|
+
this.store = [];
|
|
159
|
+
this.staged = [];
|
|
160
|
+
}
|
|
161
|
+
// ─── Private Helpers ──────────────────────────────────────────────
|
|
162
|
+
findSplitPoint(messages, tokensToArchive, estimator) {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
let accumulated = 0;
|
|
165
|
+
// Skip system messages at the beginning
|
|
166
|
+
let startIdx = 0;
|
|
167
|
+
while (startIdx < messages.length && ((_a = messages[startIdx]) === null || _a === void 0 ? void 0 : _a.role) === 'system') {
|
|
168
|
+
startIdx++;
|
|
169
|
+
}
|
|
170
|
+
for (let i = startIdx; i < messages.length; i++) {
|
|
171
|
+
const msg = messages[i];
|
|
172
|
+
if (!msg)
|
|
173
|
+
continue;
|
|
174
|
+
const content = typeof msg.content === 'string'
|
|
175
|
+
? msg.content
|
|
176
|
+
: JSON.stringify((_b = msg.content) !== null && _b !== void 0 ? _b : '');
|
|
177
|
+
accumulated += estimator.estimate(content) + 4;
|
|
178
|
+
// Don't split in the middle of a tool call/response pair
|
|
179
|
+
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (msg.role === 'tool' || msg.tool_call_id) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Safe split point: user message or assistant without pending tools
|
|
186
|
+
if (accumulated >= tokensToArchive) {
|
|
187
|
+
return i + 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return 0; // No safe split point found
|
|
191
|
+
}
|
|
192
|
+
buildSummaryMessages() {
|
|
193
|
+
const allEntries = [...this.store, ...this.staged];
|
|
194
|
+
if (allEntries.length === 0)
|
|
195
|
+
return [];
|
|
196
|
+
const combinedSummary = allEntries
|
|
197
|
+
.map((entry, i) => `--- Collapsed Section ${i + 1} ---\n${entry.summary}`)
|
|
198
|
+
.join('\n\n');
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
role: 'user',
|
|
202
|
+
content: `[Context Summary - The following sections were collapsed to manage context length. Key information has been preserved.]\n\n${combinedSummary}`,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
role: 'assistant',
|
|
206
|
+
content: 'Understood. I have the collapsed context summary and will continue from the current state.',
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
async generateGranularSummary(messages) {
|
|
211
|
+
var _a, _b, _c, _d, _e;
|
|
212
|
+
const estimator = new types_1.TokenEstimator();
|
|
213
|
+
// For small ranges, create a structural summary without LLM
|
|
214
|
+
const tokenCount = estimator.estimateForMessages(messages);
|
|
215
|
+
if (tokenCount < 5000) {
|
|
216
|
+
return this.createStructuralSummary(messages);
|
|
217
|
+
}
|
|
218
|
+
// For larger ranges, try LLM summarization
|
|
219
|
+
try {
|
|
220
|
+
const codebolt = require('@codebolt/codeboltjs');
|
|
221
|
+
const historyText = messages
|
|
222
|
+
.map((msg, i) => {
|
|
223
|
+
var _a;
|
|
224
|
+
const content = typeof msg.content === 'string'
|
|
225
|
+
? msg.content
|
|
226
|
+
: JSON.stringify((_a = msg.content) !== null && _a !== void 0 ? _a : '');
|
|
227
|
+
const truncated = content.length > 500
|
|
228
|
+
? content.slice(0, 250) + '...[truncated]...' + content.slice(-250)
|
|
229
|
+
: content;
|
|
230
|
+
return `[${i}] ${msg.role}: ${truncated}`;
|
|
231
|
+
})
|
|
232
|
+
.join('\n');
|
|
233
|
+
const prompt = `Summarize this conversation segment concisely. Preserve: key decisions, file paths, code changes, errors encountered, current task state. Be factual and specific.
|
|
234
|
+
|
|
235
|
+
${historyText}`;
|
|
236
|
+
const response = await codebolt.llm.inference({
|
|
237
|
+
messages: [
|
|
238
|
+
{ role: 'system', content: 'You are a precise conversation summarizer. Be concise but complete.' },
|
|
239
|
+
{ role: 'user', content: prompt },
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
const summary = ((_a = response === null || response === void 0 ? void 0 : response.completion) === null || _a === void 0 ? void 0 : _a.content) || ((_e = (_d = (_c = (_b = response === null || response === void 0 ? void 0 : response.completion) === null || _b === void 0 ? void 0 : _b.choices) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.content);
|
|
243
|
+
if (summary && typeof summary === 'string' && summary.trim().length > 0) {
|
|
244
|
+
return summary.trim();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Fall back to structural summary
|
|
249
|
+
}
|
|
250
|
+
return this.createStructuralSummary(messages);
|
|
251
|
+
}
|
|
252
|
+
createStructuralSummary(messages) {
|
|
253
|
+
var _a, _b;
|
|
254
|
+
const parts = [];
|
|
255
|
+
let userMessages = 0;
|
|
256
|
+
let toolCalls = 0;
|
|
257
|
+
const files = new Set();
|
|
258
|
+
for (const msg of messages) {
|
|
259
|
+
if (!msg)
|
|
260
|
+
continue;
|
|
261
|
+
if (msg.role === 'user' && !msg.tool_call_id)
|
|
262
|
+
userMessages++;
|
|
263
|
+
if (msg.tool_calls)
|
|
264
|
+
toolCalls += msg.tool_calls.length;
|
|
265
|
+
// Extract file paths from content
|
|
266
|
+
const content = typeof msg.content === 'string'
|
|
267
|
+
? msg.content
|
|
268
|
+
: JSON.stringify((_a = msg.content) !== null && _a !== void 0 ? _a : '');
|
|
269
|
+
const filePathMatches = content.match(/['"`](\/?[^\s'"`]+\.[a-zA-Z0-9]+)['"`]/g);
|
|
270
|
+
if (filePathMatches) {
|
|
271
|
+
for (const match of filePathMatches) {
|
|
272
|
+
files.add(match.replace(/['"`]/g, ''));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
parts.push(`Messages: ${messages.length} (${userMessages} user, ${toolCalls} tool calls)`);
|
|
277
|
+
if (files.size > 0) {
|
|
278
|
+
parts.push(`Files referenced: ${Array.from(files).slice(0, 10).join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
// Include first and last user message content (truncated)
|
|
281
|
+
const firstUser = messages.find(m => m.role === 'user' && !m.tool_call_id);
|
|
282
|
+
if (firstUser) {
|
|
283
|
+
const content = typeof firstUser.content === 'string'
|
|
284
|
+
? firstUser.content
|
|
285
|
+
: JSON.stringify((_b = firstUser.content) !== null && _b !== void 0 ? _b : '');
|
|
286
|
+
parts.push(`First user message: ${content.slice(0, 200)}${content.length > 200 ? '...' : ''}`);
|
|
287
|
+
}
|
|
288
|
+
return parts.join('\n');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
exports.ContextCollapse = ContextCollapse;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2: Micro Compact
|
|
3
|
+
*
|
|
4
|
+
* Three sub-mechanisms for clearing old tool results:
|
|
5
|
+
* 2a. Time-based: clear old tool results after inactivity gap, keeps N most recent.
|
|
6
|
+
* 2b. Count-based: Clear when registered results exceed threshold.
|
|
7
|
+
* 2c. Budget-based: truncate largest results when total exceeds budget.
|
|
8
|
+
*
|
|
9
|
+
* Key properties:
|
|
10
|
+
* - Cheaper than collapse/auto (no LLM call, just content replacement)
|
|
11
|
+
* - Time-based fires when cache is cold (long gap = server cache expired)
|
|
12
|
+
* - Count-based fires when tool result count exceeds threshold
|
|
13
|
+
* - Both preserve the most recent N tool results
|
|
14
|
+
*/
|
|
15
|
+
import { type CompactionContext, type CompactionLayer, type CompactionLayerKind } from './types';
|
|
16
|
+
export interface MicroCompactOptions {
|
|
17
|
+
gapThresholdMinutes?: number;
|
|
18
|
+
keepRecent?: number;
|
|
19
|
+
compactableTools?: string[];
|
|
20
|
+
tokenBudget?: number;
|
|
21
|
+
enableLogging?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class MicroCompact implements CompactionLayer {
|
|
24
|
+
readonly name: CompactionLayerKind;
|
|
25
|
+
private readonly options;
|
|
26
|
+
constructor(options?: MicroCompactOptions);
|
|
27
|
+
shouldApply(ctx: CompactionContext): boolean;
|
|
28
|
+
apply(ctx: CompactionContext): Promise<CompactionContext>;
|
|
29
|
+
reset(): void;
|
|
30
|
+
private evaluateTimeTrigger;
|
|
31
|
+
private collectCompactableToolIds;
|
|
32
|
+
private clearOldToolResults;
|
|
33
|
+
private estimateToolResultTokens;
|
|
34
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 2: Micro Compact
|
|
4
|
+
*
|
|
5
|
+
* Three sub-mechanisms for clearing old tool results:
|
|
6
|
+
* 2a. Time-based: clear old tool results after inactivity gap, keeps N most recent.
|
|
7
|
+
* 2b. Count-based: Clear when registered results exceed threshold.
|
|
8
|
+
* 2c. Budget-based: truncate largest results when total exceeds budget.
|
|
9
|
+
*
|
|
10
|
+
* Key properties:
|
|
11
|
+
* - Cheaper than collapse/auto (no LLM call, just content replacement)
|
|
12
|
+
* - Time-based fires when cache is cold (long gap = server cache expired)
|
|
13
|
+
* - Count-based fires when tool result count exceeds threshold
|
|
14
|
+
* - Both preserve the most recent N tool results
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.MicroCompact = void 0;
|
|
18
|
+
const types_1 = require("./types");
|
|
19
|
+
const CLEAR_PLACEHOLDER = '[Old tool result content cleared]';
|
|
20
|
+
const DEFAULT_GAP_THRESHOLD_MINUTES = 60;
|
|
21
|
+
const DEFAULT_KEEP_RECENT = 5;
|
|
22
|
+
const DEFAULT_COMPACTABLE_TOOLS = [
|
|
23
|
+
'read_file', 'write_file', 'edit_file', 'list_directory',
|
|
24
|
+
'search_files', 'execute_command', 'shell', 'bash',
|
|
25
|
+
'glob', 'grep', 'web_search', 'web_fetch',
|
|
26
|
+
];
|
|
27
|
+
class MicroCompact {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
var _a, _b, _c, _d, _e;
|
|
30
|
+
this.name = 'micro';
|
|
31
|
+
this.options = {
|
|
32
|
+
gapThresholdMinutes: (_a = options === null || options === void 0 ? void 0 : options.gapThresholdMinutes) !== null && _a !== void 0 ? _a : DEFAULT_GAP_THRESHOLD_MINUTES,
|
|
33
|
+
keepRecent: (_b = options === null || options === void 0 ? void 0 : options.keepRecent) !== null && _b !== void 0 ? _b : DEFAULT_KEEP_RECENT,
|
|
34
|
+
compactableTools: new Set((_c = options === null || options === void 0 ? void 0 : options.compactableTools) !== null && _c !== void 0 ? _c : DEFAULT_COMPACTABLE_TOOLS),
|
|
35
|
+
tokenBudget: (_d = options === null || options === void 0 ? void 0 : options.tokenBudget) !== null && _d !== void 0 ? _d : 50000,
|
|
36
|
+
enableLogging: (_e = options === null || options === void 0 ? void 0 : options.enableLogging) !== null && _e !== void 0 ? _e : false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
shouldApply(ctx) {
|
|
40
|
+
const messages = ctx.messages;
|
|
41
|
+
if (!messages || messages.length === 0)
|
|
42
|
+
return false;
|
|
43
|
+
// Time-based trigger
|
|
44
|
+
if (this.evaluateTimeTrigger(messages))
|
|
45
|
+
return true;
|
|
46
|
+
// Count-based trigger
|
|
47
|
+
if (this.collectCompactableToolIds(messages).length > this.options.keepRecent)
|
|
48
|
+
return true;
|
|
49
|
+
// Budget-based trigger
|
|
50
|
+
if (this.estimateToolResultTokens(messages) > this.options.tokenBudget)
|
|
51
|
+
return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async apply(ctx) {
|
|
55
|
+
const messages = ctx.messages;
|
|
56
|
+
const timeTrigger = this.evaluateTimeTrigger(messages);
|
|
57
|
+
const allIds = this.collectCompactableToolIds(messages);
|
|
58
|
+
const keepRecent = Math.max(1, this.options.keepRecent);
|
|
59
|
+
const keepSet = new Set(allIds.slice(-keepRecent));
|
|
60
|
+
const subMechanism = timeTrigger ? 'time_based' : 'count_based';
|
|
61
|
+
const { result, tokensSaved } = this.clearOldToolResults(messages, keepSet);
|
|
62
|
+
if (tokensSaved === 0) {
|
|
63
|
+
return ctx;
|
|
64
|
+
}
|
|
65
|
+
const boundary = {
|
|
66
|
+
layer: 'micro',
|
|
67
|
+
tokensFreed: tokensSaved,
|
|
68
|
+
messagesRemoved: 0,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
if (this.options.enableLogging) {
|
|
72
|
+
console.log(`[MicroCompact] ${subMechanism}: cleared tool results, ~${tokensSaved} tokens freed`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
...ctx,
|
|
76
|
+
messages: result,
|
|
77
|
+
compactionHistory: [...(ctx.compactionHistory || []), boundary],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
reset() {
|
|
81
|
+
// Stateless across turns
|
|
82
|
+
}
|
|
83
|
+
// ─── Private Helpers ──────────────────────────────────────────────
|
|
84
|
+
evaluateTimeTrigger(messages) {
|
|
85
|
+
var _a, _b;
|
|
86
|
+
let lastAssistant;
|
|
87
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
88
|
+
if (((_a = messages[i]) === null || _a === void 0 ? void 0 : _a.role) === 'assistant') {
|
|
89
|
+
lastAssistant = messages[i];
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!lastAssistant)
|
|
94
|
+
return null;
|
|
95
|
+
const timestamp = (_b = lastAssistant.timestamp) !== null && _b !== void 0 ? _b : lastAssistant.created_at;
|
|
96
|
+
if (!timestamp)
|
|
97
|
+
return null;
|
|
98
|
+
const gapMs = Date.now() - new Date(timestamp).getTime();
|
|
99
|
+
const gapMinutes = gapMs / 60000;
|
|
100
|
+
if (!Number.isFinite(gapMinutes) || gapMinutes < this.options.gapThresholdMinutes) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return { gapMinutes };
|
|
104
|
+
}
|
|
105
|
+
collectCompactableToolIds(messages) {
|
|
106
|
+
var _a, _b;
|
|
107
|
+
const ids = [];
|
|
108
|
+
for (const msg of messages) {
|
|
109
|
+
if ((msg === null || msg === void 0 ? void 0 : msg.role) === 'assistant' && Array.isArray(msg.content)) {
|
|
110
|
+
for (const block of msg.content) {
|
|
111
|
+
if (block && typeof block === 'object' &&
|
|
112
|
+
block.type === 'tool_use' &&
|
|
113
|
+
this.options.compactableTools.has(block.name)) {
|
|
114
|
+
ids.push(block.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if ((msg === null || msg === void 0 ? void 0 : msg.role) === 'assistant' && Array.isArray(msg.tool_calls)) {
|
|
119
|
+
for (const tc of msg.tool_calls) {
|
|
120
|
+
if (tc && this.options.compactableTools.has((_b = (_a = tc.function) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : tc.name)) {
|
|
121
|
+
ids.push(tc.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return ids;
|
|
127
|
+
}
|
|
128
|
+
clearOldToolResults(messages, keepSet) {
|
|
129
|
+
const clearSet = new Set(this.collectCompactableToolIds(messages).filter(id => !keepSet.has(id)));
|
|
130
|
+
if (clearSet.size === 0) {
|
|
131
|
+
return { result: messages, tokensSaved: 0 };
|
|
132
|
+
}
|
|
133
|
+
let tokensSaved = 0;
|
|
134
|
+
const estimator = new types_1.TokenEstimator();
|
|
135
|
+
const result = messages.map(msg => {
|
|
136
|
+
var _a;
|
|
137
|
+
if (msg.role === 'tool' || msg.tool_call_id) {
|
|
138
|
+
const toolCallId = msg.tool_call_id;
|
|
139
|
+
if (clearSet.has(toolCallId)) {
|
|
140
|
+
const originalContent = typeof msg.content === 'string'
|
|
141
|
+
? msg.content : JSON.stringify((_a = msg.content) !== null && _a !== void 0 ? _a : '');
|
|
142
|
+
tokensSaved += estimator.estimate(originalContent);
|
|
143
|
+
return { ...msg, content: CLEAR_PLACEHOLDER };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
147
|
+
let touched = false;
|
|
148
|
+
const newContent = msg.content.map((block) => {
|
|
149
|
+
var _a, _b;
|
|
150
|
+
if (block &&
|
|
151
|
+
(block.type === 'tool_result' || block.type === 'function_response') &&
|
|
152
|
+
clearSet.has((_a = block.tool_use_id) !== null && _a !== void 0 ? _a : block.tool_call_id)) {
|
|
153
|
+
const originalContent = typeof block.content === 'string'
|
|
154
|
+
? block.content : JSON.stringify((_b = block.content) !== null && _b !== void 0 ? _b : '');
|
|
155
|
+
tokensSaved += estimator.estimate(originalContent);
|
|
156
|
+
touched = true;
|
|
157
|
+
return { ...block, content: CLEAR_PLACEHOLDER };
|
|
158
|
+
}
|
|
159
|
+
return block;
|
|
160
|
+
});
|
|
161
|
+
if (!touched)
|
|
162
|
+
return msg;
|
|
163
|
+
return { ...msg, content: newContent };
|
|
164
|
+
}
|
|
165
|
+
return msg;
|
|
166
|
+
});
|
|
167
|
+
return { result, tokensSaved };
|
|
168
|
+
}
|
|
169
|
+
estimateToolResultTokens(messages) {
|
|
170
|
+
var _a, _b, _c;
|
|
171
|
+
const estimator = new types_1.TokenEstimator();
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const msg of messages) {
|
|
174
|
+
if (msg.role === 'tool' || msg.tool_call_id) {
|
|
175
|
+
const toolName = (_a = msg.name) !== null && _a !== void 0 ? _a : '';
|
|
176
|
+
if (this.options.compactableTools.has(toolName)) {
|
|
177
|
+
const content = typeof msg.content === 'string'
|
|
178
|
+
? msg.content : JSON.stringify((_b = msg.content) !== null && _b !== void 0 ? _b : '');
|
|
179
|
+
total += estimator.estimate(content);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
183
|
+
for (const block of msg.content) {
|
|
184
|
+
if (block && (block.type === 'tool_result' || block.type === 'function_response')) {
|
|
185
|
+
const content = typeof block.content === 'string'
|
|
186
|
+
? block.content : JSON.stringify((_c = block.content) !== null && _c !== void 0 ? _c : '');
|
|
187
|
+
total += estimator.estimate(content);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return total;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.MicroCompact = MicroCompact;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Compact Cleanup
|
|
3
|
+
*
|
|
4
|
+
* Resets ALL tracking state after any compaction event.
|
|
5
|
+
* Called after both auto-compact and manual /compact to free memory
|
|
6
|
+
* held by tracking structures that are invalidated by compaction.
|
|
7
|
+
*/
|
|
8
|
+
import type { CompactionLayerKind, CompactionLayer } from './types';
|
|
9
|
+
export interface PostCompactCleanupOptions {
|
|
10
|
+
enableLogging?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class PostCompactCleanup {
|
|
13
|
+
constructor(_options?: PostCompactCleanupOptions);
|
|
14
|
+
runCleanup(layers: Map<CompactionLayerKind, CompactionLayer>, appliedLayers: CompactionLayerKind[]): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Post-Compact Cleanup
|
|
4
|
+
*
|
|
5
|
+
* Resets ALL tracking state after any compaction event.
|
|
6
|
+
* Called after both auto-compact and manual /compact to free memory
|
|
7
|
+
* held by tracking structures that are invalidated by compaction.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.PostCompactCleanup = void 0;
|
|
11
|
+
class PostCompactCleanup {
|
|
12
|
+
constructor(_options) { }
|
|
13
|
+
runCleanup(layers, appliedLayers) {
|
|
14
|
+
for (const layerName of appliedLayers) {
|
|
15
|
+
const layer = layers.get(layerName);
|
|
16
|
+
if (layer) {
|
|
17
|
+
try {
|
|
18
|
+
layer.reset();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Always reset reactive compact even if not applied
|
|
26
|
+
const reactive = layers.get('reactive');
|
|
27
|
+
if (reactive && !appliedLayers.includes('reactive')) {
|
|
28
|
+
try {
|
|
29
|
+
reactive.reset();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.PostCompactCleanup = PostCompactCleanup;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 5: Reactive Compact
|
|
3
|
+
*
|
|
4
|
+
* Emergency compaction triggered by API errors (413 prompt-too-long,
|
|
5
|
+
* media-size errors). This is the last line of defense when all proactive
|
|
6
|
+
* layers fail to keep the context under the limit.
|
|
7
|
+
*
|
|
8
|
+
* Key properties:
|
|
9
|
+
* - Only fires on real API errors after proactive measures fail
|
|
10
|
+
* - Single-shot per turn (prevents spiral: error -> compact -> error -> compact)
|
|
11
|
+
* - Tries: strip images -> strip older messages -> full compact
|
|
12
|
+
* - Withholds error from consumers until recovery succeeds or exhausts
|
|
13
|
+
*/
|
|
14
|
+
import type { MessageObject } from '@codebolt/types/sdk';
|
|
15
|
+
import { type CompactionContext, type CompactionLayer, type CompactionLayerKind } from './types';
|
|
16
|
+
export interface ReactiveCompactOptions {
|
|
17
|
+
/** Max retry attempts per turn (default: 1) */
|
|
18
|
+
retryLimit?: number;
|
|
19
|
+
/** Model token limit (default: 128000) */
|
|
20
|
+
modelTokenLimit?: number;
|
|
21
|
+
/** Enable logging (default: false) */
|
|
22
|
+
enableLogging?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ReactiveRecoveryResult {
|
|
25
|
+
recovered: boolean;
|
|
26
|
+
messages: MessageObject[];
|
|
27
|
+
reason: string;
|
|
28
|
+
tokensBefore: number;
|
|
29
|
+
tokensAfter: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class ReactiveCompact implements CompactionLayer {
|
|
32
|
+
readonly name: CompactionLayerKind;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private hasAttemptedThisTurn;
|
|
35
|
+
private retryCount;
|
|
36
|
+
constructor(options?: ReactiveCompactOptions);
|
|
37
|
+
shouldApply(_ctx: CompactionContext): boolean;
|
|
38
|
+
apply(_ctx: CompactionContext): Promise<CompactionContext>;
|
|
39
|
+
reset(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Reset per-turn guard (call at the start of each turn).
|
|
42
|
+
*/
|
|
43
|
+
resetForTurn(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Check if an error message indicates a recoverable context error.
|
|
46
|
+
*/
|
|
47
|
+
isRecoverableError(errorMessage: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Check if an error is a media/size error (images, PDFs, etc.)
|
|
50
|
+
*/
|
|
51
|
+
isMediaSizeError(error: unknown): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Attempt to recover from a context overflow error.
|
|
54
|
+
* Single-shot per turn to prevent death spirals.
|
|
55
|
+
*
|
|
56
|
+
* Recovery strategy:
|
|
57
|
+
* 1. Strip images and retry (for media errors)
|
|
58
|
+
* 2. Strip oldest messages (keep recent context)
|
|
59
|
+
* 3. Force full compact if available
|
|
60
|
+
*/
|
|
61
|
+
tryRecoverFromError(ctx: CompactionContext, error: unknown): Promise<ReactiveRecoveryResult>;
|
|
62
|
+
private stripImages;
|
|
63
|
+
private stripOldestMessages;
|
|
64
|
+
private forceCompact;
|
|
65
|
+
}
|