# Vane Question Processing Workflow: From API Request to Streamed Answer

> Understand the Vane question processing workflow from API request to streamed answers. Explore its seven-stage pipeline for intent classification, parallel widgets, iterative research, and token-level responses.

- Repository: [Kushagra Srivastava/Vane](https://github.com/ItzCrazyKns/Vane)
- Tags: architecture
- Published: 2026-03-11

---

**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](https://github.com/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/api.ts) invokes the classifier LLM implemented in [`src/lib/agents/search/classifier.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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

```javascript
// 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

```javascript
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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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`](https://github.com/ItzCrazyKns/Vane/blob/main/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.