Vane Question Processing Workflow: From API Request to Streamed Answer

Vane processes user questions through a seven-stage pipeline that classifies intent, executes parallel widgets, runs bounded iterative research with tool calls, and streams token-level responses while aggregating sources for citation.

When a user submits a query to the ItzCrazyKns/Vane open-source search engine, the system transforms raw input into a fully cited answer through an event-driven orchestration layer. This high-level workflow for processing a question in Vane combines deterministic state management with LLM-driven reasoning to balance speed against research depth.

The Seven Stages of Vane's Question Processing Pipeline

1. API Entry Point and Session Initialization

All requests enter through POST /api/search in src/app/api/search/route.ts. The handler validates the payload, dynamically loads the requested LLM and embedding providers via src/lib/models/registry.ts, and instantiates a fresh session via SessionManager. Each session receives a unique identifier and a 30-minute TTL (time-to-live) to prevent stale state accumulation.

2. Intent Classification

The APISearchAgent defined in src/lib/agents/search/api.ts invokes the classifier LLM implemented in src/lib/agents/search/classifier.ts. This step generates a structured decision object that determines whether to skip search, which widgets to activate, and which source types (web, news, academic) to query. The classifier uses the classifierPrompt template from src/lib/prompts/ to shape its reasoning.

3. Parallel Widget Execution

If the classifier flags UI-side widgets (weather, stock prices, calculations), the WidgetExecutor fires these concurrently from src/lib/agents/search/widgets/. Widget results are collected as blocks and stored in the session state via SessionManager.emitBlock without blocking the research pipeline.

4. Iterative Research and Tool Calling

When the classifier does not request a skip, the system instantiates a Researcher from src/lib/agents/search/researcher/index.ts. This component runs a bounded loop configurable by optimization mode:

  • speed: 2 iterations
  • balanced: 6 iterations
  • quality: 25 iterations

Each iteration sends the getResearcherPrompt system prompt and conversation history to the LLM, allowing the model to emit tool calls (web search, file lookup, etc.). Tool calls are deduplicated, executed, and their results fed back to the LLM. All intermediate reasoning and tool output are stored as blocks inside the session, enabling the front-end to display a real-time research log.

5. Source Aggregation and Deduplication

After the research loop completes, the raw findings are filtered for duplicate URLs and emitted as a final source block. This aggregation happens within src/lib/session.ts before synthesis begins, ensuring the final answer cites only unique, verified sources.

6. Answer Synthesis

The APISearchAgent constructs a writer prompt using getWriterPrompt from src/lib/prompts/, combining filtered search results, widget output, and system instructions. This prompt is sent to the chosen LLM using streamText, which yields token chunks for incremental delivery.

7. Streaming and Response Delivery

The HTTP handler in src/app/api/search/route.ts subscribes to session events. For streaming requests, it returns a ReadableStream forwarding events as NDJSON with types response, sources, and done. Non-streaming requests collect all chunks from the 'data' and 'end' events to return a single JSON payload containing the complete message and citation list.

Configuration Options and Optimization Modes

The pipeline behavior is controlled via the request payload's optimizationMode parameter. The speed mode minimizes latency with shallow research, while quality mode allows up to 25 research iterations for comprehensive answers. The balanced default (6 iterations) offers a middle ground for most general queries.

Integration Examples

Streaming Request Handling

// example.js
const payload = {
  optimizationMode: "balanced",
  sources: ["web", "news"],               // any of SearchSources enum
  chatModel: { providerId: "openai", key: "gpt-4o-mini" },
  embeddingModel: { providerId: "openai", key: "text-embedding-3-large" },
  query: "What are the latest trends in renewable energy?",
  history: [],                             // optional prior turns
  stream: true,                            // set false for single JSON response
};

fetch("https://your-vane-instance/api/search", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
})
  .then((res) => {
    if (!payload.stream) return res.json();
    // Streaming NDJSON handling
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    function pump() {
      return reader.read().then(({ done, value }) => {
        if (done) return;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop(); // keep incomplete line
        for (const line of lines) {
          if (!line) continue;
          const msg = JSON.parse(line);
          if (msg.type === "response") console.log("▉", msg.data);
          if (msg.type === "sources") console.log("📚 sources:", msg.data);
          if (msg.type === "done") console.log("✅ finished");
        }
        return pump();
      });
    }
    return pump();
  })
  .catch(console.error);

Non-Streamed Synchronous Query

async function askQuestion(q) {
  const res = await fetch("/api/search", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      optimizationMode: "speed",
      sources: ["web"],
      chatModel: { providerId: "openai", key: "gpt-4o-mini" },
      embeddingModel: { providerId: "openai", key: "text-embedding-3-large" },
      query: q,
      history: [],
      stream: false,
    }),
  });

  const { message, sources } = await res.json();
  console.log("Answer:", message);
  console.log("Cited sources:", sources);
}

Summary

  • Vane's question processing workflow begins at src/app/api/search/route.ts and proceeds through classification, widget execution, and iterative research before synthesis.
  • The Researcher component in src/lib/agents/search/researcher/index.ts manages bounded tool-call loops (2-25 iterations) based on the selected optimization mode.
  • All intermediate state is stored as blocks via SessionManager.emitBlock, enabling real-time progress visualization and source deduplication.
  • Responses stream as NDJSON events for reactive UIs, or aggregate into single JSON payloads when stream: false.
  • Sessions automatically expire after 30 minutes to prevent resource leaks.

Frequently Asked Questions

What determines whether Vane skips the research step?

The classifier LLM in src/lib/agents/search/classifier.ts evaluates the query intent and returns a structured decision object. If it sets skipSearch: true, the pipeline bypasses the Researcher and proceeds directly to answer synthesis using only widget data or conversational context.

How does Vane handle real-time progress updates during research?

The system uses an event-driven session manager (src/lib/session.ts) that emits blocks for every research iteration, tool call, and source discovery. These blocks stream to the client as NDJSON with specific type identifiers, allowing the UI to render step-by-step progress indicators.

What is the difference between speed, balanced, and quality optimization modes?

These modes control the Researcher iteration limit defined in src/lib/agents/search/researcher/index.ts. Speed mode caps research at 2 iterations for minimal latency, balanced uses 6 iterations for general queries, and quality allows up to 25 iterations for deep, comprehensive investigations requiring multiple tool-call rounds.

How does Vane prevent duplicate sources from appearing in citations?

During the aggregation phase, the Researcher filters raw search findings for duplicate URLs before emitting the final source block. This deduplication occurs within the session state before the writer prompt is constructed, ensuring the synthesis step receives only unique reference material.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →