LlamaIndex ‘legal-kb’: Agentic Retrieval over Index v2 with retrieve, find, read, and grep Tools LlamaIndex released legal-kb, an open-source reference application demonstrating agentic retrieval over legal documents using Index v2. The app provides an agent with filesystem-style tools—retrieve, findFiles, readFile, and grepFile—to crawl and query a knowledge base, offering version control and hybrid search. This approach moves beyond single-shot retrieval by enabling iterative, tool-driven document analysis. LlamaIndex has published , a public reference application on GitHub. It is described as a knowledge base for legal documents, powered by LlamaIndex Index v2 the LlamaParse Platform . The project demonstrates a pattern the team calls a Retrieval Harness for agentic retrieval. legal-kb https://github.com/run-llama/legal-kb The approach differs from single-shot retrieval. Instead of one embedding search per query, an agent is given filesystem-style tools. It can then crawl a large, evolving knowledge base to solve a task. The tools mirror operations engineers already know: semantic and keyword search, regex grep, file search, and read. What is legal-kb? legal-kb is a working TanStack Start web app, not a library. You sign in, create a project, upload files, and chat with an agent. Each project is mirrored as a managed LlamaCloud Index v2. Uploaded files are parsed and indexed automatically in the background. The chat agent then queries that index live during each turn. The Retrieval Harness, in plain terms The harness provides a persistent data pipeline over your documents. It connects to a data source, indexes it, and keeps it updated. On top of that pipeline, it exposes a set of tools to the agent. Those tools are deliberately close to filesystem operations. An agent can list files, read a file, grep inside a file, or run hybrid search. Because the tools are generic, you can plug the harness into your own agents. The four agent tools The agent in src/lib/agent.ts is given four tools. Each maps to an Index v2 retrieval API. The table below lists them as implemented. | Tool | Backing API | Key parameters | What it does | |---|---|---|---| retrieve | beta.retrieval.retrieve | query , top k , score threshold , rerank top n , file name , file version | Runs hybrid semantic search; optional reranking; returns chunks plus citations | findFiles | beta.retrieval.find | file name , file name contains | Searches files by exact name or substring; paginates automatically | readFile | beta.retrieval.read | file id , offset , max length | Reads raw file content, with offset and length windows | grepFile | beta.retrieval.grep | file id , pattern , context chars , limit | Matches a pattern in one file; returns character positions | The system prompt enforces an order. The agent must call findFiles first to establish the document inventory. It then narrows with retrieve , and confirms exact wording with readFile or grepFile before citing. How it works under the hood Uploads follow a clear pipeline in src/lib/files.ts . Bytes are pushed to the project’s LlamaCloud source directory. A File and ProjectFile row are written to PostgreSQL via Prisma. An index sync is triggered but not awaited; the UI polls status until ready. Versioning is scoped to the project, filename pair. Re-uploading nda.pdf to the same project produces v1, v2, v3 side by side. The retrieval layer filters on the version metadata field. This gives version control over the knowledge base itself. The agent uses the ToolLoopAgent from Vercel AI SDK 6. You pick OpenAI or Anthropic per turn and bring your own keys. Reasoning is streamed: Claude models use extended thinking; OpenAI reasoning models use a medium reasoning effort. Here is a condensed but faithful view of the retrieve tool and the agent. js import { LlamaCloud } from '@llamaindex/llama-cloud' import { tool, ToolLoopAgent } from 'ai' import { z } from 'zod' import { makeCitationId } from './citations' // One tool closure per index. Wraps Index v2 retrieval APIs. function createLlamaParseTools apiKey: string, projectId: string, indexId: string { const client = new LlamaCloud { apiKey } const retrieve = tool { description: 'Run a semantic retrieval query against an index.', inputSchema: z.object { query: z.string , top k: z.number .nullable , score threshold: z.number .nullable , rerank top n: z.number .nullable , // set to enable reranking file name: z.string .nullable , // metadata filter file version: z.number .nullable , } , execute: async { query, top k, score threshold, rerank top n, file name } = { const custom filters = file name ? { file name: { operator: 'eq' as const, value: file name } } : undefined const response = await client.beta.retrieval.retrieve { index id: indexId, project id: projectId, query, top k, score threshold, rerank: rerank top n = null ? { enabled: true, top n: rerank top n } : undefined, custom filters, } // Return a model-readable list plus citations that drive the UI chips. const citations = response.results.map r = { id: makeCitationId , // e.g. "c7f2qa" fileName: r.metadata?.file name, score: r.rerank score ?? r.score ?? null, preview: r.content.slice 0, 500 , } const formatted = response.results .map r, i = Result ${i + 1}\n\n${r.content.slice 0, 600 } .join '\n\n---\n\n' return { formatted, citations } }, } // findFiles / readFile / grepFile follow the same shape, backed by // client.beta.retrieval.find / .read / .grep return { retrieve / , findFiles, readFile, grepFile / } } export function buildAgent model, apiKey: string, projectId: string, indexId: string { return new ToolLoopAgent { model, tools: createLlamaParseTools apiKey, projectId, indexId , instructions: 'Always call findFiles first, ground every answer in the documents, ' + 'and cite ids inline as cite: