Architecture

Architecture overview

How Cradle splits into a host-agnostic core with two entry points — the headless server and the desktop app — and how a message flows end to end.

Cradle is a single host-agnostic core with two ways to run it: as the headless cradle-server daemon, or embedded in the Electron desktop app. The core never imports Electron; each entry point supplies a HostAdapter before any core code runs.

Two entry points, one core

              ┌──────────────── core (host-agnostic) ───────────────┐
              │  · DB (better-sqlite3 + sqlite-vec) + migrations     │
              │  · Triage orchestrator, router, agent-service        │
              │  · Risk system (L1 rules + L2 classifier)            │
              │  · llama.cpp runners (chat + embedding)              │
              │  · Channels: api / telegram / widget                │
              │  · RAG: chunker / ingest / retrieve / parsers        │
              │  · Auth: api_keys + scopes + audit                  │
              └──────────────────────────────────────────────────────┘
                  ▲                                        ▲
   ElectronHostAdapter                           NodeHostAdapter
   (app.getPath, dialog,                         (~/.cradle, EventEmitter,
    BrowserWindow.send)                           JSON stdout logger)
                  │                                        │
        src/electron/  (BrowserWindow + React UI)   src/server/  (HTTP API, no GUI)

The HostAdapter contract abstracts everything platform-specific: user-data directory, temp directory, resource paths, event delivery, file pickers, and logging. The desktop app implements it with Electron APIs; the server implements it with ~/.cradle, an EventEmitter, and a structured JSON logger (and pickFile simply throws, since it is headless).

Server vs desktop app

Concerncradle-serverElectron app
Startupnode dist/server/index.jsElectron Forge + Vite, BrowserWindow
Data dir~/.cradle/OS user-data dir
Authapi_keys + scopesSame table; admin key auto-bootstraps
File pickerThrows (headless)dialog.showOpenDialog
Event deliveryEventEmitter (SSE)webContents.send to the renderer

End-to-end message flow

Every incoming message runs through the same harness, regardless of which entry point is hosting the core:

Incoming → ChatChannel (telegram | widget | api)
  → unified IncomingMessage
  → persist message + upsert ticket
  → TriageOrchestrator.processIncoming
      ├── RiskAssessor.layer1(rules)      → triggered rules + level
      ├── RiskAssessor.layer2(classifier) → { category, risk, confidence,
      │                                       reasoning, requiredSkills }
      ├── Router.pick(category, skills)   → Agent
      ├── if agent has knowledge bases:
      │     RAG.search(text, { kbIds, topK }) → source snippets
      ├── AgentService.executeAgent(agent, turns, { sources })
      │     → builds the prompt (system + language + sources + turn)
      │     → runner.generate (streamed) → draft
      ├── save messages + risk_assessment
      └── decide by final level:
          ├── green         → send automatically
          └── yellow | red  → ticket waits for operator
                              → Inbox → approve / edit / reject → send

The key property: a red or yellow verdict never auto-sends. It becomes a ticket in the operator Inbox, and a human approves, edits, or rejects the draft before it reaches the channel.

Inside the core

  • Triageorchestrator (drives the flow), router (picks an agent by role + skill intersection), agent-service (prompt building + execution), and the risk subsystem (deterministic rules, LLM classifier, categories, skills).
  • LLM — a ModelRunner interface implemented by LlamaCppRunner (wrapping node-llama-cpp), a model manager (resumable SHA-256-checked downloads), and a model registry with LRU eviction and lazy loading.
  • RAG — a paragraph-first chunker, idempotent batched ingest, top-k retrieval over sqlite-vec, and parsers for Markdown, TXT, PDF, DOCX, and XLSX plus a web crawler.
  • Channels — a common ChatChannel interface with Telegram (grammY), an embeddable widget (Fastify + WebSocket), and a direct HTTP API, all funnelled through a channel manager.

The desktop renderer

The Electron renderer is a React + Zustand app with pages for Dashboard, Inbox, Ticket detail, Agents, Models, Knowledge, Integrations, Risk & Triage, and Settings — the human side of the harness where operators review and approve.

For deeper dives, see models and RAG.