@probelabs/probe 0.6.0-rc295 → 0.6.0-rc297
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bin/binaries/{probe-v0.6.0-rc295-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc297-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc295-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc297-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc295-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc297-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc295-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc297-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc295-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc297-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/ProbeAgent.d.ts +40 -2
- package/build/agent/ProbeAgent.js +703 -11
- package/build/agent/mcp/client.js +115 -4
- package/build/agent/mcp/xmlBridge.js +13 -1
- package/build/agent/otelLogBridge.js +184 -0
- package/build/agent/simpleTelemetry.js +8 -0
- package/build/delegate.js +75 -6
- package/build/index.js +6 -2
- package/build/tools/common.js +84 -11
- package/build/tools/vercel.js +78 -18
- package/cjs/agent/ProbeAgent.cjs +1095 -185
- package/cjs/agent/simpleTelemetry.cjs +112 -0
- package/cjs/index.cjs +1207 -185
- package/index.d.ts +26 -0
- package/package.json +2 -2
- package/src/agent/ProbeAgent.d.ts +40 -2
- package/src/agent/ProbeAgent.js +703 -11
- package/src/agent/mcp/client.js +115 -4
- package/src/agent/mcp/xmlBridge.js +13 -1
- package/src/agent/otelLogBridge.js +184 -0
- package/src/agent/simpleTelemetry.js +8 -0
- package/src/delegate.js +75 -6
- package/src/index.js +6 -2
- package/src/tools/common.js +84 -11
- package/src/tools/vercel.js +78 -18
package/build/tools/common.js
CHANGED
|
@@ -10,7 +10,7 @@ import { resolve, isAbsolute } from 'path';
|
|
|
10
10
|
export const searchSchema = z.object({
|
|
11
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
|
-
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
|
|
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 OR when searching for strings with punctuation/quotes/empty values (e.g. \'description: ""\' — BM25 strips punctuation so exact=true is required for literal matching). Use true when you know the exact symbol name or need literal string matching.'),
|
|
14
14
|
maxTokens: z.number().nullable().optional().describe('Maximum tokens to return. Default is 20000. Set to null for unlimited results.'),
|
|
15
15
|
session: z.string().optional().describe('Session ID for result caching and pagination. Pass the session ID from a previous search to get additional results (next page). Results already shown in a session are automatically excluded. Omit for a fresh search.'),
|
|
16
16
|
nextPage: z.boolean().optional().default(false).describe('Set to true when requesting the next page of results. Requires passing the same session ID from the previous search output.')
|
|
@@ -188,9 +188,74 @@ export function areBothStuckResponses(response1, response2) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Parse a shell-like string into tokens, respecting quoted substrings.
|
|
193
|
+
* Supports double quotes, single quotes, and escaped characters within quotes.
|
|
194
|
+
* Splits on commas and/or whitespace outside of quotes.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} input - The string to tokenize
|
|
197
|
+
* @returns {string[]} Array of tokens with quotes stripped
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* splitQuotedString('"path with spaces/file.md" other.rs')
|
|
201
|
+
* // Returns: ["path with spaces/file.md", "other.rs"]
|
|
202
|
+
*/
|
|
203
|
+
export function splitQuotedString(input) {
|
|
204
|
+
const tokens = [];
|
|
205
|
+
let current = '';
|
|
206
|
+
let inQuote = null; // null, '"', or "'"
|
|
207
|
+
let i = 0;
|
|
208
|
+
|
|
209
|
+
while (i < input.length) {
|
|
210
|
+
const ch = input[i];
|
|
211
|
+
|
|
212
|
+
if (inQuote) {
|
|
213
|
+
if (ch === '\\' && i + 1 < input.length) {
|
|
214
|
+
// Escaped character inside quotes — keep the literal character
|
|
215
|
+
current += input[i + 1];
|
|
216
|
+
i += 2;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (ch === inQuote) {
|
|
220
|
+
// Closing quote
|
|
221
|
+
inQuote = null;
|
|
222
|
+
i++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
current += ch;
|
|
226
|
+
i++;
|
|
227
|
+
} else {
|
|
228
|
+
if (ch === '"' || ch === "'") {
|
|
229
|
+
inQuote = ch;
|
|
230
|
+
i++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (/[\s,]/.test(ch)) {
|
|
234
|
+
// Delimiter outside quotes
|
|
235
|
+
if (current.length > 0) {
|
|
236
|
+
tokens.push(current);
|
|
237
|
+
current = '';
|
|
238
|
+
}
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
current += ch;
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (current.length > 0) {
|
|
248
|
+
tokens.push(current);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return tokens;
|
|
252
|
+
}
|
|
253
|
+
|
|
191
254
|
/**
|
|
192
255
|
* Parse targets string into array of file specifications
|
|
193
|
-
* Handles both space-separated and comma-separated targets for extract tool
|
|
256
|
+
* Handles both space-separated and comma-separated targets for extract tool.
|
|
257
|
+
* Quoted strings (single or double) are preserved as single targets,
|
|
258
|
+
* allowing file paths with spaces.
|
|
194
259
|
*
|
|
195
260
|
* @param {string} targets - Space or comma-separated file targets (e.g., "file1.rs:10-20, file2.rs#symbol")
|
|
196
261
|
* @returns {string[]} Array of individual file specifications
|
|
@@ -204,16 +269,15 @@ export function areBothStuckResponses(response1, response2) {
|
|
|
204
269
|
* // Returns: ["file1.rs:10-20", "file2.rs:30-40"]
|
|
205
270
|
*
|
|
206
271
|
* @example
|
|
207
|
-
* parseTargets("
|
|
208
|
-
* // Returns: ["
|
|
272
|
+
* parseTargets('"Customers/First American/Meeting Notes.md" other.rs')
|
|
273
|
+
* // Returns: ["Customers/First American/Meeting Notes.md", "other.rs"]
|
|
209
274
|
*/
|
|
210
275
|
export function parseTargets(targets) {
|
|
211
276
|
if (!targets || typeof targets !== 'string') {
|
|
212
277
|
return [];
|
|
213
278
|
}
|
|
214
279
|
|
|
215
|
-
|
|
216
|
-
return targets.split(/[\s,]+/).filter(f => f.length > 0);
|
|
280
|
+
return splitQuotedString(targets);
|
|
217
281
|
}
|
|
218
282
|
|
|
219
283
|
/**
|
|
@@ -227,7 +291,19 @@ export function parseTargets(targets) {
|
|
|
227
291
|
export function parseAndResolvePaths(pathStr, cwd) {
|
|
228
292
|
if (!pathStr) return [];
|
|
229
293
|
|
|
230
|
-
//
|
|
294
|
+
// If the input contains quotes, use the quote-aware tokenizer which
|
|
295
|
+
// preserves quoted strings with spaces as single tokens.
|
|
296
|
+
if (/["']/.test(pathStr)) {
|
|
297
|
+
const paths = splitQuotedString(pathStr);
|
|
298
|
+
return paths.map(p => {
|
|
299
|
+
if (isAbsolute(p)) return p;
|
|
300
|
+
return cwd ? resolve(cwd, p) : p;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// No quotes: use comma-split + space-split heuristic (original behavior).
|
|
305
|
+
// Split on comma first, then auto-fix space-separated paths if each part
|
|
306
|
+
// looks like a file path.
|
|
231
307
|
let paths = pathStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
232
308
|
|
|
233
309
|
// Auto-fix: model sometimes passes space-separated file paths as one string
|
|
@@ -242,10 +318,7 @@ export function parseAndResolvePaths(pathStr, cwd) {
|
|
|
242
318
|
|
|
243
319
|
// Resolve relative paths against cwd
|
|
244
320
|
return paths.map(p => {
|
|
245
|
-
if (isAbsolute(p))
|
|
246
|
-
return p;
|
|
247
|
-
}
|
|
248
|
-
// Resolve relative path against cwd
|
|
321
|
+
if (isAbsolute(p)) return p;
|
|
249
322
|
return cwd ? resolve(cwd, p) : p;
|
|
250
323
|
});
|
|
251
324
|
}
|
package/build/tools/vercel.js
CHANGED
|
@@ -254,6 +254,10 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
|
|
|
254
254
|
'- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).',
|
|
255
255
|
'- exact=true matches the literal string only — no stemming, no splitting.',
|
|
256
256
|
'- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
|
|
257
|
+
'- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.',
|
|
258
|
+
' Default BM25 search strips punctuation and treats quoted empty strings as noise.',
|
|
259
|
+
' Example: searching for \'description: ""\' with exact=false will NOT find empty description fields — it just matches "description".',
|
|
260
|
+
' Use exact=true for literal patterns like \'description: ""\', \'value: \\\'\\\'\', or any YAML/config field with specific punctuation.',
|
|
257
261
|
'- Do NOT use exact=true for exploratory/conceptual queries — use the default for those.',
|
|
258
262
|
'',
|
|
259
263
|
'Combining searches with OR:',
|
|
@@ -313,7 +317,13 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
|
|
|
313
317
|
'WHEN TO STOP:',
|
|
314
318
|
'- After you have explored the main concept AND related subsystems.',
|
|
315
319
|
'- Once you have 5-15 targets covering different aspects of the query.',
|
|
316
|
-
'- If you get a "DUPLICATE SEARCH BLOCKED" message,
|
|
320
|
+
'- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query — try a FUNDAMENTALLY different approach:',
|
|
321
|
+
' * Switch between exact=true and exact=false',
|
|
322
|
+
' * Search for a broader term and filter results manually',
|
|
323
|
+
' * Use listFiles to browse the directory structure directly',
|
|
324
|
+
' * Look for related/surrounding patterns instead of the exact string',
|
|
325
|
+
'- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.',
|
|
326
|
+
' Do NOT keep trying variations of the same failing concept.',
|
|
317
327
|
'',
|
|
318
328
|
'Strategy:',
|
|
319
329
|
'1. Analyze the query — identify key concepts, then brainstorm SYNONYMS and alternative terms for each.',
|
|
@@ -371,10 +381,10 @@ export const searchTool = (options = {}) => {
|
|
|
371
381
|
return result;
|
|
372
382
|
};
|
|
373
383
|
|
|
374
|
-
// Track previous non-paginated searches
|
|
375
|
-
const previousSearches = new
|
|
376
|
-
// Track
|
|
377
|
-
|
|
384
|
+
// Track previous non-paginated searches: key → { hadResults: boolean }
|
|
385
|
+
const previousSearches = new Map();
|
|
386
|
+
// Track per-key consecutive block counts (not global, to avoid cross-query pollution)
|
|
387
|
+
const dupBlockCounts = new Map();
|
|
378
388
|
// Track pagination counts per query to cap runaway pagination
|
|
379
389
|
const paginationCounts = new Map();
|
|
380
390
|
const MAX_PAGES_PER_QUERY = 3;
|
|
@@ -444,22 +454,28 @@ export const searchTool = (options = {}) => {
|
|
|
444
454
|
if (!searchDelegate) {
|
|
445
455
|
// Block duplicate non-paginated searches (models sometimes repeat the exact same call)
|
|
446
456
|
// Allow pagination: only nextPage=true is a legitimate repeat of the same query
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
const searchKey = `${searchQuery}::${exact || false}`;
|
|
457
|
+
// Include path in dedup key so same query across different repos is allowed (#520)
|
|
458
|
+
const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
|
|
450
459
|
if (!nextPage) {
|
|
451
460
|
if (previousSearches.has(searchKey)) {
|
|
452
|
-
|
|
461
|
+
const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
|
|
462
|
+
dupBlockCounts.set(searchKey, blockCount);
|
|
453
463
|
if (debug) {
|
|
454
|
-
console.error(`[DEDUP] Blocked duplicate search (${
|
|
464
|
+
console.error(`[DEDUP] Blocked duplicate search (${blockCount}x): "${searchQuery}" (path: "${searchPath}")`);
|
|
455
465
|
}
|
|
456
|
-
if (
|
|
457
|
-
return 'STOP. You have been blocked ' +
|
|
466
|
+
if (blockCount >= 3) {
|
|
467
|
+
return 'STOP. You have been blocked ' + blockCount + ' times for repeating the same search. You MUST provide your final answer NOW with whatever information you have. Do NOT call any more tools.';
|
|
458
468
|
}
|
|
459
|
-
|
|
469
|
+
const prev = previousSearches.get(searchKey);
|
|
470
|
+
if (prev.hadResults) {
|
|
471
|
+
return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and found results. Do NOT repeat. Use extract to examine the files you already found, try a COMPLETELY different keyword, or provide your final answer.`;
|
|
472
|
+
}
|
|
473
|
+
const exactHint = exact
|
|
474
|
+
? 'You used exact=true. Try a broader search with exact=false, or use listFiles to browse the directory structure.'
|
|
475
|
+
: 'Try exact=true if you need literal/punctuation matching (e.g. \'description: ""\'), or use listFiles to explore directories, or search for a broader/related term and filter manually.';
|
|
476
|
+
return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and got NO results. This term does not appear in the codebase. Do NOT repeat or rephrase — try a FUNDAMENTALLY different approach: ${exactHint} If multiple approaches have failed, provide your final answer with what you know.`;
|
|
460
477
|
}
|
|
461
|
-
previousSearches.
|
|
462
|
-
consecutiveDupBlocks = 0; // Reset on successful new search
|
|
478
|
+
previousSearches.set(searchKey, { hadResults: false });
|
|
463
479
|
paginationCounts.set(searchKey, 0);
|
|
464
480
|
} else {
|
|
465
481
|
// Cap pagination to prevent runaway page-through of broad queries
|
|
@@ -474,6 +490,16 @@ export const searchTool = (options = {}) => {
|
|
|
474
490
|
}
|
|
475
491
|
try {
|
|
476
492
|
const result = maybeAnnotate(await runRawSearch());
|
|
493
|
+
// Track whether this search had results for better dedup messages
|
|
494
|
+
if (typeof result === 'string' && result.includes('No results found')) {
|
|
495
|
+
// Append contextual hint for ticket/issue ID queries
|
|
496
|
+
if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, '').trim())) {
|
|
497
|
+
return result + '\n\n⚠️ Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).';
|
|
498
|
+
}
|
|
499
|
+
} else if (typeof result === 'string') {
|
|
500
|
+
const entry = previousSearches.get(searchKey);
|
|
501
|
+
if (entry) entry.hadResults = true;
|
|
502
|
+
}
|
|
477
503
|
// Track files found in search results for staleness detection
|
|
478
504
|
if (options.fileTracker && typeof result === 'string') {
|
|
479
505
|
options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {});
|
|
@@ -862,7 +888,11 @@ export const extractTool = (options = {}) => {
|
|
|
862
888
|
* @returns {Object} Configured delegate tool
|
|
863
889
|
*/
|
|
864
890
|
export const delegateTool = (options = {}) => {
|
|
865
|
-
const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null
|
|
891
|
+
const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null,
|
|
892
|
+
// Timeout settings inherited from parent agent
|
|
893
|
+
timeoutBehavior, maxOperationTimeout, requestTimeout, gracefulTimeoutBonusSteps,
|
|
894
|
+
negotiatedTimeoutBudget, negotiatedTimeoutMaxRequests, negotiatedTimeoutMaxPerRequest,
|
|
895
|
+
parentOperationStartTime, onSubagentCreated, onSubagentCompleted } = options;
|
|
866
896
|
|
|
867
897
|
return tool({
|
|
868
898
|
name: 'delegate',
|
|
@@ -941,9 +971,32 @@ export const delegateTool = (options = {}) => {
|
|
|
941
971
|
}
|
|
942
972
|
|
|
943
973
|
// Execute delegation - let errors propagate naturally
|
|
974
|
+
// Cap delegate timeout to remaining parent budget (with 10% headroom)
|
|
975
|
+
let effectiveTimeout = timeout;
|
|
976
|
+
if (parentOperationStartTime && maxOperationTimeout) {
|
|
977
|
+
const elapsed = Date.now() - parentOperationStartTime;
|
|
978
|
+
const remaining = maxOperationTimeout - elapsed;
|
|
979
|
+
const budgetCap = Math.max(30, Math.floor(remaining * 0.9 / 1000)); // seconds, min 30s
|
|
980
|
+
if (budgetCap < effectiveTimeout) {
|
|
981
|
+
effectiveTimeout = budgetCap;
|
|
982
|
+
if (debug) {
|
|
983
|
+
console.error(`[DELEGATE] Capping timeout from ${timeout}s to ${effectiveTimeout}s (remaining parent budget: ${Math.floor(remaining/1000)}s)`);
|
|
984
|
+
}
|
|
985
|
+
if (tracer) {
|
|
986
|
+
tracer.addEvent('delegation.budget_capped', {
|
|
987
|
+
'delegation.original_timeout_s': timeout,
|
|
988
|
+
'delegation.effective_timeout_s': effectiveTimeout,
|
|
989
|
+
'delegation.parent_elapsed_ms': elapsed,
|
|
990
|
+
'delegation.parent_remaining_ms': remaining,
|
|
991
|
+
'delegation.parent_session_id': parentSessionId,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
944
997
|
const result = await delegate({
|
|
945
998
|
task,
|
|
946
|
-
timeout,
|
|
999
|
+
timeout: effectiveTimeout,
|
|
947
1000
|
debug,
|
|
948
1001
|
currentIteration: currentIteration || 0,
|
|
949
1002
|
maxIterations: maxIterations || 30,
|
|
@@ -961,7 +1014,14 @@ export const delegateTool = (options = {}) => {
|
|
|
961
1014
|
mcpConfig,
|
|
962
1015
|
mcpConfigPath,
|
|
963
1016
|
delegationManager, // Per-instance delegation limits
|
|
964
|
-
parentAbortSignal
|
|
1017
|
+
parentAbortSignal,
|
|
1018
|
+
// Inherit timeout settings for subagent
|
|
1019
|
+
timeoutBehavior,
|
|
1020
|
+
requestTimeout,
|
|
1021
|
+
gracefulTimeoutBonusSteps,
|
|
1022
|
+
// Subagent lifecycle callbacks for graceful stop coordination
|
|
1023
|
+
onSubagentCreated,
|
|
1024
|
+
onSubagentCompleted,
|
|
965
1025
|
});
|
|
966
1026
|
|
|
967
1027
|
return result;
|