How Vane's Agent Orchestration Works: A Deep Dive into the Search Pipeline
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. The handler instantiates the orchestrator and creates a per-request session.
// 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.
// 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/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.
// 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/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.
// 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/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:
- Build a system prompt with available actions from
ActionRegistry.getAvailableActionTools. - Stream the LLM response with native tool-call support.
// 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/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.
// 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.
// 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.
// 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.
// 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 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 withtype: '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/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.
// 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:
- Implement an action following the
Actioninterface (name,description,execute). - Register it in
ActionRegistryatsrc/lib/agents/search/researcher/actions/registry.ts. - 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
SearchAgentclass. - 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.tsdetermines 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
WidgetExecutorand new tools register withActionRegistrywithout 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 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. 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.
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 →