@probelabs/probe 0.6.0-rc161 → 0.6.0-rc162
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 +89 -0
- package/build/agent/ProbeAgent.js +349 -85
- package/build/agent/contextCompactor.js +271 -0
- package/build/agent/index.js +788 -84
- package/build/agent/schemaUtils.js +7 -0
- package/build/agent/xmlParsingUtils.js +24 -4
- package/build/tools/common.js +16 -1
- package/cjs/agent/ProbeAgent.cjs +823 -134
- package/cjs/index.cjs +823 -134
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +349 -85
- package/src/agent/contextCompactor.js +271 -0
- package/src/agent/index.js +30 -1
- package/src/agent/schemaUtils.js +7 -0
- package/src/agent/xmlParsingUtils.js +24 -4
- package/src/tools/common.js +16 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Compactor
|
|
3
|
+
*
|
|
4
|
+
* Handles context window overflow by intelligently removing intermediate agentic
|
|
5
|
+
* monologue sections while preserving user messages, final answers, and the most
|
|
6
|
+
* recent monologue.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context limit error patterns to detect from various AI providers
|
|
11
|
+
* Simple substring matching similar to RetryManager's approach
|
|
12
|
+
*/
|
|
13
|
+
const CONTEXT_LIMIT_ERROR_PATTERNS = [
|
|
14
|
+
// Anthropic
|
|
15
|
+
'context_length_exceeded',
|
|
16
|
+
'prompt is too long',
|
|
17
|
+
|
|
18
|
+
// OpenAI
|
|
19
|
+
'maximum context length',
|
|
20
|
+
'context length is',
|
|
21
|
+
|
|
22
|
+
// Google/Gemini
|
|
23
|
+
'input token count exceeds',
|
|
24
|
+
'token limit exceeded',
|
|
25
|
+
|
|
26
|
+
// Generic patterns
|
|
27
|
+
'context window',
|
|
28
|
+
'too many tokens',
|
|
29
|
+
'token limit',
|
|
30
|
+
'context limit',
|
|
31
|
+
'exceed', // Catches "exceeds", "exceed maximum", etc.
|
|
32
|
+
'over the limit',
|
|
33
|
+
'maximum tokens'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error message indicates a context window limit was exceeded
|
|
38
|
+
* Uses simple substring matching for reliability and maintainability
|
|
39
|
+
*
|
|
40
|
+
* @param {Error|string} error - The error object or error message
|
|
41
|
+
* @returns {boolean} - True if the error indicates context limit exceeded
|
|
42
|
+
*/
|
|
43
|
+
export function isContextLimitError(error) {
|
|
44
|
+
if (!error) return false;
|
|
45
|
+
|
|
46
|
+
// Get error message in various forms
|
|
47
|
+
const errorMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
|
|
48
|
+
const errorString = error.toString().toLowerCase();
|
|
49
|
+
|
|
50
|
+
// Check if any pattern matches
|
|
51
|
+
for (const pattern of CONTEXT_LIMIT_ERROR_PATTERNS) {
|
|
52
|
+
const lowerPattern = pattern.toLowerCase();
|
|
53
|
+
if (errorMessage.includes(lowerPattern) || errorString.includes(lowerPattern)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Identify message boundaries in conversation history
|
|
63
|
+
* Structure: <user> -> <internal agentic monologue> -> <final-agent-answer>
|
|
64
|
+
*
|
|
65
|
+
* A "segment" is:
|
|
66
|
+
* - user message (role: 'user')
|
|
67
|
+
* - followed by 0+ assistant messages (internal monologue)
|
|
68
|
+
* - ending with tool_result or attempt_completion (final answer)
|
|
69
|
+
*
|
|
70
|
+
* @param {Array} messages - Array of message objects with {role, content}
|
|
71
|
+
* @returns {Array} - Array of segments, each containing {userIndex, monologueIndices, finalIndex}
|
|
72
|
+
*/
|
|
73
|
+
export function identifyMessageSegments(messages) {
|
|
74
|
+
const segments = [];
|
|
75
|
+
let currentSegment = null;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < messages.length; i++) {
|
|
78
|
+
const msg = messages[i];
|
|
79
|
+
|
|
80
|
+
// Skip system messages
|
|
81
|
+
if (msg.role === 'system') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// User message starts a new segment
|
|
86
|
+
if (msg.role === 'user') {
|
|
87
|
+
// Check if this is a tool_result (final answer from previous segment)
|
|
88
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
89
|
+
const isToolResult = content.includes('<tool_result>');
|
|
90
|
+
|
|
91
|
+
if (isToolResult && currentSegment) {
|
|
92
|
+
// This is the final answer for the current segment
|
|
93
|
+
currentSegment.finalIndex = i;
|
|
94
|
+
segments.push(currentSegment);
|
|
95
|
+
currentSegment = null;
|
|
96
|
+
} else {
|
|
97
|
+
// Save previous segment if it exists
|
|
98
|
+
if (currentSegment) {
|
|
99
|
+
segments.push(currentSegment);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Start new segment
|
|
103
|
+
currentSegment = {
|
|
104
|
+
userIndex: i,
|
|
105
|
+
monologueIndices: [],
|
|
106
|
+
finalIndex: null
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Assistant message is part of monologue
|
|
112
|
+
if (msg.role === 'assistant' && currentSegment) {
|
|
113
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
114
|
+
|
|
115
|
+
// Check if this contains attempt_completion (marks end of segment)
|
|
116
|
+
if (content.includes('<attempt_completion>') || content.includes('attempt_completion')) {
|
|
117
|
+
currentSegment.monologueIndices.push(i);
|
|
118
|
+
currentSegment.finalIndex = i;
|
|
119
|
+
segments.push(currentSegment);
|
|
120
|
+
currentSegment = null;
|
|
121
|
+
} else {
|
|
122
|
+
// Regular monologue message
|
|
123
|
+
currentSegment.monologueIndices.push(i);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Save any remaining segment
|
|
129
|
+
if (currentSegment) {
|
|
130
|
+
segments.push(currentSegment);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return segments;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compact messages by removing intermediate monologues
|
|
138
|
+
*
|
|
139
|
+
* Strategy:
|
|
140
|
+
* 1. Keep all user messages
|
|
141
|
+
* 2. Keep all final answers (tool_results, attempt_completion)
|
|
142
|
+
* 3. Remove intermediate monologue messages from completed segments
|
|
143
|
+
* 4. Keep the most recent (active) segment intact
|
|
144
|
+
*
|
|
145
|
+
* @param {Array} messages - Array of message objects
|
|
146
|
+
* @param {Object} options - Compaction options
|
|
147
|
+
* @param {boolean} [options.keepLastSegment=true] - Keep the most recent segment intact
|
|
148
|
+
* @param {number} [options.minSegmentsToKeep=1] - Minimum number of recent segments to preserve fully
|
|
149
|
+
* @returns {Array} - Compacted message array
|
|
150
|
+
*/
|
|
151
|
+
export function compactMessages(messages, options = {}) {
|
|
152
|
+
const {
|
|
153
|
+
keepLastSegment = true,
|
|
154
|
+
minSegmentsToKeep = 1
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
if (!messages || messages.length === 0) {
|
|
158
|
+
return messages;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Identify segments
|
|
162
|
+
const segments = identifyMessageSegments(messages);
|
|
163
|
+
|
|
164
|
+
if (segments.length === 0) {
|
|
165
|
+
return messages;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Determine which segments to keep fully vs compact
|
|
169
|
+
const segmentsToPreserve = keepLastSegment
|
|
170
|
+
? Math.max(minSegmentsToKeep, 1)
|
|
171
|
+
: minSegmentsToKeep;
|
|
172
|
+
|
|
173
|
+
const compactableSegments = segments.slice(0, -segmentsToPreserve);
|
|
174
|
+
const preservedSegments = segments.slice(-segmentsToPreserve);
|
|
175
|
+
|
|
176
|
+
// Build set of indices to keep
|
|
177
|
+
const indicesToKeep = new Set();
|
|
178
|
+
|
|
179
|
+
// Keep system messages
|
|
180
|
+
messages.forEach((msg, idx) => {
|
|
181
|
+
if (msg.role === 'system') {
|
|
182
|
+
indicesToKeep.add(idx);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// For compactable segments: keep user message and final answer only
|
|
187
|
+
compactableSegments.forEach(segment => {
|
|
188
|
+
indicesToKeep.add(segment.userIndex);
|
|
189
|
+
if (segment.finalIndex !== null) {
|
|
190
|
+
indicesToKeep.add(segment.finalIndex);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// For preserved segments: keep everything
|
|
195
|
+
preservedSegments.forEach(segment => {
|
|
196
|
+
indicesToKeep.add(segment.userIndex);
|
|
197
|
+
segment.monologueIndices.forEach(idx => indicesToKeep.add(idx));
|
|
198
|
+
if (segment.finalIndex !== null) {
|
|
199
|
+
indicesToKeep.add(segment.finalIndex);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Filter messages
|
|
204
|
+
const compactedMessages = messages.filter((_, idx) => indicesToKeep.has(idx));
|
|
205
|
+
|
|
206
|
+
return compactedMessages;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Calculate reduction statistics
|
|
211
|
+
* @param {Array} originalMessages - Original message array
|
|
212
|
+
* @param {Array} compactedMessages - Compacted message array
|
|
213
|
+
* @returns {Object} - Statistics about the compaction
|
|
214
|
+
*/
|
|
215
|
+
export function calculateCompactionStats(originalMessages, compactedMessages) {
|
|
216
|
+
const originalCount = originalMessages.length;
|
|
217
|
+
const compactedCount = compactedMessages.length;
|
|
218
|
+
const removed = originalCount - compactedCount;
|
|
219
|
+
const reductionPercent = originalCount > 0
|
|
220
|
+
? ((removed / originalCount) * 100).toFixed(1)
|
|
221
|
+
: 0;
|
|
222
|
+
|
|
223
|
+
// Estimate token savings (rough approximation)
|
|
224
|
+
const estimateTokens = (msgs) => {
|
|
225
|
+
return msgs.reduce((sum, msg) => {
|
|
226
|
+
const content = typeof msg.content === 'string'
|
|
227
|
+
? msg.content
|
|
228
|
+
: JSON.stringify(msg.content);
|
|
229
|
+
// Rough estimate: 1 token ≈ 4 characters
|
|
230
|
+
return sum + Math.ceil(content.length / 4);
|
|
231
|
+
}, 0);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const originalTokens = estimateTokens(originalMessages);
|
|
235
|
+
const compactedTokens = estimateTokens(compactedMessages);
|
|
236
|
+
const tokensSaved = originalTokens - compactedTokens;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
originalCount,
|
|
240
|
+
compactedCount,
|
|
241
|
+
removed,
|
|
242
|
+
reductionPercent: parseFloat(reductionPercent),
|
|
243
|
+
originalTokens,
|
|
244
|
+
compactedTokens,
|
|
245
|
+
tokensSaved
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Main compaction handler for ProbeAgent
|
|
251
|
+
* Detects context limit errors and performs intelligent compaction
|
|
252
|
+
*
|
|
253
|
+
* @param {Error} error - The error from the AI provider
|
|
254
|
+
* @param {Array} messages - Current message array
|
|
255
|
+
* @param {Object} options - Compaction options
|
|
256
|
+
* @returns {Object|null} - { compacted: true, messages, stats } or null if not applicable
|
|
257
|
+
*/
|
|
258
|
+
export function handleContextLimitError(error, messages, options = {}) {
|
|
259
|
+
if (!isContextLimitError(error)) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const compactedMessages = compactMessages(messages, options);
|
|
264
|
+
const stats = calculateCompactionStats(messages, compactedMessages);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
compacted: true,
|
|
268
|
+
messages: compactedMessages,
|
|
269
|
+
stats
|
|
270
|
+
};
|
|
271
|
+
}
|