@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.
Files changed (32) hide show
  1. package/bin/binaries/probe-v0.6.0-rc212-aarch64-apple-darwin.tar.gz +0 -0
  2. package/bin/binaries/probe-v0.6.0-rc212-aarch64-unknown-linux-musl.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc212-x86_64-apple-darwin.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc212-x86_64-pc-windows-msvc.zip +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc212-x86_64-unknown-linux-musl.tar.gz +0 -0
  6. package/build/agent/ProbeAgent.js +19 -3
  7. package/build/agent/index.js +639 -75
  8. package/build/agent/probeTool.js +11 -2
  9. package/build/agent/tools.js +8 -0
  10. package/build/index.js +6 -1
  11. package/build/search.js +2 -2
  12. package/build/tools/analyzeAll.js +624 -0
  13. package/build/tools/common.js +149 -85
  14. package/build/tools/langchain.js +1 -1
  15. package/build/tools/vercel.js +61 -2
  16. package/cjs/agent/ProbeAgent.cjs +9698 -6756
  17. package/cjs/index.cjs +9702 -6754
  18. package/package.json +1 -1
  19. package/src/agent/ProbeAgent.js +19 -3
  20. package/src/agent/probeTool.js +11 -2
  21. package/src/agent/tools.js +8 -0
  22. package/src/index.js +6 -1
  23. package/src/search.js +2 -2
  24. package/src/tools/analyzeAll.js +624 -0
  25. package/src/tools/common.js +149 -85
  26. package/src/tools/langchain.js +1 -1
  27. package/src/tools/vercel.js +61 -2
  28. package/bin/binaries/probe-v0.6.0-rc210-aarch64-apple-darwin.tar.gz +0 -0
  29. package/bin/binaries/probe-v0.6.0-rc210-aarch64-unknown-linux-musl.tar.gz +0 -0
  30. package/bin/binaries/probe-v0.6.0-rc210-x86_64-apple-darwin.tar.gz +0 -0
  31. package/bin/binaries/probe-v0.6.0-rc210-x86_64-pc-windows-msvc.zip +0 -0
  32. 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
+ }