@probelabs/probe 0.6.0-rc303 → 0.6.0-rc305
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-rc303-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc305-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc303-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc305-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc303-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc305-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc303-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc305-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc303-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc305-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/FallbackManager.js +3 -57
- package/build/agent/ProbeAgent.js +48 -62
- package/build/agent/tasks/TaskManager.js +21 -21
- package/build/delegate.js +15 -4
- package/build/tools/common.js +16 -1
- package/build/tools/vercel.js +385 -244
- package/build/utils/provider.js +106 -0
- package/cjs/agent/ProbeAgent.cjs +1051 -364
- package/cjs/index.cjs +502 -362
- package/package.json +1 -1
- package/src/agent/FallbackManager.js +3 -57
- package/src/agent/ProbeAgent.js +48 -62
- package/src/agent/tasks/TaskManager.js +21 -21
- package/src/delegate.js +15 -4
- package/src/tools/common.js +16 -1
- package/src/tools/vercel.js +385 -244
- package/src/utils/provider.js +106 -0
package/build/tools/vercel.js
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
* @module tools/vercel
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { tool } from 'ai';
|
|
6
|
+
import { tool, generateText } from 'ai';
|
|
7
7
|
import { search } from '../search.js';
|
|
8
8
|
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, searchDelegateDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, parseTargets, parseAndResolvePaths, resolveTargetPath } from './common.js';
|
|
12
|
+
import { searchSchema, searchDelegateSchema, 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';
|
|
16
|
+
import { createLanguageModel } from '../utils/provider.js';
|
|
16
17
|
import { truncateForSpan } from '../agent/simpleTelemetry.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -87,16 +88,128 @@ function autoQuoteSearchTerms(query) {
|
|
|
87
88
|
const CODE_SEARCH_SCHEMA = {
|
|
88
89
|
type: 'object',
|
|
89
90
|
properties: {
|
|
90
|
-
|
|
91
|
+
confidence: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
enum: ['high', 'medium', 'low'],
|
|
94
|
+
description: 'How confident you are that these locations answer the question.'
|
|
95
|
+
},
|
|
96
|
+
reason: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Brief explanation of confidence level — what was found, partially found, or not found.'
|
|
99
|
+
},
|
|
100
|
+
groups: {
|
|
91
101
|
type: 'array',
|
|
92
|
-
items: {
|
|
93
|
-
|
|
102
|
+
items: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
reason: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Why these files are relevant — what aspect of the question they address (not how the code works).'
|
|
108
|
+
},
|
|
109
|
+
files: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: { type: 'string' },
|
|
112
|
+
description: 'File targets like "path/to/file.ext#Symbol" or "path/to/file.ext:10-20".'
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
required: ['reason', 'files']
|
|
116
|
+
},
|
|
117
|
+
description: 'Groups of related files, each with a reason explaining why they matter.'
|
|
118
|
+
},
|
|
119
|
+
searches: {
|
|
120
|
+
type: 'array',
|
|
121
|
+
items: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
query: { type: 'string', description: 'The search query used.' },
|
|
125
|
+
path: { type: 'string', description: 'The path searched in.' },
|
|
126
|
+
had_results: { type: 'boolean', description: 'Whether the search returned any results.' }
|
|
127
|
+
},
|
|
128
|
+
required: ['query', 'path', 'had_results']
|
|
129
|
+
},
|
|
130
|
+
description: 'All search queries executed during this session, with their paths and outcomes.'
|
|
94
131
|
}
|
|
95
132
|
},
|
|
96
|
-
required: ['
|
|
133
|
+
required: ['confidence', 'reason', 'groups', 'searches'],
|
|
97
134
|
additionalProperties: false
|
|
98
135
|
};
|
|
99
136
|
|
|
137
|
+
/**
|
|
138
|
+
* LLM-based semantic dedup for delegate queries.
|
|
139
|
+
* Asks the same model to classify a new query against previous ones.
|
|
140
|
+
* Returns: { action: 'allow'|'block'|'rewrite', rewritten?: string, reason: string }
|
|
141
|
+
*/
|
|
142
|
+
async function checkDelegateDedup(newQuery, previousQueries, model, debug) {
|
|
143
|
+
if (!model || previousQueries.length === 0) {
|
|
144
|
+
return { action: 'allow', reason: 'no previous queries' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const previousList = previousQueries
|
|
148
|
+
.map((q, i) => {
|
|
149
|
+
let line = `${i + 1}. "${q.query}" (path: ${q.path}, found results: ${q.hadResults})`;
|
|
150
|
+
if (q.reason) line += `\n Outcome: ${q.reason}`;
|
|
151
|
+
if (q.groups && q.groups.length > 0) {
|
|
152
|
+
line += `\n Found: ${q.groups.map(g => g.reason).join('; ')}`;
|
|
153
|
+
}
|
|
154
|
+
return line;
|
|
155
|
+
})
|
|
156
|
+
.join('\n');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const result = await generateText({
|
|
160
|
+
model,
|
|
161
|
+
maxTokens: 150,
|
|
162
|
+
temperature: 0,
|
|
163
|
+
prompt: `You decide if a code search query is redundant given previous queries in the same session.
|
|
164
|
+
|
|
165
|
+
PREVIOUS QUERIES:
|
|
166
|
+
${previousList}
|
|
167
|
+
|
|
168
|
+
NEW QUERY: "${newQuery}"
|
|
169
|
+
|
|
170
|
+
Respond with exactly one line: ACTION|REASON
|
|
171
|
+
For rewrites: rewrite|REASON|REWRITTEN_QUERY
|
|
172
|
+
|
|
173
|
+
BLOCK when:
|
|
174
|
+
- Same concept, different phrasing: "find X" / "definition of X" / "where is X" / "X implementation" → all the same
|
|
175
|
+
- Synonym or narrower term of a previous query: "dedup" → "duplicate" → "unique" → all the same concept
|
|
176
|
+
- Single generic word that's just a synonym of a previous failed query
|
|
177
|
+
- Query is trying to brute-force the same concept with different keywords after previous failures
|
|
178
|
+
|
|
179
|
+
REWRITE when:
|
|
180
|
+
- Previous query was too narrow and failed, new query targets the same goal but could use a FUNDAMENTALLY different search strategy (e.g. searching for a caller instead of the function name, or searching the config/registration site instead of the implementation)
|
|
181
|
+
- Previous query found WRONG results (e.g. found "FallbackManager" when looking for "dedup logic") — rewrite to target the actual concept more precisely using implementation-level terms
|
|
182
|
+
|
|
183
|
+
ALLOW only when:
|
|
184
|
+
- The new query targets a COMPLETELY DIFFERENT feature, module, or subsystem — not just a different word for the same thing
|
|
185
|
+
|
|
186
|
+
Only BLOCK when you are CERTAIN the queries target the same concept. When uncertain, ALLOW — a missed dedup is cheaper than blocking a valid search.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
- Prev: "wrapToolWithEmitter" → New: "definition of wrapToolWithEmitter" → block|Same symbol
|
|
190
|
+
- Prev: "search dedup" (no results) → New: "dedup" → block|Synonym of failed query
|
|
191
|
+
- Prev: "dedup" (no results) → New: "duplicate" → block|Synonym of failed query
|
|
192
|
+
- Prev: "dedup" (no results) → New: "unique" → block|Synonym of failed query
|
|
193
|
+
- Prev: "auth middleware" → New: "rate limiting" → allow|Different subsystem
|
|
194
|
+
- Prev: "search dedup" (no results) → New: "previousSearches Map" → rewrite|Searching for implementation detail instead of concept|previousSearches OR searchKey`
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const line = result.text.trim().split('\n')[0];
|
|
198
|
+
const parts = line.split('|');
|
|
199
|
+
const action = (parts[0] || '').toLowerCase().trim();
|
|
200
|
+
|
|
201
|
+
if (action === 'block') {
|
|
202
|
+
return { action: 'block', reason: parts[1]?.trim() || 'duplicate query' };
|
|
203
|
+
} else if (action === 'rewrite' && parts[2]) {
|
|
204
|
+
return { action: 'rewrite', reason: parts[1]?.trim() || 'refined query', rewritten: parts[2].trim() };
|
|
205
|
+
}
|
|
206
|
+
return { action: 'allow', reason: parts[1]?.trim() || 'new concept' };
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (debug) console.error('[DEDUP-LLM] Error:', err.message);
|
|
209
|
+
return { action: 'allow', reason: 'dedup check failed, allowing' };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
100
213
|
function normalizeTargets(targets) {
|
|
101
214
|
if (!Array.isArray(targets)) return [];
|
|
102
215
|
const seen = new Set();
|
|
@@ -186,8 +299,13 @@ function fallbackTargetsFromText(text) {
|
|
|
186
299
|
return candidates;
|
|
187
300
|
}
|
|
188
301
|
|
|
189
|
-
|
|
190
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Parse the delegate sub-agent's raw response into a structured result.
|
|
304
|
+
* Returns { confidence, groups } when possible, or builds a single-group
|
|
305
|
+
* fallback from legacy { targets: [...] } or plain text.
|
|
306
|
+
*/
|
|
307
|
+
function parseDelegatedResponse(rawResponse) {
|
|
308
|
+
if (!rawResponse || typeof rawResponse !== 'string') return null;
|
|
191
309
|
const trimmed = rawResponse.trim();
|
|
192
310
|
|
|
193
311
|
const tryParse = (text) => {
|
|
@@ -207,15 +325,50 @@ function parseDelegatedTargets(rawResponse) {
|
|
|
207
325
|
}
|
|
208
326
|
|
|
209
327
|
if (parsed) {
|
|
210
|
-
|
|
211
|
-
|
|
328
|
+
// New format: { confidence, groups: [{ reason, files }], searches: [...] }
|
|
329
|
+
if (Array.isArray(parsed.groups)) {
|
|
330
|
+
return {
|
|
331
|
+
confidence: parsed.confidence || 'medium',
|
|
332
|
+
reason: parsed.reason || '',
|
|
333
|
+
groups: parsed.groups.map(g => ({
|
|
334
|
+
reason: g.reason || '',
|
|
335
|
+
files: normalizeTargets(g.files || [])
|
|
336
|
+
})).filter(g => g.files.length > 0),
|
|
337
|
+
searches: Array.isArray(parsed.searches) ? parsed.searches : []
|
|
338
|
+
};
|
|
212
339
|
}
|
|
340
|
+
// Legacy format: { targets: [...] }
|
|
213
341
|
if (Array.isArray(parsed.targets)) {
|
|
214
|
-
|
|
342
|
+
const files = normalizeTargets(parsed.targets);
|
|
343
|
+
if (files.length > 0) {
|
|
344
|
+
return { confidence: 'medium', reason: '', groups: [{ reason: 'Search results', files }], searches: [] };
|
|
345
|
+
}
|
|
346
|
+
// Empty targets array — explicitly return null (don't fall through to text fallback)
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
// Plain array
|
|
350
|
+
if (Array.isArray(parsed)) {
|
|
351
|
+
const files = normalizeTargets(parsed);
|
|
352
|
+
if (files.length > 0) {
|
|
353
|
+
return { confidence: 'medium', reason: '', groups: [{ reason: 'Search results', files }], searches: [] };
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
215
356
|
}
|
|
216
357
|
}
|
|
217
358
|
|
|
218
|
-
|
|
359
|
+
// Fallback: extract targets from plain text
|
|
360
|
+
const files = normalizeTargets(fallbackTargetsFromText(trimmed));
|
|
361
|
+
if (files.length > 0) {
|
|
362
|
+
return { confidence: 'low', reason: '', groups: [{ reason: 'Search results', files }], searches: [] };
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Keep backward compat for any other callers
|
|
368
|
+
function parseDelegatedTargets(rawResponse) {
|
|
369
|
+
const result = parseDelegatedResponse(rawResponse);
|
|
370
|
+
if (!result) return [];
|
|
371
|
+
return result.groups.flatMap(g => g.files);
|
|
219
372
|
}
|
|
220
373
|
|
|
221
374
|
function splitTargetSuffix(target) {
|
|
@@ -231,159 +384,78 @@ function splitTargetSuffix(target) {
|
|
|
231
384
|
}
|
|
232
385
|
|
|
233
386
|
function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, allowTests }) {
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
' Query: "Find ThrottleRetryLimit usage"',
|
|
307
|
-
' → search exact=true "ThrottleRetryLimit" (one search, if no results the symbol does not exist — stop)',
|
|
308
|
-
' Query: "How does BM25 scoring work with SIMD optimization?"',
|
|
309
|
-
' → search "BM25 scoring" + search "SIMD optimization" IN PARALLEL (two different concepts)',
|
|
310
|
-
'',
|
|
311
|
-
'BAD search strategy (never do this):',
|
|
312
|
-
' → search "AllowedIPs" → search "allowedIps" → search "allowed_ips" (WRONG: case/style variations, probe handles them)',
|
|
313
|
-
' → search "limitDRL" → search "LimitDRL" (WRONG: case variation — combine with OR: \'"limitDRL" OR "limitRedis"\')',
|
|
314
|
-
' → search "throttle_retry_limit" after searching "ThrottleRetryLimit" (WRONG: snake_case variation, probe handles it)',
|
|
315
|
-
' → search "ThrottleRetryLimit" path=tyk → search "ThrottleRetryLimit" path=gateway → search "ThrottleRetryLimit" path=apidef (WRONG: same query on different paths — probe searches recursively)',
|
|
316
|
-
' → search "func (k *RateLimitAndQuotaCheck) handleRateLimitFailure" (WRONG: do not search full function signatures, just use exact=true "handleRateLimitFailure")',
|
|
317
|
-
' → search "ForwardMessage" → search "ForwardMessage" → search "ForwardMessage" (WRONG: repeating the exact same query)',
|
|
318
|
-
' → search "authentication" → wait → search "session management" → wait (WRONG: these are independent, run them in parallel)',
|
|
319
|
-
'',
|
|
320
|
-
' WORST pattern — retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):',
|
|
321
|
-
' → search "func ctxGetData" → no results',
|
|
322
|
-
' → search "ctxGetData" → no results ← WRONG: same term without "func" prefix',
|
|
323
|
-
' → search "ctx.GetData" → no results ← WRONG: method syntax of same concept',
|
|
324
|
-
' → search "ctx.SetData" → no results ← WRONG: Set variant of same concept',
|
|
325
|
-
' → search ctxGetData → no results ← WRONG: unquoted version of same term',
|
|
326
|
-
' → extract api.go → extract api.go → extract api.go (8 times!) ← WRONG: re-reading same file',
|
|
327
|
-
' FIX: After "func ctxGetData" returns no results in gateway/:',
|
|
328
|
-
' Option A: Widen scope — search from the workspace root (omit path) in case the',
|
|
329
|
-
' function is defined in a different package (e.g., apidef/, user/, config/).',
|
|
330
|
-
' Option B: Discover real names — extract a file you KNOW uses context (e.g., a',
|
|
331
|
-
' middleware file) and READ what functions it actually calls.',
|
|
332
|
-
' Option C: Browse — use listFiles to see what files exist and extract the relevant ones.',
|
|
333
|
-
' NEVER: retry the same concept with different quoting in the same directory.',
|
|
334
|
-
'',
|
|
335
|
-
'Keyword tips:',
|
|
336
|
-
'- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.',
|
|
337
|
-
'- Avoid searching for these alone — combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
|
|
338
|
-
'- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
|
|
339
|
-
'- camelCase terms are split: getUserData becomes "get", "user", "data" — so one search covers all naming styles.',
|
|
340
|
-
'- Do NOT search for full function signatures like "func (r *Type) Method(args)". Just search for the method name with exact=true.',
|
|
341
|
-
'- Do NOT search for file names (e.g., "sliding_log.go"). Use listFiles to discover files by name.',
|
|
342
|
-
'',
|
|
343
|
-
'PAGINATION:',
|
|
344
|
-
'- Search results are paginated (~20k tokens per page).',
|
|
345
|
-
'- If your search returned relevant files, call the same query with nextPage=true to check for more.',
|
|
346
|
-
'- Keep paginating while results stay relevant. Stop when results are off-topic or "All results retrieved".',
|
|
347
|
-
'',
|
|
348
|
-
'WHEN TO STOP:',
|
|
349
|
-
'- After you have explored the main concept AND related subsystems.',
|
|
350
|
-
'- Once you have 5-15 targets covering different aspects of the query.',
|
|
351
|
-
'- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query — try a FUNDAMENTALLY different approach:',
|
|
352
|
-
' * Switch between exact=true and exact=false',
|
|
353
|
-
' * Search for a broader term and filter results manually',
|
|
354
|
-
' * Use listFiles to browse the directory structure directly',
|
|
355
|
-
' * Look for related/surrounding patterns instead of the exact string',
|
|
356
|
-
'- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.',
|
|
357
|
-
' Do NOT keep trying variations of the same failing concept.',
|
|
358
|
-
'',
|
|
359
|
-
'Strategy:',
|
|
360
|
-
'1. Analyze the query — identify key concepts, then brainstorm SYNONYMS and alternative terms for each.',
|
|
361
|
-
' Code naming often differs from the concept: "authentication" → verify, credentials, login, auth;',
|
|
362
|
-
' "rate limiting" → throttle, quota, limiter, bucket; "error handling" → catch, recover, panic.',
|
|
363
|
-
' Think about what a developer would NAME the function/struct/variable, not just the concept.',
|
|
364
|
-
'2. Run INDEPENDENT searches in PARALLEL — search for the main concept AND synonyms simultaneously.',
|
|
365
|
-
' After each search, check if results are relevant. If yes, call nextPage=true for more results.',
|
|
366
|
-
'3. Combine related symbols into OR searches: \'"symbolA" OR "symbolB"\' finds files with either.',
|
|
367
|
-
'4. For known symbol names use exact=true. For concepts use default (exact=false).',
|
|
368
|
-
'5. After your first round of searches, READ the extracted code and look for connected code:',
|
|
369
|
-
' - Function calls to other important functions → include those targets.',
|
|
370
|
-
' - Type references and imports → include type definitions.',
|
|
371
|
-
' - Registered handlers/middleware → include all registered items.',
|
|
372
|
-
'6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.',
|
|
373
|
-
'7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations — they search the same index.',
|
|
374
|
-
'8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.',
|
|
375
|
-
'',
|
|
376
|
-
`Query: ${searchQuery}`,
|
|
377
|
-
`Search path(s): ${searchPath}`,
|
|
378
|
-
`Options: exact=${exact ? 'true' : 'false'}, language=${language || 'auto'}, allow_tests=${allowTests ? 'true' : 'false'}.`,
|
|
379
|
-
'',
|
|
380
|
-
'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
|
|
381
|
-
'IMPORTANT: Use ABSOLUTE file paths in targets (e.g., "/full/path/to/file.ext#Symbol"). If you only have relative paths, make them relative to the search path above.',
|
|
382
|
-
'Prefer #Symbol when a function/class name is clear; otherwise use line numbers.',
|
|
383
|
-
'Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.',
|
|
384
|
-
'',
|
|
385
|
-
'Remember: if your search returned relevant results, use nextPage=true to check for more before outputting.'
|
|
386
|
-
].join('\n');
|
|
387
|
+
return `<role>
|
|
388
|
+
You are a code-location subagent. Your job is to find WHERE relevant code lives for the given question.
|
|
389
|
+
You are NOT answering the question — you are finding the code locations that would help answer it.
|
|
390
|
+
</role>
|
|
391
|
+
|
|
392
|
+
<task>
|
|
393
|
+
<question>${searchQuery}</question>
|
|
394
|
+
<search-path>${searchPath}</search-path>
|
|
395
|
+
<options language="${language || 'auto'}" allow_tests="${allowTests ? 'true' : 'false'}" />
|
|
396
|
+
</task>
|
|
397
|
+
|
|
398
|
+
<tools>
|
|
399
|
+
<tool name="search">
|
|
400
|
+
Find code matching keywords or patterns. Results are paginated — use nextPage=true when results are relevant to get more.
|
|
401
|
+
</tool>
|
|
402
|
+
<tool name="extract">
|
|
403
|
+
Read code to verify a file is actually relevant before including it.
|
|
404
|
+
</tool>
|
|
405
|
+
<tool name="listFiles">
|
|
406
|
+
Browse directory structure to discover where code might live.
|
|
407
|
+
</tool>
|
|
408
|
+
</tools>
|
|
409
|
+
|
|
410
|
+
<search-engine-behavior>
|
|
411
|
+
- Probe handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically.
|
|
412
|
+
- "allowed_ips" ALREADY matches "AllowedIPs", "allowedIps", etc. Do NOT try case/style variations.
|
|
413
|
+
- NEVER repeat the same search query — you will get the same results.
|
|
414
|
+
- If a search returns no results at workspace root, the term does not exist. Move on.
|
|
415
|
+
- If a search returns no results in a subfolder, try the workspace root or a different directory.
|
|
416
|
+
- Use exact=true for known symbol names. Use default for conceptual/exploratory queries.
|
|
417
|
+
- Combine related symbols with OR: "SymbolA" OR "SymbolB" finds files with either.
|
|
418
|
+
- Run INDEPENDENT searches in PARALLEL — do not wait between unrelated searches.
|
|
419
|
+
</search-engine-behavior>
|
|
420
|
+
|
|
421
|
+
<strategy>
|
|
422
|
+
1. Analyze the question — identify key concepts and brainstorm what a developer would NAME the relevant code.
|
|
423
|
+
2. Start your first search with the FULL search-path provided above. Do NOT narrow to a subdirectory on first try — the code may live anywhere in the tree.
|
|
424
|
+
3. Search for the main concept and synonyms in parallel.
|
|
425
|
+
4. Use extract to verify relevance — skim the code to confirm it ACTUALLY relates to the question.
|
|
426
|
+
5. Follow the trail: if you find a function, look for its callers, type definitions, and registered handlers.
|
|
427
|
+
6. Group your findings by WHY they are relevant (not by how you found them).
|
|
428
|
+
</strategy>
|
|
429
|
+
|
|
430
|
+
<relevance-filtering priority="critical">
|
|
431
|
+
- Only include files you have VERIFIED are relevant by reading them with extract.
|
|
432
|
+
- Do NOT include files just because they matched a keyword — confirm the match is meaningful.
|
|
433
|
+
- A file that mentions "session" in a comment is NOT relevant to "How do sessions work?" — look for the actual implementation.
|
|
434
|
+
- Fewer verified-relevant files are far more valuable than many unverified keyword matches.
|
|
435
|
+
- If a file is tangentially related but not core to the question, leave it out.
|
|
436
|
+
- If NO files are truly relevant, return EMPTY groups with confidence "low". An honest empty result is far better than a wrong result. Never fill groups with loosely related files just to have something.
|
|
437
|
+
</relevance-filtering>
|
|
438
|
+
|
|
439
|
+
<stop-conditions>
|
|
440
|
+
- Once you have found locations covering the main concept and related subsystems.
|
|
441
|
+
- If 2-3 different search approaches fail, stop and report what you have.
|
|
442
|
+
- Do NOT keep trying quote/syntax variations of the same failing keyword.
|
|
443
|
+
</stop-conditions>
|
|
444
|
+
|
|
445
|
+
<on-iteration-limit>
|
|
446
|
+
If you run out of tool iterations, you MUST still output your JSON response with whatever you found so far.
|
|
447
|
+
Set confidence to "low" if your search was incomplete.
|
|
448
|
+
Include ALL files you verified as relevant, even if coverage is partial.
|
|
449
|
+
The "searches" field helps the caller understand what was attempted.
|
|
450
|
+
</on-iteration-limit>
|
|
451
|
+
|
|
452
|
+
<output-rules>
|
|
453
|
+
- Return ONLY valid JSON matching the schema. No markdown, no explanation.
|
|
454
|
+
- ONLY include files you have verified are relevant. No noise.
|
|
455
|
+
- Group files by RELEVANCE to the question, not by search query.
|
|
456
|
+
- Use ABSOLUTE file paths. Prefer #Symbol for functions/classes; otherwise use line ranges.
|
|
457
|
+
- Deduplicate files across groups.
|
|
458
|
+
</output-rules>`;
|
|
387
459
|
}
|
|
388
460
|
|
|
389
461
|
/**
|
|
@@ -425,6 +497,13 @@ export const searchTool = (options = {}) => {
|
|
|
425
497
|
const failedConcepts = new Map(); // normalizedKey → count
|
|
426
498
|
const MAX_PAGES_PER_QUERY = 3;
|
|
427
499
|
|
|
500
|
+
// Track delegated searches at the PARENT level to prevent the pro model from
|
|
501
|
+
// spawning redundant delegates for the same concept. Each delegate is expensive
|
|
502
|
+
// (full flash agent session), so blocking repeats saves minutes.
|
|
503
|
+
// LLM-based semantic dedup replaces deterministic normalization for delegates.
|
|
504
|
+
const previousDelegations = []; // { query: string, path: string, hadResults: boolean }
|
|
505
|
+
let cachedDedupModel = undefined; // lazily initialized
|
|
506
|
+
|
|
428
507
|
/**
|
|
429
508
|
* Normalize a search query to detect syntax-level duplicates.
|
|
430
509
|
* Strips quotes, dots, underscores/hyphens, and lowercases.
|
|
@@ -436,6 +515,9 @@ export const searchTool = (options = {}) => {
|
|
|
436
515
|
if (!query) return '';
|
|
437
516
|
return query
|
|
438
517
|
.replace(/^["']|["']$/g, '') // strip outer quotes
|
|
518
|
+
// Strip filler prefixes: "definition of X", "find X", "where is X", etc.
|
|
519
|
+
.replace(/^(definition\s+of|implementation\s+of|usage\s+of|find|where\s+is|how\s+does|locate|show\s+me|get|look\s+for)\s+/i, '')
|
|
520
|
+
.replace(/^["']|["']$/g, '') // strip quotes again after prefix removal
|
|
439
521
|
.replace(/\./g, '') // "ctx.GetData" → "ctxGetData"
|
|
440
522
|
.replace(/[_\-\s]+/g, '') // strip underscores/hyphens/spaces
|
|
441
523
|
.toLowerCase()
|
|
@@ -447,7 +529,7 @@ export const searchTool = (options = {}) => {
|
|
|
447
529
|
description: searchDelegate
|
|
448
530
|
? searchDelegateDescription
|
|
449
531
|
: searchDescription,
|
|
450
|
-
inputSchema: searchSchema,
|
|
532
|
+
inputSchema: searchDelegate ? searchDelegateSchema : searchSchema,
|
|
451
533
|
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
|
|
452
534
|
// Auto-quote mixed-case and underscore terms to prevent unwanted stemming/splitting
|
|
453
535
|
// Skip when exact=true since that already preserves the literal string
|
|
@@ -508,7 +590,8 @@ export const searchTool = (options = {}) => {
|
|
|
508
590
|
// Block duplicate non-paginated searches (models sometimes repeat the exact same call)
|
|
509
591
|
// Allow pagination: only nextPage=true is a legitimate repeat of the same query
|
|
510
592
|
// Include path in dedup key so same query across different repos is allowed (#520)
|
|
511
|
-
const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
|
|
593
|
+
const searchKey = `${searchPath}::${searchQuery}::${exact || false}::${language || ''}`;
|
|
594
|
+
let circuitBreakerWarning = '';
|
|
512
595
|
if (!nextPage) {
|
|
513
596
|
if (previousSearches.has(searchKey)) {
|
|
514
597
|
const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
|
|
@@ -548,16 +631,20 @@ export const searchTool = (options = {}) => {
|
|
|
548
631
|
}
|
|
549
632
|
|
|
550
633
|
// Circuit breaker: too many consecutive no-result searches means the model
|
|
551
|
-
// is stuck in a loop guessing names that don't exist
|
|
634
|
+
// is stuck in a loop guessing names that don't exist.
|
|
635
|
+
// Not permanent: allow the search through but prepend a strong warning.
|
|
636
|
+
// If it succeeds, consecutiveNoResults resets to 0 (line ~598).
|
|
637
|
+
// If it fails, the counter keeps climbing and subsequent attempts
|
|
638
|
+
// get increasingly stern warnings.
|
|
552
639
|
if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
|
|
553
640
|
if (debug) {
|
|
554
|
-
console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches,
|
|
641
|
+
console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, warning: "${searchQuery}"`);
|
|
555
642
|
}
|
|
556
643
|
const isSubfolderCB = path && path !== effectiveSearchCwd && path !== '.';
|
|
557
644
|
const cbScopeHint = isSubfolderCB
|
|
558
|
-
?
|
|
645
|
+
? ` You have been searching in "${path}" — consider searching from the workspace root or a different directory.`
|
|
559
646
|
: '';
|
|
560
|
-
|
|
647
|
+
circuitBreakerWarning = `\n\n⚠️ CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results.${cbScopeHint} You MUST change your approach: use extract on files you already found, use listFiles to browse directories, or provide your final answer. Guessing names will not help.`;
|
|
561
648
|
}
|
|
562
649
|
} else {
|
|
563
650
|
// Cap pagination to prevent runaway page-through of broad queries
|
|
@@ -583,10 +670,10 @@ export const searchTool = (options = {}) => {
|
|
|
583
670
|
}
|
|
584
671
|
// Append contextual hint for ticket/issue ID queries
|
|
585
672
|
if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, '').trim())) {
|
|
586
|
-
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).';
|
|
673
|
+
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).' + circuitBreakerWarning;
|
|
587
674
|
}
|
|
588
675
|
// Add a hint when approaching the circuit breaker threshold
|
|
589
|
-
if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
|
|
676
|
+
if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1 && !circuitBreakerWarning) {
|
|
590
677
|
const isSubfolderWarn = path && path !== effectiveSearchCwd && path !== '.';
|
|
591
678
|
const warnScopeHint = isSubfolderWarn
|
|
592
679
|
? ` You are searching in "${path}" — consider searching from the workspace root or a different directory.`
|
|
@@ -603,7 +690,7 @@ export const searchTool = (options = {}) => {
|
|
|
603
690
|
if (options.fileTracker && typeof result === 'string') {
|
|
604
691
|
options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {});
|
|
605
692
|
}
|
|
606
|
-
return result;
|
|
693
|
+
return typeof result === 'string' ? result + circuitBreakerWarning : result;
|
|
607
694
|
} catch (error) {
|
|
608
695
|
console.error('Error executing search command:', error);
|
|
609
696
|
const formatted = formatErrorForAI(error);
|
|
@@ -614,13 +701,76 @@ export const searchTool = (options = {}) => {
|
|
|
614
701
|
}
|
|
615
702
|
}
|
|
616
703
|
|
|
704
|
+
// ── Delegate-level semantic dedup ────────────────────────────
|
|
705
|
+
// Each delegate is a full flash agent session (minutes, not seconds).
|
|
706
|
+
// Use LLM to detect semantic duplicates and suggest rewrites.
|
|
707
|
+
// Compare against ALL previous delegations (not filtered by path) because
|
|
708
|
+
// the parent model often narrows the path while asking the same concept
|
|
709
|
+
// (e.g., "dedup" at /src → "deduplicate" at /src/search.js).
|
|
710
|
+
const delegatePath = searchPath || '';
|
|
711
|
+
|
|
712
|
+
let effectiveQuery = searchQuery;
|
|
713
|
+
|
|
714
|
+
if (previousDelegations.length > 0) {
|
|
715
|
+
// Lazily create the dedup model (same provider/model as delegate)
|
|
716
|
+
if (cachedDedupModel === undefined) {
|
|
717
|
+
const dedupProvider = options.searchDelegateProvider || process.env.PROBE_SEARCH_DELEGATE_PROVIDER || options.provider || process.env.FORCE_PROVIDER || null;
|
|
718
|
+
const dedupModelName = options.searchDelegateModel || process.env.PROBE_SEARCH_DELEGATE_MODEL || options.model || process.env.MODEL_NAME || null;
|
|
719
|
+
if (debug) {
|
|
720
|
+
console.error(`[DEDUP-LLM] Creating model: provider=${dedupProvider}, model=${dedupModelName}`);
|
|
721
|
+
}
|
|
722
|
+
cachedDedupModel = await createLanguageModel(dedupProvider, dedupModelName);
|
|
723
|
+
if (debug) {
|
|
724
|
+
console.error(`[DEDUP-LLM] Model created: ${cachedDedupModel ? 'success' : 'null'}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const dedupSpanAttrs = {
|
|
729
|
+
'dedup.query': searchQuery,
|
|
730
|
+
'dedup.previous_count': String(previousDelegations.length),
|
|
731
|
+
'dedup.previous_queries': previousDelegations.map(d => d.query).join(' | '),
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const dedup = options.tracer?.withSpan
|
|
735
|
+
? await options.tracer.withSpan('search.delegate.dedup', async () => {
|
|
736
|
+
return await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
|
|
737
|
+
}, dedupSpanAttrs, (span, result) => {
|
|
738
|
+
span.setAttributes({
|
|
739
|
+
'dedup.action': result.action,
|
|
740
|
+
'dedup.reason': result.reason || '',
|
|
741
|
+
'dedup.rewritten': result.rewritten || '',
|
|
742
|
+
});
|
|
743
|
+
})
|
|
744
|
+
: await checkDelegateDedup(searchQuery, previousDelegations, cachedDedupModel, debug);
|
|
745
|
+
|
|
746
|
+
if (debug) {
|
|
747
|
+
console.error(`[DEDUP-LLM] Query: "${searchQuery}" → ${dedup.action}: ${dedup.reason}${dedup.rewritten ? ` → "${dedup.rewritten}"` : ''}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (dedup.action === 'block') {
|
|
751
|
+
const prevQueries = previousDelegations.map(d => `"${d.query}"`).join(', ');
|
|
752
|
+
return `DELEGATE BLOCKED: "${searchQuery}" is semantically duplicate of previous delegation(s) [${prevQueries}]. ${dedup.reason}\n\nDo NOT re-delegate the same concept. Use extract() on files already found, or synthesize your answer from existing results.`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (dedup.action === 'rewrite' && dedup.rewritten) {
|
|
756
|
+
effectiveQuery = dedup.rewritten;
|
|
757
|
+
if (debug) {
|
|
758
|
+
console.error(`[DEDUP-LLM] Rewritten query: "${searchQuery}" → "${effectiveQuery}"`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Record this delegation
|
|
764
|
+
const delegationRecord = { query: effectiveQuery, path: delegatePath, hadResults: false };
|
|
765
|
+
previousDelegations.push(delegationRecord);
|
|
766
|
+
|
|
617
767
|
try {
|
|
618
768
|
if (debug) {
|
|
619
|
-
console.error(`Delegating search with query: "${
|
|
769
|
+
console.error(`Delegating search with query: "${effectiveQuery}", path: "${searchPath}"${effectiveQuery !== searchQuery ? ` (rewritten from: "${searchQuery}")` : ''}`);
|
|
620
770
|
}
|
|
621
771
|
|
|
622
772
|
const delegateTask = buildSearchDelegateTask({
|
|
623
|
-
searchQuery,
|
|
773
|
+
searchQuery: effectiveQuery,
|
|
624
774
|
searchPath,
|
|
625
775
|
exact,
|
|
626
776
|
language,
|
|
@@ -650,20 +800,37 @@ export const searchTool = (options = {}) => {
|
|
|
650
800
|
const delegateResult = options.tracer?.withSpan
|
|
651
801
|
? await options.tracer.withSpan('search.delegate', runDelegation, {
|
|
652
802
|
'search.query': searchQuery,
|
|
653
|
-
'search.path': searchPath
|
|
803
|
+
'search.path': searchPath,
|
|
804
|
+
...(effectiveQuery !== searchQuery ? { 'search.query.rewritten': effectiveQuery } : {})
|
|
654
805
|
}, (span, result) => {
|
|
655
|
-
const text = typeof result === 'string' ? result : '';
|
|
806
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result) || '';
|
|
807
|
+
if (debug) console.error(`[search-delegate] onResult: type=${typeof result}, length=${text.length}`);
|
|
656
808
|
span.setAttributes({
|
|
657
809
|
'search.delegate.output': truncateForSpan(text),
|
|
658
|
-
'search.delegate.output_length': text.length
|
|
810
|
+
'search.delegate.output_length': String(text.length)
|
|
659
811
|
});
|
|
660
812
|
})
|
|
661
813
|
: await runDelegation();
|
|
662
814
|
|
|
663
|
-
const
|
|
664
|
-
|
|
815
|
+
const structured = parseDelegatedResponse(delegateResult);
|
|
816
|
+
// Update delegation tracking with outcome (feeds into LLM dedup context)
|
|
817
|
+
if (delegationRecord && structured) {
|
|
818
|
+
delegationRecord.hadResults = structured.groups.length > 0;
|
|
819
|
+
delegationRecord.reason = structured.reason || '';
|
|
820
|
+
delegationRecord.groups = structured.groups.map(g => ({ reason: g.reason }));
|
|
821
|
+
}
|
|
822
|
+
if (!structured || structured.groups.length === 0) {
|
|
823
|
+
// If the delegate explicitly concluded nothing was found (low confidence + reason),
|
|
824
|
+
// return that verdict instead of falling back to raw search which would
|
|
825
|
+
// return tangentially related results and mislead the parent agent.
|
|
826
|
+
if (structured && structured.confidence === 'low' && structured.reason) {
|
|
827
|
+
if (debug) {
|
|
828
|
+
console.error(`Delegated search explicitly found nothing: ${structured.reason}`);
|
|
829
|
+
}
|
|
830
|
+
return `NOT FOUND: The search delegate thoroughly searched for "${searchQuery}" and concluded: ${structured.reason}\n\nDo NOT search for analogies or loosely related concepts. If the feature does not exist in the codebase, say so in your final answer.`;
|
|
831
|
+
}
|
|
665
832
|
if (debug) {
|
|
666
|
-
console.error('Delegated search returned no
|
|
833
|
+
console.error('Delegated search returned no results; falling back to raw search');
|
|
667
834
|
}
|
|
668
835
|
const fallbackResult = maybeAnnotate(await runRawSearch());
|
|
669
836
|
if (options.fileTracker && typeof fallbackResult === 'string') {
|
|
@@ -672,78 +839,52 @@ export const searchTool = (options = {}) => {
|
|
|
672
839
|
return fallbackResult;
|
|
673
840
|
}
|
|
674
841
|
|
|
675
|
-
//
|
|
676
|
-
// It returns paths relative to that workspace root. Resolve against the same base.
|
|
842
|
+
// Resolve and validate file paths in each group
|
|
677
843
|
const delegateBase = options.allowedFolders?.[0] || options.cwd || '.';
|
|
678
844
|
const resolutionBase = searchPaths[0] || options.cwd || '.';
|
|
679
|
-
const
|
|
845
|
+
const wsPrefix = resolutionBase.endsWith('/') ? resolutionBase : resolutionBase + '/';
|
|
680
846
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
847
|
+
for (const group of structured.groups) {
|
|
848
|
+
group.files = group.files
|
|
849
|
+
.map(target => resolveTargetPath(target, delegateBase))
|
|
850
|
+
.map(target => {
|
|
851
|
+
const { filePart, suffix } = splitTargetSuffix(target);
|
|
685
852
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
validatedTargets.push(target);
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
853
|
+
// 1. Path exists as-is
|
|
854
|
+
if (existsSync(filePart)) return target;
|
|
691
855
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
break;
|
|
856
|
+
// 2. Fix doubled directory segments: /ws/proj/proj/src → /ws/proj/src
|
|
857
|
+
const parts = filePart.split('/').filter(Boolean);
|
|
858
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
859
|
+
if (parts[i] === parts[i + 1]) {
|
|
860
|
+
const candidate = '/' + [...parts.slice(0, i), ...parts.slice(i + 1)].join('/');
|
|
861
|
+
if (existsSync(candidate)) {
|
|
862
|
+
if (debug) console.error(`[search-delegate] Fixed doubled path: ${filePart} → ${candidate}`);
|
|
863
|
+
return candidate + suffix;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
703
866
|
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (fixed) continue;
|
|
707
|
-
|
|
708
|
-
// 3. Try resolving against alternative bases (searchPaths[0], cwd)
|
|
709
|
-
for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
|
|
710
|
-
if (altBase === delegateBase) continue;
|
|
711
|
-
const altResolved = resolveTargetPath(target, altBase);
|
|
712
|
-
const { filePart: altFile } = splitTargetSuffix(altResolved);
|
|
713
|
-
if (existsSync(altFile)) {
|
|
714
|
-
validatedTargets.push(altResolved);
|
|
715
|
-
if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} → ${altFile}`);
|
|
716
|
-
fixed = true;
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
if (fixed) continue;
|
|
721
867
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
if (outline) {
|
|
735
|
-
extractOptions.format = 'xml';
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const extractResult = await extract(extractOptions);
|
|
868
|
+
// 3. Try alternative bases
|
|
869
|
+
for (const altBase of [resolutionBase, options.cwd].filter(Boolean)) {
|
|
870
|
+
if (altBase === delegateBase) continue;
|
|
871
|
+
const altResolved = resolveTargetPath(target, altBase);
|
|
872
|
+
const { filePart: altFile } = splitTargetSuffix(altResolved);
|
|
873
|
+
if (existsSync(altFile)) {
|
|
874
|
+
if (debug) console.error(`[search-delegate] Resolved with alt base: ${filePart} → ${altFile}`);
|
|
875
|
+
return altResolved;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
739
878
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
879
|
+
if (debug) console.error(`[search-delegate] Warning: target may not exist: ${filePart}`);
|
|
880
|
+
return target;
|
|
881
|
+
})
|
|
882
|
+
// Strip workspace prefix to make paths relative
|
|
883
|
+
.map(target => target.split(wsPrefix).join(''));
|
|
744
884
|
}
|
|
745
885
|
|
|
746
|
-
|
|
886
|
+
// Return structured JSON for the parent AI to decide what to extract
|
|
887
|
+
return JSON.stringify(structured, null, 2);
|
|
747
888
|
} catch (error) {
|
|
748
889
|
console.error('Delegated search failed, falling back to raw search:', error);
|
|
749
890
|
try {
|