@probelabs/probe 0.6.0-rc159 → 0.6.0-rc161

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.
Files changed (36) hide show
  1. package/README.md +80 -1
  2. package/build/agent/FallbackManager.d.ts +176 -0
  3. package/build/agent/FallbackManager.js +545 -0
  4. package/build/agent/ProbeAgent.d.ts +7 -1
  5. package/build/agent/ProbeAgent.js +199 -7
  6. package/build/agent/RetryManager.d.ts +157 -0
  7. package/build/agent/RetryManager.js +334 -0
  8. package/build/agent/acp/tools.js +6 -2
  9. package/build/agent/index.js +1379 -168
  10. package/build/agent/probeTool.js +20 -2
  11. package/build/agent/tools.js +16 -0
  12. package/build/index.js +13 -0
  13. package/build/tools/common.js +5 -3
  14. package/build/tools/edit.js +409 -0
  15. package/build/tools/index.js +11 -0
  16. package/cjs/agent/ProbeAgent.cjs +1822 -497
  17. package/cjs/index.cjs +1784 -454
  18. package/package.json +2 -2
  19. package/src/agent/FallbackManager.d.ts +176 -0
  20. package/src/agent/FallbackManager.js +545 -0
  21. package/src/agent/ProbeAgent.d.ts +7 -1
  22. package/src/agent/ProbeAgent.js +199 -7
  23. package/src/agent/RetryManager.d.ts +157 -0
  24. package/src/agent/RetryManager.js +334 -0
  25. package/src/agent/acp/tools.js +6 -2
  26. package/src/agent/probeTool.js +20 -2
  27. package/src/agent/tools.js +16 -0
  28. package/src/index.js +13 -0
  29. package/src/tools/common.js +5 -3
  30. package/src/tools/edit.js +409 -0
  31. package/src/tools/index.js +11 -0
  32. package/bin/binaries/probe-v0.6.0-rc159-aarch64-apple-darwin.tar.gz +0 -0
  33. package/bin/binaries/probe-v0.6.0-rc159-aarch64-unknown-linux-musl.tar.gz +0 -0
  34. package/bin/binaries/probe-v0.6.0-rc159-x86_64-apple-darwin.tar.gz +0 -0
  35. package/bin/binaries/probe-v0.6.0-rc159-x86_64-pc-windows-msvc.zip +0 -0
  36. 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,18 @@ 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';
58
63
 
59
64
  // Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
60
- const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
65
+ const MAX_TOOL_ITERATIONS = (() => {
66
+ const val = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
67
+ if (isNaN(val) || val < 1 || val > 200) {
68
+ console.warn('[ProbeAgent] MAX_TOOL_ITERATIONS must be between 1 and 200, using default: 30');
69
+ return 30;
70
+ }
71
+ return val;
72
+ })();
61
73
  const MAX_HISTORY_MESSAGES = 100;
62
74
 
63
75
  // Supported image file extensions (imported from shared config)
@@ -92,6 +104,18 @@ export class ProbeAgent {
92
104
  * @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
93
105
  * @param {Object} [options.storageAdapter] - Custom storage adapter for history management
94
106
  * @param {Object} [options.hooks] - Hook callbacks for events (e.g., {'tool:start': callback})
107
+ * @param {Object} [options.retry] - Retry configuration
108
+ * @param {number} [options.retry.maxRetries=3] - Maximum retry attempts per provider
109
+ * @param {number} [options.retry.initialDelay=1000] - Initial delay in ms
110
+ * @param {number} [options.retry.maxDelay=30000] - Maximum delay in ms
111
+ * @param {number} [options.retry.backoffFactor=2] - Exponential backoff multiplier
112
+ * @param {Array<string>} [options.retry.retryableErrors] - List of retryable error patterns
113
+ * @param {Object} [options.fallback] - Fallback configuration
114
+ * @param {string} [options.fallback.strategy] - Fallback strategy: 'same-model', 'same-provider', 'any', 'custom'
115
+ * @param {Array<string>} [options.fallback.models] - List of models for same-provider fallback
116
+ * @param {Array<Object>} [options.fallback.providers] - List of provider configurations for custom fallback
117
+ * @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
118
+ * @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
95
119
  */
96
120
  constructor(options = {}) {
97
121
  // Basic configuration
@@ -104,7 +128,13 @@ export class ProbeAgent {
104
128
  this.cancelled = false;
105
129
  this.tracer = options.tracer || null;
106
130
  this.outline = !!options.outline;
107
- this.maxResponseTokens = options.maxResponseTokens || parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10) || null;
131
+ this.maxResponseTokens = options.maxResponseTokens || (() => {
132
+ const val = parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10);
133
+ if (isNaN(val) || val < 0 || val > 200000) {
134
+ return null;
135
+ }
136
+ return val || null;
137
+ })();
108
138
  this.maxIterations = options.maxIterations || null;
109
139
  this.disableMermaidValidation = !!options.disableMermaidValidation;
110
140
  this.disableJsonValidation = !!options.disableJsonValidation;
@@ -171,6 +201,14 @@ export class ProbeAgent {
171
201
  this.mcpBridge = null;
172
202
  this._mcpInitialized = false; // Track if MCP initialization has been attempted
173
203
 
204
+ // Retry configuration
205
+ this.retryConfig = options.retry || {};
206
+ this.retryManager = null; // Will be initialized lazily when needed
207
+
208
+ // Fallback configuration
209
+ this.fallbackConfig = options.fallback || null;
210
+ this.fallbackManager = null; // Will be initialized in initializeModel
211
+
174
212
  // Initialize the AI model
175
213
  this.initializeModel();
176
214
 
@@ -284,6 +322,16 @@ export class ProbeAgent {
284
322
  this.toolImplementations.bash = wrappedTools.bashToolInstance;
285
323
  }
286
324
 
325
+ // Add edit and create tools if enabled
326
+ if (this.allowEdit) {
327
+ if (wrappedTools.editToolInstance) {
328
+ this.toolImplementations.edit = wrappedTools.editToolInstance;
329
+ }
330
+ if (wrappedTools.createToolInstance) {
331
+ this.toolImplementations.create = wrappedTools.createToolInstance;
332
+ }
333
+ }
334
+
287
335
  // Store wrapped tools for ACP system
288
336
  this.wrappedTools = wrappedTools;
289
337
 
@@ -352,15 +400,19 @@ export class ProbeAgent {
352
400
  if (forceProvider) {
353
401
  if (forceProvider === 'anthropic' && anthropicApiKey) {
354
402
  this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
403
+ this.initializeFallbackManager(forceProvider, modelName);
355
404
  return;
356
405
  } else if (forceProvider === 'openai' && openaiApiKey) {
357
406
  this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
407
+ this.initializeFallbackManager(forceProvider, modelName);
358
408
  return;
359
409
  } else if (forceProvider === 'google' && googleApiKey) {
360
410
  this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
411
+ this.initializeFallbackManager(forceProvider, modelName);
361
412
  return;
362
413
  } else if (forceProvider === 'bedrock' && ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey)) {
363
414
  this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
415
+ this.initializeFallbackManager(forceProvider, modelName);
364
416
  return;
365
417
  }
366
418
  console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
@@ -369,17 +421,146 @@ export class ProbeAgent {
369
421
  // If no provider is forced or forced provider failed, use the first available API key
370
422
  if (anthropicApiKey) {
371
423
  this.initializeAnthropicModel(anthropicApiKey, anthropicApiUrl, modelName);
424
+ this.initializeFallbackManager('anthropic', modelName);
372
425
  } else if (openaiApiKey) {
373
426
  this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
427
+ this.initializeFallbackManager('openai', modelName);
374
428
  } else if (googleApiKey) {
375
429
  this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
430
+ this.initializeFallbackManager('google', modelName);
376
431
  } else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
377
432
  this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
433
+ this.initializeFallbackManager('bedrock', modelName);
378
434
  } else {
379
435
  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
436
  }
381
437
  }
382
438
 
439
+ /**
440
+ * Initialize fallback manager based on configuration
441
+ * @param {string} primaryProvider - The primary provider being used
442
+ * @param {string} primaryModel - The primary model being used
443
+ * @private
444
+ */
445
+ initializeFallbackManager(primaryProvider, primaryModel) {
446
+ // Skip fallback initialization if explicitly disabled or in test mode
447
+ if (this.fallbackConfig === false || process.env.DISABLE_FALLBACK === '1') {
448
+ return;
449
+ }
450
+
451
+ // If fallback config is provided explicitly, use it
452
+ if (this.fallbackConfig && this.fallbackConfig.providers) {
453
+ try {
454
+ this.fallbackManager = new FallbackManager({
455
+ ...this.fallbackConfig,
456
+ debug: this.debug
457
+ });
458
+
459
+ if (this.debug) {
460
+ console.log(`[DEBUG] Fallback manager initialized with ${this.fallbackManager.providers.length} providers`);
461
+ }
462
+ } catch (error) {
463
+ console.error('[WARNING] Failed to initialize fallback manager:', error.message);
464
+ }
465
+ return;
466
+ }
467
+
468
+ // Try to load from environment variables
469
+ const envFallbackManager = createFallbackManagerFromEnv(this.debug);
470
+ if (envFallbackManager) {
471
+ this.fallbackManager = envFallbackManager;
472
+ if (this.debug) {
473
+ console.log(`[DEBUG] Fallback manager initialized from environment variables`);
474
+ }
475
+ return;
476
+ }
477
+
478
+ // Auto-build fallback from available providers if enabled
479
+ if (process.env.AUTO_FALLBACK === '1' || this.fallbackConfig?.auto) {
480
+ const providers = buildFallbackProvidersFromEnv({
481
+ primaryProvider,
482
+ primaryModel
483
+ });
484
+
485
+ if (providers.length > 1) {
486
+ try {
487
+ this.fallbackManager = new FallbackManager({
488
+ strategy: 'custom',
489
+ providers,
490
+ debug: this.debug
491
+ });
492
+
493
+ if (this.debug) {
494
+ console.log(`[DEBUG] Auto-fallback enabled with ${providers.length} providers`);
495
+ }
496
+ } catch (error) {
497
+ console.error('[WARNING] Failed to initialize auto-fallback:', error.message);
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Execute streamText with retry and fallback support
505
+ * @param {Object} options - streamText options
506
+ * @returns {Promise<Object>} - streamText result
507
+ * @private
508
+ */
509
+ async streamTextWithRetryAndFallback(options) {
510
+ // Initialize retry manager if not already created
511
+ if (!this.retryManager) {
512
+ this.retryManager = new RetryManager({
513
+ maxRetries: this.retryConfig.maxRetries ?? 3,
514
+ initialDelay: this.retryConfig.initialDelay ?? 1000,
515
+ maxDelay: this.retryConfig.maxDelay ?? 30000,
516
+ backoffFactor: this.retryConfig.backoffFactor ?? 2,
517
+ retryableErrors: this.retryConfig.retryableErrors,
518
+ debug: this.debug
519
+ });
520
+ }
521
+
522
+ // If no fallback manager, just use retry with current provider
523
+ if (!this.fallbackManager) {
524
+ return await this.retryManager.executeWithRetry(
525
+ () => streamText(options),
526
+ {
527
+ provider: this.apiType,
528
+ model: this.model
529
+ }
530
+ );
531
+ }
532
+
533
+ // Use fallback manager with retry for each provider
534
+ return await this.fallbackManager.executeWithFallback(
535
+ async (provider, model, config) => {
536
+ // Create options with the fallback provider
537
+ const fallbackOptions = {
538
+ ...options,
539
+ model: provider(model)
540
+ };
541
+
542
+ // Create a retry manager for this specific provider
543
+ const providerRetryManager = new RetryManager({
544
+ maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
545
+ initialDelay: this.retryConfig.initialDelay ?? 1000,
546
+ maxDelay: this.retryConfig.maxDelay ?? 30000,
547
+ backoffFactor: this.retryConfig.backoffFactor ?? 2,
548
+ retryableErrors: this.retryConfig.retryableErrors,
549
+ debug: this.debug
550
+ });
551
+
552
+ // Execute with retry for this provider
553
+ return await providerRetryManager.executeWithRetry(
554
+ () => streamText(fallbackOptions),
555
+ {
556
+ provider: config.provider,
557
+ model: model
558
+ }
559
+ );
560
+ }
561
+ );
562
+ }
563
+
383
564
  /**
384
565
  * Initialize Anthropic model
385
566
  */
@@ -891,6 +1072,11 @@ ${attemptCompletionToolDefinition}
891
1072
  `;
892
1073
  if (this.allowEdit) {
893
1074
  toolDefinitions += `${implementToolDefinition}\n`;
1075
+ toolDefinitions += `${editToolDefinition}\n`;
1076
+ toolDefinitions += `${createToolDefinition}\n`;
1077
+ }
1078
+ if (this.enableBash) {
1079
+ toolDefinitions += `${bashToolDefinition}\n`;
894
1080
  }
895
1081
  if (this.enableDelegate) {
896
1082
  toolDefinitions += `${delegateToolDefinition}\n`;
@@ -958,7 +1144,7 @@ Available Tools:
958
1144
  - extract: Extract specific code blocks or lines from files.
959
1145
  - listFiles: List files and directories in a specified location.
960
1146
  - 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' : ''}
1147
+ ${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
1148
  - attempt_completion: Finalize the task and provide the result to the user.
963
1149
  - attempt_complete: Quick completion using previous response (shorthand).
964
1150
  `;
@@ -974,7 +1160,10 @@ Follow these instructions carefully:
974
1160
  6. You MUST respond with exactly ONE tool call per message, using the specified XML format, until the task is complete.
975
1161
  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
1162
  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.
1163
+ 9. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results.${this.allowEdit ? `
1164
+ 10. When modifying files, choose the appropriate tool:
1165
+ - Use 'edit' for precise changes to existing files (requires exact string match)
1166
+ - Use 'create' for new files or complete file rewrites` : ''}
978
1167
  </instructions>
979
1168
  `;
980
1169
 
@@ -1263,8 +1452,8 @@ When troubleshooting:
1263
1452
  const executeAIRequest = async () => {
1264
1453
  // Prepare messages with potential image content
1265
1454
  const messagesForAI = this.prepareMessagesWithImages(currentMessages);
1266
-
1267
- const result = await streamText({
1455
+
1456
+ const result = await this.streamTextWithRetryAndFallback({
1268
1457
  model: this.provider(this.model),
1269
1458
  messages: messagesForAI,
1270
1459
  maxTokens: maxResponseTokens,
@@ -1327,7 +1516,10 @@ When troubleshooting:
1327
1516
  'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
1328
1517
  ];
1329
1518
  if (this.allowEdit) {
1330
- validTools.push('implement');
1519
+ validTools.push('implement', 'edit', 'create');
1520
+ }
1521
+ if (this.enableBash) {
1522
+ validTools.push('bash');
1331
1523
  }
1332
1524
  if (this.enableDelegate) {
1333
1525
  validTools.push('delegate');
@@ -0,0 +1,157 @@
1
+ /**
2
+ * TypeScript type definitions for RetryManager
3
+ */
4
+
5
+ /**
6
+ * Retry configuration options
7
+ */
8
+ export interface RetryOptions {
9
+ /** Maximum number of retry attempts (0-100) */
10
+ maxRetries?: number;
11
+ /** Initial delay in milliseconds (0-60000) */
12
+ initialDelay?: number;
13
+ /** Maximum delay in milliseconds (0-300000) */
14
+ maxDelay?: number;
15
+ /** Exponential backoff multiplier (1-10) */
16
+ backoffFactor?: number;
17
+ /** List of retryable error patterns */
18
+ retryableErrors?: string[];
19
+ /** Enable debug logging */
20
+ debug?: boolean;
21
+ /** Add random jitter to delays (default: true) */
22
+ jitter?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Context information for retry operations
27
+ */
28
+ export interface RetryContext {
29
+ /** Provider name for logging */
30
+ provider?: string;
31
+ /** Model name for logging */
32
+ model?: string;
33
+ /** AbortSignal for cancellation */
34
+ signal?: AbortSignal;
35
+ /** Additional context data */
36
+ [key: string]: any;
37
+ }
38
+
39
+ /**
40
+ * Retry statistics
41
+ */
42
+ export interface RetryStats {
43
+ /** Total number of attempts made */
44
+ totalAttempts: number;
45
+ /** Total number of retries (excluding initial attempts) */
46
+ totalRetries: number;
47
+ /** Number of successful retries */
48
+ successfulRetries: number;
49
+ /** Number of failed retries (exhausted max retries) */
50
+ failedRetries: number;
51
+ }
52
+
53
+ /**
54
+ * Error information extracted from an error object
55
+ */
56
+ export interface ErrorInfo {
57
+ /** Error message */
58
+ message: string;
59
+ /** Error type or constructor name */
60
+ type: string;
61
+ /** Error code if available */
62
+ code?: string;
63
+ /** HTTP status code if available */
64
+ statusCode?: number;
65
+ /** Provider name if available */
66
+ provider?: string;
67
+ /** Whether the error is retryable */
68
+ isRetryable: boolean;
69
+ }
70
+
71
+ /**
72
+ * RetryManager class for handling retry logic with exponential backoff
73
+ */
74
+ export class RetryManager {
75
+ /** Maximum retry attempts */
76
+ maxRetries: number;
77
+ /** Initial delay in milliseconds */
78
+ initialDelay: number;
79
+ /** Maximum delay in milliseconds */
80
+ maxDelay: number;
81
+ /** Exponential backoff multiplier */
82
+ backoffFactor: number;
83
+ /** List of retryable error patterns */
84
+ retryableErrors: string[];
85
+ /** Debug logging enabled */
86
+ debug: boolean;
87
+ /** Jitter enabled */
88
+ jitter: boolean;
89
+ /** Retry statistics */
90
+ stats: RetryStats;
91
+
92
+ /**
93
+ * Create a new RetryManager
94
+ * @param options - Retry configuration options
95
+ */
96
+ constructor(options?: RetryOptions);
97
+
98
+ /**
99
+ * Execute a function with retry logic
100
+ * @param fn - Async function to execute
101
+ * @param context - Context information for logging
102
+ * @returns Result from the function
103
+ * @throws Error if all retries are exhausted or operation is aborted
104
+ */
105
+ executeWithRetry<T>(
106
+ fn: () => Promise<T>,
107
+ context?: RetryContext
108
+ ): Promise<T>;
109
+
110
+ /**
111
+ * Check if an error is retryable
112
+ * @param error - The error to check
113
+ * @returns True if error should be retried
114
+ */
115
+ isRetryable(error: Error): boolean;
116
+
117
+ /**
118
+ * Get retry statistics
119
+ * @returns Statistics object (copy)
120
+ */
121
+ getStats(): RetryStats;
122
+
123
+ /**
124
+ * Reset statistics
125
+ */
126
+ resetStats(): void;
127
+ }
128
+
129
+ /**
130
+ * Check if an error is retryable based on error patterns
131
+ * @param error - The error to check
132
+ * @param retryableErrors - List of retryable error patterns
133
+ * @returns True if error should be retried
134
+ */
135
+ export function isRetryableError(
136
+ error: Error,
137
+ retryableErrors?: string[]
138
+ ): boolean;
139
+
140
+ /**
141
+ * Extract meaningful error information for logging
142
+ * @param error - The error to extract info from
143
+ * @returns Error information object
144
+ */
145
+ export function extractErrorInfo(error: Error): ErrorInfo;
146
+
147
+ /**
148
+ * Create a RetryManager from environment variables
149
+ * @param debug - Enable debug logging
150
+ * @returns Configured RetryManager instance
151
+ */
152
+ export function createRetryManagerFromEnv(debug?: boolean): RetryManager;
153
+
154
+ /**
155
+ * Default retryable error patterns
156
+ */
157
+ export const DEFAULT_RETRYABLE_ERRORS: string[];