@probelabs/probe 0.6.0-rc278 → 0.6.0-rc280
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-rc280-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc280-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc280-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc280-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc280-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +114 -28
- package/build/agent/dsl/environment.js +1 -0
- package/build/agent/index.js +248 -87
- package/build/delegate.js +62 -23
- package/build/downloader.js +28 -25
- package/build/tools/analyzeAll.js +10 -10
- package/build/tools/common.js +4 -3
- package/build/tools/vercel.js +72 -10
- package/cjs/agent/ProbeAgent.cjs +251 -88
- package/cjs/index.cjs +248 -87
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +114 -28
- package/src/agent/dsl/environment.js +1 -0
- package/src/delegate.js +62 -23
- package/src/downloader.js +28 -25
- package/src/tools/analyzeAll.js +10 -10
- package/src/tools/common.js +4 -3
- package/src/tools/vercel.js +72 -10
- package/bin/binaries/probe-v0.6.0-rc278-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc278-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc278-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc278-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc278-x86_64-unknown-linux-musl.tar.gz +0 -0
package/build/delegate.js
CHANGED
|
@@ -122,20 +122,20 @@ class DelegationManager {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// Need to wait in queue
|
|
125
|
-
|
|
126
|
-
console.error(`[DelegationManager] Slot unavailable (${this.globalActive}/${this.maxConcurrent}), queuing... (queue size: ${this.waitQueue.length}, timeout: ${effectiveTimeout}ms)`);
|
|
127
|
-
}
|
|
125
|
+
console.error(`[DelegationManager] Slot unavailable (${this.globalActive}/${this.maxConcurrent}), queuing... (queue size: ${this.waitQueue.length + 1}, timeout: ${effectiveTimeout}ms)`);
|
|
128
126
|
|
|
129
127
|
// Create a promise that will be resolved when a slot becomes available
|
|
130
128
|
// or rejected if session limit is exceeded or queue timeout expires
|
|
131
129
|
return new Promise((resolve, reject) => {
|
|
130
|
+
const queuedAt = Date.now();
|
|
132
131
|
const entry = {
|
|
133
132
|
resolve: null, // Will be wrapped below
|
|
134
133
|
reject: null, // Will be wrapped below
|
|
135
134
|
parentSessionId,
|
|
136
135
|
debug,
|
|
137
|
-
queuedAt
|
|
138
|
-
timeoutId: null
|
|
136
|
+
queuedAt,
|
|
137
|
+
timeoutId: null,
|
|
138
|
+
reminderId: null
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
// Wrap resolve/reject to clear timeout and prevent double-settling
|
|
@@ -144,12 +144,14 @@ class DelegationManager {
|
|
|
144
144
|
if (settled) return;
|
|
145
145
|
settled = true;
|
|
146
146
|
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
147
|
+
if (entry.reminderId) clearInterval(entry.reminderId);
|
|
147
148
|
resolve(value);
|
|
148
149
|
};
|
|
149
150
|
entry.reject = (error) => {
|
|
150
151
|
if (settled) return;
|
|
151
152
|
settled = true;
|
|
152
153
|
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
154
|
+
if (entry.reminderId) clearInterval(entry.reminderId);
|
|
153
155
|
reject(error);
|
|
154
156
|
};
|
|
155
157
|
|
|
@@ -165,6 +167,15 @@ class DelegationManager {
|
|
|
165
167
|
}, effectiveTimeout);
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
// Always emit periodic wait visibility while queued.
|
|
171
|
+
entry.reminderId = setInterval(() => {
|
|
172
|
+
const waitedSeconds = Math.round((Date.now() - queuedAt) / 1000);
|
|
173
|
+
console.error(`[DelegationManager] Still waiting for slot (${waitedSeconds}s). ${this.globalActive}/${this.maxConcurrent} active, ${this.waitQueue.length} queued.`);
|
|
174
|
+
}, 15000);
|
|
175
|
+
if (entry.reminderId.unref) {
|
|
176
|
+
entry.reminderId.unref();
|
|
177
|
+
}
|
|
178
|
+
|
|
168
179
|
this.waitQueue.push(entry);
|
|
169
180
|
});
|
|
170
181
|
}
|
|
@@ -221,9 +232,7 @@ class DelegationManager {
|
|
|
221
232
|
if (sessionCount >= this.maxPerSession) {
|
|
222
233
|
// Session limit reached - reject with error (consistent with tryAcquire behavior)
|
|
223
234
|
// This is a hard limit, not something that will resolve by waiting longer
|
|
224
|
-
|
|
225
|
-
console.error(`[DelegationManager] Session limit (${this.maxPerSession}) reached for queued item, rejecting`);
|
|
226
|
-
}
|
|
235
|
+
console.error(`[DelegationManager] Session limit (${this.maxPerSession}) reached for queued item, rejecting`);
|
|
227
236
|
toReject.push({ reject, error: new Error(`Maximum delegations per session (${this.maxPerSession}) reached for session ${parentSessionId}`) });
|
|
228
237
|
// Continue to process next item in queue
|
|
229
238
|
continue;
|
|
@@ -233,10 +242,8 @@ class DelegationManager {
|
|
|
233
242
|
// Grant the slot
|
|
234
243
|
this._incrementCounters(parentSessionId);
|
|
235
244
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
console.error(`[DelegationManager] Granted slot from queue (waited ${waitTime}ms). Active: ${this.globalActive}/${this.maxConcurrent}`);
|
|
239
|
-
}
|
|
245
|
+
const waitTime = Date.now() - queuedAt;
|
|
246
|
+
console.error(`[DelegationManager] Granted slot from queue (waited ${waitTime}ms). Active: ${this.globalActive}/${this.maxConcurrent}`);
|
|
240
247
|
|
|
241
248
|
toResolve.push(resolve);
|
|
242
249
|
}
|
|
@@ -296,6 +303,9 @@ class DelegationManager {
|
|
|
296
303
|
if (entry.timeoutId) {
|
|
297
304
|
clearTimeout(entry.timeoutId);
|
|
298
305
|
}
|
|
306
|
+
if (entry.reminderId) {
|
|
307
|
+
clearInterval(entry.reminderId);
|
|
308
|
+
}
|
|
299
309
|
// Reject pending entries so they don't hang
|
|
300
310
|
if (entry.reject) {
|
|
301
311
|
entry.reject(new Error('DelegationManager was cleaned up'));
|
|
@@ -386,12 +396,18 @@ export async function delegate({
|
|
|
386
396
|
mcpConfig = null,
|
|
387
397
|
mcpConfigPath = null,
|
|
388
398
|
delegationManager = null, // Optional per-instance manager, falls back to default singleton
|
|
389
|
-
concurrencyLimiter = null // Optional global AI concurrency limiter
|
|
399
|
+
concurrencyLimiter = null, // Optional global AI concurrency limiter
|
|
400
|
+
parentAbortSignal = null // Optional AbortSignal from parent to cancel this delegation
|
|
390
401
|
}) {
|
|
391
402
|
if (!task || typeof task !== 'string') {
|
|
392
403
|
throw new Error('Task parameter is required and must be a string');
|
|
393
404
|
}
|
|
394
405
|
|
|
406
|
+
// Check if parent has already been cancelled
|
|
407
|
+
if (parentAbortSignal?.aborted) {
|
|
408
|
+
throw new Error('Delegation cancelled: parent operation was aborted');
|
|
409
|
+
}
|
|
410
|
+
|
|
395
411
|
// Support runtime timeout override via environment variables when timeout not explicitly passed
|
|
396
412
|
// This allows operators to configure delegation timeouts without code changes
|
|
397
413
|
// Priority: DELEGATION_TIMEOUT_MS (milliseconds) > DELEGATION_TIMEOUT_SECONDS > DELEGATION_TIMEOUT (seconds)
|
|
@@ -481,24 +497,47 @@ export async function delegate({
|
|
|
481
497
|
console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
|
|
482
498
|
}
|
|
483
499
|
|
|
484
|
-
// Set up timeout
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
// This is acceptable since:
|
|
488
|
-
// 1. The promise will eventually resolve/reject and be garbage collected
|
|
489
|
-
// 2. The delegation slot is properly released on timeout
|
|
490
|
-
// 3. The parent receives timeout error and can handle it
|
|
491
|
-
// Future improvement: Add signal parameter to ProbeAgent.answer(task, [], { signal })
|
|
500
|
+
// Set up timeout and parent abort handling.
|
|
501
|
+
// When timeout fires or parent aborts, we cancel the subagent so it
|
|
502
|
+
// stops making API calls and releases resources promptly.
|
|
492
503
|
const timeoutPromise = new Promise((_, reject) => {
|
|
493
504
|
timeoutId = setTimeout(() => {
|
|
505
|
+
subagent.cancel();
|
|
494
506
|
reject(new Error(`Delegation timed out after ${timeout} seconds`));
|
|
495
507
|
}, timeout * 1000);
|
|
496
508
|
});
|
|
497
509
|
|
|
498
|
-
//
|
|
510
|
+
// Listen for parent abort signal
|
|
511
|
+
let parentAbortHandler;
|
|
512
|
+
const parentAbortPromise = new Promise((_, reject) => {
|
|
513
|
+
if (parentAbortSignal) {
|
|
514
|
+
if (parentAbortSignal.aborted) {
|
|
515
|
+
subagent.cancel();
|
|
516
|
+
reject(new Error('Delegation cancelled: parent operation was aborted'));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
parentAbortHandler = () => {
|
|
520
|
+
subagent.cancel();
|
|
521
|
+
reject(new Error('Delegation cancelled: parent operation was aborted'));
|
|
522
|
+
};
|
|
523
|
+
parentAbortSignal.addEventListener('abort', parentAbortHandler, { once: true });
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Execute the task with timeout and parent abort
|
|
499
528
|
const answerOptions = schema ? { schema } : undefined;
|
|
500
529
|
const answerPromise = answerOptions ? subagent.answer(task, [], answerOptions) : subagent.answer(task);
|
|
501
|
-
const
|
|
530
|
+
const racers = [answerPromise, timeoutPromise];
|
|
531
|
+
if (parentAbortSignal) racers.push(parentAbortPromise);
|
|
532
|
+
let response;
|
|
533
|
+
try {
|
|
534
|
+
response = await Promise.race(racers);
|
|
535
|
+
} finally {
|
|
536
|
+
// Clean up parent abort listener to prevent memory leaks
|
|
537
|
+
if (parentAbortHandler && parentAbortSignal) {
|
|
538
|
+
parentAbortSignal.removeEventListener('abort', parentAbortHandler);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
502
541
|
|
|
503
542
|
// Clear timeout immediately after race completes to prevent memory leak
|
|
504
543
|
// Note: timeoutId is always set by this point (synchronous in Promise constructor)
|
package/build/downloader.js
CHANGED
|
@@ -95,9 +95,7 @@ async function acquireFileLock(lockPath, version) {
|
|
|
95
95
|
try {
|
|
96
96
|
// Try to create lock file atomically (fails if already exists)
|
|
97
97
|
await fs.writeFile(lockPath, JSON.stringify(lockData), { flag: 'wx' });
|
|
98
|
-
|
|
99
|
-
console.log(`Acquired file lock: ${lockPath}`);
|
|
100
|
-
}
|
|
98
|
+
console.log(`Acquired file lock: ${lockPath}`);
|
|
101
99
|
return true;
|
|
102
100
|
} catch (error) {
|
|
103
101
|
if (error.code === 'EEXIST') {
|
|
@@ -108,17 +106,13 @@ async function acquireFileLock(lockPath, version) {
|
|
|
108
106
|
|
|
109
107
|
if (lockAge > LOCK_TIMEOUT_MS) {
|
|
110
108
|
// Lock is stale, remove it
|
|
111
|
-
|
|
112
|
-
console.log(`Removing stale lock file (age: ${Math.round(lockAge / 1000)}s, pid: ${existingLock.pid})`);
|
|
113
|
-
}
|
|
109
|
+
console.log(`Removing stale lock file (age: ${Math.round(lockAge / 1000)}s, pid: ${existingLock.pid})`);
|
|
114
110
|
await fs.remove(lockPath);
|
|
115
111
|
return false; // Caller should retry
|
|
116
112
|
}
|
|
117
113
|
|
|
118
114
|
// Lock is fresh, another process is downloading
|
|
119
|
-
|
|
120
|
-
console.log(`Download in progress by process ${existingLock.pid}, waiting...`);
|
|
121
|
-
}
|
|
115
|
+
console.log(`Download in progress by process ${existingLock.pid}, waiting...`);
|
|
122
116
|
return false;
|
|
123
117
|
} catch (readError) {
|
|
124
118
|
// Can't read lock file, might be corrupted - remove it
|
|
@@ -180,23 +174,23 @@ async function releaseFileLock(lockPath) {
|
|
|
180
174
|
*/
|
|
181
175
|
async function waitForFileLock(lockPath, binaryPath) {
|
|
182
176
|
const startTime = Date.now();
|
|
177
|
+
let lastStatusTime = startTime;
|
|
178
|
+
|
|
179
|
+
console.log(`Waiting for file lock to clear: ${lockPath}`);
|
|
183
180
|
|
|
184
181
|
// Poll in a loop until binary appears, lock expires, or we timeout
|
|
185
182
|
while (Date.now() - startTime < MAX_LOCK_WAIT_MS) {
|
|
186
183
|
// Check #1: Is the binary now available?
|
|
187
184
|
if (await fs.pathExists(binaryPath)) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
185
|
+
const waitedSeconds = Math.round((Date.now() - startTime) / 1000);
|
|
186
|
+
console.log(`Binary now available at ${binaryPath}, download completed by another process (waited ${waitedSeconds}s)`);
|
|
191
187
|
return true;
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
// Check #2: Is the lock file gone? (download finished or failed)
|
|
195
191
|
const lockExists = await fs.pathExists(lockPath);
|
|
196
192
|
if (!lockExists) {
|
|
197
|
-
|
|
198
|
-
console.log(`Lock file removed but binary not found - download may have failed`);
|
|
199
|
-
}
|
|
193
|
+
console.log(`Lock file removed but binary not found - download may have failed`);
|
|
200
194
|
return false;
|
|
201
195
|
}
|
|
202
196
|
|
|
@@ -205,22 +199,24 @@ async function waitForFileLock(lockPath, binaryPath) {
|
|
|
205
199
|
const lockData = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
|
|
206
200
|
const lockAge = Date.now() - lockData.timestamp;
|
|
207
201
|
if (lockAge > LOCK_TIMEOUT_MS) {
|
|
208
|
-
|
|
209
|
-
console.log(`Lock expired (age: ${Math.round(lockAge / 1000)}s), will retry download`);
|
|
210
|
-
}
|
|
202
|
+
console.log(`Lock expired (age: ${Math.round(lockAge / 1000)}s), will retry download`);
|
|
211
203
|
return false;
|
|
212
204
|
}
|
|
213
205
|
} catch {
|
|
214
206
|
// Ignore errors reading lock file - will retry on next poll
|
|
215
207
|
}
|
|
216
208
|
|
|
209
|
+
if (Date.now() - lastStatusTime >= 15000) {
|
|
210
|
+
const elapsedSeconds = Math.round((Date.now() - startTime) / 1000);
|
|
211
|
+
console.log(`Still waiting for file lock (${elapsedSeconds}s/${MAX_LOCK_WAIT_MS / 1000}s max)`);
|
|
212
|
+
lastStatusTime = Date.now();
|
|
213
|
+
}
|
|
214
|
+
|
|
217
215
|
// Wait 1 second before checking again
|
|
218
216
|
await new Promise(resolve => setTimeout(resolve, LOCK_POLL_INTERVAL_MS));
|
|
219
217
|
}
|
|
220
218
|
|
|
221
|
-
|
|
222
|
-
console.log(`Timeout waiting for file lock`);
|
|
223
|
-
}
|
|
219
|
+
console.log(`Timeout waiting for file lock after ${MAX_LOCK_WAIT_MS / 1000}s`);
|
|
224
220
|
return false;
|
|
225
221
|
}
|
|
226
222
|
|
|
@@ -247,9 +243,7 @@ async function withDownloadLock(version, downloadFn) {
|
|
|
247
243
|
}
|
|
248
244
|
downloadLocks.delete(lockKey);
|
|
249
245
|
} else {
|
|
250
|
-
|
|
251
|
-
console.log(`Download already in progress in this process for version ${lockKey}, waiting...`);
|
|
252
|
-
}
|
|
246
|
+
console.log(`Download already in progress in this process for version ${lockKey}, waiting...`);
|
|
253
247
|
try {
|
|
254
248
|
return await lock.promise;
|
|
255
249
|
} catch (error) {
|
|
@@ -262,10 +256,16 @@ async function withDownloadLock(version, downloadFn) {
|
|
|
262
256
|
}
|
|
263
257
|
|
|
264
258
|
// Create new download promise with timeout protection
|
|
259
|
+
let timeoutId = null;
|
|
265
260
|
const downloadPromise = Promise.race([
|
|
266
261
|
downloadFn(),
|
|
267
262
|
new Promise((_, reject) =>
|
|
268
|
-
|
|
263
|
+
{
|
|
264
|
+
timeoutId = setTimeout(() => reject(new Error(`Download timeout after ${LOCK_TIMEOUT_MS / 1000}s`)), LOCK_TIMEOUT_MS);
|
|
265
|
+
if (timeoutId.unref) {
|
|
266
|
+
timeoutId.unref();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
269
|
)
|
|
270
270
|
]);
|
|
271
271
|
|
|
@@ -278,6 +278,9 @@ async function withDownloadLock(version, downloadFn) {
|
|
|
278
278
|
const result = await downloadPromise;
|
|
279
279
|
return result;
|
|
280
280
|
} finally {
|
|
281
|
+
if (timeoutId) {
|
|
282
|
+
clearTimeout(timeoutId);
|
|
283
|
+
}
|
|
281
284
|
// Clean up lock after download completes (success or failure)
|
|
282
285
|
downloadLocks.delete(lockKey);
|
|
283
286
|
}
|
|
@@ -192,7 +192,8 @@ Instructions:
|
|
|
192
192
|
promptType: 'code-researcher',
|
|
193
193
|
allowedTools: ['extract'],
|
|
194
194
|
maxIterations: 5,
|
|
195
|
-
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
195
|
+
delegationManager: options.delegationManager, // Per-instance delegation limits
|
|
196
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
196
197
|
// timeout removed - inherit default from delegate (300s)
|
|
197
198
|
});
|
|
198
199
|
|
|
@@ -226,18 +227,14 @@ async function processChunksParallel(chunks, extractionPrompt, maxWorkers, optio
|
|
|
226
227
|
|
|
227
228
|
active.add(promise);
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
console.error(`[analyze_all] Started processing chunk ${chunk.id}/${chunk.total}`);
|
|
231
|
-
}
|
|
230
|
+
console.error(`[analyze_all] Started processing chunk ${chunk.id}/${chunk.total}`);
|
|
232
231
|
}
|
|
233
232
|
|
|
234
233
|
if (active.size > 0) {
|
|
235
234
|
const result = await Promise.race(active);
|
|
236
235
|
results.push(result);
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
console.error(`[analyze_all] Completed chunk ${result.chunk.id}/${result.chunk.total}`);
|
|
240
|
-
}
|
|
237
|
+
console.error(`[analyze_all] Completed chunk ${result.chunk.id}/${result.chunk.total}`);
|
|
241
238
|
}
|
|
242
239
|
}
|
|
243
240
|
|
|
@@ -328,7 +325,8 @@ Organize all findings into clear categories with items listed under each.${compl
|
|
|
328
325
|
promptType: 'code-researcher',
|
|
329
326
|
allowedTools: [],
|
|
330
327
|
maxIterations: 5,
|
|
331
|
-
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
328
|
+
delegationManager: options.delegationManager, // Per-instance delegation limits
|
|
329
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
332
330
|
// timeout removed - inherit default from delegate (300s)
|
|
333
331
|
});
|
|
334
332
|
|
|
@@ -404,7 +402,8 @@ CRITICAL: Do NOT guess keywords. Actually run searches and see what returns resu
|
|
|
404
402
|
promptType: 'code-researcher',
|
|
405
403
|
// Full tool access for exploration and experimentation
|
|
406
404
|
maxIterations: 15,
|
|
407
|
-
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
405
|
+
delegationManager: options.delegationManager, // Per-instance delegation limits
|
|
406
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
408
407
|
// timeout removed - inherit default from delegate (300s)
|
|
409
408
|
});
|
|
410
409
|
|
|
@@ -475,7 +474,8 @@ When done, use the attempt_completion tool with your answer as the result.`;
|
|
|
475
474
|
promptType: 'code-researcher',
|
|
476
475
|
allowedTools: [],
|
|
477
476
|
maxIterations: 5,
|
|
478
|
-
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
477
|
+
delegationManager: options.delegationManager, // Per-instance delegation limits
|
|
478
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
479
479
|
// timeout removed - inherit default from delegate (300s)
|
|
480
480
|
});
|
|
481
481
|
|
package/build/tools/common.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolve, isAbsolute } from 'path';
|
|
|
8
8
|
|
|
9
9
|
// Common schemas for tool parameters (used for internal execution after XML parsing)
|
|
10
10
|
export const searchSchema = z.object({
|
|
11
|
-
query: z.string().describe('Search query
|
|
11
|
+
query: z.string().describe('Search query — natural language questions or Elasticsearch-style keywords both work. For keywords: use quotes for exact phrases, AND/OR for boolean logic, - for negation. Probe handles stemming and camelCase/snake_case splitting automatically, so do NOT try case or style variations of the same keyword.'),
|
|
12
12
|
path: z.string().optional().default('.').describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.'),
|
|
13
13
|
exact: z.boolean().optional().default(false).describe('Default (false) enables stemming and keyword splitting for exploratory search - "getUserData" matches "get", "user", "data", etc. Set true for precise symbol lookup where "getUserData" matches only "getUserData". Use true when you know the exact symbol name.'),
|
|
14
14
|
maxTokens: z.number().nullable().optional().describe('Maximum tokens to return. Default is 20000. Set to null for unlimited results.'),
|
|
@@ -17,7 +17,7 @@ export const searchSchema = z.object({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
export const searchAllSchema = z.object({
|
|
20
|
-
query: z.string().describe('Search query
|
|
20
|
+
query: z.string().describe('Search query — natural language questions or Elasticsearch-style keywords both work. For keywords: use quotes for exact phrases, AND/OR for boolean logic, - for negation. Probe handles stemming and camelCase/snake_case splitting automatically, so do NOT try case or style variations of the same keyword.'),
|
|
21
21
|
path: z.string().optional().default('.').describe('Path to search in.'),
|
|
22
22
|
exact: z.boolean().optional().default(false).describe('Use exact matching instead of stemming.'),
|
|
23
23
|
maxTokensPerPage: z.number().optional().default(20000).describe('Tokens per page when paginating. Default 20000.'),
|
|
@@ -149,7 +149,8 @@ export const attemptCompletionSchema = {
|
|
|
149
149
|
|
|
150
150
|
// Tool descriptions (used by Vercel tool() definitions)
|
|
151
151
|
|
|
152
|
-
export const searchDescription = 'Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions.';
|
|
152
|
+
export const searchDescription = 'Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions. NOTE: By default, search handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically — do NOT manually try keyword variations like "getAllUsers" then "get_all_users" then "GetAllUsers". One search covers all variations.';
|
|
153
|
+
export const searchDelegateDescription = 'Search code in the repository by asking a question. Accepts natural language questions (e.g., "How does authentication work?", "Where is the user validation logic?"). A specialized subagent breaks down your question into targeted keyword searches and returns extracted code blocks. Do NOT formulate keyword queries yourself — just ask the question naturally.';
|
|
153
154
|
export const queryDescription = 'Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.';
|
|
154
155
|
export const extractDescription = 'Extract code blocks from files based on file paths and optional line numbers. Use this tool to see complete context after finding relevant files. Line numbers from output can be used with edit start_line/end_line for precise editing.';
|
|
155
156
|
export const delegateDescription = 'Automatically delegate big distinct tasks to specialized probe subagents within the agentic loop. Used by AI agents to break down complex requests into focused, parallel tasks.';
|
package/build/tools/vercel.js
CHANGED
|
@@ -9,7 +9,7 @@ import { query } from '../query.js';
|
|
|
9
9
|
import { extract } from '../extract.js';
|
|
10
10
|
import { delegate } from '../delegate.js';
|
|
11
11
|
import { analyzeAll } from './analyzeAll.js';
|
|
12
|
-
import { searchSchema, querySchema, extractSchema, delegateSchema, analyzeAllSchema, searchDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, parseTargets, parseAndResolvePaths, resolveTargetPath } from './common.js';
|
|
12
|
+
import { searchSchema, querySchema, extractSchema, delegateSchema, analyzeAllSchema, searchDescription, searchDelegateDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, parseTargets, parseAndResolvePaths, resolveTargetPath } from './common.js';
|
|
13
13
|
import { existsSync } from 'fs';
|
|
14
14
|
import { formatErrorForAI } from '../utils/error-types.js';
|
|
15
15
|
import { annotateOutputWithHashes } from './hashline.js';
|
|
@@ -143,11 +143,41 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
|
|
|
143
143
|
'- extract: Verify code snippets to ensure targets are actually relevant before including them.',
|
|
144
144
|
'- listFiles: Understand directory structure to find where relevant code might live.',
|
|
145
145
|
'',
|
|
146
|
-
'
|
|
146
|
+
'CRITICAL - How probe search works (do NOT ignore):',
|
|
147
|
+
'- By default (exact=false), probe ALREADY handles stemming, case-insensitive matching, and camelCase/snake_case splitting.',
|
|
148
|
+
'- Searching "allowed_ips" ALREADY matches "AllowedIPs", "allowedIps", "allowed_ips", etc. Do NOT manually try case/style variations.',
|
|
149
|
+
'- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
|
|
150
|
+
'- NEVER repeat the same search query — you will get the same results.',
|
|
151
|
+
'- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful — probe handles it.',
|
|
152
|
+
'- If a search returns no results, the term likely does not exist in that path. Try a genuinely DIFFERENT keyword or concept, not a variation.',
|
|
153
|
+
'- If 2-3 consecutive searches return no results for a concept, STOP searching for it and move on.',
|
|
154
|
+
'',
|
|
155
|
+
'GOOD search strategy (do this):',
|
|
156
|
+
' Query: "How does authentication work and how are sessions managed?"',
|
|
157
|
+
' → search "authentication" → search "session management" (two different concepts)',
|
|
158
|
+
' Query: "Find the IP allowlist middleware"',
|
|
159
|
+
' → search "allowlist middleware" (one search, probe handles IP/ip/Ip variations)',
|
|
160
|
+
' Query: "How does BM25 scoring work with SIMD optimization?"',
|
|
161
|
+
' → search "BM25 scoring" → search "SIMD optimization" (two different concepts)',
|
|
162
|
+
'',
|
|
163
|
+
'BAD search strategy (never do this):',
|
|
164
|
+
' → search "AllowedIPs" → search "allowedIps" → search "allowed_ips" (WRONG: these are trivial case variations, probe handles them)',
|
|
165
|
+
' → search "CIDR" → search "cidr" → search "Cidr" → search "*cidr*" (WRONG: same keyword repeated with variations)',
|
|
166
|
+
' → search "error handling" → search "error handling" → search "error handling" (WRONG: repeating exact same query)',
|
|
167
|
+
'',
|
|
168
|
+
'Keyword tips:',
|
|
169
|
+
'- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.',
|
|
170
|
+
'- Avoid searching for these alone — combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
|
|
171
|
+
'- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
|
|
172
|
+
'- Multiple words without operators use OR logic: foo bar = foo OR bar. Use AND explicitly if you need both: foo AND bar.',
|
|
173
|
+
'- camelCase terms are split: getUserData becomes "get", "user", "data" — so one search covers all naming styles.',
|
|
174
|
+
'',
|
|
175
|
+
'Strategy:',
|
|
147
176
|
'1. Analyze the query - identify key concepts, entities, and relationships',
|
|
148
|
-
'2. Run focused
|
|
149
|
-
'3.
|
|
150
|
-
'4.
|
|
177
|
+
'2. Run ONE focused search per concept with the most natural keyword. Trust probe to handle variations.',
|
|
178
|
+
'3. If a search returns results, use extract to verify relevance',
|
|
179
|
+
'4. Only try a different keyword if the first one returned irrelevant results (not if it returned no results — that means the concept is absent)',
|
|
180
|
+
'5. Combine all relevant targets in your final response',
|
|
151
181
|
'',
|
|
152
182
|
`Query: ${searchQuery}`,
|
|
153
183
|
`Search path(s): ${searchPath}`,
|
|
@@ -186,10 +216,16 @@ export const searchTool = (options = {}) => {
|
|
|
186
216
|
return result;
|
|
187
217
|
};
|
|
188
218
|
|
|
219
|
+
// Track previous non-paginated searches to detect and block duplicates
|
|
220
|
+
const previousSearches = new Set();
|
|
221
|
+
// Track pagination counts per query to cap runaway pagination
|
|
222
|
+
const paginationCounts = new Map();
|
|
223
|
+
const MAX_PAGES_PER_QUERY = 3;
|
|
224
|
+
|
|
189
225
|
return tool({
|
|
190
226
|
name: 'search',
|
|
191
227
|
description: searchDelegate
|
|
192
|
-
?
|
|
228
|
+
? searchDelegateDescription
|
|
193
229
|
: searchDescription,
|
|
194
230
|
inputSchema: searchSchema,
|
|
195
231
|
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage }) => {
|
|
@@ -236,6 +272,29 @@ export const searchTool = (options = {}) => {
|
|
|
236
272
|
};
|
|
237
273
|
|
|
238
274
|
if (!searchDelegate) {
|
|
275
|
+
// Block duplicate non-paginated searches (models sometimes repeat the exact same call)
|
|
276
|
+
// Allow pagination: only nextPage=true is a legitimate repeat of the same query
|
|
277
|
+
const searchKey = `${searchQuery}::${searchPath}::${exact || false}`;
|
|
278
|
+
if (!nextPage) {
|
|
279
|
+
if (previousSearches.has(searchKey)) {
|
|
280
|
+
if (debug) {
|
|
281
|
+
console.error(`[DEDUP] Blocked duplicate search: "${searchQuery}" in "${searchPath}"`);
|
|
282
|
+
}
|
|
283
|
+
return 'DUPLICATE SEARCH BLOCKED: You already searched for this exact query in this path. Do NOT repeat the same search. If you need more results, set nextPage=true with the session ID from the previous search. Otherwise, try a genuinely different keyword, use extract to examine results you already found, or use attempt_completion if you have enough information.';
|
|
284
|
+
}
|
|
285
|
+
previousSearches.add(searchKey);
|
|
286
|
+
paginationCounts.set(searchKey, 0);
|
|
287
|
+
} else {
|
|
288
|
+
// Cap pagination to prevent runaway page-through of broad queries
|
|
289
|
+
const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
|
|
290
|
+
paginationCounts.set(searchKey, pageCount);
|
|
291
|
+
if (pageCount > MAX_PAGES_PER_QUERY) {
|
|
292
|
+
if (debug) {
|
|
293
|
+
console.error(`[DEDUP] Blocked excessive pagination (page ${pageCount}/${MAX_PAGES_PER_QUERY}): "${searchQuery}" in "${searchPath}"`);
|
|
294
|
+
}
|
|
295
|
+
return `PAGINATION LIMIT REACHED: You have already retrieved ${MAX_PAGES_PER_QUERY} pages of results for this query. You have enough results — use extract to examine specific files, or use attempt_completion to return your findings.`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
239
298
|
try {
|
|
240
299
|
const result = maybeAnnotate(await runRawSearch());
|
|
241
300
|
// Track files found in search results for staleness detection
|
|
@@ -277,7 +336,8 @@ export const searchTool = (options = {}) => {
|
|
|
277
336
|
promptType: 'code-searcher',
|
|
278
337
|
allowedTools: ['search', 'extract', 'listFiles', 'attempt_completion'],
|
|
279
338
|
searchDelegate: false,
|
|
280
|
-
schema: CODE_SEARCH_SCHEMA
|
|
339
|
+
schema: CODE_SEARCH_SCHEMA,
|
|
340
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
281
341
|
});
|
|
282
342
|
|
|
283
343
|
const delegateResult = options.tracer?.withSpan
|
|
@@ -581,7 +641,7 @@ export const delegateTool = (options = {}) => {
|
|
|
581
641
|
name: 'delegate',
|
|
582
642
|
description: delegateDescription,
|
|
583
643
|
inputSchema: delegateSchema,
|
|
584
|
-
execute: async ({ task, currentIteration, maxIterations, parentSessionId, path, provider, model, tracer, searchDelegate }) => {
|
|
644
|
+
execute: async ({ task, currentIteration, maxIterations, parentSessionId, path, provider, model, tracer, searchDelegate, parentAbortSignal }) => {
|
|
585
645
|
// Validate required parameters - throw errors for consistency
|
|
586
646
|
if (!task || typeof task !== 'string') {
|
|
587
647
|
throw new Error('Task parameter is required and must be a non-empty string');
|
|
@@ -673,7 +733,8 @@ export const delegateTool = (options = {}) => {
|
|
|
673
733
|
enableMcp,
|
|
674
734
|
mcpConfig,
|
|
675
735
|
mcpConfigPath,
|
|
676
|
-
delegationManager // Per-instance delegation limits
|
|
736
|
+
delegationManager, // Per-instance delegation limits
|
|
737
|
+
parentAbortSignal
|
|
677
738
|
});
|
|
678
739
|
|
|
679
740
|
return result;
|
|
@@ -733,7 +794,8 @@ export const analyzeAllTool = (options = {}) => {
|
|
|
733
794
|
provider: options.provider,
|
|
734
795
|
model: options.model,
|
|
735
796
|
tracer: options.tracer,
|
|
736
|
-
delegationManager // Per-instance delegation limits
|
|
797
|
+
delegationManager, // Per-instance delegation limits
|
|
798
|
+
parentAbortSignal: options.parentAbortSignal || null
|
|
737
799
|
});
|
|
738
800
|
|
|
739
801
|
return result;
|