@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc161",
3
+ "version": "0.6.0-rc162",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -60,6 +60,7 @@ import {
60
60
  } from './mcp/index.js';
61
61
  import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
62
62
  import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
63
+ import { handleContextLimitError } from './contextCompactor.js';
63
64
 
64
65
  // Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
65
66
  const MAX_TOOL_ITERATIONS = (() => {
@@ -104,6 +105,8 @@ export class ProbeAgent {
104
105
  * @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
105
106
  * @param {Object} [options.storageAdapter] - Custom storage adapter for history management
106
107
  * @param {Object} [options.hooks] - Hook callbacks for events (e.g., {'tool:start': callback})
108
+ * @param {Array<string>|null} [options.allowedTools] - List of allowed tool names. Use ['*'] for all tools (default), [] or null for no tools (raw AI mode), or specific tool names like ['search', 'query', 'extract']. Supports exclusion with '!' prefix (e.g., ['*', '!bash'])
109
+ * @param {boolean} [options.disableTools=false] - Convenience flag to disable all tools (equivalent to allowedTools: []). Takes precedence over allowedTools if set.
107
110
  * @param {Object} [options.retry] - Retry configuration
108
111
  * @param {number} [options.retry.maxRetries=3] - Maximum retry attempts per provider
109
112
  * @param {number} [options.retry.initialDelay=1000] - Initial delay in ms
@@ -139,6 +142,13 @@ export class ProbeAgent {
139
142
  this.disableMermaidValidation = !!options.disableMermaidValidation;
140
143
  this.disableJsonValidation = !!options.disableJsonValidation;
141
144
 
145
+ // Tool filtering configuration
146
+ // Parse allowedTools option: ['*'] = all tools, [] or null = no tools, ['tool1', 'tool2'] = specific tools
147
+ // Supports exclusion with '!' prefix: ['*', '!bash'] = all tools except bash
148
+ // disableTools is a convenience flag that overrides allowedTools to []
149
+ const effectiveAllowedTools = options.disableTools ? [] : options.allowedTools;
150
+ this.allowedTools = this._parseAllowedTools(effectiveAllowedTools);
151
+
142
152
  // Storage adapter (defaults to in-memory)
143
153
  this.storageAdapter = options.storageAdapter || new InMemoryStorageAdapter();
144
154
 
@@ -216,6 +226,74 @@ export class ProbeAgent {
216
226
  // Constructor must remain synchronous for backward compatibility
217
227
  }
218
228
 
229
+ /**
230
+ * Parse allowedTools configuration
231
+ * @param {Array<string>|null|undefined} allowedTools - Tool filtering configuration
232
+ * @returns {Object} Parsed configuration with isEnabled method
233
+ * @private
234
+ */
235
+ _parseAllowedTools(allowedTools) {
236
+ // Helper to check if tool matches a pattern (supports * wildcard)
237
+ const matchesPattern = (toolName, pattern) => {
238
+ if (!pattern.includes('*')) {
239
+ return toolName === pattern;
240
+ }
241
+ const regexPattern = pattern.replace(/\*/g, '.*');
242
+ return new RegExp(`^${regexPattern}$`).test(toolName);
243
+ };
244
+
245
+ // Default: all tools allowed
246
+ if (!allowedTools || (Array.isArray(allowedTools) && allowedTools.includes('*'))) {
247
+ const exclusions = Array.isArray(allowedTools)
248
+ ? allowedTools.filter(t => t.startsWith('!')).map(t => t.slice(1))
249
+ : [];
250
+
251
+ return {
252
+ mode: 'all',
253
+ exclusions,
254
+ isEnabled: (toolName) => !exclusions.some(pattern => matchesPattern(toolName, pattern))
255
+ };
256
+ }
257
+
258
+ // Empty array or null: no tools (raw AI mode)
259
+ if (Array.isArray(allowedTools) && allowedTools.length === 0) {
260
+ return {
261
+ mode: 'none',
262
+ isEnabled: () => false
263
+ };
264
+ }
265
+
266
+ // Specific tools allowed (with wildcard support)
267
+ const allowedPatterns = allowedTools.filter(t => !t.startsWith('!'));
268
+ return {
269
+ mode: 'whitelist',
270
+ allowed: allowedPatterns,
271
+ isEnabled: (toolName) => allowedPatterns.some(pattern => matchesPattern(toolName, pattern))
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Check if an MCP tool is allowed based on allowedTools configuration
277
+ * Uses mcp__ prefix convention (like Claude Code)
278
+ * @param {string} toolName - The MCP tool name (without mcp__ prefix)
279
+ * @returns {boolean} - Whether the tool is allowed
280
+ * @private
281
+ */
282
+ _isMcpToolAllowed(toolName) {
283
+ const mcpToolName = `mcp__${toolName}`;
284
+ return this.allowedTools.isEnabled(mcpToolName) || this.allowedTools.isEnabled(toolName);
285
+ }
286
+
287
+ /**
288
+ * Filter MCP tools based on allowedTools configuration
289
+ * @param {string[]} mcpToolNames - Array of MCP tool names
290
+ * @returns {string[]} - Filtered array of allowed MCP tool names
291
+ * @private
292
+ */
293
+ _filterMcpTools(mcpToolNames) {
294
+ return mcpToolNames.filter(toolName => this._isMcpToolAllowed(toolName));
295
+ }
296
+
219
297
  /**
220
298
  * Initialize the agent asynchronously (must be called after constructor)
221
299
  * This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
@@ -248,10 +326,15 @@ export class ProbeAgent {
248
326
  await this.initializeMCP();
249
327
 
250
328
  // Merge MCP tools into toolImplementations for unified access
329
+ // Apply allowedTools filtering using mcp__ prefix (like Claude Code)
251
330
  if (this.mcpBridge) {
252
331
  const mcpTools = this.mcpBridge.mcpTools || {};
253
332
  for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
254
- this.toolImplementations[toolName] = toolImpl;
333
+ if (this._isMcpToolAllowed(toolName)) {
334
+ this.toolImplementations[toolName] = toolImpl;
335
+ } else if (this.debug) {
336
+ console.error(`[DEBUG] MCP tool '${toolName}' filtered out by allowedTools`);
337
+ }
255
338
  }
256
339
  }
257
340
 
@@ -1047,10 +1130,15 @@ export class ProbeAgent {
1047
1130
  await this.initializeMCP();
1048
1131
 
1049
1132
  // Merge MCP tools into toolImplementations for unified access
1133
+ // Apply allowedTools filtering using mcp__ prefix (like Claude Code)
1050
1134
  if (this.mcpBridge) {
1051
1135
  const mcpTools = this.mcpBridge.mcpTools || {};
1052
1136
  for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
1053
- this.toolImplementations[toolName] = toolImpl;
1137
+ if (this._isMcpToolAllowed(toolName)) {
1138
+ this.toolImplementations[toolName] = toolImpl;
1139
+ } else if (this.debug) {
1140
+ console.error(`[DEBUG] MCP tool '${toolName}' filtered out by allowedTools`);
1141
+ }
1054
1142
  }
1055
1143
  }
1056
1144
  } catch (error) {
@@ -1061,24 +1149,53 @@ export class ProbeAgent {
1061
1149
  }
1062
1150
  }
1063
1151
 
1064
- // Build tool definitions
1065
- let toolDefinitions = `
1066
- ${searchToolDefinition}
1067
- ${queryToolDefinition}
1068
- ${extractToolDefinition}
1069
- ${listFilesToolDefinition}
1070
- ${searchFilesToolDefinition}
1071
- ${attemptCompletionToolDefinition}
1072
- `;
1073
- if (this.allowEdit) {
1152
+ // Build tool definitions based on allowedTools configuration
1153
+ let toolDefinitions = '';
1154
+
1155
+ // Helper to check if a tool is allowed
1156
+ const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
1157
+
1158
+ // Core tools (filtered by allowedTools)
1159
+ if (isToolAllowed('search')) {
1160
+ toolDefinitions += `${searchToolDefinition}\n`;
1161
+ }
1162
+ if (isToolAllowed('query')) {
1163
+ toolDefinitions += `${queryToolDefinition}\n`;
1164
+ }
1165
+ if (isToolAllowed('extract')) {
1166
+ toolDefinitions += `${extractToolDefinition}\n`;
1167
+ }
1168
+ if (isToolAllowed('listFiles')) {
1169
+ toolDefinitions += `${listFilesToolDefinition}\n`;
1170
+ }
1171
+ if (isToolAllowed('searchFiles')) {
1172
+ toolDefinitions += `${searchFilesToolDefinition}\n`;
1173
+ }
1174
+
1175
+ // Edit tools (require both allowEdit flag AND allowedTools permission)
1176
+ if (this.allowEdit && isToolAllowed('implement')) {
1074
1177
  toolDefinitions += `${implementToolDefinition}\n`;
1178
+ }
1179
+ if (this.allowEdit && isToolAllowed('edit')) {
1075
1180
  toolDefinitions += `${editToolDefinition}\n`;
1181
+ }
1182
+ if (this.allowEdit && isToolAllowed('create')) {
1076
1183
  toolDefinitions += `${createToolDefinition}\n`;
1077
1184
  }
1078
- if (this.enableBash) {
1185
+
1186
+ // Bash tool (require both enableBash flag AND allowedTools permission)
1187
+ if (this.enableBash && isToolAllowed('bash')) {
1079
1188
  toolDefinitions += `${bashToolDefinition}\n`;
1080
1189
  }
1081
- if (this.enableDelegate) {
1190
+
1191
+ // Always include attempt_completion (unless explicitly disabled in raw AI mode)
1192
+ if (isToolAllowed('attempt_completion')) {
1193
+ toolDefinitions += `${attemptCompletionToolDefinition}\n`;
1194
+ }
1195
+
1196
+ // Delegate tool (require both enableDelegate flag AND allowedTools permission)
1197
+ // Place after attempt_completion as it's an optional tool
1198
+ if (this.enableDelegate && isToolAllowed('delegate')) {
1082
1199
  toolDefinitions += `${delegateToolDefinition}\n`;
1083
1200
  }
1084
1201
 
@@ -1257,11 +1374,17 @@ When troubleshooting:
1257
1374
  // Add Tool Definitions
1258
1375
  systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
1259
1376
 
1260
- // Add MCP tools if available
1377
+ // Add MCP tools if available (filtered by allowedTools)
1261
1378
  if (this.mcpBridge && this.mcpBridge.getToolNames().length > 0) {
1262
- systemMessage += `\n## MCP Tools (JSON parameters in <params> tag)\n`;
1263
- systemMessage += this.mcpBridge.getXmlToolDefinitions();
1264
- systemMessage += `\n\nFor MCP tools, use JSON format within the params tag, e.g.:\n<mcp_tool>\n<params>\n{"key": "value"}\n</params>\n</mcp_tool>\n`;
1379
+ const allMcpTools = this.mcpBridge.getToolNames();
1380
+ const allowedMcpTools = this._filterMcpTools(allMcpTools);
1381
+
1382
+ if (allowedMcpTools.length > 0) {
1383
+ systemMessage += `\n## MCP Tools (JSON parameters in <params> tag)\n`;
1384
+ // Get only allowed MCP tool definitions
1385
+ systemMessage += this.mcpBridge.getXmlToolDefinitions(allowedMcpTools);
1386
+ systemMessage += `\n\nFor MCP tools, use JSON format within the params tag, e.g.:\n<mcp_tool>\n<params>\n{"key": "value"}\n</params>\n</mcp_tool>\n`;
1387
+ }
1265
1388
  }
1266
1389
 
1267
1390
  // Add folder information
@@ -1447,57 +1570,120 @@ When troubleshooting:
1447
1570
 
1448
1571
  // Make AI request
1449
1572
  let assistantResponseContent = '';
1450
- try {
1451
- // Wrap AI request with tracing if available
1452
- const executeAIRequest = async () => {
1453
- // Prepare messages with potential image content
1454
- const messagesForAI = this.prepareMessagesWithImages(currentMessages);
1455
-
1456
- const result = await this.streamTextWithRetryAndFallback({
1457
- model: this.provider(this.model),
1458
- messages: messagesForAI,
1459
- maxTokens: maxResponseTokens,
1460
- temperature: 0.3,
1461
- });
1573
+ let compactionAttempted = false;
1574
+
1575
+ // Retry loop for context compaction - separate from streamTextWithRetryAndFallback
1576
+ // which handles transient errors (rate limits, network issues, etc.)
1577
+ while (true) {
1578
+ try {
1579
+ // Wrap AI request with tracing if available
1580
+ const executeAIRequest = async () => {
1581
+ // Prepare messages with potential image content
1582
+ const messagesForAI = this.prepareMessagesWithImages(currentMessages);
1583
+
1584
+ const result = await this.streamTextWithRetryAndFallback({
1585
+ model: this.provider(this.model),
1586
+ messages: messagesForAI,
1587
+ maxTokens: maxResponseTokens,
1588
+ temperature: 0.3,
1589
+ });
1590
+
1591
+ // Get the promise reference BEFORE consuming stream (doesn't lock it)
1592
+ const usagePromise = result.usage;
1462
1593
 
1463
- // Get the promise reference BEFORE consuming stream (doesn't lock it)
1464
- const usagePromise = result.usage;
1594
+ // Collect the streamed response - stream all content for now
1595
+ for await (const delta of result.textStream) {
1596
+ assistantResponseContent += delta;
1597
+ // For now, stream everything - we'll handle segmentation after tools execute
1598
+ if (options.onStream) {
1599
+ options.onStream(delta);
1600
+ }
1601
+ }
1465
1602
 
1466
- // Collect the streamed response - stream all content for now
1467
- for await (const delta of result.textStream) {
1468
- assistantResponseContent += delta;
1469
- // For now, stream everything - we'll handle segmentation after tools execute
1470
- if (options.onStream) {
1471
- options.onStream(delta);
1603
+ // Record token usage - await the promise AFTER stream is consumed
1604
+ const usage = await usagePromise;
1605
+ if (usage) {
1606
+ this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
1472
1607
  }
1608
+
1609
+ return result;
1610
+ };
1611
+
1612
+ if (this.tracer) {
1613
+ await this.tracer.withSpan('ai.request', executeAIRequest, {
1614
+ 'ai.model': this.model,
1615
+ 'ai.provider': this.clientApiProvider || 'auto',
1616
+ 'iteration': currentIteration,
1617
+ 'max_tokens': maxResponseTokens,
1618
+ 'temperature': 0.3,
1619
+ 'message_count': currentMessages.length
1620
+ });
1621
+ } else {
1622
+ await executeAIRequest();
1473
1623
  }
1474
1624
 
1475
- // Record token usage - await the promise AFTER stream is consumed
1476
- const usage = await usagePromise;
1477
- if (usage) {
1478
- this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
1625
+ // Success - break out of compaction retry loop
1626
+ break;
1627
+
1628
+ } catch (error) {
1629
+ // Check if this is a context limit error (only try compaction once per iteration)
1630
+ if (!compactionAttempted && handleContextLimitError) {
1631
+ const compactionResult = handleContextLimitError(error, currentMessages, {
1632
+ keepLastSegment: true,
1633
+ minSegmentsToKeep: 1
1634
+ });
1635
+
1636
+ if (compactionResult) {
1637
+ // Context limit error detected - compact and retry once
1638
+ const { messages: compactedMessages, stats } = compactionResult;
1639
+
1640
+ // Check if compaction actually reduced message count
1641
+ if (stats.removed === 0) {
1642
+ // No messages removed - compaction won't help, fail immediately
1643
+ console.error(`[ERROR] Context window exceeded but no messages can be compacted.`);
1644
+ console.error(`[ERROR] The conversation history is already minimal (${stats.originalCount} messages).`);
1645
+ finalResult = `Error: Context window limit exceeded and conversation cannot be compacted further. Consider starting a new session or reducing system message size.`;
1646
+ throw new Error(finalResult);
1647
+ }
1648
+
1649
+ compactionAttempted = true;
1650
+
1651
+ console.log(`[INFO] Context window limit exceeded. Compacting conversation...`);
1652
+ console.log(`[INFO] Removed ${stats.removed} messages (${stats.reductionPercent}% reduction)`);
1653
+ console.log(`[INFO] Estimated token savings: ${stats.tokensSaved} tokens`);
1654
+
1655
+ if (this.debug) {
1656
+ console.log(`[DEBUG] Compaction stats:`, stats);
1657
+ console.log(`[DEBUG] Original message count: ${stats.originalCount}`);
1658
+ console.log(`[DEBUG] Compacted message count: ${stats.compactedCount}`);
1659
+ }
1660
+
1661
+ // Replace currentMessages with compacted version (creates new array reference)
1662
+ // This ensures we don't mutate the original history array
1663
+ currentMessages = [...compactedMessages];
1664
+
1665
+ // Log compaction event if tracer is available
1666
+ if (this.tracer) {
1667
+ this.tracer.addEvent('context.compacted', {
1668
+ 'iteration': currentIteration,
1669
+ 'original_count': stats.originalCount,
1670
+ 'compacted_count': stats.compactedCount,
1671
+ 'reduction_percent': stats.reductionPercent,
1672
+ 'tokens_saved': stats.tokensSaved
1673
+ });
1674
+ }
1675
+
1676
+ // Continue to retry with compacted messages
1677
+ continue;
1678
+ }
1479
1679
  }
1480
1680
 
1481
- return result;
1482
- };
1483
-
1484
- if (this.tracer) {
1485
- await this.tracer.withSpan('ai.request', executeAIRequest, {
1486
- 'ai.model': this.model,
1487
- 'ai.provider': this.clientApiProvider || 'auto',
1488
- 'iteration': currentIteration,
1489
- 'max_tokens': maxResponseTokens,
1490
- 'temperature': 0.3,
1491
- 'message_count': currentMessages.length
1492
- });
1493
- } else {
1494
- await executeAIRequest();
1681
+ // Not a context limit error, compaction already attempted, or compaction not available
1682
+ // IMPORTANT: This break prevents infinite loop if compacted messages still exceed limit
1683
+ console.error(`Error during streamText (Iter ${currentIteration}):`, error);
1684
+ finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
1685
+ throw new Error(finalResult);
1495
1686
  }
1496
-
1497
- } catch (error) {
1498
- console.error(`Error during streamText (Iter ${currentIteration}):`, error);
1499
- finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
1500
- throw new Error(finalResult);
1501
1687
  }
1502
1688
 
1503
1689
  // Log preview of assistant response for debugging loops
@@ -1937,15 +2123,13 @@ NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
1937
2123
  Convert your previous response content into actual JSON data that follows this schema structure.`;
1938
2124
 
1939
2125
  // Call answer recursively with _schemaFormatted flag to prevent infinite loop
1940
- finalResult = await this.answer(schemaPrompt, [], {
1941
- ...options,
1942
- _schemaFormatted: true
2126
+ finalResult = await this.answer(schemaPrompt, [], {
2127
+ ...options,
2128
+ _schemaFormatted: true
1943
2129
  });
1944
-
1945
- // Step 2: Clean the response (remove code blocks)
1946
- finalResult = cleanSchemaResponse(finalResult);
1947
-
1948
- // Step 3: Validate and fix Mermaid diagrams if present
2130
+
2131
+ // Step 2: Validate and fix Mermaid diagrams if present (BEFORE cleaning schema)
2132
+ // This ensures mermaid validation sees the full response before JSON extraction strips content
1949
2133
  if (!this.disableMermaidValidation) {
1950
2134
  try {
1951
2135
  if (this.debug) {
@@ -2007,19 +2191,16 @@ Convert your previous response content into actual JSON data that follows this s
2007
2191
  } else if (this.debug) {
2008
2192
  console.log(`[DEBUG] Mermaid validation: Skipped due to disableMermaidValidation option`);
2009
2193
  }
2010
-
2194
+
2195
+ // Step 3: Clean the response (remove code blocks, extract JSON)
2196
+ // This happens AFTER mermaid validation to preserve full content for validation
2197
+ finalResult = cleanSchemaResponse(finalResult);
2198
+
2011
2199
  // Step 4: Validate and potentially correct JSON responses
2012
2200
  if (isJsonSchema(options.schema)) {
2013
2201
  if (this.debug) {
2014
2202
  console.log(`[DEBUG] JSON validation: Starting validation process for schema response`);
2015
- console.log(`[DEBUG] JSON validation: Response length: ${finalResult.length} chars`);
2016
- }
2017
-
2018
- // Clean the response first to extract JSON from markdown/code blocks
2019
- finalResult = cleanSchemaResponse(finalResult);
2020
-
2021
- if (this.debug) {
2022
- console.log(`[DEBUG] JSON validation: After cleaning, length: ${finalResult.length} chars`);
2203
+ console.log(`[DEBUG] JSON validation: Cleaned response length: ${finalResult.length} chars`);
2023
2204
  }
2024
2205
 
2025
2206
  // Record JSON validation start in telemetry
@@ -2131,17 +2312,16 @@ Convert your previous response content into actual JSON data that follows this s
2131
2312
  } else if (reachedMaxIterations && options.schema && this.debug) {
2132
2313
  console.log('[DEBUG] Skipping schema formatting due to max iterations reached without completion');
2133
2314
  } else if (completionAttempted && options.schema && !options._schemaFormatted && !options._skipValidation) {
2134
- // For attempt_completion results with schema, still clean markdown if needed
2315
+ // For attempt_completion results with schema, validate mermaid diagrams BEFORE cleaning schema
2316
+ // This ensures mermaid validation sees the full response before JSON extraction strips content
2135
2317
  // Skip this validation if we're in a recursive correction call (_skipValidation flag)
2136
2318
  try {
2137
- finalResult = cleanSchemaResponse(finalResult);
2138
-
2139
- // Validate and fix Mermaid diagrams if present
2319
+ // Validate and fix Mermaid diagrams if present (BEFORE schema cleaning)
2140
2320
  if (!this.disableMermaidValidation) {
2141
2321
  if (this.debug) {
2142
- console.log(`[DEBUG] Mermaid validation: Validating attempt_completion result...`);
2322
+ console.log(`[DEBUG] Mermaid validation: Validating attempt_completion result BEFORE schema cleaning...`);
2143
2323
  }
2144
-
2324
+
2145
2325
  const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
2146
2326
  debug: this.debug,
2147
2327
  path: this.allowedFolders[0],
@@ -2149,7 +2329,7 @@ Convert your previous response content into actual JSON data that follows this s
2149
2329
  model: this.model,
2150
2330
  tracer: this.tracer
2151
2331
  });
2152
-
2332
+
2153
2333
  if (mermaidValidation.wasFixed) {
2154
2334
  finalResult = mermaidValidation.fixedResponse;
2155
2335
  if (this.debug) {
@@ -2164,6 +2344,9 @@ Convert your previous response content into actual JSON data that follows this s
2164
2344
  } else if (this.debug) {
2165
2345
  console.log(`[DEBUG] Mermaid validation: Skipped for attempt_completion result due to disableMermaidValidation option`);
2166
2346
  }
2347
+
2348
+ // Now clean the schema response (may extract JSON and discard other content)
2349
+ finalResult = cleanSchemaResponse(finalResult);
2167
2350
 
2168
2351
  // Validate and potentially correct JSON for attempt_completion results
2169
2352
  if (isJsonSchema(options.schema)) {
@@ -2377,6 +2560,75 @@ Convert your previous response content into actual JSON data that follows this s
2377
2560
  }
2378
2561
  }
2379
2562
 
2563
+ /**
2564
+ * Manually compact conversation history
2565
+ * Removes intermediate monologues from older segments while preserving
2566
+ * user messages, final answers, and the most recent segment
2567
+ *
2568
+ * @param {Object} options - Compaction options
2569
+ * @param {boolean} [options.keepLastSegment=true] - Keep the most recent segment intact
2570
+ * @param {number} [options.minSegmentsToKeep=1] - Number of recent segments to preserve fully
2571
+ * @returns {Object} Compaction statistics
2572
+ */
2573
+ async compactHistory(options = {}) {
2574
+ const { compactMessages, calculateCompactionStats } = await import('./contextCompactor.js');
2575
+
2576
+ if (this.history.length === 0) {
2577
+ if (this.debug) {
2578
+ console.log(`[DEBUG] No history to compact for session ${this.sessionId}`);
2579
+ }
2580
+ return {
2581
+ originalCount: 0,
2582
+ compactedCount: 0,
2583
+ removed: 0,
2584
+ reductionPercent: 0,
2585
+ originalTokens: 0,
2586
+ compactedTokens: 0,
2587
+ tokensSaved: 0
2588
+ };
2589
+ }
2590
+
2591
+ // Perform compaction
2592
+ const compactedMessages = compactMessages(this.history, options);
2593
+ const stats = calculateCompactionStats(this.history, compactedMessages);
2594
+
2595
+ // Update history
2596
+ this.history = compactedMessages;
2597
+
2598
+ // Save to storage (clear old history first, then save compacted messages)
2599
+ try {
2600
+ // Clear existing history to avoid duplicates
2601
+ await this.storageAdapter.clearHistory(this.sessionId);
2602
+
2603
+ // Save compacted messages
2604
+ // Note: Using sequential saves as storage adapter interface doesn't support batch operations
2605
+ // For large histories, consider implementing a batch save method in your custom adapter
2606
+ for (const message of compactedMessages) {
2607
+ await this.storageAdapter.saveMessage(this.sessionId, message);
2608
+ }
2609
+ } catch (error) {
2610
+ console.error(`[ERROR] Failed to save compacted messages to storage:`, error);
2611
+ }
2612
+
2613
+ // Log results
2614
+ console.log(`[INFO] Manually compacted conversation history`);
2615
+ console.log(`[INFO] Removed ${stats.removed} messages (${stats.reductionPercent}% reduction)`);
2616
+ console.log(`[INFO] Estimated token savings: ${stats.tokensSaved} tokens`);
2617
+
2618
+ if (this.debug) {
2619
+ console.log(`[DEBUG] Compaction stats:`, stats);
2620
+ }
2621
+
2622
+ // Emit hook
2623
+ await this.hooks.emit(HOOK_TYPES.STORAGE_SAVE, {
2624
+ sessionId: this.sessionId,
2625
+ compacted: true,
2626
+ stats
2627
+ });
2628
+
2629
+ return stats;
2630
+ }
2631
+
2380
2632
  /**
2381
2633
  * Clone this agent's session to create a new agent with shared conversation history
2382
2634
  * @param {Object} options - Clone options
@@ -2407,6 +2659,17 @@ Convert your previous response content into actual JSON data that follows this s
2407
2659
  }
2408
2660
 
2409
2661
  // Create new agent with same configuration
2662
+ // Reconstruct the original allowedTools array from the parsed configuration
2663
+ let allowedToolsArray = null;
2664
+ if (this.allowedTools.mode === 'whitelist') {
2665
+ allowedToolsArray = [...this.allowedTools.allowed];
2666
+ } else if (this.allowedTools.mode === 'none') {
2667
+ allowedToolsArray = [];
2668
+ } else if (this.allowedTools.mode === 'all' && this.allowedTools.exclusions.length > 0) {
2669
+ allowedToolsArray = ['*', ...this.allowedTools.exclusions.map(t => '!' + t)];
2670
+ }
2671
+ // If mode is 'all' with no exclusions, leave as null (default)
2672
+
2410
2673
  const clonedAgent = new ProbeAgent({
2411
2674
  // Copy current agent's config
2412
2675
  customPrompt: this.customPrompt,
@@ -2423,6 +2686,7 @@ Convert your previous response content into actual JSON data that follows this s
2423
2686
  maxIterations: this.maxIterations,
2424
2687
  disableMermaidValidation: this.disableMermaidValidation,
2425
2688
  disableJsonValidation: this.disableJsonValidation,
2689
+ allowedTools: allowedToolsArray,
2426
2690
  enableMcp: !!this.mcpBridge,
2427
2691
  mcpConfig: this.mcpConfig,
2428
2692
  enableBash: this.enableBash,