@probelabs/probe 0.6.0-rc210 → 0.6.0-rc212
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-rc212-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc212-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc212-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc212-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc212-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +19 -3
- package/build/agent/index.js +639 -75
- package/build/agent/probeTool.js +11 -2
- package/build/agent/tools.js +8 -0
- package/build/index.js +6 -1
- package/build/search.js +2 -2
- package/build/tools/analyzeAll.js +624 -0
- package/build/tools/common.js +149 -85
- package/build/tools/langchain.js +1 -1
- package/build/tools/vercel.js +61 -2
- package/cjs/agent/ProbeAgent.cjs +9698 -6756
- package/cjs/index.cjs +9702 -6754
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +19 -3
- package/src/agent/probeTool.js +11 -2
- package/src/agent/tools.js +8 -0
- package/src/index.js +6 -1
- package/src/search.js +2 -2
- package/src/tools/analyzeAll.js +624 -0
- package/src/tools/common.js +149 -85
- package/src/tools/langchain.js +1 -1
- package/src/tools/vercel.js +61 -2
- package/bin/binaries/probe-v0.6.0-rc210-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent bulk data analysis tool using agentic planning + map-reduce pattern
|
|
3
|
+
*
|
|
4
|
+
* Three-phase approach:
|
|
5
|
+
* 1. PLANNING: Analyze the question, explore data, determine optimal strategy
|
|
6
|
+
* 2. PROCESSING: Map-reduce with the determined strategy
|
|
7
|
+
* 3. SYNTHESIS: Comprehensive final answer with evidence
|
|
8
|
+
*
|
|
9
|
+
* @module tools/analyzeAll
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { search } from '../search.js';
|
|
13
|
+
import { delegate } from '../delegate.js';
|
|
14
|
+
|
|
15
|
+
// Default chunk size in tokens (should fit comfortably in LLM context)
|
|
16
|
+
const DEFAULT_CHUNK_SIZE_TOKENS = 8000;
|
|
17
|
+
// Maximum parallel workers for map phase
|
|
18
|
+
const MAX_PARALLEL_WORKERS = 3;
|
|
19
|
+
// Maximum chunks to process (safety limit)
|
|
20
|
+
const MAX_CHUNKS = 50;
|
|
21
|
+
// Rough estimate: 1 token ≈ 4 characters
|
|
22
|
+
const CHARS_PER_TOKEN = 4;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Estimate token count from string length
|
|
26
|
+
* @param {string} text - Text to estimate tokens for
|
|
27
|
+
* @returns {number} Estimated token count
|
|
28
|
+
*/
|
|
29
|
+
function estimateTokens(text) {
|
|
30
|
+
if (!text) return 0;
|
|
31
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip <result> tags from AI response
|
|
36
|
+
* @param {string} text - Text that may contain <result> tags
|
|
37
|
+
* @returns {string} Text with tags removed
|
|
38
|
+
*/
|
|
39
|
+
function stripResultTags(text) {
|
|
40
|
+
if (!text) return text;
|
|
41
|
+
return text
|
|
42
|
+
.replace(/^<result>\s*/i, '')
|
|
43
|
+
.replace(/\s*<\/result>$/i, '')
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse the planning phase result to extract strategy
|
|
49
|
+
* @param {string} planningResult - Raw planning result from AI
|
|
50
|
+
* @returns {Object} Parsed strategy with query, aggregation, extractionPrompt
|
|
51
|
+
*/
|
|
52
|
+
function parsePlanningResult(planningResult) {
|
|
53
|
+
const result = {
|
|
54
|
+
searchQuery: null,
|
|
55
|
+
aggregation: 'summarize',
|
|
56
|
+
extractionPrompt: null,
|
|
57
|
+
reasoning: null
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Extract SEARCH_QUERY
|
|
61
|
+
const queryMatch = planningResult.match(/SEARCH_QUERY:\s*(.+?)(?=\n[A-Z_]+:|$)/s);
|
|
62
|
+
if (queryMatch) {
|
|
63
|
+
result.searchQuery = queryMatch[1].trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract AGGREGATION
|
|
67
|
+
const aggMatch = planningResult.match(/AGGREGATION:\s*(summarize|list_unique|count|group_by)/i);
|
|
68
|
+
if (aggMatch) {
|
|
69
|
+
result.aggregation = aggMatch[1].toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract EXTRACTION_PROMPT
|
|
73
|
+
const extractMatch = planningResult.match(/EXTRACTION_PROMPT:\s*(.+?)(?=\n[A-Z_]+:|$)/s);
|
|
74
|
+
if (extractMatch) {
|
|
75
|
+
result.extractionPrompt = extractMatch[1].trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Extract REASONING (optional)
|
|
79
|
+
const reasoningMatch = planningResult.match(/REASONING:\s*(.+?)$/s);
|
|
80
|
+
if (reasoningMatch) {
|
|
81
|
+
result.reasoning = reasoningMatch[1].trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Split search results into chunks that fit within token limits
|
|
89
|
+
* @param {string} searchResults - Raw search results string
|
|
90
|
+
* @param {number} chunkSizeTokens - Maximum tokens per chunk
|
|
91
|
+
* @returns {Array<{id: number, total: number, content: string, estimatedTokens: number}>}
|
|
92
|
+
*/
|
|
93
|
+
function chunkResults(searchResults, chunkSizeTokens) {
|
|
94
|
+
const chunks = [];
|
|
95
|
+
const chunkSizeChars = chunkSizeTokens * CHARS_PER_TOKEN;
|
|
96
|
+
|
|
97
|
+
// Split by file blocks (each file block starts with ```)
|
|
98
|
+
// This ensures we don't split in the middle of a code block
|
|
99
|
+
const fileBlocks = searchResults.split(/(?=^```)/m);
|
|
100
|
+
|
|
101
|
+
let currentChunk = '';
|
|
102
|
+
let currentTokens = 0;
|
|
103
|
+
|
|
104
|
+
for (const block of fileBlocks) {
|
|
105
|
+
const blockTokens = estimateTokens(block);
|
|
106
|
+
|
|
107
|
+
// If a single block is larger than chunk size, we need to include it anyway
|
|
108
|
+
// but in its own chunk
|
|
109
|
+
if (blockTokens > chunkSizeTokens && currentChunk.length > 0) {
|
|
110
|
+
// Save current chunk first
|
|
111
|
+
chunks.push({
|
|
112
|
+
id: chunks.length + 1,
|
|
113
|
+
total: 0,
|
|
114
|
+
content: currentChunk.trim(),
|
|
115
|
+
estimatedTokens: currentTokens
|
|
116
|
+
});
|
|
117
|
+
currentChunk = '';
|
|
118
|
+
currentTokens = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if adding this block would exceed chunk size
|
|
122
|
+
if (currentTokens + blockTokens > chunkSizeTokens && currentChunk.length > 0) {
|
|
123
|
+
// Save current chunk
|
|
124
|
+
chunks.push({
|
|
125
|
+
id: chunks.length + 1,
|
|
126
|
+
total: 0,
|
|
127
|
+
content: currentChunk.trim(),
|
|
128
|
+
estimatedTokens: currentTokens
|
|
129
|
+
});
|
|
130
|
+
currentChunk = '';
|
|
131
|
+
currentTokens = 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add block to current chunk
|
|
135
|
+
currentChunk += block;
|
|
136
|
+
currentTokens += blockTokens;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Don't forget the last chunk
|
|
140
|
+
if (currentChunk.trim().length > 0) {
|
|
141
|
+
chunks.push({
|
|
142
|
+
id: chunks.length + 1,
|
|
143
|
+
total: 0,
|
|
144
|
+
content: currentChunk.trim(),
|
|
145
|
+
estimatedTokens: currentTokens
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update total count in all chunks
|
|
150
|
+
const totalChunks = chunks.length;
|
|
151
|
+
for (const chunk of chunks) {
|
|
152
|
+
chunk.total = totalChunks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return chunks;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Process a single chunk using delegate
|
|
160
|
+
* @param {Object} chunk - Chunk to process
|
|
161
|
+
* @param {string} extractionPrompt - What to extract from the chunk
|
|
162
|
+
* @param {Object} options - Delegate options
|
|
163
|
+
* @returns {Promise<{chunk: Object, result: string}>}
|
|
164
|
+
*/
|
|
165
|
+
async function processChunk(chunk, extractionPrompt, options) {
|
|
166
|
+
const task = `You are analyzing search results (chunk ${chunk.id} of ${chunk.total}).
|
|
167
|
+
|
|
168
|
+
Your task: ${extractionPrompt}
|
|
169
|
+
|
|
170
|
+
Search Results:
|
|
171
|
+
${chunk.content}
|
|
172
|
+
|
|
173
|
+
Instructions:
|
|
174
|
+
- Extract ALL relevant information matching the analysis task
|
|
175
|
+
- Be specific and include actual names, values, patterns found
|
|
176
|
+
- Format as a structured list if multiple items found
|
|
177
|
+
- If nothing relevant is found in this chunk, respond with "No relevant items found in this chunk."
|
|
178
|
+
- Do NOT summarize the code - extract the specific information requested
|
|
179
|
+
- IMPORTANT: When completing, always use the FULL format: <attempt_completion><result>YOUR ANSWER HERE</result></attempt_completion>
|
|
180
|
+
- Do NOT use the shorthand <attempt_complete></attempt_complete> format`;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await delegate({
|
|
184
|
+
task,
|
|
185
|
+
debug: options.debug,
|
|
186
|
+
parentSessionId: options.sessionId,
|
|
187
|
+
path: options.path,
|
|
188
|
+
allowedFolders: options.allowedFolders,
|
|
189
|
+
provider: options.provider,
|
|
190
|
+
model: options.model,
|
|
191
|
+
tracer: options.tracer,
|
|
192
|
+
enableBash: false,
|
|
193
|
+
promptType: 'code-researcher',
|
|
194
|
+
allowedTools: ['extract'],
|
|
195
|
+
maxIterations: 5,
|
|
196
|
+
timeout: 120
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { chunk, result };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { chunk, result: `Error processing chunk ${chunk.id}: ${error.message}` };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Process chunks in parallel with concurrency limit
|
|
207
|
+
* @param {Array} chunks - Chunks to process
|
|
208
|
+
* @param {string} extractionPrompt - What to extract
|
|
209
|
+
* @param {number} maxWorkers - Maximum concurrent workers
|
|
210
|
+
* @param {Object} options - Options to pass to processChunk
|
|
211
|
+
* @returns {Promise<Array<{chunk: Object, result: string}>>}
|
|
212
|
+
*/
|
|
213
|
+
async function processChunksParallel(chunks, extractionPrompt, maxWorkers, options) {
|
|
214
|
+
const results = [];
|
|
215
|
+
const queue = [...chunks];
|
|
216
|
+
const active = new Set();
|
|
217
|
+
|
|
218
|
+
while (queue.length > 0 || active.size > 0) {
|
|
219
|
+
while (active.size < maxWorkers && queue.length > 0) {
|
|
220
|
+
const chunk = queue.shift();
|
|
221
|
+
|
|
222
|
+
const promise = processChunk(chunk, extractionPrompt, options).then(result => {
|
|
223
|
+
active.delete(promise);
|
|
224
|
+
return result;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
active.add(promise);
|
|
228
|
+
|
|
229
|
+
if (options.debug) {
|
|
230
|
+
console.error(`[analyze_all] Started processing chunk ${chunk.id}/${chunk.total}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (active.size > 0) {
|
|
235
|
+
const result = await Promise.race(active);
|
|
236
|
+
results.push(result);
|
|
237
|
+
|
|
238
|
+
if (options.debug) {
|
|
239
|
+
console.error(`[analyze_all] Completed chunk ${result.chunk.id}/${result.chunk.total}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
results.sort((a, b) => a.chunk.id - b.chunk.id);
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Aggregate results from all chunks based on aggregation strategy
|
|
250
|
+
* @param {Array<{chunk: Object, result: string}>} chunkResults - Results from all chunks
|
|
251
|
+
* @param {string} aggregation - Aggregation strategy
|
|
252
|
+
* @param {string} extractionPrompt - Original extraction prompt for context
|
|
253
|
+
* @param {Object} options - Delegate options
|
|
254
|
+
* @returns {Promise<string>}
|
|
255
|
+
*/
|
|
256
|
+
async function aggregateResults(chunkResults, aggregation, extractionPrompt, options) {
|
|
257
|
+
const meaningfulResults = chunkResults.filter(r =>
|
|
258
|
+
r.result &&
|
|
259
|
+
!r.result.toLowerCase().includes('no relevant items found') &&
|
|
260
|
+
r.result.trim().length > 0
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (meaningfulResults.length === 0) {
|
|
264
|
+
return 'No relevant information found across all chunks.';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (meaningfulResults.length === 1) {
|
|
268
|
+
return stripResultTags(meaningfulResults[0].result);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const chunkSummaries = meaningfulResults
|
|
272
|
+
.map(r => `--- Chunk ${r.chunk.id} ---\n${stripResultTags(r.result)}`)
|
|
273
|
+
.join('\n\n');
|
|
274
|
+
|
|
275
|
+
const completionNote = `\n\nIMPORTANT: When completing, always use the FULL format: <attempt_completion><result>YOUR ANSWER HERE</result></attempt_completion>`;
|
|
276
|
+
|
|
277
|
+
const aggregationPrompts = {
|
|
278
|
+
summarize: `Synthesize these analyses into a comprehensive summary. Combine related findings, remove redundancy, and present a coherent overview.
|
|
279
|
+
|
|
280
|
+
Original task: ${extractionPrompt}
|
|
281
|
+
|
|
282
|
+
Chunk analyses:
|
|
283
|
+
${chunkSummaries}
|
|
284
|
+
|
|
285
|
+
Provide a unified summary that captures all key findings.${completionNote}`,
|
|
286
|
+
|
|
287
|
+
list_unique: `Combine these lists and remove duplicates. Create a single deduplicated list of all unique items found.
|
|
288
|
+
|
|
289
|
+
Original task: ${extractionPrompt}
|
|
290
|
+
|
|
291
|
+
Chunk analyses:
|
|
292
|
+
${chunkSummaries}
|
|
293
|
+
|
|
294
|
+
Return a deduplicated, organized list of all unique items. Group related items if helpful.${completionNote}`,
|
|
295
|
+
|
|
296
|
+
count: `Count and aggregate the findings from these analyses. Provide total counts and breakdowns.
|
|
297
|
+
|
|
298
|
+
Original task: ${extractionPrompt}
|
|
299
|
+
|
|
300
|
+
Chunk analyses:
|
|
301
|
+
${chunkSummaries}
|
|
302
|
+
|
|
303
|
+
Provide accurate counts and a summary of all occurrences found.${completionNote}`,
|
|
304
|
+
|
|
305
|
+
group_by: `Group and categorize all items from these analyses. Organize findings into logical categories.
|
|
306
|
+
|
|
307
|
+
Original task: ${extractionPrompt}
|
|
308
|
+
|
|
309
|
+
Chunk analyses:
|
|
310
|
+
${chunkSummaries}
|
|
311
|
+
|
|
312
|
+
Organize all findings into clear categories with items listed under each.${completionNote}`
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const aggregationTask = aggregationPrompts[aggregation] || aggregationPrompts.summarize;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const result = await delegate({
|
|
319
|
+
task: aggregationTask,
|
|
320
|
+
debug: options.debug,
|
|
321
|
+
parentSessionId: options.sessionId,
|
|
322
|
+
path: options.path,
|
|
323
|
+
allowedFolders: options.allowedFolders,
|
|
324
|
+
provider: options.provider,
|
|
325
|
+
model: options.model,
|
|
326
|
+
tracer: options.tracer,
|
|
327
|
+
enableBash: false,
|
|
328
|
+
promptType: 'code-researcher',
|
|
329
|
+
allowedTools: [],
|
|
330
|
+
maxIterations: 5,
|
|
331
|
+
timeout: 120
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return `Aggregation failed (${error.message}). Raw results:\n\n${chunkSummaries}`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* PHASE 1: Planning - Analyze the question and determine optimal strategy
|
|
342
|
+
* @param {string} question - The user's free-form question
|
|
343
|
+
* @param {string} path - Path to search in
|
|
344
|
+
* @param {Object} options - Delegate options
|
|
345
|
+
* @returns {Promise<Object>} Strategy object with searchQuery, aggregation, extractionPrompt
|
|
346
|
+
*/
|
|
347
|
+
async function planAnalysis(question, path, options) {
|
|
348
|
+
if (options.debug) {
|
|
349
|
+
console.error(`[analyze_all] Phase 1: Planning analysis strategy...`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const planningTask = `Create an analysis plan for this question about a codebase:
|
|
353
|
+
|
|
354
|
+
"${question}"
|
|
355
|
+
|
|
356
|
+
Search scope: ${path}
|
|
357
|
+
|
|
358
|
+
Use attempt_completion to output your plan in this EXACT format:
|
|
359
|
+
|
|
360
|
+
SEARCH_QUERY: <elasticsearch query using OR for multiple terms, quotes for exact phrases>
|
|
361
|
+
AGGREGATION: <summarize | list_unique | count | group_by>
|
|
362
|
+
EXTRACTION_PROMPT: <what to extract from each search result>
|
|
363
|
+
REASONING: <brief explanation>
|
|
364
|
+
|
|
365
|
+
Example plan:
|
|
366
|
+
SEARCH_QUERY: export OR function OR class OR tool
|
|
367
|
+
AGGREGATION: list_unique
|
|
368
|
+
EXTRACTION_PROMPT: Extract tool names and their purpose
|
|
369
|
+
REASONING: Using list_unique to deduplicate tool definitions
|
|
370
|
+
|
|
371
|
+
IMPORTANT: Use attempt_completion immediately with your plan. Do NOT try to search or answer the question - just create the analysis plan.`;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Planning phase - attempt_completion only
|
|
375
|
+
const result = await delegate({
|
|
376
|
+
task: planningTask,
|
|
377
|
+
debug: options.debug,
|
|
378
|
+
parentSessionId: options.sessionId,
|
|
379
|
+
path: path,
|
|
380
|
+
allowedFolders: [path],
|
|
381
|
+
provider: options.provider,
|
|
382
|
+
model: options.model,
|
|
383
|
+
tracer: options.tracer,
|
|
384
|
+
enableBash: false,
|
|
385
|
+
promptType: 'code-researcher',
|
|
386
|
+
allowedTools: [], // attempt_completion only (default tool)
|
|
387
|
+
maxIterations: 3,
|
|
388
|
+
timeout: 60
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const plan = parsePlanningResult(stripResultTags(result));
|
|
392
|
+
|
|
393
|
+
if (options.debug) {
|
|
394
|
+
console.error(`[analyze_all] Planning complete:`);
|
|
395
|
+
console.error(`[analyze_all] Search Query: ${plan.searchQuery}`);
|
|
396
|
+
console.error(`[analyze_all] Aggregation: ${plan.aggregation}`);
|
|
397
|
+
console.error(`[analyze_all] Extraction: ${plan.extractionPrompt?.substring(0, 100)}...`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return plan;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new Error(`Planning phase failed: ${error.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* PHASE 3: Synthesis - Create comprehensive final answer
|
|
408
|
+
* @param {string} question - Original question
|
|
409
|
+
* @param {string} aggregatedData - Results from map-reduce phase
|
|
410
|
+
* @param {Object} plan - The analysis plan used
|
|
411
|
+
* @param {Object} options - Delegate options
|
|
412
|
+
* @returns {Promise<string>} Final comprehensive answer
|
|
413
|
+
*/
|
|
414
|
+
async function synthesizeAnswer(question, aggregatedData, plan, options) {
|
|
415
|
+
if (options.debug) {
|
|
416
|
+
console.error(`[analyze_all] Phase 3: Synthesizing final answer...`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const synthesisTask = `You analyzed a codebase to answer this question:
|
|
420
|
+
|
|
421
|
+
"${question}"
|
|
422
|
+
|
|
423
|
+
Analysis Strategy Used:
|
|
424
|
+
- Search Query: ${plan.searchQuery}
|
|
425
|
+
- Aggregation Method: ${plan.aggregation}
|
|
426
|
+
- Extraction Focus: ${plan.extractionPrompt}
|
|
427
|
+
|
|
428
|
+
Aggregated Analysis Results:
|
|
429
|
+
${aggregatedData}
|
|
430
|
+
|
|
431
|
+
Now provide a COMPREHENSIVE, DETAILED answer to the original question.
|
|
432
|
+
|
|
433
|
+
Your answer should:
|
|
434
|
+
1. **Directly answer the question** with a clear summary at the top
|
|
435
|
+
2. **Provide specific evidence** - include actual names, values, file locations where relevant
|
|
436
|
+
3. **Organize the information** logically (use categories, lists, or sections as appropriate)
|
|
437
|
+
4. **Note completeness** - mention if the analysis covered all relevant data or if there might be gaps
|
|
438
|
+
5. **Be thorough** - this is the final answer the user will see, make it complete and useful
|
|
439
|
+
|
|
440
|
+
Format your response as a well-structured document that fully answers: "${question}"
|
|
441
|
+
|
|
442
|
+
IMPORTANT: When completing, use the FULL format: <attempt_completion><result>YOUR ANSWER HERE</result></attempt_completion>`;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const result = await delegate({
|
|
446
|
+
task: synthesisTask,
|
|
447
|
+
debug: options.debug,
|
|
448
|
+
parentSessionId: options.sessionId,
|
|
449
|
+
path: options.path,
|
|
450
|
+
allowedFolders: options.allowedFolders,
|
|
451
|
+
provider: options.provider,
|
|
452
|
+
model: options.model,
|
|
453
|
+
tracer: options.tracer,
|
|
454
|
+
enableBash: false,
|
|
455
|
+
promptType: 'code-researcher',
|
|
456
|
+
allowedTools: [],
|
|
457
|
+
maxIterations: 5,
|
|
458
|
+
timeout: 180
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return stripResultTags(result);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// If synthesis fails, return the aggregated data as fallback
|
|
464
|
+
return `Analysis Results for: "${question}"\n\n${aggregatedData}`;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Analyze all data matching a question using intelligent 3-phase approach:
|
|
470
|
+
* 1. PLANNING: Analyze question, determine optimal search and aggregation strategy
|
|
471
|
+
* 2. PROCESSING: Map-reduce with parallel chunk processing
|
|
472
|
+
* 3. SYNTHESIS: Comprehensive final answer with evidence
|
|
473
|
+
*
|
|
474
|
+
* @param {Object} options - Analysis options
|
|
475
|
+
* @param {string} options.question - Free-form question to answer (e.g., "What features are customers using?")
|
|
476
|
+
* @param {string} [options.path='.'] - Directory to search in
|
|
477
|
+
* @param {string} [options.sessionId] - Session ID for caching
|
|
478
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
479
|
+
* @param {string} [options.cwd] - Working directory
|
|
480
|
+
* @param {string[]} [options.allowedFolders] - Allowed folders
|
|
481
|
+
* @param {string} [options.provider] - AI provider
|
|
482
|
+
* @param {string} [options.model] - AI model
|
|
483
|
+
* @param {Object} [options.tracer] - Telemetry tracer
|
|
484
|
+
* @param {number} [options.chunkSizeTokens] - Custom chunk size (default: 8000)
|
|
485
|
+
* @param {number} [options.maxChunks] - Maximum chunks to process (default: 50)
|
|
486
|
+
* @returns {Promise<string>} Comprehensive answer to the question
|
|
487
|
+
*/
|
|
488
|
+
export async function analyzeAll(options) {
|
|
489
|
+
const {
|
|
490
|
+
question,
|
|
491
|
+
path = '.',
|
|
492
|
+
sessionId,
|
|
493
|
+
debug = false,
|
|
494
|
+
cwd,
|
|
495
|
+
allowedFolders,
|
|
496
|
+
provider,
|
|
497
|
+
model,
|
|
498
|
+
tracer,
|
|
499
|
+
chunkSizeTokens = DEFAULT_CHUNK_SIZE_TOKENS,
|
|
500
|
+
maxChunks = MAX_CHUNKS
|
|
501
|
+
} = options;
|
|
502
|
+
|
|
503
|
+
if (!question) {
|
|
504
|
+
throw new Error('The "question" parameter is required.');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const delegateOptions = {
|
|
508
|
+
debug,
|
|
509
|
+
sessionId,
|
|
510
|
+
path: allowedFolders?.[0] || cwd || path,
|
|
511
|
+
allowedFolders,
|
|
512
|
+
provider,
|
|
513
|
+
model,
|
|
514
|
+
tracer
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// ============================================================
|
|
518
|
+
// PHASE 1: Planning
|
|
519
|
+
// ============================================================
|
|
520
|
+
if (debug) {
|
|
521
|
+
console.error(`[analyze_all] Starting analysis`);
|
|
522
|
+
console.error(`[analyze_all] Question: ${question}`);
|
|
523
|
+
console.error(`[analyze_all] Path: ${path}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const plan = await planAnalysis(question, path, delegateOptions);
|
|
527
|
+
|
|
528
|
+
if (!plan.searchQuery) {
|
|
529
|
+
throw new Error('Planning phase failed to determine a search query.');
|
|
530
|
+
}
|
|
531
|
+
if (!plan.extractionPrompt) {
|
|
532
|
+
throw new Error('Planning phase failed to determine an extraction prompt.');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============================================================
|
|
536
|
+
// PHASE 2: Processing (Map-Reduce)
|
|
537
|
+
// ============================================================
|
|
538
|
+
if (debug) {
|
|
539
|
+
console.error(`[analyze_all] Phase 2: Processing data with map-reduce...`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Get ALL search results (no token limit)
|
|
543
|
+
const searchResults = await search({
|
|
544
|
+
query: plan.searchQuery,
|
|
545
|
+
path,
|
|
546
|
+
cwd,
|
|
547
|
+
maxTokens: null,
|
|
548
|
+
allowTests: true,
|
|
549
|
+
session: sessionId
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (!searchResults || searchResults.trim().length === 0) {
|
|
553
|
+
return `No data found matching the analysis plan for: "${question}"\n\nSearch query used: ${plan.searchQuery}\n\nTry rephrasing your question or broadening the scope.`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const totalTokens = estimateTokens(searchResults);
|
|
557
|
+
if (debug) {
|
|
558
|
+
console.error(`[analyze_all] Total search results: ~${totalTokens} tokens`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let aggregatedData;
|
|
562
|
+
|
|
563
|
+
// If results fit in a single chunk, process directly
|
|
564
|
+
if (totalTokens <= chunkSizeTokens) {
|
|
565
|
+
if (debug) {
|
|
566
|
+
console.error(`[analyze_all] Results fit in single chunk, processing directly`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const singleChunk = {
|
|
570
|
+
id: 1,
|
|
571
|
+
total: 1,
|
|
572
|
+
content: searchResults,
|
|
573
|
+
estimatedTokens: totalTokens
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const result = await processChunk(singleChunk, plan.extractionPrompt, delegateOptions);
|
|
577
|
+
aggregatedData = stripResultTags(result.result);
|
|
578
|
+
} else {
|
|
579
|
+
// Chunk and process in parallel
|
|
580
|
+
const chunks = chunkResults(searchResults, chunkSizeTokens);
|
|
581
|
+
|
|
582
|
+
if (debug) {
|
|
583
|
+
console.error(`[analyze_all] Split into ${chunks.length} chunks`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (chunks.length > maxChunks) {
|
|
587
|
+
console.error(`[analyze_all] Warning: Truncating from ${chunks.length} to ${maxChunks} chunks`);
|
|
588
|
+
chunks.length = maxChunks;
|
|
589
|
+
for (const chunk of chunks) {
|
|
590
|
+
chunk.total = maxChunks;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const chunkResultsProcessed = await processChunksParallel(
|
|
595
|
+
chunks,
|
|
596
|
+
plan.extractionPrompt,
|
|
597
|
+
MAX_PARALLEL_WORKERS,
|
|
598
|
+
delegateOptions
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if (debug) {
|
|
602
|
+
console.error(`[analyze_all] All ${chunks.length} chunks processed, starting aggregation`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
aggregatedData = await aggregateResults(
|
|
606
|
+
chunkResultsProcessed,
|
|
607
|
+
plan.aggregation,
|
|
608
|
+
plan.extractionPrompt,
|
|
609
|
+
delegateOptions
|
|
610
|
+
);
|
|
611
|
+
aggregatedData = stripResultTags(aggregatedData);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ============================================================
|
|
615
|
+
// PHASE 3: Synthesis
|
|
616
|
+
// ============================================================
|
|
617
|
+
const finalAnswer = await synthesizeAnswer(question, aggregatedData, plan, delegateOptions);
|
|
618
|
+
|
|
619
|
+
if (debug) {
|
|
620
|
+
console.error(`[analyze_all] Analysis complete`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return finalAnswer;
|
|
624
|
+
}
|