@probelabs/probe 0.6.0-rc161 → 0.6.0-rc163
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 +361 -97
- package/build/agent/contextCompactor.js +271 -0
- package/build/agent/index.js +854 -94
- package/build/agent/schemaUtils.js +81 -0
- package/build/agent/xmlParsingUtils.js +24 -4
- package/build/tools/common.js +16 -1
- package/cjs/agent/ProbeAgent.cjs +889 -144
- package/cjs/index.cjs +889 -144
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +361 -97
- package/src/agent/contextCompactor.js +271 -0
- package/src/agent/index.js +30 -1
- package/src/agent/schemaUtils.js +81 -0
- package/src/agent/xmlParsingUtils.js +24 -4
- package/src/tools/common.js +16 -1
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
isJsonSchema,
|
|
49
49
|
validateJsonResponse,
|
|
50
50
|
createJsonCorrectionPrompt,
|
|
51
|
+
generateSchemaInstructions,
|
|
51
52
|
isJsonSchemaDefinition,
|
|
52
53
|
createSchemaDefinitionCorrectionPrompt,
|
|
53
54
|
validateAndFixMermaidResponse
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
} from './mcp/index.js';
|
|
61
62
|
import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
|
|
62
63
|
import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
|
|
64
|
+
import { handleContextLimitError } from './contextCompactor.js';
|
|
63
65
|
|
|
64
66
|
// Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
|
|
65
67
|
const MAX_TOOL_ITERATIONS = (() => {
|
|
@@ -104,6 +106,8 @@ export class ProbeAgent {
|
|
|
104
106
|
* @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
|
|
105
107
|
* @param {Object} [options.storageAdapter] - Custom storage adapter for history management
|
|
106
108
|
* @param {Object} [options.hooks] - Hook callbacks for events (e.g., {'tool:start': callback})
|
|
109
|
+
* @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'])
|
|
110
|
+
* @param {boolean} [options.disableTools=false] - Convenience flag to disable all tools (equivalent to allowedTools: []). Takes precedence over allowedTools if set.
|
|
107
111
|
* @param {Object} [options.retry] - Retry configuration
|
|
108
112
|
* @param {number} [options.retry.maxRetries=3] - Maximum retry attempts per provider
|
|
109
113
|
* @param {number} [options.retry.initialDelay=1000] - Initial delay in ms
|
|
@@ -139,6 +143,13 @@ export class ProbeAgent {
|
|
|
139
143
|
this.disableMermaidValidation = !!options.disableMermaidValidation;
|
|
140
144
|
this.disableJsonValidation = !!options.disableJsonValidation;
|
|
141
145
|
|
|
146
|
+
// Tool filtering configuration
|
|
147
|
+
// Parse allowedTools option: ['*'] = all tools, [] or null = no tools, ['tool1', 'tool2'] = specific tools
|
|
148
|
+
// Supports exclusion with '!' prefix: ['*', '!bash'] = all tools except bash
|
|
149
|
+
// disableTools is a convenience flag that overrides allowedTools to []
|
|
150
|
+
const effectiveAllowedTools = options.disableTools ? [] : options.allowedTools;
|
|
151
|
+
this.allowedTools = this._parseAllowedTools(effectiveAllowedTools);
|
|
152
|
+
|
|
142
153
|
// Storage adapter (defaults to in-memory)
|
|
143
154
|
this.storageAdapter = options.storageAdapter || new InMemoryStorageAdapter();
|
|
144
155
|
|
|
@@ -216,6 +227,74 @@ export class ProbeAgent {
|
|
|
216
227
|
// Constructor must remain synchronous for backward compatibility
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Parse allowedTools configuration
|
|
232
|
+
* @param {Array<string>|null|undefined} allowedTools - Tool filtering configuration
|
|
233
|
+
* @returns {Object} Parsed configuration with isEnabled method
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_parseAllowedTools(allowedTools) {
|
|
237
|
+
// Helper to check if tool matches a pattern (supports * wildcard)
|
|
238
|
+
const matchesPattern = (toolName, pattern) => {
|
|
239
|
+
if (!pattern.includes('*')) {
|
|
240
|
+
return toolName === pattern;
|
|
241
|
+
}
|
|
242
|
+
const regexPattern = pattern.replace(/\*/g, '.*');
|
|
243
|
+
return new RegExp(`^${regexPattern}$`).test(toolName);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Default: all tools allowed
|
|
247
|
+
if (!allowedTools || (Array.isArray(allowedTools) && allowedTools.includes('*'))) {
|
|
248
|
+
const exclusions = Array.isArray(allowedTools)
|
|
249
|
+
? allowedTools.filter(t => t.startsWith('!')).map(t => t.slice(1))
|
|
250
|
+
: [];
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
mode: 'all',
|
|
254
|
+
exclusions,
|
|
255
|
+
isEnabled: (toolName) => !exclusions.some(pattern => matchesPattern(toolName, pattern))
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Empty array or null: no tools (raw AI mode)
|
|
260
|
+
if (Array.isArray(allowedTools) && allowedTools.length === 0) {
|
|
261
|
+
return {
|
|
262
|
+
mode: 'none',
|
|
263
|
+
isEnabled: () => false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Specific tools allowed (with wildcard support)
|
|
268
|
+
const allowedPatterns = allowedTools.filter(t => !t.startsWith('!'));
|
|
269
|
+
return {
|
|
270
|
+
mode: 'whitelist',
|
|
271
|
+
allowed: allowedPatterns,
|
|
272
|
+
isEnabled: (toolName) => allowedPatterns.some(pattern => matchesPattern(toolName, pattern))
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if an MCP tool is allowed based on allowedTools configuration
|
|
278
|
+
* Uses mcp__ prefix convention (like Claude Code)
|
|
279
|
+
* @param {string} toolName - The MCP tool name (without mcp__ prefix)
|
|
280
|
+
* @returns {boolean} - Whether the tool is allowed
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
_isMcpToolAllowed(toolName) {
|
|
284
|
+
const mcpToolName = `mcp__${toolName}`;
|
|
285
|
+
return this.allowedTools.isEnabled(mcpToolName) || this.allowedTools.isEnabled(toolName);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Filter MCP tools based on allowedTools configuration
|
|
290
|
+
* @param {string[]} mcpToolNames - Array of MCP tool names
|
|
291
|
+
* @returns {string[]} - Filtered array of allowed MCP tool names
|
|
292
|
+
* @private
|
|
293
|
+
*/
|
|
294
|
+
_filterMcpTools(mcpToolNames) {
|
|
295
|
+
return mcpToolNames.filter(toolName => this._isMcpToolAllowed(toolName));
|
|
296
|
+
}
|
|
297
|
+
|
|
219
298
|
/**
|
|
220
299
|
* Initialize the agent asynchronously (must be called after constructor)
|
|
221
300
|
* This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
|
|
@@ -248,10 +327,15 @@ export class ProbeAgent {
|
|
|
248
327
|
await this.initializeMCP();
|
|
249
328
|
|
|
250
329
|
// Merge MCP tools into toolImplementations for unified access
|
|
330
|
+
// Apply allowedTools filtering using mcp__ prefix (like Claude Code)
|
|
251
331
|
if (this.mcpBridge) {
|
|
252
332
|
const mcpTools = this.mcpBridge.mcpTools || {};
|
|
253
333
|
for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
|
|
254
|
-
this.
|
|
334
|
+
if (this._isMcpToolAllowed(toolName)) {
|
|
335
|
+
this.toolImplementations[toolName] = toolImpl;
|
|
336
|
+
} else if (this.debug) {
|
|
337
|
+
console.error(`[DEBUG] MCP tool '${toolName}' filtered out by allowedTools`);
|
|
338
|
+
}
|
|
255
339
|
}
|
|
256
340
|
}
|
|
257
341
|
|
|
@@ -1047,10 +1131,15 @@ export class ProbeAgent {
|
|
|
1047
1131
|
await this.initializeMCP();
|
|
1048
1132
|
|
|
1049
1133
|
// Merge MCP tools into toolImplementations for unified access
|
|
1134
|
+
// Apply allowedTools filtering using mcp__ prefix (like Claude Code)
|
|
1050
1135
|
if (this.mcpBridge) {
|
|
1051
1136
|
const mcpTools = this.mcpBridge.mcpTools || {};
|
|
1052
1137
|
for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
|
|
1053
|
-
this.
|
|
1138
|
+
if (this._isMcpToolAllowed(toolName)) {
|
|
1139
|
+
this.toolImplementations[toolName] = toolImpl;
|
|
1140
|
+
} else if (this.debug) {
|
|
1141
|
+
console.error(`[DEBUG] MCP tool '${toolName}' filtered out by allowedTools`);
|
|
1142
|
+
}
|
|
1054
1143
|
}
|
|
1055
1144
|
}
|
|
1056
1145
|
} catch (error) {
|
|
@@ -1061,24 +1150,53 @@ export class ProbeAgent {
|
|
|
1061
1150
|
}
|
|
1062
1151
|
}
|
|
1063
1152
|
|
|
1064
|
-
// Build tool definitions
|
|
1065
|
-
let toolDefinitions =
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
`;
|
|
1073
|
-
|
|
1153
|
+
// Build tool definitions based on allowedTools configuration
|
|
1154
|
+
let toolDefinitions = '';
|
|
1155
|
+
|
|
1156
|
+
// Helper to check if a tool is allowed
|
|
1157
|
+
const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
|
|
1158
|
+
|
|
1159
|
+
// Core tools (filtered by allowedTools)
|
|
1160
|
+
if (isToolAllowed('search')) {
|
|
1161
|
+
toolDefinitions += `${searchToolDefinition}\n`;
|
|
1162
|
+
}
|
|
1163
|
+
if (isToolAllowed('query')) {
|
|
1164
|
+
toolDefinitions += `${queryToolDefinition}\n`;
|
|
1165
|
+
}
|
|
1166
|
+
if (isToolAllowed('extract')) {
|
|
1167
|
+
toolDefinitions += `${extractToolDefinition}\n`;
|
|
1168
|
+
}
|
|
1169
|
+
if (isToolAllowed('listFiles')) {
|
|
1170
|
+
toolDefinitions += `${listFilesToolDefinition}\n`;
|
|
1171
|
+
}
|
|
1172
|
+
if (isToolAllowed('searchFiles')) {
|
|
1173
|
+
toolDefinitions += `${searchFilesToolDefinition}\n`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Edit tools (require both allowEdit flag AND allowedTools permission)
|
|
1177
|
+
if (this.allowEdit && isToolAllowed('implement')) {
|
|
1074
1178
|
toolDefinitions += `${implementToolDefinition}\n`;
|
|
1179
|
+
}
|
|
1180
|
+
if (this.allowEdit && isToolAllowed('edit')) {
|
|
1075
1181
|
toolDefinitions += `${editToolDefinition}\n`;
|
|
1182
|
+
}
|
|
1183
|
+
if (this.allowEdit && isToolAllowed('create')) {
|
|
1076
1184
|
toolDefinitions += `${createToolDefinition}\n`;
|
|
1077
1185
|
}
|
|
1078
|
-
|
|
1186
|
+
|
|
1187
|
+
// Bash tool (require both enableBash flag AND allowedTools permission)
|
|
1188
|
+
if (this.enableBash && isToolAllowed('bash')) {
|
|
1079
1189
|
toolDefinitions += `${bashToolDefinition}\n`;
|
|
1080
1190
|
}
|
|
1081
|
-
|
|
1191
|
+
|
|
1192
|
+
// Always include attempt_completion (unless explicitly disabled in raw AI mode)
|
|
1193
|
+
if (isToolAllowed('attempt_completion')) {
|
|
1194
|
+
toolDefinitions += `${attemptCompletionToolDefinition}\n`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Delegate tool (require both enableDelegate flag AND allowedTools permission)
|
|
1198
|
+
// Place after attempt_completion as it's an optional tool
|
|
1199
|
+
if (this.enableDelegate && isToolAllowed('delegate')) {
|
|
1082
1200
|
toolDefinitions += `${delegateToolDefinition}\n`;
|
|
1083
1201
|
}
|
|
1084
1202
|
|
|
@@ -1257,11 +1375,17 @@ When troubleshooting:
|
|
|
1257
1375
|
// Add Tool Definitions
|
|
1258
1376
|
systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
|
|
1259
1377
|
|
|
1260
|
-
// Add MCP tools if available
|
|
1378
|
+
// Add MCP tools if available (filtered by allowedTools)
|
|
1261
1379
|
if (this.mcpBridge && this.mcpBridge.getToolNames().length > 0) {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1380
|
+
const allMcpTools = this.mcpBridge.getToolNames();
|
|
1381
|
+
const allowedMcpTools = this._filterMcpTools(allMcpTools);
|
|
1382
|
+
|
|
1383
|
+
if (allowedMcpTools.length > 0) {
|
|
1384
|
+
systemMessage += `\n## MCP Tools (JSON parameters in <params> tag)\n`;
|
|
1385
|
+
// Get only allowed MCP tool definitions
|
|
1386
|
+
systemMessage += this.mcpBridge.getXmlToolDefinitions(allowedMcpTools);
|
|
1387
|
+
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`;
|
|
1388
|
+
}
|
|
1265
1389
|
}
|
|
1266
1390
|
|
|
1267
1391
|
// Add folder information
|
|
@@ -1332,11 +1456,18 @@ When troubleshooting:
|
|
|
1332
1456
|
|
|
1333
1457
|
// Create user message with optional image support
|
|
1334
1458
|
let userMessage = { role: 'user', content: message.trim() };
|
|
1335
|
-
|
|
1459
|
+
|
|
1460
|
+
// If schema is provided, prepend JSON format requirement to user message
|
|
1461
|
+
if (options.schema && !options._schemaFormatted) {
|
|
1462
|
+
const schemaInstructions = generateSchemaInstructions(options.schema, { debug: this.debug });
|
|
1463
|
+
userMessage.content = message.trim() + schemaInstructions;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1336
1466
|
// If images are provided, use multi-modal message format
|
|
1337
1467
|
if (images && images.length > 0) {
|
|
1468
|
+
const textContent = userMessage.content;
|
|
1338
1469
|
userMessage.content = [
|
|
1339
|
-
{ type: 'text', text:
|
|
1470
|
+
{ type: 'text', text: textContent },
|
|
1340
1471
|
...images.map(image => ({
|
|
1341
1472
|
type: 'image',
|
|
1342
1473
|
image: image
|
|
@@ -1447,57 +1578,120 @@ When troubleshooting:
|
|
|
1447
1578
|
|
|
1448
1579
|
// Make AI request
|
|
1449
1580
|
let assistantResponseContent = '';
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
messages
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1581
|
+
let compactionAttempted = false;
|
|
1582
|
+
|
|
1583
|
+
// Retry loop for context compaction - separate from streamTextWithRetryAndFallback
|
|
1584
|
+
// which handles transient errors (rate limits, network issues, etc.)
|
|
1585
|
+
while (true) {
|
|
1586
|
+
try {
|
|
1587
|
+
// Wrap AI request with tracing if available
|
|
1588
|
+
const executeAIRequest = async () => {
|
|
1589
|
+
// Prepare messages with potential image content
|
|
1590
|
+
const messagesForAI = this.prepareMessagesWithImages(currentMessages);
|
|
1591
|
+
|
|
1592
|
+
const result = await this.streamTextWithRetryAndFallback({
|
|
1593
|
+
model: this.provider(this.model),
|
|
1594
|
+
messages: messagesForAI,
|
|
1595
|
+
maxTokens: maxResponseTokens,
|
|
1596
|
+
temperature: 0.3,
|
|
1597
|
+
});
|
|
1462
1598
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1599
|
+
// Get the promise reference BEFORE consuming stream (doesn't lock it)
|
|
1600
|
+
const usagePromise = result.usage;
|
|
1465
1601
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1602
|
+
// Collect the streamed response - stream all content for now
|
|
1603
|
+
for await (const delta of result.textStream) {
|
|
1604
|
+
assistantResponseContent += delta;
|
|
1605
|
+
// For now, stream everything - we'll handle segmentation after tools execute
|
|
1606
|
+
if (options.onStream) {
|
|
1607
|
+
options.onStream(delta);
|
|
1608
|
+
}
|
|
1472
1609
|
}
|
|
1610
|
+
|
|
1611
|
+
// Record token usage - await the promise AFTER stream is consumed
|
|
1612
|
+
const usage = await usagePromise;
|
|
1613
|
+
if (usage) {
|
|
1614
|
+
this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return result;
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
if (this.tracer) {
|
|
1621
|
+
await this.tracer.withSpan('ai.request', executeAIRequest, {
|
|
1622
|
+
'ai.model': this.model,
|
|
1623
|
+
'ai.provider': this.clientApiProvider || 'auto',
|
|
1624
|
+
'iteration': currentIteration,
|
|
1625
|
+
'max_tokens': maxResponseTokens,
|
|
1626
|
+
'temperature': 0.3,
|
|
1627
|
+
'message_count': currentMessages.length
|
|
1628
|
+
});
|
|
1629
|
+
} else {
|
|
1630
|
+
await executeAIRequest();
|
|
1473
1631
|
}
|
|
1474
1632
|
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1633
|
+
// Success - break out of compaction retry loop
|
|
1634
|
+
break;
|
|
1635
|
+
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
// Check if this is a context limit error (only try compaction once per iteration)
|
|
1638
|
+
if (!compactionAttempted && handleContextLimitError) {
|
|
1639
|
+
const compactionResult = handleContextLimitError(error, currentMessages, {
|
|
1640
|
+
keepLastSegment: true,
|
|
1641
|
+
minSegmentsToKeep: 1
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
if (compactionResult) {
|
|
1645
|
+
// Context limit error detected - compact and retry once
|
|
1646
|
+
const { messages: compactedMessages, stats } = compactionResult;
|
|
1647
|
+
|
|
1648
|
+
// Check if compaction actually reduced message count
|
|
1649
|
+
if (stats.removed === 0) {
|
|
1650
|
+
// No messages removed - compaction won't help, fail immediately
|
|
1651
|
+
console.error(`[ERROR] Context window exceeded but no messages can be compacted.`);
|
|
1652
|
+
console.error(`[ERROR] The conversation history is already minimal (${stats.originalCount} messages).`);
|
|
1653
|
+
finalResult = `Error: Context window limit exceeded and conversation cannot be compacted further. Consider starting a new session or reducing system message size.`;
|
|
1654
|
+
throw new Error(finalResult);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
compactionAttempted = true;
|
|
1658
|
+
|
|
1659
|
+
console.log(`[INFO] Context window limit exceeded. Compacting conversation...`);
|
|
1660
|
+
console.log(`[INFO] Removed ${stats.removed} messages (${stats.reductionPercent}% reduction)`);
|
|
1661
|
+
console.log(`[INFO] Estimated token savings: ${stats.tokensSaved} tokens`);
|
|
1662
|
+
|
|
1663
|
+
if (this.debug) {
|
|
1664
|
+
console.log(`[DEBUG] Compaction stats:`, stats);
|
|
1665
|
+
console.log(`[DEBUG] Original message count: ${stats.originalCount}`);
|
|
1666
|
+
console.log(`[DEBUG] Compacted message count: ${stats.compactedCount}`);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Replace currentMessages with compacted version (creates new array reference)
|
|
1670
|
+
// This ensures we don't mutate the original history array
|
|
1671
|
+
currentMessages = [...compactedMessages];
|
|
1672
|
+
|
|
1673
|
+
// Log compaction event if tracer is available
|
|
1674
|
+
if (this.tracer) {
|
|
1675
|
+
this.tracer.addEvent('context.compacted', {
|
|
1676
|
+
'iteration': currentIteration,
|
|
1677
|
+
'original_count': stats.originalCount,
|
|
1678
|
+
'compacted_count': stats.compactedCount,
|
|
1679
|
+
'reduction_percent': stats.reductionPercent,
|
|
1680
|
+
'tokens_saved': stats.tokensSaved
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Continue to retry with compacted messages
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1479
1687
|
}
|
|
1480
1688
|
|
|
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();
|
|
1689
|
+
// Not a context limit error, compaction already attempted, or compaction not available
|
|
1690
|
+
// IMPORTANT: This break prevents infinite loop if compacted messages still exceed limit
|
|
1691
|
+
console.error(`Error during streamText (Iter ${currentIteration}):`, error);
|
|
1692
|
+
finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
|
|
1693
|
+
throw new Error(finalResult);
|
|
1495
1694
|
}
|
|
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
1695
|
}
|
|
1502
1696
|
|
|
1503
1697
|
// Log preview of assistant response for debugging loops
|
|
@@ -1818,7 +2012,7 @@ When troubleshooting:
|
|
|
1818
2012
|
// Build appropriate reminder message based on whether schema is provided
|
|
1819
2013
|
let reminderContent;
|
|
1820
2014
|
if (options.schema) { // Apply for ANY schema, not just JSON schemas
|
|
1821
|
-
// When schema is provided,
|
|
2015
|
+
// When schema is provided, use the same instructions as initial message
|
|
1822
2016
|
reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
|
|
1823
2017
|
|
|
1824
2018
|
Remember: Use proper XML format with BOTH opening and closing tags:
|
|
@@ -1826,15 +2020,7 @@ Remember: Use proper XML format with BOTH opening and closing tags:
|
|
|
1826
2020
|
<tool_name>
|
|
1827
2021
|
<parameter>value</parameter>
|
|
1828
2022
|
</tool_name>
|
|
1829
|
-
|
|
1830
|
-
IMPORTANT: A schema was provided for the final output format.
|
|
1831
|
-
|
|
1832
|
-
You MUST use attempt_completion to provide your answer:
|
|
1833
|
-
<attempt_completion>
|
|
1834
|
-
[Your complete answer here - provide in natural language, it will be automatically formatted to match the schema]
|
|
1835
|
-
</attempt_completion>
|
|
1836
|
-
|
|
1837
|
-
Your response will be automatically formatted to JSON. You can provide your answer in natural language or as JSON - either will work.`;
|
|
2023
|
+
` + generateSchemaInstructions(options.schema, { debug: this.debug }).trim();
|
|
1838
2024
|
} else {
|
|
1839
2025
|
// Standard reminder without schema
|
|
1840
2026
|
reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
|
|
@@ -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,
|