RAG and knowledge bases
How Cradle grounds agent replies in your own documents — chunking, embedding, retrieval, citations, and web crawl.
Cradle does not answer from a generic cloud model. When an agent has one or more knowledge bases, the incoming message is embedded, matched against local vector storage, and the top snippets are injected into the prompt with inline citations.
End-to-end flow
User message
│
▼
TriageOrchestrator
├── risk assessment
├── router picks an agent
└── if agent.knowledgeBaseIds.length > 0:
├── acquireEmbedder()
├── search(query, embedder, { kbIds, topK: 6 })
│ embed(query) → vector
│ sqlite-vec MATCH → top chunks
│ join documents + knowledge_bases
└── buildPrompt(agent, turns, sources)
«Use the numbered sources below if relevant. Cite like [1].»
[1] Document title (source URI)
content...
[2] ...
│
▼
runner.generate → draft with citations → Inbox / auto-replyVector storage
Cradle uses sqlite-vec for vector search. Each knowledge base gets its own
virtual table sized to the embedding model's dimension:
CREATE VIRTUAL TABLE vec_kb_<kbId> USING vec0(
chunk_id TEXT PRIMARY KEY,
embedding FLOAT[<dim>]
);Older knowledge bases without a registry entry continue to work through the
legacy shared vec_chunks table.
Tables
knowledge_bases (id, name, description, embedding_model_id, embedding_dim, created_at)
documents (id, kb_id, source_kind, source_uri, title, mime, sha256,
status, char_count, chunk_count, error_message, ingested_at)
document_chunks (id, document_id, kb_id, position, content, token_count, metadata)agents.knowledge_base_ids is a JSON list of knowledge base IDs the agent may
use.
Chunking strategy
The paragraph-first chunker keeps related text together:
- Split on blank lines to get paragraphs.
- Accumulate paragraphs while the running total is under 2 000 characters.
- If a paragraph exceeds the limit, split on sentence boundaries.
- If a single sentence still exceeds the limit, hard-cut by characters.
- Preserve a 200-character overlap between consecutive chunks.
Each chunk stores content, position, and an estimated token count
(chars / 4).
Ingestion
await ingestText({
kbId,
title,
text,
sourceKind: 'upload' | 'url' | 'crawl' | 'inline',
sourceUri,
mime,
embedder,
batchSize = 16,
})- Idempotency by SHA-256 of the text.
- Chunks are inserted as
indexing, embedded in batches, then markedready. - Errors roll back to
status='error'with the message preserved. - Progress is reported between batches.
Parsers
| Extension | Implementation |
|---|---|
.md, .markdown | read as plain text |
.txt, .log, .csv | read as plain text |
.pdf | pdf-parse v2 |
.docx | mammoth.extractRawText |
.xlsx, .xls | exceljs, one text line per row |
parseFile(filePath) returns { text, title, mime }. isSupported(filePath)
checks the extension before attempting ingestion.
Retrieval
await search(query, embedder, { kbIds, topK = 6, maxDistance })- Embed the query.
- Run
SELECT chunk_id, distance FROM vec_kb_... WHERE embedding MATCH ?. - Join
document_chunksanddocumentsto enrich results. - Filter to the requested knowledge bases.
- Optionally filter by
maxDistance. - Return
topKresults asSearchHit[].
Helpers deleteDocument(id) and deleteKnowledgeBase(id) clean up chunks
cascadingly. Drizzle FK cascades cover most tables, but sqlite-vec virtual
tables are cleaned explicitly.
Citations
Every chunk used in a reply is saved to ticket_sources with a snapshot:
ticket_sources (
id, ticket_id, message_id, position,
document_id, chunk_id, kb_id,
document_title, source_uri,
content_preview, content_full,
chunk_position, distance, created_at
)Snapshots make tickets reproducible even if the original document is later
removed or re-ingested. The UI shows numbered badges matching the [1],
[2], ... references the model placed in the text.
Web crawl
Knowledge bases can be fed from a web crawl:
rag:ingestUrl({ kbId, url })— one page or document.rag:crawlSite({ kbId, seedUrl, maxPages, maxDepth, sameDomain, followDocs, respectRobots })— BFS job with live progress.
Crawler modules live in src/core/rag/crawler/:
url.ts— URL normalization and same-domain checks.html.ts— cheerio + turndown extraction, scripts and navigation stripped.robots.ts— cached robots.txt per origin.fetcher.ts— fetches HTML or documents.crawler.ts— BFS with politeness delay (500 ms), depth/pages limits, and document following.
Default limits: HTML up to 10 MB, documents up to 50 MB, 20 s request timeout.
RAG + finetuning
RAG supplies facts (prices, specs, clauses). Finetuning supplies style and reasoning patterns. The two features complement each other; see finetune for the training workflow.