@probelabs/probe 0.6.0-rc226 ā 0.6.0-rc227
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/bin/binaries/probe-v0.6.0-rc227-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +24 -0
- package/build/agent/ProbeAgent.js +228 -108
- package/build/agent/engines/enhanced-claude-code.js +72 -3
- package/build/agent/index.js +224 -80
- package/cjs/agent/ProbeAgent.cjs +522 -341
- package/cjs/index.cjs +519 -341
- package/package.json +1 -1
- package/src/agent/ProbeAgent.d.ts +24 -0
- package/src/agent/ProbeAgent.js +228 -108
- package/src/agent/engines/enhanced-claude-code.js +72 -3
- package/bin/binaries/probe-v0.6.0-rc226-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3,6 +3,26 @@ import { EventEmitter } from 'events';
|
|
|
3
3
|
import type { RetryOptions } from './RetryManager';
|
|
4
4
|
import type { FallbackOptions } from './FallbackManager';
|
|
5
5
|
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Timeout Configuration Constants
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default activity timeout for engine streams (3 minutes / 180000ms).
|
|
12
|
+
* This is the time allowed between stream chunks before considering the stream stalled.
|
|
13
|
+
*/
|
|
14
|
+
export const ENGINE_ACTIVITY_TIMEOUT_DEFAULT: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Minimum allowed activity timeout (5 seconds / 5000ms).
|
|
18
|
+
*/
|
|
19
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MIN: number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Maximum allowed activity timeout (10 minutes / 600000ms).
|
|
23
|
+
*/
|
|
24
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MAX: number;
|
|
25
|
+
|
|
6
26
|
/**
|
|
7
27
|
* Configuration options for creating a ProbeAgent instance
|
|
8
28
|
*/
|
|
@@ -80,6 +100,10 @@ export interface ProbeAgentOptions {
|
|
|
80
100
|
completionPrompt?: string;
|
|
81
101
|
/** Enable task management system for tracking multi-step progress */
|
|
82
102
|
enableTasks?: boolean;
|
|
103
|
+
/** Timeout in ms for AI requests (default: 120000 or REQUEST_TIMEOUT env var). Used to abort hung requests. */
|
|
104
|
+
requestTimeout?: number;
|
|
105
|
+
/** Maximum timeout in ms for the entire operation including all retries and fallbacks (default: 300000 or MAX_OPERATION_TIMEOUT env var). This is the absolute maximum time for streamTextWithRetryAndFallback. */
|
|
106
|
+
maxOperationTimeout?: number;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
/**
|
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
dotenv.config();
|
|
6
6
|
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Timeout Configuration Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default activity timeout for engine streams (3 minutes).
|
|
13
|
+
* This is the time allowed between stream chunks before considering the stream stalled.
|
|
14
|
+
* Conservative default to handle extended thinking models that may not stream during thinking.
|
|
15
|
+
*/
|
|
16
|
+
export const ENGINE_ACTIVITY_TIMEOUT_DEFAULT = 180000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimum allowed activity timeout (5 seconds).
|
|
20
|
+
* Prevents unreasonably short timeouts that could cause premature failures.
|
|
21
|
+
*/
|
|
22
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MIN = 5000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maximum allowed activity timeout (10 minutes).
|
|
26
|
+
* Prevents excessively long waits for stalled streams.
|
|
27
|
+
*/
|
|
28
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MAX = 600000;
|
|
29
|
+
|
|
7
30
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
8
31
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
9
32
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
@@ -189,6 +212,8 @@ export class ProbeAgent {
|
|
|
189
212
|
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
190
213
|
* @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
|
|
191
214
|
* @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
|
|
215
|
+
* @param {number} [options.requestTimeout] - Timeout in ms for AI requests (default: 120000 or REQUEST_TIMEOUT env var). Used to abort hung requests.
|
|
216
|
+
* @param {number} [options.maxOperationTimeout] - Maximum timeout in ms for the entire operation including all retries and fallbacks (default: 300000 or MAX_OPERATION_TIMEOUT env var). This is the absolute maximum time for streamTextWithRetryAndFallback.
|
|
192
217
|
*/
|
|
193
218
|
constructor(options = {}) {
|
|
194
219
|
// Basic configuration
|
|
@@ -330,6 +355,41 @@ export class ProbeAgent {
|
|
|
330
355
|
// Each ProbeAgent instance has its own limits, not shared globally
|
|
331
356
|
this.delegationManager = new DelegationManager();
|
|
332
357
|
|
|
358
|
+
// Request timeout configuration (default 2 minutes)
|
|
359
|
+
// Validates env var to prevent NaN or unreasonable values
|
|
360
|
+
this.requestTimeout = options.requestTimeout ?? (() => {
|
|
361
|
+
if (process.env.REQUEST_TIMEOUT) {
|
|
362
|
+
const parsed = parseInt(process.env.REQUEST_TIMEOUT, 10);
|
|
363
|
+
// Validate: must be positive number between 1s and 1 hour
|
|
364
|
+
if (isNaN(parsed) || parsed < 1000 || parsed > 3600000) {
|
|
365
|
+
return 120000; // Default 2 minutes
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
return 120000;
|
|
370
|
+
})();
|
|
371
|
+
if (this.debug) {
|
|
372
|
+
console.log(`[DEBUG] Request timeout: ${this.requestTimeout}ms`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Maximum operation timeout for entire streamTextWithRetryAndFallback operation (default 5 minutes)
|
|
376
|
+
// This is the absolute maximum time including all retries and fallbacks
|
|
377
|
+
// Validates env var to prevent NaN or unreasonable values
|
|
378
|
+
this.maxOperationTimeout = options.maxOperationTimeout ?? (() => {
|
|
379
|
+
if (process.env.MAX_OPERATION_TIMEOUT) {
|
|
380
|
+
const parsed = parseInt(process.env.MAX_OPERATION_TIMEOUT, 10);
|
|
381
|
+
// Validate: must be positive number between 1s and 2 hours
|
|
382
|
+
if (isNaN(parsed) || parsed < 1000 || parsed > 7200000) {
|
|
383
|
+
return 300000; // Default 5 minutes
|
|
384
|
+
}
|
|
385
|
+
return parsed;
|
|
386
|
+
}
|
|
387
|
+
return 300000;
|
|
388
|
+
})();
|
|
389
|
+
if (this.debug) {
|
|
390
|
+
console.log(`[DEBUG] Max operation timeout: ${this.maxOperationTimeout}ms`);
|
|
391
|
+
}
|
|
392
|
+
|
|
333
393
|
// Retry configuration
|
|
334
394
|
this.retryConfig = options.retry || {};
|
|
335
395
|
this.retryManager = null; // Will be initialized lazily when needed
|
|
@@ -1105,120 +1165,128 @@ export class ProbeAgent {
|
|
|
1105
1165
|
}
|
|
1106
1166
|
|
|
1107
1167
|
/**
|
|
1108
|
-
*
|
|
1109
|
-
* @param {
|
|
1110
|
-
* @
|
|
1168
|
+
* Create a streamText-compatible result from an engine stream with timeout handling
|
|
1169
|
+
* @param {AsyncGenerator} engineStream - The engine's query result
|
|
1170
|
+
* @param {AbortSignal} abortSignal - Signal for aborting the operation
|
|
1171
|
+
* @param {number} requestTimeout - Per-request timeout in ms
|
|
1172
|
+
* @param {Object} timeoutState - Object with timeoutId property (mutable for cleanup)
|
|
1173
|
+
* @returns {Object} - streamText-compatible result with textStream
|
|
1111
1174
|
* @private
|
|
1112
1175
|
*/
|
|
1113
|
-
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1176
|
+
_createEngineTextStreamResult(engineStream, abortSignal, requestTimeout, timeoutState) {
|
|
1177
|
+
// Activity timeout for engine stream - validates env var against defined bounds
|
|
1178
|
+
const activityTimeout = (() => {
|
|
1179
|
+
const parsed = parseInt(process.env.ENGINE_ACTIVITY_TIMEOUT, 10);
|
|
1180
|
+
return isNaN(parsed) || parsed < ENGINE_ACTIVITY_TIMEOUT_MIN || parsed > ENGINE_ACTIVITY_TIMEOUT_MAX
|
|
1181
|
+
? ENGINE_ACTIVITY_TIMEOUT_DEFAULT
|
|
1182
|
+
: parsed;
|
|
1183
|
+
})();
|
|
1184
|
+
const startTime = Date.now();
|
|
1185
|
+
|
|
1186
|
+
// Create a text stream that extracts text from engine messages with timeout
|
|
1187
|
+
// The generator clears the operation timeout when done to handle the case
|
|
1188
|
+
// where the stream is returned immediately but consumed later
|
|
1189
|
+
async function* createTextStream() {
|
|
1190
|
+
let lastActivity = Date.now();
|
|
1191
|
+
|
|
1116
1192
|
try {
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
m.role === 'user' &&
|
|
1124
|
-
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1125
|
-
);
|
|
1126
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1127
|
-
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1128
|
-
|
|
1129
|
-
// Pass system message and other options
|
|
1130
|
-
const engineOptions = {
|
|
1131
|
-
maxTokens: options.maxTokens,
|
|
1132
|
-
temperature: options.temperature,
|
|
1133
|
-
messages: options.messages,
|
|
1134
|
-
systemPrompt: options.messages.find(m => m.role === 'system')?.content
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
// Get the engine's query result (async generator)
|
|
1138
|
-
const engineStream = engine.query(prompt, engineOptions);
|
|
1139
|
-
|
|
1140
|
-
// Create a text stream that extracts text from engine messages
|
|
1141
|
-
async function* createTextStream() {
|
|
1142
|
-
for await (const message of engineStream) {
|
|
1143
|
-
if (message.type === 'text' && message.content) {
|
|
1144
|
-
yield message.content;
|
|
1145
|
-
} else if (typeof message === 'string') {
|
|
1146
|
-
// If engine returns plain strings, pass them through
|
|
1147
|
-
yield message;
|
|
1148
|
-
}
|
|
1149
|
-
// Ignore other message types for the text stream
|
|
1150
|
-
}
|
|
1193
|
+
for await (const message of engineStream) {
|
|
1194
|
+
// Check for abort signal
|
|
1195
|
+
if (abortSignal.aborted) {
|
|
1196
|
+
const abortError = new Error('Operation aborted');
|
|
1197
|
+
abortError.name = 'AbortError';
|
|
1198
|
+
throw abortError;
|
|
1151
1199
|
}
|
|
1152
1200
|
|
|
1153
|
-
|
|
1154
|
-
return {
|
|
1155
|
-
textStream: createTextStream(),
|
|
1156
|
-
usage: Promise.resolve({}), // Engine should handle its own usage tracking
|
|
1157
|
-
// Add other streamText-compatible properties as needed
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
if (this.debug) {
|
|
1162
|
-
console.log(`[DEBUG] Failed to use Claude Code engine, falling back to Vercel:`, error.message);
|
|
1163
|
-
}
|
|
1164
|
-
// Fall through to use Vercel engine as fallback
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1201
|
+
const now = Date.now();
|
|
1167
1202
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const engine = await this.getEngine();
|
|
1172
|
-
if (engine && engine.query) {
|
|
1173
|
-
// Convert Vercel AI SDK format to engine format
|
|
1174
|
-
// Extract the ORIGINAL user message as the main prompt (skip any warning messages)
|
|
1175
|
-
// Look for user messages that are NOT the warning message
|
|
1176
|
-
const userMessages = options.messages.filter(m =>
|
|
1177
|
-
m.role === 'user' &&
|
|
1178
|
-
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1179
|
-
);
|
|
1180
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1181
|
-
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1182
|
-
|
|
1183
|
-
// Pass system message and other options
|
|
1184
|
-
const engineOptions = {
|
|
1185
|
-
maxTokens: options.maxTokens,
|
|
1186
|
-
temperature: options.temperature,
|
|
1187
|
-
messages: options.messages,
|
|
1188
|
-
systemPrompt: options.messages.find(m => m.role === 'system')?.content
|
|
1189
|
-
};
|
|
1190
|
-
|
|
1191
|
-
// Get the engine's query result (async generator)
|
|
1192
|
-
const engineStream = engine.query(prompt, engineOptions);
|
|
1193
|
-
|
|
1194
|
-
// Create a text stream that extracts text from engine messages
|
|
1195
|
-
async function* createTextStream() {
|
|
1196
|
-
for await (const message of engineStream) {
|
|
1197
|
-
if (message.type === 'text' && message.content) {
|
|
1198
|
-
yield message.content;
|
|
1199
|
-
} else if (typeof message === 'string') {
|
|
1200
|
-
// If engine returns plain strings, pass them through
|
|
1201
|
-
yield message;
|
|
1202
|
-
}
|
|
1203
|
-
// Ignore other message types for the text stream
|
|
1204
|
-
}
|
|
1203
|
+
// Check for activity timeout (no data received for too long)
|
|
1204
|
+
if (now - lastActivity > activityTimeout) {
|
|
1205
|
+
throw new Error(`Engine stream timeout - no activity for ${activityTimeout}ms`);
|
|
1205
1206
|
}
|
|
1206
1207
|
|
|
1207
|
-
//
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1208
|
+
// Check for overall request timeout
|
|
1209
|
+
if (requestTimeout > 0 && now - startTime > requestTimeout) {
|
|
1210
|
+
throw new Error(`Engine stream timeout - request exceeded ${requestTimeout}ms`);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
lastActivity = now;
|
|
1214
|
+
|
|
1215
|
+
if (message.type === 'text' && message.content) {
|
|
1216
|
+
yield message.content;
|
|
1217
|
+
} else if (typeof message === 'string') {
|
|
1218
|
+
// If engine returns plain strings, pass them through
|
|
1219
|
+
yield message;
|
|
1220
|
+
}
|
|
1221
|
+
// Ignore other message types for the text stream
|
|
1213
1222
|
}
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1223
|
+
} finally {
|
|
1224
|
+
// Clear operation timeout when stream completes (success or error)
|
|
1225
|
+
// This is done here because for engine paths, the stream is returned
|
|
1226
|
+
// immediately but consumed later by the caller
|
|
1227
|
+
if (timeoutState.timeoutId) {
|
|
1228
|
+
clearTimeout(timeoutState.timeoutId);
|
|
1229
|
+
timeoutState.timeoutId = null;
|
|
1217
1230
|
}
|
|
1218
|
-
// Fall through to use Vercel engine as fallback
|
|
1219
1231
|
}
|
|
1220
1232
|
}
|
|
1221
1233
|
|
|
1234
|
+
// Wrap the engine result to match streamText interface
|
|
1235
|
+
// Note: maxOperationTimeout cleanup is handled by the generator's finally block
|
|
1236
|
+
// since the stream is consumed after this function returns.
|
|
1237
|
+
return {
|
|
1238
|
+
textStream: createTextStream(),
|
|
1239
|
+
usage: Promise.resolve({}), // Engine should handle its own usage tracking
|
|
1240
|
+
// Add other streamText-compatible properties as needed
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Try to use an engine (claude-code or codex) for streaming
|
|
1246
|
+
* @param {Object} options - streamText options
|
|
1247
|
+
* @param {AbortController} controller - Abort controller for the operation
|
|
1248
|
+
* @param {Object} timeoutState - Mutable timeout state for cleanup
|
|
1249
|
+
* @returns {Promise<Object|null>} - Stream result or null if engine unavailable
|
|
1250
|
+
* @private
|
|
1251
|
+
*/
|
|
1252
|
+
async _tryEngineStreamPath(options, controller, timeoutState) {
|
|
1253
|
+
const engine = await this.getEngine();
|
|
1254
|
+
if (!engine || !engine.query) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Extract the ORIGINAL user message as the main prompt (skip any warning messages)
|
|
1259
|
+
const userMessages = options.messages.filter(m =>
|
|
1260
|
+
m.role === 'user' &&
|
|
1261
|
+
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1262
|
+
);
|
|
1263
|
+
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1264
|
+
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1265
|
+
|
|
1266
|
+
// Pass system message and other options including abort signal
|
|
1267
|
+
const engineOptions = {
|
|
1268
|
+
maxTokens: options.maxTokens,
|
|
1269
|
+
temperature: options.temperature,
|
|
1270
|
+
messages: options.messages,
|
|
1271
|
+
systemPrompt: options.messages.find(m => m.role === 'system')?.content,
|
|
1272
|
+
abortSignal: controller.signal
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// Get the engine's query result and wrap with timeout handling
|
|
1276
|
+
const engineStream = engine.query(prompt, engineOptions);
|
|
1277
|
+
return this._createEngineTextStreamResult(
|
|
1278
|
+
engineStream, controller.signal, this.requestTimeout, timeoutState
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Execute streamText with Vercel AI SDK using retry/fallback logic
|
|
1284
|
+
* @param {Object} options - streamText options
|
|
1285
|
+
* @param {AbortController} controller - Abort controller for the operation
|
|
1286
|
+
* @returns {Promise<Object>} - Stream result
|
|
1287
|
+
* @private
|
|
1288
|
+
*/
|
|
1289
|
+
async _executeWithVercelProvider(options, controller) {
|
|
1222
1290
|
// Initialize retry manager if not already created
|
|
1223
1291
|
if (!this.retryManager) {
|
|
1224
1292
|
this.retryManager = new RetryManager({
|
|
@@ -1234,10 +1302,11 @@ export class ProbeAgent {
|
|
|
1234
1302
|
// If no fallback manager, just use retry with current provider
|
|
1235
1303
|
if (!this.fallbackManager) {
|
|
1236
1304
|
return await this.retryManager.executeWithRetry(
|
|
1237
|
-
() => streamText(options),
|
|
1305
|
+
() => streamText({ ...options, abortSignal: controller.signal }),
|
|
1238
1306
|
{
|
|
1239
1307
|
provider: this.apiType,
|
|
1240
|
-
model: this.model
|
|
1308
|
+
model: this.model,
|
|
1309
|
+
signal: controller.signal
|
|
1241
1310
|
}
|
|
1242
1311
|
);
|
|
1243
1312
|
}
|
|
@@ -1245,13 +1314,12 @@ export class ProbeAgent {
|
|
|
1245
1314
|
// Use fallback manager with retry for each provider
|
|
1246
1315
|
return await this.fallbackManager.executeWithFallback(
|
|
1247
1316
|
async (provider, model, config) => {
|
|
1248
|
-
// Create options with the fallback provider
|
|
1249
1317
|
const fallbackOptions = {
|
|
1250
1318
|
...options,
|
|
1251
|
-
model: provider(model)
|
|
1319
|
+
model: provider(model),
|
|
1320
|
+
abortSignal: controller.signal
|
|
1252
1321
|
};
|
|
1253
1322
|
|
|
1254
|
-
// Create a retry manager for this specific provider
|
|
1255
1323
|
const providerRetryManager = new RetryManager({
|
|
1256
1324
|
maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
|
|
1257
1325
|
initialDelay: this.retryConfig.initialDelay ?? 1000,
|
|
@@ -1261,18 +1329,70 @@ export class ProbeAgent {
|
|
|
1261
1329
|
debug: this.debug
|
|
1262
1330
|
});
|
|
1263
1331
|
|
|
1264
|
-
// Execute with retry for this provider
|
|
1265
1332
|
return await providerRetryManager.executeWithRetry(
|
|
1266
1333
|
() => streamText(fallbackOptions),
|
|
1267
1334
|
{
|
|
1268
1335
|
provider: config.provider,
|
|
1269
|
-
model: model
|
|
1336
|
+
model: model,
|
|
1337
|
+
signal: controller.signal
|
|
1270
1338
|
}
|
|
1271
1339
|
);
|
|
1272
1340
|
}
|
|
1273
1341
|
);
|
|
1274
1342
|
}
|
|
1275
1343
|
|
|
1344
|
+
/**
|
|
1345
|
+
* Execute streamText with retry and fallback support
|
|
1346
|
+
* @param {Object} options - streamText options
|
|
1347
|
+
* @returns {Promise<Object>} - streamText result
|
|
1348
|
+
* @private
|
|
1349
|
+
*/
|
|
1350
|
+
async streamTextWithRetryAndFallback(options) {
|
|
1351
|
+
// Create AbortController for overall operation timeout
|
|
1352
|
+
const controller = new AbortController();
|
|
1353
|
+
const timeoutState = { timeoutId: null };
|
|
1354
|
+
|
|
1355
|
+
// Set up overall operation timeout (default 5 minutes)
|
|
1356
|
+
if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
|
|
1357
|
+
timeoutState.timeoutId = setTimeout(() => {
|
|
1358
|
+
controller.abort();
|
|
1359
|
+
if (this.debug) {
|
|
1360
|
+
console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
|
|
1361
|
+
}
|
|
1362
|
+
}, this.maxOperationTimeout);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
// Try engine paths (claude-code or codex)
|
|
1367
|
+
const useClaudeCode = this.clientApiProvider === 'claude-code' || process.env.USE_CLAUDE_CODE === 'true';
|
|
1368
|
+
const useCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
|
|
1369
|
+
|
|
1370
|
+
if (useClaudeCode || useCodex) {
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await this._tryEngineStreamPath(options, controller, timeoutState);
|
|
1373
|
+
if (result) {
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
if (this.debug) {
|
|
1378
|
+
const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
|
|
1379
|
+
console.log(`[DEBUG] Failed to use ${engineType} engine, falling back to Vercel:`, error.message);
|
|
1380
|
+
}
|
|
1381
|
+
// Fall through to Vercel provider
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Use Vercel AI SDK with retry/fallback
|
|
1386
|
+
return await this._executeWithVercelProvider(options, controller);
|
|
1387
|
+
} finally {
|
|
1388
|
+
// Clean up timeout (for non-engine paths; engine paths clean up in the generator)
|
|
1389
|
+
if (timeoutState.timeoutId) {
|
|
1390
|
+
clearTimeout(timeoutState.timeoutId);
|
|
1391
|
+
timeoutState.timeoutId = null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1276
1396
|
/**
|
|
1277
1397
|
* Initialize Anthropic model
|
|
1278
1398
|
*/
|
|
@@ -15,7 +15,7 @@ import { Session } from '../shared/Session.js';
|
|
|
15
15
|
* Enhanced Claude Code Engine
|
|
16
16
|
*/
|
|
17
17
|
export async function createEnhancedClaudeCLIEngine(options = {}) {
|
|
18
|
-
const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools } = options;
|
|
18
|
+
const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools, timeout = 120000 } = options;
|
|
19
19
|
|
|
20
20
|
// Create or reuse session
|
|
21
21
|
const session = new Session(
|
|
@@ -154,6 +154,37 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
|
|
|
154
154
|
stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin since echo handles it
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
// Subprocess timeout handling
|
|
158
|
+
let killed = false;
|
|
159
|
+
let timeoutHandle;
|
|
160
|
+
let sigkillHandle;
|
|
161
|
+
|
|
162
|
+
if (timeout > 0) {
|
|
163
|
+
timeoutHandle = setTimeout(() => {
|
|
164
|
+
if (!killed) {
|
|
165
|
+
killed = true;
|
|
166
|
+
processEnded = true;
|
|
167
|
+
proc.kill('SIGTERM');
|
|
168
|
+
|
|
169
|
+
if (debug) {
|
|
170
|
+
console.log(`[DEBUG] Process timed out after ${timeout}ms, sending SIGTERM`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Force kill after 5 seconds if still running
|
|
174
|
+
sigkillHandle = setTimeout(() => {
|
|
175
|
+
if (proc.exitCode === null) {
|
|
176
|
+
proc.kill('SIGKILL');
|
|
177
|
+
if (debug) {
|
|
178
|
+
console.log('[DEBUG] Process did not exit, sending SIGKILL');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, 5000);
|
|
182
|
+
|
|
183
|
+
emitter.emit('error', new Error(`Claude CLI process timed out after ${timeout}ms`));
|
|
184
|
+
}
|
|
185
|
+
}, timeout);
|
|
186
|
+
}
|
|
187
|
+
|
|
157
188
|
// Handle stdout
|
|
158
189
|
proc.stdout.on('data', (data) => {
|
|
159
190
|
buffer += data.toString();
|
|
@@ -179,11 +210,25 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
|
|
|
179
210
|
|
|
180
211
|
// Handle process end
|
|
181
212
|
proc.on('close', (code) => {
|
|
213
|
+
// Clear the timeouts to prevent memory leaks
|
|
214
|
+
if (timeoutHandle) {
|
|
215
|
+
clearTimeout(timeoutHandle);
|
|
216
|
+
}
|
|
217
|
+
if (sigkillHandle) {
|
|
218
|
+
clearTimeout(sigkillHandle);
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
processEnded = true;
|
|
183
222
|
if (code !== 0 && debug) {
|
|
184
223
|
console.log(`[DEBUG] Process exited with code ${code}`);
|
|
185
224
|
}
|
|
186
225
|
|
|
226
|
+
// If killed by timeout, the error was already emitted
|
|
227
|
+
if (killed) {
|
|
228
|
+
emitter.emit('end');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
187
232
|
// Process any remaining buffer
|
|
188
233
|
if (buffer.trim()) {
|
|
189
234
|
processJsonBuffer(buffer, emitter, session, debug, toolCollector);
|
|
@@ -205,6 +250,14 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
|
|
|
205
250
|
});
|
|
206
251
|
|
|
207
252
|
proc.on('error', (error) => {
|
|
253
|
+
// Clear the timeouts to prevent memory leaks
|
|
254
|
+
if (timeoutHandle) {
|
|
255
|
+
clearTimeout(timeoutHandle);
|
|
256
|
+
}
|
|
257
|
+
if (sigkillHandle) {
|
|
258
|
+
clearTimeout(sigkillHandle);
|
|
259
|
+
}
|
|
260
|
+
processEnded = true;
|
|
208
261
|
emitter.emit('error', error);
|
|
209
262
|
});
|
|
210
263
|
|
|
@@ -259,8 +312,24 @@ export async function createEnhancedClaudeCLIEngine(options = {}) {
|
|
|
259
312
|
content: `\nš§ Using ${msg.name}: ${JSON.stringify(msg.input)}\n`
|
|
260
313
|
};
|
|
261
314
|
|
|
262
|
-
// Execute tool
|
|
263
|
-
const
|
|
315
|
+
// Execute tool with timeout to prevent indefinite blocking
|
|
316
|
+
const toolTimeout = 30000; // 30 seconds
|
|
317
|
+
let toolTimeoutId;
|
|
318
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
319
|
+
toolTimeoutId = setTimeout(() => reject(new Error(`Tool ${msg.name} timed out after ${toolTimeout}ms`)), toolTimeout);
|
|
320
|
+
});
|
|
321
|
+
let result;
|
|
322
|
+
try {
|
|
323
|
+
result = await Promise.race([
|
|
324
|
+
executeProbleTool(agent, msg.name, msg.input),
|
|
325
|
+
timeoutPromise
|
|
326
|
+
]);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
result = `Tool error: ${error.message}`;
|
|
329
|
+
} finally {
|
|
330
|
+
// Always clear timeout to prevent memory leaks
|
|
331
|
+
clearTimeout(toolTimeoutId);
|
|
332
|
+
}
|
|
264
333
|
yield { type: 'text', content: `${result}\n` };
|
|
265
334
|
} else if (msg.type === 'toolBatch') {
|
|
266
335
|
// Pass through the tool batch for ProbeAgent to emit
|