@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.
@@ -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
+ }