RAG in Vibes is a tool pattern: the agent calls a retrieval tool that searches a knowledge base, then uses the retrieved context to answer the question. This example uses an in-memory mock - replace vectorSearch() with your actual vector DB client.
What you’ll learn
- Using
plainTool() for tools without dependency injection
- Passing retrieved context to the model via tool results
- Structuring a RAG pipeline with Vibes
Prerequisites
ANTHROPIC_API_KEY set in your environment
- Vibes installed (
deno add npm:@vibesjs/sdk npm:@ai-sdk/anthropic npm:zod)
Complete example
import { Agent, plainTool } from "npm:@vibesjs/sdk";
import { anthropic } from "npm:@ai-sdk/anthropic";
import { z } from "npm:zod";
// --- Mock knowledge base ---
// In production: replace with calls to Pinecone, pgvector, Qdrant, etc.
const documents = [
{
id: "1",
content: "Vibes is a TypeScript agent framework built on the Vercel AI SDK.",
metadata: { source: "overview" },
},
{
id: "2",
content: "Vibes supports dependency injection via RunContext, passed to all tools and prompts.",
metadata: { source: "concepts" },
},
{
id: "3",
content: "The Graph API uses BaseNode classes with next() and output() free functions.",
metadata: { source: "graph" },
},
{
id: "4",
content: "Vibes agents can be wrapped with A2AAdapter to expose them as A2A-compatible servers.",
metadata: { source: "integrations" },
},
];
// Mock vector search - replace with real vector DB in production
async function vectorSearch(query: string, topK = 3): Promise<string[]> {
// Naive keyword overlap scoring (real impl uses embedding similarity)
const queryWords = query.toLowerCase().split(/\s+/);
const scored = documents.map((doc) => {
const docWords = doc.content.toLowerCase().split(/\s+/);
const overlap = queryWords.filter((w) => docWords.includes(w)).length;
return { content: doc.content, score: overlap };
});
return scored
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map((d) => d.content);
}
// Retrieval tool using plainTool (no deps needed)
const searchDocs = plainTool({
name: "search_docs",
description: "Search the Vibes documentation knowledge base for relevant information",
parameters: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
const results = await vectorSearch(query);
if (results.length === 0) {
return "No relevant documents found.";
}
return results.map((r, i) => `[${i + 1}] ${r}`).join("\n\n");
},
});
// RAG agent
const ragAgent = new Agent({
model: anthropic("claude-sonnet-4-6"),
systemPrompt:
"You are a documentation assistant. Always use the search_docs tool to find relevant " +
"information before answering. Base your answer on the retrieved documents.",
tools: [searchDocs],
});
// Run
const result = await ragAgent.run("How does dependency injection work in Vibes?");
console.log(result.output);
Run it
deno run --allow-net --allow-env rag.ts
How it works
plainTool(): A simpler variant of tool() - no RunContext, the execute function receives args directly. Use plainTool() when your tool doesn’t need access to deps. Use tool() (or tool<TDeps>()) when it does.
Tool result as context: When the agent calls search_docs, Vibes executes vectorSearch() and passes the result back to the model as a tool response. The model uses this retrieved context to form its answer - no prompt engineering needed.
Plugging in a real vector DB: Replace the vectorSearch() function body with your vector DB client. The Vibes pattern is identical regardless of the underlying store. For dependency-injected DB clients (e.g., a shared connection pool), use tool<Deps>() and access the client via ctx.deps.
The retrieval quality in this example is intentionally minimal (keyword overlap). A real RAG system uses embedding-based semantic search. The Vibes pattern - tool calls retriever, model uses results - is identical.
Next steps