How Wigolo Decomposes Questions and Synthesizes Cited Reports: A Technical Deep Dive
Wigolo’s research pipeline transforms free-form user questions into structured, citation-rich reports by decomposing queries into sub-questions, executing parallel searches with time budgets, and synthesizing markdown output through LLM sampling or deterministic fallback methods.
The open-source project KnockOutEZ/wigolo implements a sophisticated research agent that breaks down complex inquiries into manageable search operations. This article examines the exact mechanisms the tool uses to parse questions, validate sources, and generate properly cited reports based on the actual TypeScript implementation.
The Research Pipeline Architecture
At the highest level, the system orchestrates three distinct phases: question decomposition, parallel source retrieval, and report synthesis. The entry point in src/tools/research.ts exposes the handleResearch function, which validates incoming requests and delegates to runResearchPipeline in src/research/pipeline.ts. This pipeline returns a ResearchOutput object containing the final markdown report, structured citations, and metadata about sub-queries and timing.
The architecture prioritizes resilience. If the primary LLM sampling method fails at any stage, the system cascades through template-based generation, local model inference, or deterministic fallback reports to ensure the user receives a substantive answer.
Stage 1: Decomposing the Question
The decomposition logic resides in src/research/decompose.ts. The decomposeQuestion function selects a target sub-query count based on the requested depth parameter (quick, standard, or comprehensive), then attempts multiple strategies to generate search queries.
LLM Sampling Approach
When a SamplingCapableServer is available, decomposeWithSampling sends a structured prompt requesting exactly N concise search queries in JSON format. This method leverages the host LLM’s reasoning capabilities to generate contextually relevant sub-questions that cover different aspects of the original inquiry.
Template-Based Fallback
If sampling fails or is unavailable, the system invokes decomposeWithTemplate. This method uses detectQueryType to classify the question as comparison, how-to, concept, or general, then selects a predefined template strategy. Templates construct queries around identified entities or tasks, ensuring domain-appropriate search terms.
Fallback Generator
When templates produce insufficient queries, decomposeWithFallback creates additional sub-queries through linguistic analysis. It extracts noun phrases, splits clauses, and generates keyword variants to populate the query list. The final step deduplicates results, optionally prepends the original verbatim question, and returns the list with a flag indicating whether sampling was used.
Stage 2: Parallel Search and Source Validation
The exploreInParallel function in src/research/pipeline.ts executes each sub-query against a reduced set of search engines simultaneously. This phase operates under strict time constraints defined by SEARCH_PER_QUERY_BUDGET_MS and SEARCH_TOTAL_BUDGET_MS.
Time-Budgeted Search Execution
Results from parallel searches undergo merging and deduplication via deduplicateResults, followed by filtering based on domain reputation, URL structure, and relevance scores. The rerankResults function reorders candidates by quality. An OVER_FETCH_BUFFER_FACTOR ensures the pipeline retrieves extra sources to compensate for potential losses during subsequent content validation.
Content Fetching and Gating
The fetchSources function processes candidate URLs through router.fetch, which retrieves pages with optional JavaScript rendering. The getExtractProvider utility extracts markdown content, titles, and snippets, truncating text to PER_SOURCE_CHAR_CAP. Validated content is cached via cacheContent and optionally embedded for downstream use.
The content gate in src/research/source-validation.ts (invoked via gateContent) filters out shell pages—URLs that return empty or paywalled content. Sources failing validation are tracked as rejected_sources and excluded from the synthesis phase.
Stage 3: Synthesizing the Cited Report
Report generation occurs in src/research/synthesize.ts. The synthesizeReport function branches based on the availability of sampling capabilities.
Sampling-Based Synthesis
When sampling is available, synthesizeWithSampling constructs a prompt containing numbered source blocks (formatted as [1], [2], etc.) and instructs the LLM to produce a markdown report with inline citation markers. The response is trimmed to respect limits.reportChars, ensuring token budget compliance.
Deterministic Fallback Reports
If sampling fails, buildFallbackReport generates a structured markdown document by iterating over fetched sources. Each entry includes a header, source title, URL, and a trimmed excerpt of the markdown content, respecting both per-source and total character caps.
Local Model Fallback
When host-sampling is disabled but a local LLM tier or keystore is configured, synthesizeLocal attempts deterministic synthesis using the local model. Successful local synthesis replaces the report content and regenerates citations to match the new text.
Research Brief Construction
If neither host-sampling nor local synthesis produces output, the pipeline falls back to src/research/brief.ts. The buildResearchBrief function aggregates key facts, comparison entities, and any available local-LLM text into a structured document. renderBriefReport then converts this brief into a readable markdown format, ensuring users receive an organized summary even when full synthesis is unavailable.
Programmatic Usage Examples
You can invoke the research tool directly in your TypeScript applications:
// Example: invoke the research tool programmatically
import { handleResearch } from './src/tools/research.js';
import { mySearchEngines } from './src/search/engines.js';
import { router } from './src/fetch/router.js';
// Input shape matches the API contract
const input = {
question: "How does the Rust async runtime compare to Node.js event loop?",
depth: "standard",
max_sources: 12,
};
const result = await handleResearch(input, mySearchEngines, router);
if (result.ok) {
console.log(result.data.report); // Full markdown report
console.log(result.data.citations); // Structured citations
} else {
console.error(`Research failed: ${result.error_reason}`);
}
For testing or custom workflows, run the pipeline directly:
// Example: directly run the pipeline (useful for testing)
import { runResearchPipeline } from './src/research/pipeline.js';
import { engines } from './src/search/engines.js';
import { router } from './src/fetch/router.js';
const researchOut = await runResearchPipeline(
{ question: "Explain the benefits of GraphQL over REST", depth: "quick" },
engines,
router,
);
console.log(researchOut.report);
Summary
- Question decomposition in
src/research/decompose.tsuses a cascading strategy: LLM sampling first, then template-based generation by query type (comparison, how-to, concept, general), followed by linguistic fallback extraction. - Parallel search execution respects strict time budgets (
SEARCH_PER_QUERY_BUDGET_MS,SEARCH_TOTAL_BUDGET_MS) and uses an over-fetch buffer to ensure sufficient candidates survive content gating. - Source validation removes shell pages and paywalled content through
gateContentinsrc/research/source-validation.tsbefore synthesis begins. - Report synthesis prioritizes LLM sampling with citation markers
[1],[2], but falls back to deterministic templates or local model inference if sampling fails. - Structured output always returns a
ResearchOutputcontaining the markdown report, citation array with URLs and snippets, source objects, and metadata about rejected sources and sub-queries.
Frequently Asked Questions
How does Wigolo handle complex comparison questions?
Wigolo detects comparison queries through detectQueryType in src/research/decompose.ts and selects a comparison-specific template. This template generates sub-queries targeting each entity in the comparison separately, then synthesizes results into a unified report with citations from both sides.
What happens when the LLM sampling service is unavailable?
The system cascades through three fallback layers: first, template-based decomposition using query-type heuristics; second, local model synthesis if configured; third, deterministic fallback reports that structure source excerpts into markdown. This ensures users receive a substantive answer even without sampling capabilities.
How does the research tool prevent citation of low-quality sources?
Sources undergo multi-stage validation. The deduplicateResults and rerankResults functions in src/research/pipeline.ts filter by domain and relevance score. The gateContent function in src/research/source-validation.ts removes shell pages and empty content. Only validated sources with actual markdown content proceed to the synthesis phase.
Can I customize the depth of research performed?
Yes. The depth parameter accepts quick, standard, or comprehensive, which determines the target number of sub-queries generated by decomposeQuestion. Deeper settings increase parallel search breadth and source limits, producing more thorough reports at the cost of higher latency and token consumption.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →