Обзор архитектуры
Как Cradle разделяется на хост-независимое ядро с двумя точками входа — headless-сервер и desktop-приложение — и как сообщение проходит от начала до конца.
Cradle — это одно хост-независимое ядро с двумя способами запуска: как
headless-демон cradle-server или встроенный в Electron desktop app. Ядро
никогда не импортирует Electron; каждая точка входа предоставляет
HostAdapter до первого вызова кода ядра.
Две точки входа, одно ядро
┌──────────────── 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)Контракт HostAdapter абстрагирует всё платформенно-специфичное: директорию
пользовательских данных, временную директорию, пути к ресурсам, доставку
событий, выбор файлов и логирование. Desktop-приложение реализует его через
API Electron; сервер реализует его через ~/.cradle, EventEmitter и
структурированный JSON-логгер (pickFile в headless-режиме просто бросает
исключение).
Сервер против desktop-приложения
| Аспект | cradle-server | Electron app |
|---|---|---|
| Запуск | node dist/server/index.js | Electron Forge + Vite, BrowserWindow |
| Директория данных | ~/.cradle/ | OS user-data dir |
| Аутентификация | api_keys + scopes | Та же таблица; admin-ключ bootstrap |
| Выбор файлов | Бросает (headless) | dialog.showOpenDialog |
| Доставка событий | EventEmitter (SSE) | webContents.send в renderer |
Поток сообщения от начала до конца
Каждое входящее сообщение проходит через одну и ту же упряжь независимо от точки входа:
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Ключевое свойство: вердикт red или yellow никогда не уходит автоматически. Он становится тикетом в Inbox оператора, и человек одобряет, редактирует или отклоняет черновик, прежде чем он достигнет канала.
Внутри ядра
- Triage —
orchestrator(управляет потоком),router(выбирает агента по роли + пересечению навыков),agent-service(сборка промпта + выполнение) и подсистемаrisk(детерминированные правила, LLM-классификатор, категории, навыки). - LLM — интерфейс
ModelRunner, реализованныйLlamaCppRunner(обёртка надnode-llama-cpp), менеджер моделей (докачка с проверкой SHA-256) и реестр моделей с LRU-эвикцией и ленивой загрузкой. - RAG — paragraph-first chunker, идемпотентное batched-ингестирование,
top-k retrieval через
sqlite-vec, парсеры Markdown, TXT, PDF, DOCX, XLSX и веб-краулер. - Channels — общий интерфейс
ChatChannelс Telegram (grammY), встраиваемым виджетом (Fastify + WebSocket) и прямым HTTP API, всё через channel manager.
Desktop renderer
Electron renderer — это React + Zustand приложение со страницами Dashboard, Inbox, Ticket detail, Agents, Models, Knowledge, Integrations, Risk & Triage и Settings — человеческая сторона упряжи, где операторы ревьюят и одобряют.