Initial spark-mind: heartbeat + memory + persona + mind on Spark X2.5-4B

This commit is contained in:
n1kt4r committed 2026-09-09 22:16:29 +05:00
commit 709d7ce17d
10 files changed
+991

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
node_modules/
dist/
.build/
.env
.env.local
*.tsbuildinfo
.DS_Store
data/*.db
data/*.sqlite
data/embeddings/
+75
View File
@@ -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).
+37
View File
@@ -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);
+409
View File
@@ -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"
}
}
}
+17
View File
@@ -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"
}
}
+99
View File
@@ -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<string, unknown> };
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<typeof setTimeout> | null = null;
private lastTick = 0;
private sessionState: SessionState = 'cold';
private lastSessionId: string | null = null;
private readonly config: HeartbeatConfig;
constructor(config: Partial<HeartbeatConfig> = {}) {
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<void> {
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<string> {
// 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;
}
}
+109
View File
@@ -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<string, ContactNotepad>();
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;
}
+134
View File
@@ -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<LLMResult>;
}
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<LLMResult> {
// 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<void> {
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());
}
}
+88
View File
@@ -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 ?? [];
}
+13
View File
@@ -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"]
}