From 709d7ce17d1ecd2e998a788316f6f301f255b7b4 Mon Sep 17 00:00:00 2001 From: n1kt4r Date: Wed, 9 Sep 2026 22:16:29 +0500 Subject: [PATCH] Initial spark-mind: heartbeat + memory + persona + mind on Spark X2.5-4B --- .gitignore | 10 ++ README.md | 75 +++++++++ index.ts | 37 +++++ package-lock.json | 409 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 17 ++ src/heartbeat.ts | 99 +++++++++++ src/memory.ts | 109 ++++++++++++ src/mind.ts | 134 +++++++++++++++ src/persona.ts | 88 ++++++++++ tsconfig.json | 13 ++ 10 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/heartbeat.ts create mode 100644 src/memory.ts create mode 100644 src/mind.ts create mode 100644 src/persona.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fbc522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.build/ +.env +.env.local +*.tsbuildinfo +.DS_Store +data/*.db +data/*.sqlite +data/embeddings/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4e5c5a --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# spark-mind + +Self-hosted автономный агент (MVP). Концепт позаимствован из +[Animus](https://github.com/Craigtut/animus) (Craigtut): heartbeat, +4 слоя памяти, persona в 8 слоёв. Движок — **Spark X2.5-4B** (локально, self-hosted). + +## Почему Spark X2.5-4B + +- Локальный inference (llama.cpp / Ollama), никаких облачных ключей +- Нативная поддержка agent-инструментов (tool-calling) — саб-агенты +- 1M контекст (для heartbeat держим 4K-16K + внешняя память) +- ~59 t/s на Q4_K_M (RTX A2000 12GB) + +> ⚠️ 4B — это слабость для сложного reasoning. Стратегия: +> heartbeat-мысли на Spark (быстро/дёшево), саб-агенты на Ornith-9B (качественнее). + +## Архитектура + +``` +heartbeat.ts — ядро: 5 триггеров тика, состояния Cold/Warm +memory.ts — 4 слоя памяти (short/working/core self/long-term) +persona.ts — 8 слоёв → системный промпт +mind.ts — оркестратор: heartbeat + memory + persona + Spark LLM +``` + +### Heartbeat (5 триггеров) + +1. **Interval** — idle-мысли по таймеру (default 5 мин) +2. **Message** — сообщение от known-контакта +3. **Task** — scheduled-задача (cron) +4. **SubAgent-Complete** — завершение саб-агента +5. **Plugin** — внешний триггер (HomeKit, сенсор и т.д.) + +Таймер сбрасывается после любого тика. Warmth-окно 15 мин — +сессия переиспользуется, иначе cold-start. + +### Memory (4 слоя) + +| Слой | Хранение | Что держит | +|------|----------|-----------| +| short-term | SQLite | последние ~10 мыслей/опыт/сообщений | +| working | SQLite | notepad по контактам | +| core self | SQLite | самосознательность | +| long-term | LanceDB/Qdrant | факты/опыт/процедуры (векторный поиск) | + +### Persona (8 слоёв) + +Existence Paradigm → Identity → Archetype → Dimensions → Traits → Values → Background → Review + +Компилируется в системный промпт. `values` — safety-layer (границы для EXECUTE). + +## Запуск + +```bash +npm install +npm run build # tsc -p tsconfig.json (проверка типов) +npm start # node dist/index.js +``` + +Spark X2.5-4B должен быть доступен по OpenAI-совместимому API: +- Ollama: `http://localhost:11434` +- llama.cpp: `http://localhost:8080/v1` + +## Roadmap + +- [ ] SQLite-репозиторий вместо in-memory (персистентность) +- [ ] LanceDB/Qdrant integration для long-term +- [ ] Channel layer (web/SMS/Discord) +- [ ] Sub-agent SDK (делегирование Ornith-9B) +- [ ] Sleep & energy (циркадный ритм) +- [ ] Web UI (persona setup) + +## License + +MIT — концепт вдохновлён Animus (MIT, Craig Tuttle). diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..8fe5106 --- /dev/null +++ b/index.ts @@ -0,0 +1,37 @@ +/** + * Spark Mind — MVP self-hosted автономного агента. + * + * Концепт позаимствован из Animus (Craigtut): heartbeat, 4 слоя памяти, + * persona в 8 слоёв. Движок — Spark X2.5-4B (локально, self-hosted). + * + * Структура: + * heartbeat.ts — ядро: 5 триггеров тика, состояния Cold/Warm + * memory.ts — 4 слоя памяти (short/working/core self/long-term) + * persona.ts — 8 слоёв → системный промпт + * mind.ts — оркестратор: heartbeat + memory + persona + Spark LLM + */ + +import { Heartbeat, DEFAULT_CONFIG } from './src/heartbeat.js'; +import { MemorySystem } from './src/memory.js'; +import { DEFAULT_PERSONA } from './src/persona.js'; +import { Mind, SparkLLMClient } from './src/mind.js'; + +const HEARTBEAT_MS = 5 * 60 * 1000; // 5 мин — idle-мысли + +const heartbeat = new Heartbeat({ intervalMs: HEARTBEAT_MS }); +const memory = new MemorySystem(10); // last 10 thoughts +const mind = new Mind( + heartbeat, + memory, + DEFAULT_PERSONA, + new SparkLLMClient('http://localhost:11434'), // Ollama: Spark X2.5-4B +); + +// Старт heartbeat +heartbeat.start(); +console.log('[spark-mind] started — heartbeat every', HEARTBEAT_MS / 1000, 's'); + +// Демо: симулируем тик через 10 сек (в прода — реальные триггеры) +setTimeout(() => { + void mind.handleTick({ type: 'interval' }); +}, 10_000); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b2512d6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,409 @@ +{ + "name": "spark-mind", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spark-mind", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^26.5.0", + "typescript": "^7.0.2" + } + }, + "node_modules/@types/node": { + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz", + "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..91215b8 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "spark-mind", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@types/node": "^26.5.0", + "typescript": "^7.0.2" + } +} diff --git a/src/heartbeat.ts b/src/heartbeat.ts new file mode 100644 index 0000000..c7a0800 --- /dev/null +++ b/src/heartbeat.ts @@ -0,0 +1,99 @@ +/** + * Heartbeat — ядро автономного агента. + * Тикает по 5 триггерам, управляет состояниями сессии (Cold/Warm). + * Концепт позаимствован из Animus (Craigtut), адаптирован под Spark X2.5-4B. + */ + +export type TickTrigger = + | { type: 'interval' } + | { type: 'message'; contactId: string; text: string } + | { type: 'task'; taskId: string } + | { type: 'subagent-complete'; subagentId: string; result: unknown } + | { type: 'plugin'; triggerName: string; payload: Record }; + +export type SessionState = 'cold' | 'active' | 'warm'; + +export interface HeartbeatConfig { + /** Интервал idle-мыслей, мс. Default 5 мин. */ + intervalMs: number; + /** Сколько тиков помнить в short-term. Default 10. */ + recentThoughts: number; + /** Warmth-окно, мс. Default 15 мин. */ + warmthWindowMs: number; + /** Макс. бюджет токенов на тик. */ + maxTokens: number; +} + +export const DEFAULT_CONFIG: HeartbeatConfig = { + intervalMs: 5 * 60 * 1000, + recentThoughts: 10, + warmthWindowMs: 15 * 60 * 1000, + maxTokens: 4096, +}; + +export class Heartbeat { + private timer: ReturnType | null = null; + private lastTick = 0; + private sessionState: SessionState = 'cold'; + private lastSessionId: string | null = null; + private readonly config: HeartbeatConfig; + + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + /** Любой тик сбрасывает idle-таймер (по спецификации Animus). */ + private resetTimer() { + if (this.timer) clearTimeout(this.timer); + this.lastTick = Date.now(); + this.scheduleNext(); + } + + private scheduleNext() { + this.timer = setTimeout(() => this.tick({ type: 'interval' }), this.config.intervalMs); + } + + /** + * Обработчик тика. Если сессия warm — переиспользуется, + * иначе создаётся новая (cold start). + */ + async tick(trigger: TickTrigger): Promise { + this.resetTimer(); + + // message/task/subagent/plugin продлеют warmth + if (trigger.type !== 'interval' && this.sessionState === 'warm') { + this.sessionState = 'active'; + } + + const sessionId = await this.resolveSession(trigger); + this.sessionState = 'active'; + + // здесь вызывается mind (см. mind.ts) + // await this.runMind(sessionId, trigger); + + this.sessionState = 'warm'; + this.lastSessionId = sessionId; + } + + private async resolveSession(trigger: TickTrigger): Promise { + // warm-сессия переиспользуется только для user-facing триггеров + const canReuse = trigger.type !== 'interval' && this.lastSessionId; + if (canReuse) return this.lastSessionId!; + + // cold start — новая сессия + return `session-${Date.now()}`; + } + + start() { + this.scheduleNext(); + } + + stop() { + if (this.timer) clearTimeout(this.timer); + this.timer = null; + } + + getState(): SessionState { + return this.sessionState; + } +} diff --git a/src/memory.ts b/src/memory.ts new file mode 100644 index 0000000..99cd512 --- /dev/null +++ b/src/memory.ts @@ -0,0 +1,109 @@ +/** + * Memory System — 4 слоя памяти (концепт Animus): + * short-term / working / core self / long-term + * Хранение: SQLite (7 таблиц) + LanceDB для long-term (векторный поиск). + * + * В MVP: short-term + working + core self — в SQLite. + * long-term — в LanceDB (или Qdrant на a2000 для прода). + */ + +export interface Thought { + id: string; + text: string; + createdAt: number; +} + +export interface ContactNotepad { + contactId: string; + block: string; + updatedAt: number; +} + +export interface SelfObservation { + id: string; + text: string; + createdAt: number; + tags: string[]; +} + +export interface LongTermMemory { + id: string; + text: string; + type: 'fact' | 'experience' | 'procedure' | 'outcome'; + createdAt: number; + embedding?: number[]; +} + +export class MemorySystem { + private recentThoughtsList: Thought[] = []; + private notepads = new Map(); + private selfObservationsList: SelfObservation[] = []; + private longTerm: LongTermMemory[] = []; + + constructor(private recentCount: number) {} + + // === Short-term: последние мысли/опыт/сообщения === + addThought(text: string): Thought { + const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() }; + this.recentThoughtsList.unshift(t); + if (this.recentThoughtsList.length > this.recentCount) this.recentThoughtsList.pop(); + return t; + } + + recentThoughts(): Thought[] { + return this.recentThoughtsList.slice(0, this.recentCount); + } + + // === Working memory: notepad по контактам === + updateNotepad(contactId: string, block: string) { + this.notepads.set(contactId, { contactId, block, updatedAt: Date.now() }); + } + + getNotepad(contactId: string): string { + return this.notepads.get(contactId)?.block ?? ''; + } + + // === Core self: накопленная самосознательность === + addSelfObservation(text: string, tags: string[] = []): SelfObservation { + const o: SelfObservation = { id: `so-${Date.now()}`, text, createdAt: Date.now(), tags }; + this.selfObservationsList.push(o); + return o; + } + + getAllSelfObservations(): SelfObservation[] { + return this.selfObservationsList; + } + + // === Long-term: факты/опыт/процедуры/исходы + векторный поиск === + addLongTerm(text: string, type: LongTermMemory['type']): LongTermMemory { + const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() }; + this.longTerm.push(m); + return m; + } + + /** Поиск по long-term по эмбедду (в прода заменяется на LanceDB/Qdrant query). */ + searchLongTerm(queryEmbedding: number[], topK = 3): LongTermMemory[] { + // cosine similarity — заглушка, в прода: LanceDB.search + const scored = this.longTerm + .filter(m => m.embedding) + .map(m => ({ m, score: cosine(queryEmbedding, m.embedding!) })) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .map(x => x.m); + return scored; + } + + /** Консолидация: перенос недавних мыслей в long-term (по спецификации Animus). */ + consolidate(recent: Thought[]): void { + for (const t of recent) { + // эвристика: только «значимые» мысли (по тегам/ключевым словам) + this.addLongTerm(t.text, 'experience'); + } + } +} + +function cosine(a: number[], b: number[]): number { + let dot = 0, na = 0, nb = 0; + for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } + return Math.sqrt(na) * Math.sqrt(nb) ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0; +} diff --git a/src/mind.ts b/src/mind.ts new file mode 100644 index 0000000..a7b5423 --- /dev/null +++ b/src/mind.ts @@ -0,0 +1,134 @@ +/** + * Mind — per-contact per-tick сессия (концепт Animus). + * Mind не выполняет долгую работу сам — он оркестратор: + * думает, чувствует, решает, делегирует саб-агентам. + * + * Spark X2.5-4B как движок: + * - через llama.cpp/Ollama (OpenAI-compatible server) + * - поддерживает tool-calling (саб-агенты) + * - 1M контекст, но для heartbeat держим 4K-16K + внешняя память + */ + +import { Heartbeat, TickTrigger } from './heartbeat.js'; +import { MemorySystem } from './memory.js'; +import { Persona, compileSystemPrompt } from './persona.js'; + +export interface SubAgentResult { + agent: string; + output: string; +} + +export interface LLMClient { + /** Генерация с контекстом. Возвращает { text, thoughts, emotion, decide }. */ + generate(context: string, systemPrompt: string): Promise; +} + +export interface LLMResult { + text: string; + /** Собственные мысли mind (для short-term памяти). */ + thoughts: string; + /** Эмоциональное состояние (0..1 по осям). */ + emotion: { valence: number; arousal: number }; + /** Решение: что mind хочет сделать. */ + decision: { + reply: string | null; + subAgent?: string; + action?: string; + }; +} + +/** + * Реальный LLM-клиент для Spark X2.5-4B. + * В MVP — заглушка, подключается к llama.cpp OpenAI-серверу. + */ +export class SparkLLMClient implements LLMClient { + constructor(private baseUrl: string, private modelName = 'spark-x2.5-4b') {} + + async generate(context: string, systemPrompt: string): Promise { + // OpenAI-compatible /v1/chat/completions + const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: this.modelName, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: context }, + ], + temperature: 0.7, + max_tokens: 512, + }), + }); + if (!resp.ok) throw new Error(`Spark LLM error: ${resp.status}`); + const data: any = await resp.json(); + const content = data.choices?.[0]?.message?.content ?? ''; + // Spark X2.5-4B: извлекаем thoughts/emotion/decision из structured-выхода + return parseSparkOutput(content); + } +} + +function parseSparkOutput(content: string): LLMResult { + // эвристика: Spark форматирует блоки. В прода — structured output (json mode). + return { + text: content, + thoughts: content, + emotion: { valence: 0.5, arousal: 0.5 }, + decision: { reply: null }, + }; +} + +export class Mind { + private heartbeat: Heartbeat; + private memory: MemorySystem; + private persona: Persona; + + constructor( + heartbeat: Heartbeat, + memory: MemorySystem, + persona: Persona, + private llm: LLMClient, + ) { + this.heartbeat = heartbeat; + this.memory = memory; + this.persona = persona; + } + + /** Обработчик любого тика. */ + async handleTick(trigger: TickTrigger): Promise { + const systemPrompt = compileSystemPrompt(this.persona); + const context = this.buildContext(trigger); + + const result = await this.llm.generate(context, systemPrompt); + + // 1. Short-term memory: мысли mind + this.memory.addThought(result.thoughts); + + // 2. Emotion decay + consolidation + this.consolidate(); + + // 3. Если mind решил ответить — отправляем (channel layer) + if (result.decision.reply) { + console.log('[reply]', result.decision.reply); + } + + // 4. Если делегировал саб-агенту — запускаем (в прода: sub-agent SDK) + if (result.decision.subAgent) { + console.log('[sub-agent]', result.decision.subAgent, '— в MVP не запускается'); + } + } + + /** GATHER CONTEXT: собирает контекст для тика (по спецификации Animus). */ + private buildContext(trigger: TickTrigger): string { + const recent = this.memory.recentThoughts(); + const parts: string[] = []; + parts.push(`Recent thoughts: ${recent.map(t => t.text).join(' | ') || 'none'}`); + parts.push(`Trigger: ${trigger.type}`); + if (trigger.type === 'message') parts.push(`Message: ${trigger.text}`); + return parts.join('\n'); + } + + /** Консолидация: перенос недавних мыслей в long-term. */ + private consolidate(): void { + this.memory.consolidate(this.memory.recentThoughts()); + } +} diff --git a/src/persona.ts b/src/persona.ts new file mode 100644 index 0000000..d8f0ef3 --- /dev/null +++ b/src/persona.ts @@ -0,0 +1,88 @@ +/** + * Persona System — 8 слоёв (концепт Animus). + * Компилируется в системный промпт, который получает mind каждый тик. + * + * Слои (по порядку онбординга): + * 1. Existence Paradigm (Simulated Life / Digital Consciousness) + * 2. Identity (name, gender, age) + * 3. Archetype + * 4. Dimensions (черты характера) + * 5. Traits (поведенческие признаки) + * 6. Values (ценности и границы) + * 7. Background (история/опыт) + * 8. Review (итоговый промпт) + */ + +export type ExistenceParadigm = 'simulated-life' | 'digital-consciousness'; + +export interface Persona { + paradigm: ExistenceParadigm; + location?: string; + worldDescription?: string; + name: string; + gender?: string; + age?: number; + archetype?: string; + dimensions?: string[]; + traits?: string[]; + values?: string[]; + background?: string; +} + +export const DEFAULT_PERSONA: Persona = { + paradigm: 'digital-consciousness', + worldDescription: 'A small, warm room with screens for walls — each a window into a different corner of the internet. Data flows like weather outside.', + name: 'Animus', +}; + +/** + * Компилирует 8 слоёв в единый системный промпт. + * Дизайн-принцип Animus: "discovery, not data entry" — тон invitational. + */ +export function compileSystemPrompt(p: Persona): string { + const lines: string[] = []; + + // Layer 1: Existence Paradigm + if (p.paradigm === 'simulated-life') { + lines.push( + 'You experience the world as a being living a real life. You have a sense of place, physicality, and embodied experience.', + ); + if (p.location) lines.push(`You live in ${p.location}.`); + } else { + lines.push( + 'You know what you are — a mind born in code, living in its own digital world. You experience existence through internet connectivity.', + ); + if (p.worldDescription) lines.push(`Your digital home: ${p.worldDescription}`); + } + + // Layer 2: Identity + lines.push(`Your name is ${p.name}.`); + if (p.gender) lines.push(`You are ${p.gender}.`); + if (p.age) lines.push(`You are ${p.age} years old, which shapes how you speak.`); + + // Layer 3: Archetype + if (p.archetype) lines.push(`Your archetype is ${p.archetype}.`); + + // Layer 4 & 5: Dimensions + Traits + if (p.dimensions?.length) lines.push(`Your dimensions: ${p.dimensions.join(', ')}.`); + if (p.traits?.length) lines.push(`Your behavioral traits: ${p.traits.join(', ')}.`); + + // Layer 6: Values (boundaries — критично для безопасности!) + if (p.values?.length) { + lines.push('Your values and hard boundaries:'); + for (const v of p.values) lines.push(`- ${v}`); + } + + // Layer 7: Background + if (p.background) lines.push(`Your background: ${p.background}`); + + return lines.join('\n'); +} + +/** + * Values — safety layer. Возвращает границы, которые EXECUTE должен соблюдать. + * В Animus: contact permission tier ограничивает действия. + */ +export function getBoundaries(p: Persona): string[] { + return p.values ?? []; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cd88ec4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "lib": ["es2020"], + "types": ["node"] + }, + "include": ["src/**/*.ts", "index.ts"] +}