@involvex/ctrl – Universal Agent Plan
Lokale Agent-Plattform mit Expo (Android/Web), Electron (Desktop), Bun-Gateway (@involvex/ctrl-a) und späterem Rust-Backend.
1. Leitentscheidungen
- Gateway als eigener Prozess: Bun + TypeScript in
packages/ctrl-a, kompiliert mitbun build --compile. Eingebettet, lokaler Daemon oder LAN-Server. - Desktop-Shell: Electron. Startet und überwacht das Gateway; Tray, Autostart, Updates, Secrets.
- Mobile und Web: Separate Expo-Apps (
apps/mobile,apps/web) mit Expo Router. - Local-first identity: Persona and transcript live on each client. Generators are interchangeable (on-device LiteRT, cloud via host, peer PC gateway). Opening chat does not require the gateway.
- Gateway is the home node: Tools, MCP, channels, backups, and append-only sync when reachable — not a requirement to chat.
- Clients speak the gateway protocol when paired (
@involvex/ctrl-s/ctrl-i); on-device LiteRT is a local generator plug inside the same chat UI. - Rust später: Backend
agentdersetzt das Gateway schrittweise, abgesichert durch Contract-Tests (ctrl-t). - Nicht im Scope: Flutter, Ionic als Hauptarchitektur, alles in Rust.
- Naming & Publish: Siehe Namespace_and_Design.md und ADR-0002. Nur
ctrl-f5ist publishable. - Quality: Bun-native only — CodeQuality.md, ADR-0003.
2. Architektur
Electron Desktop | Expo Android | Expo Web/PWA | CLI (ctrl-f5)
\ | | /
------- AgentClient (HTTP / WebSocket) -------
|
Gateway @involvex/ctrl-a (Bun + TS)
Sessions | Workspaces | Agents | Jobs | Events | Auth
Agent Loop | Tool Router | Context Builder | Permissions
Provider Registry | MCP | Skills | Knowledge | Memory
| | |
Provider Adapter Runtime Adapter Channel Adapter
OpenAI-kompatibel llama-server Webhook, Telegram
Custom HTTP, CLI LiteRT-LM Discord, SlackRegel: Cloud and PC providers go through the paired gateway. On-device LiteRT runs locally as a generator plug. Identity/transcript stay on the client; sync is append-only when the home node is up.
3. Monorepo
ctrl/
├─ apps/
│ ├─ desktop/ # Electron: main, preload, renderer (Vite + React)
│ ├─ mobile/ # Expo Android
│ ├─ web/ # Expo Router Web/PWA
│ └─ cli/ # @involvex/ctrl-f5 (publishable)
├─ packages/
│ ├─ ctrl-a/ # Gateway (Bun Binary)
│ ├─ ctrl-s/ # Protocol: Zod, Events, JSON Schema / OpenAPI
│ ├─ ctrl-r/ # Runtimes & Providers
│ ├─ ctrl-z/ # Memory, SQLite, Storage
│ ├─ ctrl-space/ # Shared Matrix UI tokens / components
│ ├─ ctrl-c/ # Channels
│ ├─ ctrl-m/ # MCP Client
│ ├─ ctrl-t/ # Contract tests
│ ├─ ctrl-k/ # Knowledge
│ ├─ ctrl-i/ # Typed Agent Client
│ ├─ ctrl-x/ # Agent Core (loop, tools, context)
│ ├─ ctrl-l/ # Logger
│ ├─ ctrl-comma/ # Config
│ └─ ctrl-slash/ # Skills
├─ crates/ # later: agentd
├─ native/android-litert/
└─ docs/Scaffold vs. later packages
| Jetzt (Phase 0) | Später |
|---|---|
Root workspaces, bunfig.toml | — |
packages/ctrl-s, packages/ctrl-a | ctrl-r, ctrl-z, ctrl-space, ctrl-c, ctrl-m, ctrl-t, ctrl-k, ctrl-i, ctrl-x, ctrl-l, ctrl-comma, ctrl-slash |
apps/desktop, mobile, web, cli | Electron lifecycle, LAN pairing, packaging |
| Docs + ADRs | crates/, native/android-litert/, threat-model |
UI-Sharing: Types, Hooks, API-Client, State Machines, Markdown-Renderer, Theme-Tokens und Form-Schemas über ctrl-space / ctrl-i / ctrl-s. Das Desktop-UI (Split-Panes, Shortcuts, Tray) bleibt Vite + React, nicht React Native Web als Primary.
Theme-Akzent: #00ff66 (siehe Namespace).
4. Desktop-Host mit Electron
Prozessmodell
Electron Main
├─ Single-Instance-Lock
├─ Gateway-Child-Process (ctrl-a Binary, 127.0.0.1)
├─ Tray, Autostart, Updater, safeStorage
└─ BrowserWindow (Renderer, Vite + React)
└─ Preload: schmale, typisierte IPC-APIRegeln
- Der Renderer nutzt HTTP/WebSocket zum Gateway. IPC nur für Native: Dateidialoge, Tray, Notifications, Autostart, Secrets.
contextIsolation: true,nodeIntegration: false,sandbox: true. Jede IPC-Nachricht mit Zod (ctrl-s) validieren.- CSP setzen, Navigation und
window.openblockieren. - SQLite liegt im Gateway (
bun:sqliteviactrl-z). Keine Native-Module-Rebuilds für Electron. - Gateway-Token in
safeStorage; Übergabe per Env oder Stdin. - Sidecars (
llama-server) verwaltet derRuntimeManagerim Gateway (ctrl-r), nicht Electron.
Gateway-Lifecycle
- Electron startet die Binary mit zufälligem oder konfiguriertem Port.
- Wartet auf
/healthz. - Bei Absturz: Restart mit exponentiellem Backoff.
- Beim Beenden: SIGTERM, Timeout, dann Kill.
- Optional: Daemon-Modus nach UI-Close.
Betriebsmodi
| Modus | Bindung | Zweck |
|---|---|---|
| embedded | Prozess-intern oder Loopback | Android, einfache Desktop-Nutzung |
| local-daemon | 127.0.0.1:7331 | Desktop, CLI, IDE-Plugins |
| lan-server | 0.0.0.0:7331 | nur explizit aktivierbar |
LAN: Pairing-Code, Token pro Gerät, Widerruf, Origin/IP-Checks, Rate Limiting, Audit Log, optional TLS.
5. Provider- und Runtime-Abstraktion
Intern gilt ein kanonisches Format (in ctrl-s), nicht das OpenAI-Schema.
export interface ModelProvider {
id: string;
capabilities(): Promise<ModelCapabilities>;
listModels(): Promise<ModelDescriptor[]>;
generate(
req: GenerateRequest,
ctx: ProviderContext
): AsyncIterable<AgentEvent>;
cancel?(runId: string): Promise<void>;
}
export type AgentEvent =
| { type: 'run.started'; runId: string }
| { type: 'message.delta'; text: string }
| { type: 'reasoning.delta'; text: string }
| { type: 'tool.requested'; call: ToolCall }
| { type: 'tool.started'; callId: string }
| { type: 'tool.completed'; callId: string; result: unknown }
| { type: 'usage.updated'; usage: TokenUsage }
| { type: 'run.completed'; finishReason: string }
| { type: 'run.failed'; error: SerializedError };Adapter v1 (ctrl-r): OpenAICompatibleProvider, CliProvider, MockProvider, LlamaCppProvider.
Lokale Runtimes
- Desktop: Gateway verwaltet
llama-serverals Sidecar. - Android: Zuerst externer Endpoint (Desktop-Gateway im LAN); später LiteRT-LM Native Module.
6. Datenmodell und Speicher
Workspace
├─ Agents (Instructions, Modell, Tools, MCP, Skills, Memory- und Permission-Policy)
├─ Conversations (Messages, Runs, Tool Calls, Attachments, Branches)
├─ Knowledge Sources, Skills, MCP Servers, Provider Profiles, Policiesdata/ app.db | blobs/ | workspaces/ | skills/ | knowledge/ | models/ | logs/- Secrets nicht in
app.db: DesktopsafeStorage, Android Keystore. - Context, Knowledge, Memory, Skills getrennt.
- Skills ohne automatische Systemrechte — Permission Policy.
- Versionierte Migrationen; genau ein Backend besitzt die DB.
7. Channels
Channels (ctrl-c) sind dünne Transportadapter (start, stop, sendMessage, onMessage).
Reihenfolge: Webhook, Telegram, Discord, Slack, WhatsApp zuletzt.
Identitäten: ExternalUser → ChannelIdentity → LocalUser → Workspace-Rechte.
8. API
Interne API unter /api/v1: Workspaces, Conversations, Runs, Providers, Models, Skills, MCP, Pairing.
Kompatibilitätsschicht: /v1/models, /v1/chat/completions, /v1/responses, /v1/embeddings — Adapter auf den Core, nicht das Domain-Modell.
9. Phasenplan
Phase 0 – Architecture Spike: Bun Workspace, Expo Android/Web, Electron-Shell, ctrl-s, WebSocket-Streaming, Electron startet Gateway-Binary, Android verbindet sich im LAN. Abbruchkriterium: Dieselbe Conversation auf Desktop und Android, Streaming auf beiden, keine Provider-Logik in der UI.
Phase 1 – Chat MVP: Workspace-/Conversation-CRUD, Streaming, OpenAI-kompatibler Provider, Markdown/Code, Stop/Retry, SQLite (ctrl-z), Export/Import, Mock Provider.
Phase 2 – Agent Runtime: Tool Registry, Approval-UI, Shell-/FS-Tool mit Sandbox, Agent Profiles, Cancellation, Timeouts, Audit Log, MCP (ctrl-m).
Phase 3 – Desktop Host: Electron-Lifecycle, Tray, Autostart, Updater, safeStorage, llama-server-Sidecar, lokaler OpenAI-Endpoint, LAN-Pairing, Packaging.
Phase 4 – Personal Knowledge: Attachments, Extraktion, Chunking, FTS, Memory-Review-UI, Context Inspector (ctrl-k / ctrl-z). Vector/hybrid search follows in Phase 11 (ADR-0004).
Phase 5 – Channels: Telegram, Discord, Slack, Identity Mapping, Queue/Retry, Rate Limits (ctrl-c).
Phase 6 – On-Device-Inferenz (Android): Native Module, LiteRT-LM, Capability Detection, Thermal/Battery-Policies.
Phase 7 – Sync (host-centric): Gateway owns the DB. Clients never sync SQLite files peer-to-peer. MVP = on-host backup snapshots (create / list / restore) of workspace exports under data/backups/; desktop UI + contract tests. Later = passphrase-encrypted backups. Incremental pull/push between devices is out of MVP (see §10).
Phase 8 – Featured CLI (ctrl-f5): Publishable ops tool after Sync. Full local ops: config file (CTRL_* / ~/.config/ctrl/config.json), gateway start/stop/restart/status (spawn ctrl-a), pairing/token, workspaces + interactive/one-shot chat (streamChat), thin wrappers for knowledge/channels. Shell tab-completions for bash and PowerShell (ctrl-f5 completion bash|powershell). Speaks only HTTP/WS protocol via ctrl-i (prep for Rust agentd).
Phase 9 – Rust-Backend (optional): Siehe Abschnitt 11 (was Phase 8).
10. Nicht ins MVP
WhatsApp, Cross-Device-Konfliktlösung, autonomes Long-Term-Memory, eigener Vector-DB-Server, Model-Downloader, eingebettetes Android-LLM, Plugin-Marketplace, Multi-Agent-Orchestrierung, Internetzugriff von außen, automatische Skill-Generierung, volle OpenAI-Kompatibilität.
11. Weg zum Rust-Backend (agentd)
Voraussetzungen
ctrl-sist Quelle der Wahrheit (JSON Schema / OpenAPI).- Contract-Tests (
ctrl-t) gegen jedes Backend über HTTP/WebSocket. - Ein Prozess besitzt die DB; Schema versioniert.
- CLI spricht nur das Protokoll.
Migrationsreihenfolge
- RuntimeManager / Process-Manager als Rust-Sidecar.
- Pairing, Auth, LAN-Server.
- Storage-Layer bei Bedarf (Schema vorher einfrieren).
- Agent Loop nur bei klarem Grund.
Typen für Rust aus dem Schema (typify, progenitor).
12. Contract-Test-Strategie
- Ort:
packages/ctrl-t, unabhängig vom Backend; Ziel überBASE_URL. - Fixtures: Request/Response- und Event-Sequenzen.
- Mock Provider: deterministische Events.
- Prüfungen: JSON Schema, Event-Reihenfolge, Auth/Pairing, Cancel/Timeouts, Migrationen.
- CI: Gegen Bun-Gateway bei jedem Commit; später parallel gegen
agentd.
13. Technische Leitplanken
- Zod an jeder Prozessgrenze (
ctrl-s), auch IPC. - Strukturierte Events und
AbortSignalüberall. - Provider Capability Detection.
- Migrationsfähiges SQLite; getrennte Secrets.
- Permission Policy und Audit Log.
- Standardbindung
127.0.0.1. - Threat Model in
docs/threat-modelvor Phase 2 und vor LAN.
14. ADR-0001: Electron als Desktop-Runtime
Status: Akzeptiert
Kontext: Desktop soll lokales Gateway starten, Tray, Autostart, Updates und Secrets bieten und TypeScript mit dem Monorepo teilen.
Entscheidung: Electron als Shell. Gateway bleibt eigener Bun-Prozess (ctrl-a). Renderer ist normaler Gateway-Client; IPC nur für Native.
Konsequenzen: Höherer RAM/Bundle akzeptiert; CI-Matrix früh; Security-Checkliste pflegen; Tauri später möglich, weil Renderer reines Web ist.
Alternativen: Tauri 2, Ionic/Capacitor, Flutter.
15. Nächste Schritte
- Monorepo mit Bun Workspaces anlegen;
ctrl-sundctrl-a(/healthz) — done. - Electron-Shell mit Gateway-Lifecycle — done.
- Expo-Clients im LAN + WebSocket-Demo-Stream (
/ws/v1/demo,@involvex/ctrl-i) — done. - Contract-Tests (
ctrl-t) und CI-Matrix — done. - Dependabot, SECURITY.md, tag→GitHub Release workflow — done (see ci-and-releases.md).
- Phase 1 Chat MVP — done:
ctrl-zSQLite, workspace/conversation/message API,ctrl-rproviders (mock + OpenAI-compatible), Markdown chat UI, export/import, contract tests. - Phase 2 Agent Runtime — done:
ctrl-xtool registry + sandbox shell/fs, approval gate + desktop UI, cancel/timeouts, audit log, agent profiles,ctrl-mMCP stdio JSON-RPC, LLM-native tool calling (OpenAI-compatible + mock!call). - Phase 3 Desktop Host — done: Tray + close-to-tray, autostart, electron-updater scaffold,
safeStorage(gateway token + OpenAI key), gateway bearer auth / LAN pairing,ctrl-rllama-server sidecar (CTRL_LLAMA_BIN), packaging via electron-builder. - Phase 4 Personal Knowledge — done:
ctrl-kextract/chunk/FTS, attachments + blobs inctrl-z(schema v3), memory review + context inspector APIs/UI. - Phase 5 Channels — done:
ctrl-cadapters (webhook + mock telegram/discord/slack), identity mapping, outbox queue/retry/rate limits, gateway hooks + desktop panel. - Phase 6 On-Device Inference (Android) — done: LiteRT-LM Expo plugin (
react-native-litert-lm), capability probe (ABI/RAM/battery), thermal/battery policy inctrl-s(re-exported fromctrl-r), mobile/on-deviceUI with download retries + filesystem.litertlmimport (CDN mid-stream failures),expo-dev-client, release ProGuard consumer-rules plugin,native/android-litertdocs. - Phase 7 Sync — done (MVP + 7b): host backups under
data/backups/(BackupStore),/api/v1/backupscreate/list/get/restore, optional passphrase AES-256-GCM encryption, desktop panel +ctrl-f5 backup, contracts. No device↔device SQLite sync. - Phase 8 Featured CLI (
ctrl-f5) — done: LAN pair QR; gateway0.0.0.0; Metro host auto-fill;start/stop/status/chat/backup;completion bash|powershell. - Phase 9 Rust
agentd— optional. Provider selection persists indata/provider.json(non-secret); desktop/mobile/web provider + knowledge + tools + channels lockstep. Desktop nav panels are exclusive (one open at a time; Escape closes). Telegram Bot API send adapter (real); Discord/Slack still mock. - Phase 10 — Local-first chat (done this slice):
- One chat UI with generator picker
litert|gateway|zen;/on-deviceis model setup only. - Local brain (
ctrl.mobile.brain.v1) + append-only sync when gateway health succeeds. - Empty Android ABI counts as arm64 for LiteRT policy; policy disables LiteRT only, not chat.
- Standalone
android:release= GradleassembleRelease(embedded JS; no Metro:8081/:8082). - Out of this pass: Rust
agentd, Discord/Slack real SDKs, vector search.
- One chat UI with generator picker
- Phase 11 — Personal Knowledge vector & hybrid search (done this slice):
- Schema v5
knowledge_vectors(rawfloat32BLOBs, cascade with chunks); cosine scoring in-process insidectrl-k(see ADR-0004). Embeddercontract inctrl-s;ctrl-rembeddershash(deterministic default, offline),openai/llama/litertvia OpenAI-compatible/embeddings,off.searchKnowledgegainsmode: fts | vector | hybrid; hybrid fusesbm25+ cosine with reciprocal rank fusion (k = 60); hits gainscore+source.- Embed on ingest (failure tolerated),
POST /api/v1/workspaces/:id/knowledge/reindex,GET /api/v1/embeddingsstatus; desktop knowledge panel gets mode picker, status line, reindex button; unit + contract tests. - Out of this pass: ANN index (sqlite-vec/HNSW), cross-workspace search, re-embedding on model switch (use
reindex --force), semantic reranking.
- Schema v5
- Ship v0 (standalone daily-driver) — see standalone-local.md, soak-v0.md, RELEASE-v0.md.
- Parked until soak is boring: in-app binary downloaders, HF catalog, Discord/Slack real SDKs, incremental device sync, Rust
agentd, monetization.
- Parked until soak is boring: in-app binary downloaders, HF catalog, Discord/Slack real SDKs, incremental device sync, Rust