@probelabs/probe 0.6.0-rc159 → 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 +169 -1
- package/build/agent/FallbackManager.d.ts +176 -0
- package/build/agent/FallbackManager.js +545 -0
- package/build/agent/ProbeAgent.d.ts +7 -1
- package/build/agent/ProbeAgent.js +545 -89
- package/build/agent/RetryManager.d.ts +157 -0
- package/build/agent/RetryManager.js +334 -0
- package/build/agent/acp/tools.js +6 -2
- package/build/agent/contextCompactor.js +271 -0
- package/build/agent/index.js +2165 -250
- package/build/agent/probeTool.js +20 -2
- package/build/agent/schemaUtils.js +7 -0
- package/build/agent/tools.js +16 -0
- package/build/agent/xmlParsingUtils.js +24 -4
- package/build/index.js +13 -0
- package/build/tools/common.js +21 -4
- package/build/tools/edit.js +409 -0
- package/build/tools/index.js +11 -0
- package/cjs/agent/ProbeAgent.cjs +2620 -606
- package/cjs/index.cjs +2582 -563
- package/package.json +2 -2
- package/src/agent/FallbackManager.d.ts +176 -0
- package/src/agent/FallbackManager.js +545 -0
- package/src/agent/ProbeAgent.d.ts +7 -1
- package/src/agent/ProbeAgent.js +545 -89
- package/src/agent/RetryManager.d.ts +157 -0
- package/src/agent/RetryManager.js +334 -0
- package/src/agent/acp/tools.js +6 -2
- package/src/agent/contextCompactor.js +271 -0
- package/src/agent/index.js +30 -1
- package/src/agent/probeTool.js +20 -2
- package/src/agent/schemaUtils.js +7 -0
- package/src/agent/tools.js +16 -0
- package/src/agent/xmlParsingUtils.js +24 -4
- package/src/index.js +13 -0
- package/src/tools/common.js +21 -4
- package/src/tools/edit.js +409 -0
- package/src/tools/index.js +11 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -24,10 +24,13 @@ import {
|
|
|
24
24
|
queryToolDefinition,
|
|
25
25
|
extractToolDefinition,
|
|
26
26
|
delegateToolDefinition,
|
|
27
|
+
bashToolDefinition,
|
|
27
28
|
listFilesToolDefinition,
|
|
28
29
|
searchFilesToolDefinition,
|
|
29
30
|
attemptCompletionToolDefinition,
|
|
30
31
|
implementToolDefinition,
|
|
32
|
+
editToolDefinition,
|
|
33
|
+
createToolDefinition,
|
|
31
34
|
attemptCompletionSchema,
|
|
32
35
|
parseXmlToolCallWithThinking
|
|
33
36
|
} from './tools.js';
|
|
@@ -55,9 +58,19 @@ import {
|
|
|
55
58
|
parseHybridXmlToolCall,
|
|
56
59
|
loadMCPConfigurationFromPath
|
|
57
60
|
} from './mcp/index.js';
|
|
61
|
+
import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
|
|
62
|
+
import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
|
|
63
|
+
import { handleContextLimitError } from './contextCompactor.js';
|
|
58
64
|
|
|
59
65
|
// Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
|
|
60
|
-
const MAX_TOOL_ITERATIONS =
|
|
66
|
+
const MAX_TOOL_ITERATIONS = (() => {
|
|
67
|
+
const val = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
|
|
68
|
+
if (isNaN(val) || val < 1 || val > 200) {
|
|
69
|
+
console.warn('[ProbeAgent] MAX_TOOL_ITERATIONS must be between 1 and 200, using default: 30');
|
|
70
|
+
return 30;
|
|
71
|
+
}
|
|
72
|
+
return val;
|
|
73
|
+
})();
|
|
61
74
|
const MAX_HISTORY_MESSAGES = 100;
|
|
62
75
|
|
|
63
76
|
// Supported image file extensions (imported from shared config)
|
|
@@ -92,6 +105,20 @@ export class ProbeAgent {
|
|
|
92
105
|
* @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
|
|
93
106
|
* @param {Object} [options.storageAdapter] - Custom storage adapter for history management
|
|
94
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.
|
|
110
|
+
* @param {Object} [options.retry] - Retry configuration
|
|
111
|
+
* @param {number} [options.retry.maxRetries=3] - Maximum retry attempts per provider
|
|
112
|
+
* @param {number} [options.retry.initialDelay=1000] - Initial delay in ms
|
|
113
|
+
* @param {number} [options.retry.maxDelay=30000] - Maximum delay in ms
|
|
114
|
+
* @param {number} [options.retry.backoffFactor=2] - Exponential backoff multiplier
|
|
115
|
+
* @param {Array<string>} [options.retry.retryableErrors] - List of retryable error patterns
|
|
116
|
+
* @param {Object} [options.fallback] - Fallback configuration
|
|
117
|
+
* @param {string} [options.fallback.strategy] - Fallback strategy: 'same-model', 'same-provider', 'any', 'custom'
|
|
118
|
+
* @param {Array<string>} [options.fallback.models] - List of models for same-provider fallback
|
|
119
|
+
* @param {Array<Object>} [options.fallback.providers] - List of provider configurations for custom fallback
|
|
120
|
+
* @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
|
|
121
|
+
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
95
122
|
*/
|
|
96
123
|
constructor(options = {}) {
|
|
97
124
|
// Basic configuration
|
|
@@ -104,11 +131,24 @@ export class ProbeAgent {
|
|
|
104
131
|
this.cancelled = false;
|
|
105
132
|
this.tracer = options.tracer || null;
|
|
106
133
|
this.outline = !!options.outline;
|
|
107
|
-
this.maxResponseTokens = options.maxResponseTokens ||
|
|
134
|
+
this.maxResponseTokens = options.maxResponseTokens || (() => {
|
|
135
|
+
const val = parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10);
|
|
136
|
+
if (isNaN(val) || val < 0 || val > 200000) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return val || null;
|
|
140
|
+
})();
|
|
108
141
|
this.maxIterations = options.maxIterations || null;
|
|
109
142
|
this.disableMermaidValidation = !!options.disableMermaidValidation;
|
|
110
143
|
this.disableJsonValidation = !!options.disableJsonValidation;
|
|
111
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
|
+
|
|
112
152
|
// Storage adapter (defaults to in-memory)
|
|
113
153
|
this.storageAdapter = options.storageAdapter || new InMemoryStorageAdapter();
|
|
114
154
|
|
|
@@ -171,6 +211,14 @@ export class ProbeAgent {
|
|
|
171
211
|
this.mcpBridge = null;
|
|
172
212
|
this._mcpInitialized = false; // Track if MCP initialization has been attempted
|
|
173
213
|
|
|
214
|
+
// Retry configuration
|
|
215
|
+
this.retryConfig = options.retry || {};
|
|
216
|
+
this.retryManager = null; // Will be initialized lazily when needed
|
|
217
|
+
|
|
218
|
+
// Fallback configuration
|
|
219
|
+
this.fallbackConfig = options.fallback || null;
|
|
220
|
+
this.fallbackManager = null; // Will be initialized in initializeModel
|
|
221
|
+
|
|
174
222
|
// Initialize the AI model
|
|
175
223
|
this.initializeModel();
|
|
176
224
|
|
|
@@ -178,6 +226,74 @@ export class ProbeAgent {
|
|
|
178
226
|
// Constructor must remain synchronous for backward compatibility
|
|
179
227
|
}
|
|
180
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
|
+
|
|
181
297
|
/**
|
|
182
298
|
* Initialize the agent asynchronously (must be called after constructor)
|
|
183
299
|
* This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
|
|
@@ -210,10 +326,15 @@ export class ProbeAgent {
|
|
|
210
326
|
await this.initializeMCP();
|
|
211
327
|
|
|
212
328
|
// Merge MCP tools into toolImplementations for unified access
|
|
329
|
+
// Apply allowedTools filtering using mcp__ prefix (like Claude Code)
|
|
213
330
|
if (this.mcpBridge) {
|
|
214
331
|
const mcpTools = this.mcpBridge.mcpTools || {};
|
|
215
332
|
for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
|
|
216
|
-
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
|
+
}
|
|
217
338
|
}
|
|
218
339
|
}
|
|
219
340
|
|
|
@@ -284,6 +405,16 @@ export class ProbeAgent {
|
|
|
284
405
|
this.toolImplementations.bash = wrappedTools.bashToolInstance;
|
|
285
406
|
}
|
|
286
407
|
|
|
408
|
+
// Add edit and create tools if enabled
|
|
409
|
+
if (this.allowEdit) {
|
|
410
|
+
if (wrappedTools.editToolInstance) {
|
|
411
|
+
this.toolImplementations.edit = wrappedTools.editToolInstance;
|
|
412
|
+
}
|
|
413
|
+
if (wrappedTools.createToolInstance) {
|
|
414
|
+
this.toolImplementations.create = wrappedTools.createToolInstance;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
287
418
|
// Store wrapped tools for ACP system
|
|
288
419
|
this.wrappedTools = wrappedTools;
|
|
289
420
|
|
|
@@ -352,15 +483,19 @@ export class ProbeAgent {
|
|
|
352
483
|
if (forceProvider) {
|
|
353
484
|
if (forceProvider === 'anthropic' && anthropicApiKey) {
|
|
354
485
|
this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
|
|
486
|
+
this.initializeFallbackManager(forceProvider, modelName);
|
|
355
487
|
return;
|
|
356
488
|
} else if (forceProvider === 'openai' && openaiApiKey) {
|
|
357
489
|
this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
|
|
490
|
+
this.initializeFallbackManager(forceProvider, modelName);
|
|
358
491
|
return;
|
|
359
492
|
} else if (forceProvider === 'google' && googleApiKey) {
|
|
360
493
|
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
494
|
+
this.initializeFallbackManager(forceProvider, modelName);
|
|
361
495
|
return;
|
|
362
496
|
} else if (forceProvider === 'bedrock' && ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey)) {
|
|
363
497
|
this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
|
|
498
|
+
this.initializeFallbackManager(forceProvider, modelName);
|
|
364
499
|
return;
|
|
365
500
|
}
|
|
366
501
|
console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
|
|
@@ -369,17 +504,146 @@ export class ProbeAgent {
|
|
|
369
504
|
// If no provider is forced or forced provider failed, use the first available API key
|
|
370
505
|
if (anthropicApiKey) {
|
|
371
506
|
this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
|
|
507
|
+
this.initializeFallbackManager('anthropic', modelName);
|
|
372
508
|
} else if (openaiApiKey) {
|
|
373
509
|
this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
|
|
510
|
+
this.initializeFallbackManager('openai', modelName);
|
|
374
511
|
} else if (googleApiKey) {
|
|
375
512
|
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
513
|
+
this.initializeFallbackManager('google', modelName);
|
|
376
514
|
} else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
|
|
377
515
|
this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
|
|
516
|
+
this.initializeFallbackManager('bedrock', modelName);
|
|
378
517
|
} else {
|
|
379
518
|
throw new Error('No API key provided. Please set ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN), OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY), AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
|
|
380
519
|
}
|
|
381
520
|
}
|
|
382
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Initialize fallback manager based on configuration
|
|
524
|
+
* @param {string} primaryProvider - The primary provider being used
|
|
525
|
+
* @param {string} primaryModel - The primary model being used
|
|
526
|
+
* @private
|
|
527
|
+
*/
|
|
528
|
+
initializeFallbackManager(primaryProvider, primaryModel) {
|
|
529
|
+
// Skip fallback initialization if explicitly disabled or in test mode
|
|
530
|
+
if (this.fallbackConfig === false || process.env.DISABLE_FALLBACK === '1') {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// If fallback config is provided explicitly, use it
|
|
535
|
+
if (this.fallbackConfig && this.fallbackConfig.providers) {
|
|
536
|
+
try {
|
|
537
|
+
this.fallbackManager = new FallbackManager({
|
|
538
|
+
...this.fallbackConfig,
|
|
539
|
+
debug: this.debug
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
if (this.debug) {
|
|
543
|
+
console.log(`[DEBUG] Fallback manager initialized with ${this.fallbackManager.providers.length} providers`);
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error('[WARNING] Failed to initialize fallback manager:', error.message);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Try to load from environment variables
|
|
552
|
+
const envFallbackManager = createFallbackManagerFromEnv(this.debug);
|
|
553
|
+
if (envFallbackManager) {
|
|
554
|
+
this.fallbackManager = envFallbackManager;
|
|
555
|
+
if (this.debug) {
|
|
556
|
+
console.log(`[DEBUG] Fallback manager initialized from environment variables`);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Auto-build fallback from available providers if enabled
|
|
562
|
+
if (process.env.AUTO_FALLBACK === '1' || this.fallbackConfig?.auto) {
|
|
563
|
+
const providers = buildFallbackProvidersFromEnv({
|
|
564
|
+
primaryProvider,
|
|
565
|
+
primaryModel
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (providers.length > 1) {
|
|
569
|
+
try {
|
|
570
|
+
this.fallbackManager = new FallbackManager({
|
|
571
|
+
strategy: 'custom',
|
|
572
|
+
providers,
|
|
573
|
+
debug: this.debug
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
if (this.debug) {
|
|
577
|
+
console.log(`[DEBUG] Auto-fallback enabled with ${providers.length} providers`);
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('[WARNING] Failed to initialize auto-fallback:', error.message);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Execute streamText with retry and fallback support
|
|
588
|
+
* @param {Object} options - streamText options
|
|
589
|
+
* @returns {Promise<Object>} - streamText result
|
|
590
|
+
* @private
|
|
591
|
+
*/
|
|
592
|
+
async streamTextWithRetryAndFallback(options) {
|
|
593
|
+
// Initialize retry manager if not already created
|
|
594
|
+
if (!this.retryManager) {
|
|
595
|
+
this.retryManager = new RetryManager({
|
|
596
|
+
maxRetries: this.retryConfig.maxRetries ?? 3,
|
|
597
|
+
initialDelay: this.retryConfig.initialDelay ?? 1000,
|
|
598
|
+
maxDelay: this.retryConfig.maxDelay ?? 30000,
|
|
599
|
+
backoffFactor: this.retryConfig.backoffFactor ?? 2,
|
|
600
|
+
retryableErrors: this.retryConfig.retryableErrors,
|
|
601
|
+
debug: this.debug
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// If no fallback manager, just use retry with current provider
|
|
606
|
+
if (!this.fallbackManager) {
|
|
607
|
+
return await this.retryManager.executeWithRetry(
|
|
608
|
+
() => streamText(options),
|
|
609
|
+
{
|
|
610
|
+
provider: this.apiType,
|
|
611
|
+
model: this.model
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Use fallback manager with retry for each provider
|
|
617
|
+
return await this.fallbackManager.executeWithFallback(
|
|
618
|
+
async (provider, model, config) => {
|
|
619
|
+
// Create options with the fallback provider
|
|
620
|
+
const fallbackOptions = {
|
|
621
|
+
...options,
|
|
622
|
+
model: provider(model)
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Create a retry manager for this specific provider
|
|
626
|
+
const providerRetryManager = new RetryManager({
|
|
627
|
+
maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
|
|
628
|
+
initialDelay: this.retryConfig.initialDelay ?? 1000,
|
|
629
|
+
maxDelay: this.retryConfig.maxDelay ?? 30000,
|
|
630
|
+
backoffFactor: this.retryConfig.backoffFactor ?? 2,
|
|
631
|
+
retryableErrors: this.retryConfig.retryableErrors,
|
|
632
|
+
debug: this.debug
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Execute with retry for this provider
|
|
636
|
+
return await providerRetryManager.executeWithRetry(
|
|
637
|
+
() => streamText(fallbackOptions),
|
|
638
|
+
{
|
|
639
|
+
provider: config.provider,
|
|
640
|
+
model: model
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
383
647
|
/**
|
|
384
648
|
* Initialize Anthropic model
|
|
385
649
|
*/
|
|
@@ -866,10 +1130,15 @@ export class ProbeAgent {
|
|
|
866
1130
|
await this.initializeMCP();
|
|
867
1131
|
|
|
868
1132
|
// Merge MCP tools into toolImplementations for unified access
|
|
1133
|
+
// Apply allowedTools filtering using mcp__ prefix (like Claude Code)
|
|
869
1134
|
if (this.mcpBridge) {
|
|
870
1135
|
const mcpTools = this.mcpBridge.mcpTools || {};
|
|
871
1136
|
for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
|
|
872
|
-
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
|
+
}
|
|
873
1142
|
}
|
|
874
1143
|
}
|
|
875
1144
|
} catch (error) {
|
|
@@ -880,19 +1149,53 @@ export class ProbeAgent {
|
|
|
880
1149
|
}
|
|
881
1150
|
}
|
|
882
1151
|
|
|
883
|
-
// Build tool definitions
|
|
884
|
-
let toolDefinitions =
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
`;
|
|
892
|
-
|
|
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')) {
|
|
893
1177
|
toolDefinitions += `${implementToolDefinition}\n`;
|
|
894
1178
|
}
|
|
895
|
-
if (this.
|
|
1179
|
+
if (this.allowEdit && isToolAllowed('edit')) {
|
|
1180
|
+
toolDefinitions += `${editToolDefinition}\n`;
|
|
1181
|
+
}
|
|
1182
|
+
if (this.allowEdit && isToolAllowed('create')) {
|
|
1183
|
+
toolDefinitions += `${createToolDefinition}\n`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Bash tool (require both enableBash flag AND allowedTools permission)
|
|
1187
|
+
if (this.enableBash && isToolAllowed('bash')) {
|
|
1188
|
+
toolDefinitions += `${bashToolDefinition}\n`;
|
|
1189
|
+
}
|
|
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')) {
|
|
896
1199
|
toolDefinitions += `${delegateToolDefinition}\n`;
|
|
897
1200
|
}
|
|
898
1201
|
|
|
@@ -958,7 +1261,7 @@ Available Tools:
|
|
|
958
1261
|
- extract: Extract specific code blocks or lines from files.
|
|
959
1262
|
- listFiles: List files and directories in a specified location.
|
|
960
1263
|
- searchFiles: Find files matching a glob pattern with recursive search capability.
|
|
961
|
-
${this.allowEdit ? '- implement: Implement a feature or fix a bug using aider.\n' : ''}${this.enableDelegate ? '- delegate: Delegate big distinct tasks to specialized probe subagents.\n' : ''}
|
|
1264
|
+
${this.allowEdit ? '- implement: Implement a feature or fix a bug using aider.\n- edit: Edit files using exact string replacement.\n- create: Create new files with specified content.\n' : ''}${this.enableDelegate ? '- delegate: Delegate big distinct tasks to specialized probe subagents.\n' : ''}${this.enableBash ? '- bash: Execute bash commands for system operations.\n' : ''}
|
|
962
1265
|
- attempt_completion: Finalize the task and provide the result to the user.
|
|
963
1266
|
- attempt_complete: Quick completion using previous response (shorthand).
|
|
964
1267
|
`;
|
|
@@ -974,7 +1277,10 @@ Follow these instructions carefully:
|
|
|
974
1277
|
6. You MUST respond with exactly ONE tool call per message, using the specified XML format, until the task is complete.
|
|
975
1278
|
7. Wait for the tool execution result (provided in the next user message in a <tool_result> block) before proceeding to the next step.
|
|
976
1279
|
8. Once the task is fully completed, use the '<attempt_completion>' tool to provide the final result. This is the ONLY way to signal completion.
|
|
977
|
-
9. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results.
|
|
1280
|
+
9. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results.${this.allowEdit ? `
|
|
1281
|
+
10. When modifying files, choose the appropriate tool:
|
|
1282
|
+
- Use 'edit' for precise changes to existing files (requires exact string match)
|
|
1283
|
+
- Use 'create' for new files or complete file rewrites` : ''}
|
|
978
1284
|
</instructions>
|
|
979
1285
|
`;
|
|
980
1286
|
|
|
@@ -1068,11 +1374,17 @@ When troubleshooting:
|
|
|
1068
1374
|
// Add Tool Definitions
|
|
1069
1375
|
systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
|
|
1070
1376
|
|
|
1071
|
-
// Add MCP tools if available
|
|
1377
|
+
// Add MCP tools if available (filtered by allowedTools)
|
|
1072
1378
|
if (this.mcpBridge && this.mcpBridge.getToolNames().length > 0) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
}
|
|
1076
1388
|
}
|
|
1077
1389
|
|
|
1078
1390
|
// Add folder information
|
|
@@ -1258,57 +1570,120 @@ When troubleshooting:
|
|
|
1258
1570
|
|
|
1259
1571
|
// Make AI request
|
|
1260
1572
|
let assistantResponseContent = '';
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
messages
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
+
});
|
|
1273
1590
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1591
|
+
// Get the promise reference BEFORE consuming stream (doesn't lock it)
|
|
1592
|
+
const usagePromise = result.usage;
|
|
1593
|
+
|
|
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
|
+
}
|
|
1276
1602
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (options.onStream) {
|
|
1282
|
-
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);
|
|
1283
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();
|
|
1284
1623
|
}
|
|
1285
1624
|
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
+
}
|
|
1290
1679
|
}
|
|
1291
1680
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
'ai.model': this.model,
|
|
1298
|
-
'ai.provider': this.clientApiProvider || 'auto',
|
|
1299
|
-
'iteration': currentIteration,
|
|
1300
|
-
'max_tokens': maxResponseTokens,
|
|
1301
|
-
'temperature': 0.3,
|
|
1302
|
-
'message_count': currentMessages.length
|
|
1303
|
-
});
|
|
1304
|
-
} else {
|
|
1305
|
-
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);
|
|
1306
1686
|
}
|
|
1307
|
-
|
|
1308
|
-
} catch (error) {
|
|
1309
|
-
console.error(`Error during streamText (Iter ${currentIteration}):`, error);
|
|
1310
|
-
finalResult = `Error: Failed to get response from AI model during iteration ${currentIteration}. ${error.message}`;
|
|
1311
|
-
throw new Error(finalResult);
|
|
1312
1687
|
}
|
|
1313
1688
|
|
|
1314
1689
|
// Log preview of assistant response for debugging loops
|
|
@@ -1327,7 +1702,10 @@ When troubleshooting:
|
|
|
1327
1702
|
'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
|
|
1328
1703
|
];
|
|
1329
1704
|
if (this.allowEdit) {
|
|
1330
|
-
validTools.push('implement');
|
|
1705
|
+
validTools.push('implement', 'edit', 'create');
|
|
1706
|
+
}
|
|
1707
|
+
if (this.enableBash) {
|
|
1708
|
+
validTools.push('bash');
|
|
1331
1709
|
}
|
|
1332
1710
|
if (this.enableDelegate) {
|
|
1333
1711
|
validTools.push('delegate');
|
|
@@ -1745,15 +2123,13 @@ NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
|
|
|
1745
2123
|
Convert your previous response content into actual JSON data that follows this schema structure.`;
|
|
1746
2124
|
|
|
1747
2125
|
// Call answer recursively with _schemaFormatted flag to prevent infinite loop
|
|
1748
|
-
finalResult = await this.answer(schemaPrompt, [], {
|
|
1749
|
-
...options,
|
|
1750
|
-
_schemaFormatted: true
|
|
2126
|
+
finalResult = await this.answer(schemaPrompt, [], {
|
|
2127
|
+
...options,
|
|
2128
|
+
_schemaFormatted: true
|
|
1751
2129
|
});
|
|
1752
|
-
|
|
1753
|
-
// Step 2:
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
// 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
|
|
1757
2133
|
if (!this.disableMermaidValidation) {
|
|
1758
2134
|
try {
|
|
1759
2135
|
if (this.debug) {
|
|
@@ -1815,19 +2191,16 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
1815
2191
|
} else if (this.debug) {
|
|
1816
2192
|
console.log(`[DEBUG] Mermaid validation: Skipped due to disableMermaidValidation option`);
|
|
1817
2193
|
}
|
|
1818
|
-
|
|
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
|
+
|
|
1819
2199
|
// Step 4: Validate and potentially correct JSON responses
|
|
1820
2200
|
if (isJsonSchema(options.schema)) {
|
|
1821
2201
|
if (this.debug) {
|
|
1822
2202
|
console.log(`[DEBUG] JSON validation: Starting validation process for schema response`);
|
|
1823
|
-
console.log(`[DEBUG] JSON validation:
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
// Clean the response first to extract JSON from markdown/code blocks
|
|
1827
|
-
finalResult = cleanSchemaResponse(finalResult);
|
|
1828
|
-
|
|
1829
|
-
if (this.debug) {
|
|
1830
|
-
console.log(`[DEBUG] JSON validation: After cleaning, length: ${finalResult.length} chars`);
|
|
2203
|
+
console.log(`[DEBUG] JSON validation: Cleaned response length: ${finalResult.length} chars`);
|
|
1831
2204
|
}
|
|
1832
2205
|
|
|
1833
2206
|
// Record JSON validation start in telemetry
|
|
@@ -1939,17 +2312,16 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
1939
2312
|
} else if (reachedMaxIterations && options.schema && this.debug) {
|
|
1940
2313
|
console.log('[DEBUG] Skipping schema formatting due to max iterations reached without completion');
|
|
1941
2314
|
} else if (completionAttempted && options.schema && !options._schemaFormatted && !options._skipValidation) {
|
|
1942
|
-
// 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
|
|
1943
2317
|
// Skip this validation if we're in a recursive correction call (_skipValidation flag)
|
|
1944
2318
|
try {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
// Validate and fix Mermaid diagrams if present
|
|
2319
|
+
// Validate and fix Mermaid diagrams if present (BEFORE schema cleaning)
|
|
1948
2320
|
if (!this.disableMermaidValidation) {
|
|
1949
2321
|
if (this.debug) {
|
|
1950
|
-
console.log(`[DEBUG] Mermaid validation: Validating attempt_completion result...`);
|
|
2322
|
+
console.log(`[DEBUG] Mermaid validation: Validating attempt_completion result BEFORE schema cleaning...`);
|
|
1951
2323
|
}
|
|
1952
|
-
|
|
2324
|
+
|
|
1953
2325
|
const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
1954
2326
|
debug: this.debug,
|
|
1955
2327
|
path: this.allowedFolders[0],
|
|
@@ -1957,7 +2329,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
1957
2329
|
model: this.model,
|
|
1958
2330
|
tracer: this.tracer
|
|
1959
2331
|
});
|
|
1960
|
-
|
|
2332
|
+
|
|
1961
2333
|
if (mermaidValidation.wasFixed) {
|
|
1962
2334
|
finalResult = mermaidValidation.fixedResponse;
|
|
1963
2335
|
if (this.debug) {
|
|
@@ -1972,6 +2344,9 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
1972
2344
|
} else if (this.debug) {
|
|
1973
2345
|
console.log(`[DEBUG] Mermaid validation: Skipped for attempt_completion result due to disableMermaidValidation option`);
|
|
1974
2346
|
}
|
|
2347
|
+
|
|
2348
|
+
// Now clean the schema response (may extract JSON and discard other content)
|
|
2349
|
+
finalResult = cleanSchemaResponse(finalResult);
|
|
1975
2350
|
|
|
1976
2351
|
// Validate and potentially correct JSON for attempt_completion results
|
|
1977
2352
|
if (isJsonSchema(options.schema)) {
|
|
@@ -2185,6 +2560,75 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
2185
2560
|
}
|
|
2186
2561
|
}
|
|
2187
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
|
+
|
|
2188
2632
|
/**
|
|
2189
2633
|
* Clone this agent's session to create a new agent with shared conversation history
|
|
2190
2634
|
* @param {Object} options - Clone options
|
|
@@ -2215,6 +2659,17 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
2215
2659
|
}
|
|
2216
2660
|
|
|
2217
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
|
+
|
|
2218
2673
|
const clonedAgent = new ProbeAgent({
|
|
2219
2674
|
// Copy current agent's config
|
|
2220
2675
|
customPrompt: this.customPrompt,
|
|
@@ -2231,6 +2686,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
2231
2686
|
maxIterations: this.maxIterations,
|
|
2232
2687
|
disableMermaidValidation: this.disableMermaidValidation,
|
|
2233
2688
|
disableJsonValidation: this.disableJsonValidation,
|
|
2689
|
+
allowedTools: allowedToolsArray,
|
|
2234
2690
|
enableMcp: !!this.mcpBridge,
|
|
2235
2691
|
mcpConfig: this.mcpConfig,
|
|
2236
2692
|
enableBash: this.enableBash,
|