# How Vane's Agent Orchestration Works: A Deep Dive into the Search Pipeline

> Explore Vane's agent orchestration pipeline. Learn how it classifies intent, runs parallel widgets, and uses an iterative LLM research loop for streamed answers. Discover the Vane search flow.

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

---

**Vane's agent orchestration coordinates user requests through a session-driven pipeline that classifies intent, executes parallel widgets, and runs an iterative LLM-driven research loop until generating a final streamed answer.**

Vane's agent orchestration is the core architectural pattern powering the ItzCrazyKns/Vane open-source search assistant. Built around an event-driven **SessionManager** and modular agent components, the system transforms a simple chat message into a coordinated sequence of classification, parallel execution, and iterative tool calling. This article examines the actual TypeScript implementation to show how the `SearchAgent` orchestrates widgets, research tools, and LLM streams to produce contextual answers.

## Understanding the Orchestration Architecture

Vane organizes its backend around three primary concerns managed by the `SearchAgent` class. First, it **classifies the user’s intent** to determine which downstream actions are required. Second, it runs **auxiliary widgets** like weather or stock tickers that provide instant data without full search cycles. Third, it executes an **iterative research loop** where an LLM can invoke arbitrary tools—web search, academic retrieval, or file upload processing—until sufficient information is gathered to generate a final answer.

All components communicate through the **SessionManager**, an event emitter that maintains blocks of data and streams real-time updates to the client via Server-Sent Events (SSE).

## The Entry Point: HTTP API and Session Initialization

When a client POSTs a chat message to `/api/chat`, the orchestration begins in [`src/app/api/chat/route.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/app/api/chat/route.ts). The handler instantiates the orchestrator and creates a per-request session.

```typescript
// src/app/api/chat/route.ts
const agent = new SearchAgent();               // <-- creates the orchestrator
const session = SessionManager.createSession(); // <-- per-request session
agent.searchAsync(session, { … });             // <-- kicks off orchestration

```

The `ModelRegistry` loads the configured LLM and embedding models, validates the request body, and initializes the **SessionManager** instance. This session acts as the central bus for all subsequent events and blocks emitted during the request lifecycle.

## Intent Classification: Deciding What Actions to Take

Inside `SearchAgent.searchAsync`, the first step calls the **classifier** to transform the raw query into a structured decision object. The `classify` function accepts chat history, enabled sources, and the current query to return a schema containing flags like `skipSearch`, `personalSearch`, and `showWeatherWidget`.

```typescript
// src/lib/agents/search/index.ts
const classification = await classify({
  chatHistory: input.chatHistory,
  enabledSources: input.config.sources,
  query: input.followUp,
  llm: input.config.llm,
});

```

*Source:* [[`src/lib/agents/search/classifier.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/classifier.ts)](https://github.com/ItzCrazyKns/Vane/blob/master/src/lib/agents/search/classifier.ts#L37-L52)

This classification object dictates which branches the orchestrator will follow—whether to skip the research loop entirely, trigger specific widgets, or proceed with a full search operation.

## Widget Execution: Parallel Side-Effect-Free Enhancements

If any widget flags return true, the orchestrator invokes `WidgetExecutor.executeAll` to run them **in parallel**. Each widget implements a `shouldExecute` predicate and an `execute` method.

```typescript
// src/lib/agents/search/widgets/executor.ts
if (widget.shouldExecute(input.classification)) {
  const output = await widget.execute(input);
  results.push(output);
}

```

*Source:* [[`src/lib/agents/search/widgets/executor.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/widgets/executor.ts)](https://github.com/ItzCrazyKns/Vane/blob/master/src/lib/agents/search/widgets/executor.ts#L14-L30)

Widgets such as weather calculators or stock tickers emit their output as **widget blocks** via the session, allowing the frontend to display supplemental data instantly while the main research loop potentially continues in the background.

## The Research Loop: Iterative LLM-Driven Tool Calling

When `classification.skipSearch` is **false**, the orchestrator instantiates the **Researcher** class to drive the core research cycle.

```typescript
// src/lib/agents/search/index.ts
if (!classification.classification.skipSearch) {
  const researcher = new Researcher();
  searchPromise = researcher.research(session, { … });
}

```

*Source:* [[`src/lib/agents/search/index.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/index.ts)](https://github.com/ItzCrazyKns/Vane/blob/master/src/lib/agents/search/index.ts#L82-L90)

### Initial Prompt and Tool Set Configuration

Inside `Researcher.research`, the system creates a **research block** to hold sub-steps, then initiates a loop running up to `maxIteration` (depending on speed, balanced, or quality mode). For each iteration:

1. Build a system prompt with available actions from `ActionRegistry.getAvailableActionTools`.
2. Stream the LLM response with native tool-call support.

```typescript
// src/lib/agents/search/researcher/index.ts
const actionStream = input.config.llm.streamText({
  messages: [{ role: 'system', content: researcherPrompt }, ...agentMessageHistory],
  tools: availableTools,
});

```

*Source:* Lines 68-76 in [[`src/lib/agents/search/researcher/index.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/researcher/index.ts)](https://github.com/ItzCrazyKns/Vane/blob/master/src/lib/agents/search/researcher/index.ts).

### Collecting and Handling Tool Calls

The stream yields partial results containing `toolCallChunk` objects. The researcher aggregates these into a final list (`finalToolCalls`). If the special `__reasoning_preamble` tool appears, the system converts it into a **reasoning sub-step** displayed in the UI.

```typescript
// src/lib/agents/search/researcher/index.ts
if (tc.name === '__reasoning_preamble' && tc.arguments['plan'] && !reasoningEmitted) {
  // push reasoning block
}

```

*Source:* Lines 90-115 (reasoning handling) and lines 136-146 (tool call aggregation).

### Executing Actions via the Action Registry

After the LLM finishes emitting tool calls, `ActionRegistry.executeAll` runs each requested tool—such as web search, academic search, or file upload processing—and returns structured results.

```typescript
// src/lib/agents/search/researcher/index.ts
const actionResults = await ActionRegistry.executeAll(finalToolCalls, {
  llm: input.config.llm,
  embedding: input.config.embedding,
  session,
  researchBlockId,
  fileIds: input.config.fileIds,
});

```

*Source:* Lines 64-71.

These results are appended to `agentMessageHistory` as `tool` messages, allowing the LLM to continue reasoning in the next iteration. The loop terminates when the LLM returns a `done` tool call or produces no further tool requests.

### Post-Processing Search Results

Upon loop completion, the researcher aggregates all `search_results` actions, de-duplicates URLs, and emits a **source block** containing the final filtered results.

```typescript
// src/lib/agents/search/researcher/index.ts
session.emitBlock({ id: crypto.randomUUID(), type: 'source', data: filteredSearchResults });

```

*Source:* Lines 109-115.

## Final Answer Generation and Response Streaming

Back in `SearchAgent.searchAsync`, the orchestrator combines widget output and search findings into a single XML-like context string. It feeds this context plus the original user query to the LLM’s **writer prompt** (`getWriterPrompt`), then streams the answer back to the client.

```typescript
// src/lib/agents/search/index.ts
const writerPrompt = getWriterPrompt(finalContextWithWidgets, input.config.systemInstructions, input.config.mode);
const answerStream = input.config.llm.streamText({ 
  messages: [{ role: 'system', content: writerPrompt }, ...input.chatHistory, { role: 'user', content: input.followUp }] 
});
for await (const chunk of answerStream) {
  session.emit('data', { type: 'response', data: chunk.contentChunk });
}

```

*Source:* Lines 71-90 (prompt creation) and 92-98 (streaming response).

When the stream finishes, the session emits an `end` event, signaling the API handler to close the SSE connection.

## Session Management: The Real-Time Event Bus

The **SessionManager** defined in [`src/lib/session.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/session.ts) holds **blocks** (research, widget, source, text) and manages event distribution. It provides four critical methods:

- **`emit(event, data)`**: Pushes raw events (`data`, `end`, `error`) to subscribers.
- **`emitBlock(block)`**: Stores a block and notifies listeners with `type: 'block'`.
- **`updateBlock(id, patch)`**: Applies RFC-6902 patches to update block content dynamically, used for evolving reasoning text.
- **`subscribe(listener)`**: Returns an unsubscribe function; the API handler uses this to pipe events to the client SSE stream.

*Source:* Full implementation in [[`src/lib/session.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/session.ts)](https://github.com/ItzCrazyKns/Vane/blob/master/src/lib/session.ts).

## Extending the Orchestration

Vane's agent orchestration supports modular extensions through widgets and actions.

### Adding Custom Widgets

To add a new widget, implement the `Widget` interface with `shouldExecute` and `execute` methods, then register it with the `WidgetExecutor`.

```typescript
// src/lib/agents/search/widgets/myWidget.ts
import { Widget } from '../types';
import { WidgetExecutor } from './executor';

const myWidget: Widget = {
  type: 'myWidget',
  shouldExecute: (c) => c.classification.showMyWidget,
  async execute(input) {
    const data = await fetchMyData(input.llm);
    return { type: 'myWidget', llmContext: data };
  },
};

WidgetExecutor.register(myWidget);

```

The widget automatically runs in parallel with others when `searchAsync` invokes `WidgetExecutor.executeAll`.

### Registering New Tools

To add a new research tool:

1. Implement an **action** following the `Action` interface (`name`, `description`, `execute`).
2. Register it in `ActionRegistry` at [`src/lib/agents/search/researcher/actions/registry.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/researcher/actions/registry.ts).
3. The LLM can now request the new tool during the research loop, and the orchestrator handles execution and result integration identically to existing search tools.

## Summary

- **Vane's agent orchestration** operates through a pipeline of classification, parallel widget execution, and iterative LLM tool-calling orchestrated by the `SearchAgent` class.
- The **SessionManager** provides the event bus and block storage that enables real-time streaming to clients via Server-Sent Events.
- **Intent classification** in [`src/lib/agents/search/classifier.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/classifier.ts) determines whether to trigger widgets, skip search, or initiate the full research loop.
- The **Researcher** class manages the iterative loop where the LLM invokes tools from the `ActionRegistry`, aggregates results, and emits reasoning and source blocks.
- **Widget execution** runs independently in parallel, providing fast side-effect-free enhancements like weather or stock data.
- The system is **extensible**—new widgets register with `WidgetExecutor` and new tools register with `ActionRegistry` without modifying core orchestration logic.

## Frequently Asked Questions

### How does Vane decide whether to perform a web search or just answer directly?

Vane uses the **classifier** function in [`src/lib/agents/search/classifier.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/classifier.ts) to analyze the query against chat history and configuration. If the classification returns `skipSearch: true`, the orchestrator bypasses the `Researcher` loop and proceeds directly to final answer generation using only widget data and chat context. Otherwise, it initializes the iterative research loop.

### What is the difference between widgets and actions in Vane's architecture?

**Widgets** are side-effect-free enhancements (like weather displays) that execute in parallel via `WidgetExecutor.executeAll` and emit immediate UI blocks. **Actions** are tools the LLM can invoke during the research loop—such as web search or file retrieval—managed by `ActionRegistry.executeAll`. Widgets run based on classification flags, while actions are chosen dynamically by the LLM during iteration.

### How does the SessionManager handle real-time updates to the frontend?

The **SessionManager** maintains blocks and events, exposing `emit`, `emitBlock`, and `updateBlock` methods. When the `Researcher` creates a reasoning sub-step or the `WidgetExecutor` returns data, the session emits events that the `/api/chat` route subscribes to, streaming them to the client via Server-Sent Events (SSE). This allows the UI to render intermediate states like "searching" or "reasoning" before the final answer arrives.

### Can I add custom tools to Vane's research loop without modifying core files?

Yes. You can extend Vane's capabilities by implementing the `Action` interface and registering your tool in [`src/lib/agents/search/researcher/actions/registry.ts`](https://github.com/ItzCrazyKns/Vane/blob/main/src/lib/agents/search/researcher/actions/registry.ts). The `ActionRegistry` exposes the tool to the LLM through `getAvailableActionTools`, and `ActionRegistry.executeAll` will automatically include your custom logic in the research iteration cycle alongside built-in search and retrieval tools.