@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
package/package.json
CHANGED
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
`;
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
messages
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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:
|
|
1946
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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,
|