@codebolt/agent 6.0.0 → 6.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -153
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/processor-pieces/messageModifiers/chatHistoryMessageModifier.js +2 -4
- package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.d.ts +81 -0
- package/dist/processor-pieces/messageModifiers/contextAssemblyModifier.js +316 -0
- package/dist/processor-pieces/messageModifiers/coreSystemPromptModifier.js +2 -19
- package/dist/processor-pieces/messageModifiers/directoryContextModifier.js +2 -6
- package/dist/processor-pieces/messageModifiers/environmentContextModifier.js +2 -7
- package/dist/processor-pieces/messageModifiers/ideContextModifier.js +2 -21
- package/dist/processor-pieces/messageModifiers/index.d.ts +3 -0
- package/dist/processor-pieces/messageModifiers/index.js +7 -1
- package/dist/processor-pieces/messageModifiers/toolInjectionModifier.js +2 -1
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.d.ts +34 -5
- package/dist/processor-pieces/postToolCallProcessors/conversationCompactorModifier.js +374 -89
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.d.ts +3 -0
- package/dist/processor-pieces/preInferenceProcessors/chatCompressionModifier.js +50 -27
- package/dist/unified/agent/agent.d.ts +10 -0
- package/dist/unified/agent/agent.js +198 -10
- package/dist/unified/agent/codeboltAgent.d.ts +19 -100
- package/dist/unified/agent/codeboltAgent.js +209 -109
- package/dist/unified/base/agentStep.js +17 -17
- package/dist/unified/base/initialPromptGenerator.js +13 -31
- package/dist/unified/base/promptContext.d.ts +13 -0
- package/dist/unified/base/promptContext.js +213 -0
- package/dist/unified/base/responseExecutor.d.ts +7 -19
- package/dist/unified/base/responseExecutor.js +280 -259
- package/dist/unified/index.d.ts +9 -0
- package/dist/unified/index.js +20 -13
- package/dist/unified/services/CompressionCoordinator.d.ts +67 -0
- package/dist/unified/services/CompressionCoordinator.js +214 -0
- package/dist/unified/services/compaction/autoCompact.d.ts +52 -0
- package/dist/unified/services/compaction/autoCompact.js +294 -0
- package/dist/unified/services/compaction/compactionOrchestrator.d.ts +78 -0
- package/dist/unified/services/compaction/compactionOrchestrator.js +230 -0
- package/dist/unified/services/compaction/contextCollapse.d.ts +63 -0
- package/dist/unified/services/compaction/contextCollapse.js +291 -0
- package/dist/unified/services/compaction/microCompact.d.ts +34 -0
- package/dist/unified/services/compaction/microCompact.js +195 -0
- package/dist/unified/services/compaction/postCompactCleanup.d.ts +15 -0
- package/dist/unified/services/compaction/postCompactCleanup.js +37 -0
- package/dist/unified/services/compaction/reactiveCompact.d.ts +65 -0
- package/dist/unified/services/compaction/reactiveCompact.js +301 -0
- package/dist/unified/services/compaction/snipCompact.d.ts +31 -0
- package/dist/unified/services/compaction/snipCompact.js +124 -0
- package/dist/unified/services/compaction/types.d.ts +66 -0
- package/dist/unified/services/compaction/types.js +39 -0
- package/package.json +25 -29
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 5: Reactive Compact
|
|
4
|
+
*
|
|
5
|
+
* Emergency compaction triggered by API errors (413 prompt-too-long,
|
|
6
|
+
* media-size errors). This is the last line of defense when all proactive
|
|
7
|
+
* layers fail to keep the context under the limit.
|
|
8
|
+
*
|
|
9
|
+
* Key properties:
|
|
10
|
+
* - Only fires on real API errors after proactive measures fail
|
|
11
|
+
* - Single-shot per turn (prevents spiral: error -> compact -> error -> compact)
|
|
12
|
+
* - Tries: strip images -> strip older messages -> full compact
|
|
13
|
+
* - Withholds error from consumers until recovery succeeds or exhausts
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ReactiveCompact = void 0;
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
/** Error patterns that indicate context overflow */
|
|
19
|
+
const CONTEXT_ERROR_PATTERNS = [
|
|
20
|
+
/prompt too long/i,
|
|
21
|
+
/context length/i,
|
|
22
|
+
/maximum context/i,
|
|
23
|
+
/context window/i,
|
|
24
|
+
/too many tokens/i,
|
|
25
|
+
/token limit/i,
|
|
26
|
+
/request too large/i,
|
|
27
|
+
/input is too long/i,
|
|
28
|
+
/reduced_image_max_pixels/i,
|
|
29
|
+
/image.*too large/i,
|
|
30
|
+
];
|
|
31
|
+
/** Messages to always preserve during reactive compact */
|
|
32
|
+
const MIN_PRESERVED_MESSAGES = 6;
|
|
33
|
+
class ReactiveCompact {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
var _a, _b, _c;
|
|
36
|
+
this.name = 'reactive';
|
|
37
|
+
this.hasAttemptedThisTurn = false;
|
|
38
|
+
this.retryCount = 0;
|
|
39
|
+
this.options = {
|
|
40
|
+
retryLimit: (_a = options === null || options === void 0 ? void 0 : options.retryLimit) !== null && _a !== void 0 ? _a : 1,
|
|
41
|
+
modelTokenLimit: (_b = options === null || options === void 0 ? void 0 : options.modelTokenLimit) !== null && _b !== void 0 ? _b : 128000,
|
|
42
|
+
enableLogging: (_c = options === null || options === void 0 ? void 0 : options.enableLogging) !== null && _c !== void 0 ? _c : false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
shouldApply(_ctx) {
|
|
46
|
+
// Reactive only fires when explicitly triggered by an error
|
|
47
|
+
// via tryRecoverFromError(), not proactively
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
async apply(_ctx) {
|
|
51
|
+
// Reactive compact is not applied proactively
|
|
52
|
+
return _ctx;
|
|
53
|
+
}
|
|
54
|
+
reset() {
|
|
55
|
+
this.hasAttemptedThisTurn = false;
|
|
56
|
+
this.retryCount = 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reset per-turn guard (call at the start of each turn).
|
|
60
|
+
*/
|
|
61
|
+
resetForTurn() {
|
|
62
|
+
this.hasAttemptedThisTurn = false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if an error message indicates a recoverable context error.
|
|
66
|
+
*/
|
|
67
|
+
isRecoverableError(errorMessage) {
|
|
68
|
+
return CONTEXT_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if an error is a media/size error (images, PDFs, etc.)
|
|
72
|
+
*/
|
|
73
|
+
isMediaSizeError(error) {
|
|
74
|
+
if (!error)
|
|
75
|
+
return false;
|
|
76
|
+
const msg = error instanceof Error
|
|
77
|
+
? error.message
|
|
78
|
+
: typeof error === 'string'
|
|
79
|
+
? error
|
|
80
|
+
: String(error);
|
|
81
|
+
return (/image.*too large/i.test(msg) ||
|
|
82
|
+
/reduced_image_max_pixels/i.test(msg) ||
|
|
83
|
+
/media.*size/i.test(msg) ||
|
|
84
|
+
/too many images/i.test(msg));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Attempt to recover from a context overflow error.
|
|
88
|
+
* Single-shot per turn to prevent death spirals.
|
|
89
|
+
*
|
|
90
|
+
* Recovery strategy:
|
|
91
|
+
* 1. Strip images and retry (for media errors)
|
|
92
|
+
* 2. Strip oldest messages (keep recent context)
|
|
93
|
+
* 3. Force full compact if available
|
|
94
|
+
*/
|
|
95
|
+
async tryRecoverFromError(ctx, error) {
|
|
96
|
+
const estimator = new types_1.TokenEstimator();
|
|
97
|
+
const messages = [...ctx.messages];
|
|
98
|
+
const tokensBefore = estimator.estimateForMessages(messages);
|
|
99
|
+
// Single-shot guard
|
|
100
|
+
if (this.hasAttemptedThisTurn) {
|
|
101
|
+
return {
|
|
102
|
+
recovered: false,
|
|
103
|
+
messages,
|
|
104
|
+
reason: 'Already attempted reactive compact this turn',
|
|
105
|
+
tokensBefore,
|
|
106
|
+
tokensAfter: tokensBefore,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Check retry limit
|
|
110
|
+
if (this.retryCount >= this.options.retryLimit) {
|
|
111
|
+
return {
|
|
112
|
+
recovered: false,
|
|
113
|
+
messages,
|
|
114
|
+
reason: `Reactive compact retry limit reached (${this.options.retryLimit})`,
|
|
115
|
+
tokensBefore,
|
|
116
|
+
tokensAfter: tokensBefore,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
this.hasAttemptedThisTurn = true;
|
|
120
|
+
this.retryCount++;
|
|
121
|
+
// Strategy 1: Strip images (for media-size errors)
|
|
122
|
+
if (this.isMediaSizeError(error)) {
|
|
123
|
+
const stripped = this.stripImages(messages);
|
|
124
|
+
const tokensAfter = estimator.estimateForMessages(stripped);
|
|
125
|
+
if (tokensAfter < tokensBefore) {
|
|
126
|
+
if (this.options.enableLogging) {
|
|
127
|
+
console.log(`[ReactiveCompact] Stripped images: ${tokensBefore} -> ${tokensAfter} tokens`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
recovered: true,
|
|
131
|
+
messages: stripped,
|
|
132
|
+
reason: 'Stripped oversized images',
|
|
133
|
+
tokensBefore,
|
|
134
|
+
tokensAfter,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Strategy 2: Strip oldest messages (aggressive)
|
|
139
|
+
const aggressiveResult = this.stripOldestMessages(messages);
|
|
140
|
+
if (aggressiveResult) {
|
|
141
|
+
const tokensAfter = estimator.estimateForMessages(aggressiveResult);
|
|
142
|
+
const threshold = this.options.modelTokenLimit * 0.7;
|
|
143
|
+
if (tokensAfter < threshold) {
|
|
144
|
+
if (this.options.enableLogging) {
|
|
145
|
+
console.log(`[ReactiveCompact] Stripped old messages: ${tokensBefore} -> ${tokensAfter} tokens`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
recovered: true,
|
|
149
|
+
messages: aggressiveResult,
|
|
150
|
+
reason: 'Stripped oldest messages',
|
|
151
|
+
tokensBefore,
|
|
152
|
+
tokensAfter,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Strategy 3: Force compact - summarize everything aggressively
|
|
157
|
+
const forceResult = await this.forceCompact(messages);
|
|
158
|
+
if (forceResult) {
|
|
159
|
+
const tokensAfter = estimator.estimateForMessages(forceResult);
|
|
160
|
+
if (tokensAfter < tokensBefore * 0.8) {
|
|
161
|
+
if (this.options.enableLogging) {
|
|
162
|
+
console.log(`[ReactiveCompact] Force compact: ${tokensBefore} -> ${tokensAfter} tokens`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
recovered: true,
|
|
166
|
+
messages: forceResult,
|
|
167
|
+
reason: 'Force compacted conversation',
|
|
168
|
+
tokensBefore,
|
|
169
|
+
tokensAfter,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
recovered: false,
|
|
175
|
+
messages,
|
|
176
|
+
reason: 'All reactive recovery strategies exhausted',
|
|
177
|
+
tokensBefore,
|
|
178
|
+
tokensAfter: tokensBefore,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ─── Private Helpers ──────────────────────────────────────────────
|
|
182
|
+
stripImages(messages) {
|
|
183
|
+
return messages.map(msg => {
|
|
184
|
+
if (!msg || typeof msg.content !== 'object' || !Array.isArray(msg.content)) {
|
|
185
|
+
return msg;
|
|
186
|
+
}
|
|
187
|
+
const filtered = msg.content.filter((block) => {
|
|
188
|
+
if (!block)
|
|
189
|
+
return true;
|
|
190
|
+
if (block.type === 'image_url' ||
|
|
191
|
+
block.type === 'image' ||
|
|
192
|
+
(block.type === 'text' &&
|
|
193
|
+
typeof block.text === 'string' &&
|
|
194
|
+
block.text.startsWith('data:image'))) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
if (filtered.length === msg.content.length) {
|
|
200
|
+
return msg;
|
|
201
|
+
}
|
|
202
|
+
if (filtered.length < msg.content.length) {
|
|
203
|
+
filtered.push({
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `[${msg.content.length - filtered.length} image(s) removed due to size limits]`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { ...msg, content: filtered };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
stripOldestMessages(messages) {
|
|
212
|
+
const systemMessages = messages.filter(m => (m === null || m === void 0 ? void 0 : m.role) === 'system');
|
|
213
|
+
const nonSystemMessages = messages.filter(m => (m === null || m === void 0 ? void 0 : m.role) !== 'system');
|
|
214
|
+
if (nonSystemMessages.length <= MIN_PRESERVED_MESSAGES) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const recentMessages = nonSystemMessages.slice(-MIN_PRESERVED_MESSAGES);
|
|
218
|
+
const strippedCount = nonSystemMessages.length - MIN_PRESERVED_MESSAGES;
|
|
219
|
+
return [
|
|
220
|
+
...systemMessages,
|
|
221
|
+
{
|
|
222
|
+
role: 'user',
|
|
223
|
+
content: `[Emergency context reduction: ${strippedCount} older messages removed due to context overflow. The current task context is preserved below.]`,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
role: 'assistant',
|
|
227
|
+
content: 'Understood. I will work with the available context.',
|
|
228
|
+
},
|
|
229
|
+
...recentMessages,
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
async forceCompact(messages) {
|
|
233
|
+
var _a, _b, _c, _d, _e;
|
|
234
|
+
try {
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
236
|
+
const codebolt = require('@codebolt/codeboltjs');
|
|
237
|
+
const systemMessages = messages.filter(m => (m === null || m === void 0 ? void 0 : m.role) === 'system');
|
|
238
|
+
const nonSystemMessages = messages.filter(m => (m === null || m === void 0 ? void 0 : m.role) !== 'system');
|
|
239
|
+
if (nonSystemMessages.length <= MIN_PRESERVED_MESSAGES) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const toSummarize = nonSystemMessages.slice(0, -MIN_PRESERVED_MESSAGES);
|
|
243
|
+
const toKeep = nonSystemMessages.slice(-MIN_PRESERVED_MESSAGES);
|
|
244
|
+
const historyText = toSummarize
|
|
245
|
+
.map((msg, i) => {
|
|
246
|
+
var _a;
|
|
247
|
+
const content = typeof msg.content === 'string'
|
|
248
|
+
? msg.content
|
|
249
|
+
: JSON.stringify((_a = msg.content) !== null && _a !== void 0 ? _a : '');
|
|
250
|
+
const truncated = content.length > 500
|
|
251
|
+
? content.slice(0, 250) + '...[truncated]...' + content.slice(-250)
|
|
252
|
+
: content;
|
|
253
|
+
return `[${i}] ${msg.role}: ${truncated}`;
|
|
254
|
+
})
|
|
255
|
+
.join('\n');
|
|
256
|
+
const prompt = `Emergency context compression. Create a very concise summary preserving only:
|
|
257
|
+
1. The user's original task/request
|
|
258
|
+
2. Key files being worked on (paths)
|
|
259
|
+
3. Current state and next step
|
|
260
|
+
4. Any critical errors/blockers
|
|
261
|
+
|
|
262
|
+
Conversation:
|
|
263
|
+
${historyText}`;
|
|
264
|
+
const response = await codebolt.llm.inference({
|
|
265
|
+
messages: [
|
|
266
|
+
{
|
|
267
|
+
role: 'system',
|
|
268
|
+
content: 'Create an extremely concise summary. Focus on actionable facts only.',
|
|
269
|
+
},
|
|
270
|
+
{ role: 'user', content: prompt },
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
let summary;
|
|
274
|
+
if (typeof ((_a = response === null || response === void 0 ? void 0 : response.completion) === null || _a === void 0 ? void 0 : _a.content) === 'string') {
|
|
275
|
+
summary = response.completion.content;
|
|
276
|
+
}
|
|
277
|
+
else if ((_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) {
|
|
278
|
+
summary = response.completion.choices[0].message.content;
|
|
279
|
+
}
|
|
280
|
+
if (!summary || summary.trim().length === 0) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
return [
|
|
284
|
+
...systemMessages,
|
|
285
|
+
{
|
|
286
|
+
role: 'user',
|
|
287
|
+
content: `[Emergency context recovery. Previous conversation summary:\n\n${summary}]\n\nRecent context continues below.`,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
role: 'assistant',
|
|
291
|
+
content: 'Understood. I have the compressed context and will continue.',
|
|
292
|
+
},
|
|
293
|
+
...toKeep,
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
exports.ReactiveCompact = ReactiveCompact;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1: Snip Compact
|
|
3
|
+
*
|
|
4
|
+
* Proactively removes older messages from the model-facing view each turn.
|
|
5
|
+
* Keeps them in the conversation for scrollback/UI purposes, but removes
|
|
6
|
+
* them from what gets sent to the LLM.
|
|
7
|
+
*
|
|
8
|
+
* Key properties:
|
|
9
|
+
* - Runs every turn, proactively
|
|
10
|
+
* - tokensFreed is plumbed to autocompact so its threshold check is accurate
|
|
11
|
+
* - Cheapest compaction layer (no LLM call, just array slicing)
|
|
12
|
+
*/
|
|
13
|
+
import { type CompactionContext, type CompactionLayer, type CompactionLayerKind } from './types';
|
|
14
|
+
export interface SnipCompactOptions {
|
|
15
|
+
/** Number of recent turns to protect (default: 4) */
|
|
16
|
+
protectedTurns?: number;
|
|
17
|
+
/** Max fraction of messages to remove (default: 0.4) */
|
|
18
|
+
maxSnipFraction?: number;
|
|
19
|
+
/** Enable logging (default: false) */
|
|
20
|
+
enableLogging?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class SnipCompact implements CompactionLayer {
|
|
23
|
+
readonly name: CompactionLayerKind;
|
|
24
|
+
private readonly options;
|
|
25
|
+
constructor(options?: SnipCompactOptions);
|
|
26
|
+
shouldApply(ctx: CompactionContext): boolean;
|
|
27
|
+
apply(ctx: CompactionContext): Promise<CompactionContext>;
|
|
28
|
+
reset(): void;
|
|
29
|
+
private findTurnStarts;
|
|
30
|
+
private isToolResponse;
|
|
31
|
+
}
|