Architecture

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-reply

Vector 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:

  1. Split on blank lines to get paragraphs.
  2. Accumulate paragraphs while the running total is under 2 000 characters.
  3. If a paragraph exceeds the limit, split on sentence boundaries.
  4. If a single sentence still exceeds the limit, hard-cut by characters.
  5. 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 marked ready.
  • Errors roll back to status='error' with the message preserved.
  • Progress is reported between batches.

Parsers

ExtensionImplementation
.md, .markdownread as plain text
.txt, .log, .csvread as plain text
.pdfpdf-parse v2
.docxmammoth.extractRawText
.xlsx, .xlsexceljs, 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 })
  1. Embed the query.
  2. Run SELECT chunk_id, distance FROM vec_kb_... WHERE embedding MATCH ?.
  3. Join document_chunks and documents to enrich results.
  4. Filter to the requested knowledge bases.
  5. Optionally filter by maxDistance.
  6. Return topK results as SearchHit[].

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.