@probelabs/probe 0.6.0-rc323 → 0.6.0-rc325

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.
@@ -1453,30 +1453,45 @@ export class ProbeAgent {
1453
1453
  );
1454
1454
  }
1455
1455
 
1456
+ /**
1457
+ * Create a RetryManager from the agent's retry configuration.
1458
+ * @param {Object} [overrides={}] - Retry option overrides
1459
+ * @returns {RetryManager}
1460
+ * @private
1461
+ */
1462
+ _createRetryManager(overrides = {}) {
1463
+ return new RetryManager({
1464
+ maxRetries: overrides.maxRetries ?? this.retryConfig.maxRetries ?? 3,
1465
+ initialDelay: overrides.initialDelay ?? this.retryConfig.initialDelay ?? 1000,
1466
+ maxDelay: overrides.maxDelay ?? this.retryConfig.maxDelay ?? 30000,
1467
+ backoffFactor: overrides.backoffFactor ?? this.retryConfig.backoffFactor ?? 2,
1468
+ retryableErrors: overrides.retryableErrors ?? this.retryConfig.retryableErrors,
1469
+ jitter: overrides.jitter ?? this.retryConfig.jitter,
1470
+ debug: overrides.debug ?? this.debug
1471
+ });
1472
+ }
1473
+
1456
1474
  /**
1457
1475
  * Execute streamText with Vercel AI SDK using retry/fallback logic
1458
1476
  * @param {Object} options - streamText options
1459
1477
  * @param {AbortController} controller - Abort controller for the operation
1478
+ * @param {Function} [consumeResult] - Optional callback to consume the stream result inside the same retry budget
1460
1479
  * @returns {Promise<Object>} - Stream result
1461
1480
  * @private
1462
1481
  */
1463
- async _executeWithVercelProvider(options, controller) {
1482
+ async _executeWithVercelProvider(options, controller, consumeResult) {
1464
1483
  // Initialize retry manager if not already created
1465
1484
  if (!this.retryManager) {
1466
- this.retryManager = new RetryManager({
1467
- maxRetries: this.retryConfig.maxRetries ?? 3,
1468
- initialDelay: this.retryConfig.initialDelay ?? 1000,
1469
- maxDelay: this.retryConfig.maxDelay ?? 30000,
1470
- backoffFactor: this.retryConfig.backoffFactor ?? 2,
1471
- retryableErrors: this.retryConfig.retryableErrors,
1472
- debug: this.debug
1473
- });
1485
+ this.retryManager = this._createRetryManager();
1474
1486
  }
1475
1487
 
1476
1488
  // If no fallback manager, just use retry with current provider
1477
1489
  if (!this.fallbackManager) {
1478
1490
  return await this.retryManager.executeWithRetry(
1479
- () => streamText({ ...options, abortSignal: controller.signal }),
1491
+ async () => {
1492
+ const result = streamText({ ...options, abortSignal: controller.signal });
1493
+ return consumeResult ? await consumeResult(result) : result;
1494
+ },
1480
1495
  {
1481
1496
  provider: this.apiType,
1482
1497
  model: this.model,
@@ -1510,17 +1525,15 @@ export class ProbeAgent {
1510
1525
  }
1511
1526
  }
1512
1527
 
1513
- const providerRetryManager = new RetryManager({
1514
- maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
1515
- initialDelay: this.retryConfig.initialDelay ?? 1000,
1516
- maxDelay: this.retryConfig.maxDelay ?? 30000,
1517
- backoffFactor: this.retryConfig.backoffFactor ?? 2,
1518
- retryableErrors: this.retryConfig.retryableErrors,
1519
- debug: this.debug
1528
+ const providerRetryManager = this._createRetryManager({
1529
+ maxRetries: config.maxRetries ?? this.retryConfig.maxRetries
1520
1530
  });
1521
1531
 
1522
1532
  return await providerRetryManager.executeWithRetry(
1523
- () => streamText(fallbackOptions),
1533
+ async () => {
1534
+ const result = streamText(fallbackOptions);
1535
+ return consumeResult ? await consumeResult(result) : result;
1536
+ },
1524
1537
  {
1525
1538
  provider: config.provider,
1526
1539
  model: model,
@@ -1671,10 +1684,11 @@ export class ProbeAgent {
1671
1684
  /**
1672
1685
  * Execute streamText with retry and fallback support
1673
1686
  * @param {Object} options - streamText options
1674
- * @returns {Promise<Object>} - streamText result
1687
+ * @param {Function} [consumeResult] - Optional callback to consume the result inside the same retry attempt
1688
+ * @returns {Promise<Object>} - streamText result or consumer result
1675
1689
  * @private
1676
1690
  */
1677
- async streamTextWithRetryAndFallback(options) {
1691
+ async streamTextWithRetryAndFallback(options, consumeResult) {
1678
1692
  // Wrap the model with per-call concurrency gating if limiter is configured.
1679
1693
  // This acquires/releases the slot around each individual LLM API call (doStream/doGenerate)
1680
1694
  // instead of holding it for the entire multi-step agent session.
@@ -1727,6 +1741,7 @@ export class ProbeAgent {
1727
1741
  const useCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
1728
1742
 
1729
1743
  let result;
1744
+ let usedVercelProvider = false;
1730
1745
  if (useClaudeCode || useCodex) {
1731
1746
  try {
1732
1747
  result = await this._tryEngineStreamPath(options, controller, timeoutState);
@@ -1747,7 +1762,12 @@ export class ProbeAgent {
1747
1762
 
1748
1763
  if (!result) {
1749
1764
  // Use Vercel AI SDK with retry/fallback
1750
- result = await this._executeWithVercelProvider(options, controller);
1765
+ usedVercelProvider = true;
1766
+ result = await this._executeWithVercelProvider(options, controller, consumeResult);
1767
+ }
1768
+
1769
+ if (!usedVercelProvider && result && consumeResult) {
1770
+ return await consumeResult(result);
1751
1771
  }
1752
1772
 
1753
1773
  return result;
@@ -3763,8 +3783,6 @@ Follow these instructions carefully:
3763
3783
  activeTools.delete(key);
3764
3784
  }
3765
3785
  };
3766
- this.events.on('toolCall', onToolCall);
3767
-
3768
3786
  // Timeout observer: separate LLM call that decides whether to extend.
3769
3787
  // Runs independently of the main agent loop — works even when blocked by delegates.
3770
3788
  const runTimeoutObserver = async () => {
@@ -4426,80 +4444,92 @@ Double-check your response based on the criteria above. If everything looks good
4426
4444
  }
4427
4445
 
4428
4446
  const executeAIRequest = async () => {
4429
- const result = await this.streamTextWithRetryAndFallback(streamOptions);
4430
-
4431
- // Set up timeout timer now that streamText is running.
4432
- // streamText() returns immediately the actual tool loop runs asynchronously
4433
- // and completes when we await result.steps/result.text below.
4434
- let gracefulTimeoutId = null;
4435
- let hardAbortTimeoutId = null;
4436
- if (this.timeoutBehavior === 'graceful' && gracefulTimeoutState && this.maxOperationTimeout > 0) {
4437
- gracefulTimeoutId = setTimeout(() => {
4438
- gracefulTimeoutState.triggered = true;
4439
- if (this.debug) {
4440
- console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms — entering wind-down mode (${gracefulTimeoutState.bonusStepsMax} bonus steps)`);
4441
- }
4442
- // Safety net: hard abort after 60s if wind-down doesn't complete
4443
- hardAbortTimeoutId = setTimeout(() => {
4444
- if (this._abortController) {
4445
- this._abortController.abort();
4447
+ return await this.streamTextWithRetryAndFallback(streamOptions, async (result) => {
4448
+ this.events.on('toolCall', onToolCall);
4449
+
4450
+ // Set up timeout timer now that streamText is running.
4451
+ // streamText() returns immediately the actual tool loop runs asynchronously
4452
+ // and completes when we await result.steps/result.text below.
4453
+ let gracefulTimeoutId = null;
4454
+ let hardAbortTimeoutId = null;
4455
+ if (this.timeoutBehavior === 'graceful' && gracefulTimeoutState && this.maxOperationTimeout > 0) {
4456
+ gracefulTimeoutId = setTimeout(() => {
4457
+ gracefulTimeoutState.triggered = true;
4458
+ if (this.debug) {
4459
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms — entering wind-down mode (${gracefulTimeoutState.bonusStepsMax} bonus steps)`);
4446
4460
  }
4461
+ // Safety net: hard abort after 60s if wind-down doesn't complete
4462
+ hardAbortTimeoutId = setTimeout(() => {
4463
+ if (this._abortController) {
4464
+ this._abortController.abort();
4465
+ }
4466
+ if (this.debug) {
4467
+ console.log(`[DEBUG] Hard abort — wind-down safety net expired after 60s`);
4468
+ }
4469
+ }, 60000);
4470
+ }, this.maxOperationTimeout);
4471
+ }
4472
+
4473
+ // Negotiated timeout: run the timeout observer (separate LLM call)
4474
+ if (this.timeoutBehavior === 'negotiated' && this.maxOperationTimeout > 0) {
4475
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
4447
4476
  if (this.debug) {
4448
- console.log(`[DEBUG] Hard abort wind-down safety net expired after 60s`);
4477
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms invoking timeout observer`);
4449
4478
  }
4450
- }, 60000);
4451
- }, this.maxOperationTimeout);
4452
- }
4479
+ runTimeoutObserver();
4480
+ }, this.maxOperationTimeout);
4481
+ }
4453
4482
 
4454
- // Negotiated timeout: run the timeout observer (separate LLM call)
4455
- if (this.timeoutBehavior === 'negotiated' && this.maxOperationTimeout > 0) {
4456
- negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
4457
- if (this.debug) {
4458
- console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms — invoking timeout observer`);
4483
+ try {
4484
+ // Use only the last step's text as the final answer.
4485
+ // result.text concatenates ALL steps (including intermediate planning text),
4486
+ // but the user should only see the final answer from the last step.
4487
+ const steps = await result.steps;
4488
+ let finalText;
4489
+ if (steps && steps.length > 1) {
4490
+ // Multi-step: use last step's text (the actual answer after tool calls)
4491
+ const lastStepText = steps[steps.length - 1].text;
4492
+ finalText = lastStepText || await result.text;
4493
+ } else {
4494
+ finalText = await result.text;
4459
4495
  }
4460
- runTimeoutObserver();
4461
- }, this.maxOperationTimeout);
4462
- }
4463
4496
 
4464
- try {
4465
- // Use only the last step's text as the final answer.
4466
- // result.text concatenates ALL steps (including intermediate planning text),
4467
- // but the user should only see the final answer from the last step.
4468
- const steps = await result.steps;
4469
- let finalText;
4470
- if (steps && steps.length > 1) {
4471
- // Multi-step: use last step's text (the actual answer after tool calls)
4472
- const lastStepText = steps[steps.length - 1].text;
4473
- finalText = lastStepText || await result.text;
4474
- } else {
4475
- finalText = await result.text;
4476
- }
4497
+ // Native schema responses can legitimately carry their payload in result.output
4498
+ // while text and steps stay empty. Plain text answers with no steps and no
4499
+ // text are empty streams and should be retried by the active retry manager.
4500
+ if (!options.schema && (!steps || steps.length === 0) && (!finalText || !finalText.trim())) {
4501
+ throw Object.assign(
4502
+ new Error('No output generated. Check the stream for errors.'),
4503
+ { name: 'AI_NoOutputGeneratedError' }
4504
+ );
4505
+ }
4477
4506
 
4478
- if (this.debug) {
4479
- console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
4480
- }
4507
+ if (this.debug) {
4508
+ console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
4509
+ }
4481
4510
 
4482
- // Record final token usage
4483
- const usage = await result.usage;
4484
- if (usage) {
4485
- this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
4486
- }
4511
+ // Record final token usage
4512
+ const usage = await result.usage;
4513
+ if (usage) {
4514
+ this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
4515
+ }
4487
4516
 
4488
- return { finalText, result };
4489
- } finally {
4490
- // Clean up graceful timeout timers
4491
- if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
4492
- if (hardAbortTimeoutId) clearTimeout(hardAbortTimeoutId);
4493
- // Clean up negotiated timeout timer
4494
- if (negotiatedTimeoutState.softTimeoutId) clearTimeout(negotiatedTimeoutState.softTimeoutId);
4495
- // Clean up graceful stop hard abort timer
4496
- if (this._gracefulStopHardAbortId) {
4497
- clearTimeout(this._gracefulStopHardAbortId);
4498
- this._gracefulStopHardAbortId = null;
4517
+ return { finalText, result };
4518
+ } finally {
4519
+ // Clean up graceful timeout timers
4520
+ if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
4521
+ if (hardAbortTimeoutId) clearTimeout(hardAbortTimeoutId);
4522
+ // Clean up negotiated timeout timer
4523
+ if (negotiatedTimeoutState.softTimeoutId) clearTimeout(negotiatedTimeoutState.softTimeoutId);
4524
+ // Clean up graceful stop hard abort timer
4525
+ if (this._gracefulStopHardAbortId) {
4526
+ clearTimeout(this._gracefulStopHardAbortId);
4527
+ this._gracefulStopHardAbortId = null;
4528
+ }
4529
+ // Remove in-flight tool tracker
4530
+ this.events.removeListener('toolCall', onToolCall);
4499
4531
  }
4500
- // Remove in-flight tool tracker
4501
- this.events.removeListener('toolCall', onToolCall);
4502
- }
4532
+ });
4503
4533
  };
4504
4534
 
4505
4535
  let aiResult;
@@ -25,7 +25,9 @@ const DEFAULT_RETRYABLE_ERRORS = [
25
25
  'ECONNRESET',
26
26
  'ETIMEDOUT',
27
27
  'ENOTFOUND',
28
- 'api_error'
28
+ 'api_error',
29
+ 'No output generated',
30
+ 'AI_NoOutputGeneratedError'
29
31
  ];
30
32
 
31
33
  /**
@@ -39,6 +41,7 @@ function isRetryableError(error, retryableErrors = DEFAULT_RETRYABLE_ERRORS) {
39
41
 
40
42
  const errorString = error.toString().toLowerCase();
41
43
  const errorMessage = (error.message || '').toLowerCase();
44
+ const errorName = (error.name || error.constructor?.name || '').toLowerCase();
42
45
  const errorCode = (error.code || '').toLowerCase();
43
46
  const errorType = (error.type || '').toLowerCase();
44
47
  const statusCode = error.statusCode || error.status;
@@ -49,6 +52,7 @@ function isRetryableError(error, retryableErrors = DEFAULT_RETRYABLE_ERRORS) {
49
52
 
50
53
  if (errorString.includes(lowerPattern) ||
51
54
  errorMessage.includes(lowerPattern) ||
55
+ errorName.includes(lowerPattern) ||
52
56
  errorCode.includes(lowerPattern) ||
53
57
  errorType.includes(lowerPattern) ||
54
58
  statusCode?.toString() === pattern) {
@@ -106,7 +110,7 @@ export class RetryManager {
106
110
  this.backoffFactor = this._validateNumber(options.backoffFactor, 2, 'backoffFactor', 1, 10);
107
111
  this.retryableErrors = options.retryableErrors || DEFAULT_RETRYABLE_ERRORS;
108
112
  this.debug = options.debug ?? false;
109
- this.jitter = options.jitter ?? true; // Add random jitter by default
113
+ this.jitter = typeof options.jitter === 'boolean' ? options.jitter : true; // Add random jitter by default
110
114
 
111
115
  // Validate that maxDelay >= initialDelay
112
116
  if (this.maxDelay < this.initialDelay) {