Compare commits
9
Commits
8d87b2cff4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f03a9dd5 | ||
|
|
436dd5fa31 | ||
|
|
288d05825f | ||
|
|
bae171d6b4 | ||
|
|
a01f183680 | ||
|
|
89b73db8a4 | ||
|
|
a6fde100ca | ||
|
|
702700dcb9 | ||
|
|
709d7ce17d |
No files matched your search
+10
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.build/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.tsbuildinfo
|
||||||
|
.DS_Store
|
||||||
|
data/*.db
|
||||||
|
data/*.sqlite
|
||||||
|
data/embeddings/
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
# spark-mind — DESIGN: автономный контент-продюсер с заземлением
|
||||||
|
|
||||||
|
Обновление плана от 2026-09-23 (вторая редакция: фокус смещён с рекламы/метрик
|
||||||
|
на **производство контента + анализ его метрик + SEO**).
|
||||||
|
|
||||||
|
## 1. Тезис
|
||||||
|
|
||||||
|
iLands строит общество на 80 000 агентов, чтобы получить «судью без автора».
|
||||||
|
Нам толпа не нужна: у нас есть **мир, который отвечает сам** — поисковики и
|
||||||
|
аудитория. Агент производит контент (текст, изображение, подкаст, видео),
|
||||||
|
публикует, и через заданный горизонт **замеряет, что сработало**: показы,
|
||||||
|
позиции, CTR, дочитывания, прослушивания, переходы, конверсии.
|
||||||
|
|
||||||
|
Два принципа, которых нет у iLands в рабочем виде:
|
||||||
|
|
||||||
|
1. **Заземление.** Качество контента оценивается не рассуждением агента,
|
||||||
|
а метриками, которые он не контролирует, с отложенным вердиктом.
|
||||||
|
2. **Автономия по заслугам.** Права выдаются по подтверждённому трек-рекорду:
|
||||||
|
доказал, что контент улучшает метрики — получил свободу публиковать
|
||||||
|
без спроса и выбирать темы сам. Провал — права сужаются.
|
||||||
|
Границы persona `values` непреодолимы на любом уровне.
|
||||||
|
|
||||||
|
**Что это даёт на практике:** агент не «генерирует текст вообще», а учится,
|
||||||
|
какой заголовок, структура и формат работают — на своих же сайтах
|
||||||
|
(orabota.ru, нейросети.рус, bzp02.ru и др.) и на заказах клиентов.
|
||||||
|
|
||||||
|
## 2. Три контура + конвейер
|
||||||
|
|
||||||
|
```
|
||||||
|
КОНВЕЙЕР ПРОИЗВОДСТВА brief → generated → optimized → published → measured
|
||||||
|
content_items — центральный объект, на нём замеряются метрики
|
||||||
|
|
||||||
|
A. КОНТУР ЗАКАЗОВ «для кого и за что» counterparties → deals
|
||||||
|
B. КОНТУР ЗАЗЕМЛЕНИЯ «что сработало» outcomes → capabilities → ledger
|
||||||
|
C. КОНТУР МЕТОДОВ «как производить» skills + reflection
|
||||||
|
```
|
||||||
|
|
||||||
|
Плюс сквозные: `noAction` (P5) и тонкий `Channel` (бывший P7).
|
||||||
|
|
||||||
|
## 3. Модель данных (SQLite, тот же файл БД)
|
||||||
|
|
||||||
|
### 2.1 Конвейер: content_items — центральный объект
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE content_items (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
kind TEXT NOT NULL CHECK(kind IN ('text','image','podcast','video')),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
topic TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'brief'
|
||||||
|
CHECK(status IN ('brief','generated','optimized','published',
|
||||||
|
'measured','archived')),
|
||||||
|
target_url TEXT, -- где опубликован
|
||||||
|
site TEXT, -- orabota.ru | нейросети.рус | ...
|
||||||
|
deal_id TEXT REFERENCES deals(id), -- nullable: для своих сайтов
|
||||||
|
published_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Конвейер — это переходы `status`. Каждый переход пишется в `deal_events`-подобный
|
||||||
|
журнал, если привязан к сделке.
|
||||||
|
|
||||||
|
### A. Контур заказов
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE counterparties (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
kind TEXT NOT NULL CHECK(kind IN ('client','platform','external')),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
trust_score REAL NOT NULL DEFAULT 0, -- derived из истории
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE deals (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
counterparty_id TEXT NOT NULL REFERENCES counterparties(id),
|
||||||
|
brief TEXT NOT NULL, -- что заказали (тема/формат/объём)
|
||||||
|
price REAL,
|
||||||
|
currency TEXT DEFAULT 'RUB',
|
||||||
|
status TEXT NOT NULL CHECK(status IN
|
||||||
|
('offered','accepted','delivered','paid','refused','defaulted')),
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
closed_at INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE deal_events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
deal_id TEXT NOT NULL REFERENCES deals(id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
payload_json TEXT,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Контур заземления
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Один outcome = один content_item + одно измерение метрики.
|
||||||
|
CREATE TABLE outcomes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
item_id TEXT REFERENCES content_items(id), -- что мерим
|
||||||
|
deal_id TEXT REFERENCES deals(id),
|
||||||
|
area TEXT NOT NULL, -- text|image|podcast|video|seo_onpage|seo_semantics|publish
|
||||||
|
metric_kind TEXT NOT NULL CHECK(metric_kind IN
|
||||||
|
('reach','engagement','position','traffic',
|
||||||
|
'conversion','revenue','speed','quality')),
|
||||||
|
metric_name TEXT NOT NULL, -- 'impressions','ctr','avg_position','dwell_sec',
|
||||||
|
-- 'listens','completion_rate','indexing_hours','clicks'
|
||||||
|
unit TEXT, -- 'шт','%','сек','место','руб','ч'
|
||||||
|
direction TEXT NOT NULL CHECK(direction IN ('up_good','down_good')),
|
||||||
|
value_before REAL, -- null для нового контента
|
||||||
|
value_after REAL,
|
||||||
|
source TEXT NOT NULL CHECK(source IN
|
||||||
|
('metrika','gsc','webmaster','wordstat','platform','manual','client')),
|
||||||
|
horizon_days INTEGER NOT NULL,
|
||||||
|
due_at INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK(status IN ('pending','measured','missed','unavailable')),
|
||||||
|
measured_at INTEGER,
|
||||||
|
score REAL -- нормализованный вердикт [-1..+1]
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Автономия по заслугам: тир НА ОБЛАСТЬ, не общий.
|
||||||
|
CREATE TABLE capabilities (
|
||||||
|
area TEXT PRIMARY KEY,
|
||||||
|
tier INTEGER NOT NULL DEFAULT 0 CHECK(tier BETWEEN 0 AND 4),
|
||||||
|
positive_outcomes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
negative_outcomes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ledger (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
ts INTEGER NOT NULL,
|
||||||
|
delta REAL NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
balance_after REAL NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Контур методов
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE skills (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL, -- 'podcast-silero','image-comfyui','seo-onpage-editor'
|
||||||
|
area TEXT NOT NULL, -- привязка к capabilities.area
|
||||||
|
method_md TEXT NOT NULL, -- как делать (промпт/алгоритм/порядок шагов)
|
||||||
|
tool TEXT, -- реальный инструмент: ace-step|silero|comfyui|editor-pro|astro|wordstat
|
||||||
|
source TEXT NOT NULL CHECK(source IN ('builtin','learned')),
|
||||||
|
validated INTEGER NOT NULL DEFAULT 0,
|
||||||
|
uses INTEGER NOT NULL DEFAULT 0,
|
||||||
|
success_rate REAL,
|
||||||
|
embedding BLOB -- bge-m3, dim=1024 — retrieval под задачу
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Правила
|
||||||
|
|
||||||
|
### 4.1 Области (area) и типы метрик
|
||||||
|
|
||||||
|
| area | Что производит/делает | Типовые метрики |
|
||||||
|
|------|----------------------|-----------------|
|
||||||
|
| `text` | статьи, лонгриды, описания | показы, CTR, дочитывания, позиции |
|
||||||
|
| `image` | изображения, обложки, карточки | показы, CTR, скачивания |
|
||||||
|
| `podcast` | аудио, подкасты (Silero TTS) | прослушивания, completion rate, дослушивания |
|
||||||
|
| `video` | видео, шортсы | просмотры, watch time, удержание |
|
||||||
|
| `seo_onpage` | заголовки, структура, разметка | позиции, CTR, индексация |
|
||||||
|
| `seo_semantics` | семантика, кластеры, запросы | охват запросов, релевантность |
|
||||||
|
| `publish` | публикация, распространение | скорость публикации, индексация |
|
||||||
|
|
||||||
|
### 4.2 Нормализация метрики в score
|
||||||
|
|
||||||
|
```
|
||||||
|
raw = (value_after - value_before) / max(|value_before|, eps)
|
||||||
|
score = clamp(raw * (direction == 'up_good' ? 1 : -1), -1, 1)
|
||||||
|
```
|
||||||
|
- Нет `value_before` (новый контент) → сравнение с базлином области
|
||||||
|
(медиана последних 10 measured в этой `area`).
|
||||||
|
- `quality` — дискретно: `+1 / 0 / −1` (эксперт/клиент/человек).
|
||||||
|
- `speed` — `direction = 'down_good'` (быстрее индексация = лучше).
|
||||||
|
- `status != 'measured'` → score не участвует в трек-рекорде.
|
||||||
|
|
||||||
|
### 4.3 Автономия по заслугам
|
||||||
|
|
||||||
|
| tier | Имя | Что разрешено |
|
||||||
|
|------|-----|----------------|
|
||||||
|
| 0 | observer | только анализ и предложения, публикаций нет |
|
||||||
|
| 1 | suggest | предлагает готовый контент человеку, сам не публикует |
|
||||||
|
| 2 | ask | публикует после подтверждения |
|
||||||
|
| 3 | act+report | публикует и отчитывается; человек может откатить |
|
||||||
|
| 4 | autonomous | публикует и выбирает темы сам, в границах persona |
|
||||||
|
|
||||||
|
Переходы:
|
||||||
|
- `+1 тир` за **3 нетто-положительных** outcome в области.
|
||||||
|
- `−1 тир` за **1 нетто-отрицательный** — асимметрия: падать легче.
|
||||||
|
- Затухание: outcome старше 90 дней весит 0.5.
|
||||||
|
- Новая область стартует с tier 0.
|
||||||
|
- **Границы persona `values` непреодолимы на любом тире.**
|
||||||
|
|
||||||
|
### 4.4 Энергия
|
||||||
|
|
||||||
|
- Расход: тик, генерация (текст/фото/аудио/видео — дороже всего), публикация,
|
||||||
|
вызов саб-агента.
|
||||||
|
- Приход: **подтверждённые положительные outcomes** (особенно `revenue`),
|
||||||
|
плюс донат человека.
|
||||||
|
- Низкий баланс → интервал тиков растёт → Deep Rest.
|
||||||
|
- Отличие от iLands: энергия восполняется результатом, а не покупкой.
|
||||||
|
|
||||||
|
## 5. Триггеры heartbeat (добавляются)
|
||||||
|
|
||||||
|
| Триггер | Что делает |
|
||||||
|
|---------|------------|
|
||||||
|
| `{type:'pipeline'}` | продвинуть content_items по конвейеру (draft→generated→optimized→published) |
|
||||||
|
| `{type:'measure'}` | наступил `due_at` → тянет метрику из источника → `value_after`, `score` |
|
||||||
|
| `{type:'reflection'}` | ночной цикл: обобщает items + outcomes + thoughts → long-term и `self_observations` |
|
||||||
|
| `{type:'task'}` | cron-планировщик (нужен для measure/reflection) |
|
||||||
|
|
||||||
|
Плюс `decision.noAction: true` (P5).
|
||||||
|
|
||||||
|
Источники метрик: **Метрика API, Google Search Console, Yandex Webmaster API,
|
||||||
|
Wordstat** — у всех доступ уже настроен. Плюс `platform` (прослушивания/просмотры)
|
||||||
|
и `manual`/`client`.
|
||||||
|
|
||||||
|
## 6. Тонкий Channel (бывший P7)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Channel { send(text: string): Promise<void>; }
|
||||||
|
```
|
||||||
|
Реализации: `ConsoleChannel`, `TelegramChannel`, `FileChannel`.
|
||||||
|
`LLMClient` уже есть. Полный harness-реестр отложен — абстракция окупается
|
||||||
|
с ≥2 реализациями.
|
||||||
|
|
||||||
|
## 7. Методы (skills) — привязка к реальному стеку
|
||||||
|
|
||||||
|
| area | tool | Что уже есть |
|
||||||
|
|------|------|--------------|
|
||||||
|
| text | editor-pro | 7-layer markdown pipeline (типографика, SEO-слой) |
|
||||||
|
| image | comfyui | генерация изображений через diffusion-воркфлоу |
|
||||||
|
| podcast | silero | Silero TTS 24kHz, пайплайн через normalizar/tts |
|
||||||
|
| video | video-pipeline | загрузка/обработка видео |
|
||||||
|
| seo_onpage | editor-pro, astro | структура, заголовки, schema |
|
||||||
|
| seo_semantics | wordstat | Wordstat API: частотность, кластеры |
|
||||||
|
| publish | astro + deploy | сборка Astro → публикация |
|
||||||
|
|
||||||
|
Это ключевое: **скиллы агента = твой существующий производственный стек**,
|
||||||
|
а не абстрактные описания. Retrieval по bge-m3 подгружает нужный метод под задачу.
|
||||||
|
|
||||||
|
## 8. Порядок внедрения
|
||||||
|
|
||||||
|
| Шаг | Что | Критерий приёмки |
|
||||||
|
|-----|-----|------------------|
|
||||||
|
| 0 | P5 `noAction` + тонкий `Channel` | агент умеет не действовать; вывод через интерфейс |
|
||||||
|
| 1 | Планировщик (`task`/`pipeline`/`measure`) | задача с датой выполняется в срок |
|
||||||
|
| 2 | `content_items` + конвейер | item проходит brief→published, переходы в журнале |
|
||||||
|
| 3 | `outcomes` + `measure` (Метрика/GSC/Webmaster/Wordstat) | метрика замеряется через horizon, score пишется |
|
||||||
|
| 4 | `capabilities` — автономия по областям | тир растёт после 3 успехов, падает после провала |
|
||||||
|
| 5 | `skills` + retrieval + `reflection` | нужный метод подгружается по задаче; self_observations оживают |
|
||||||
|
| 6 | `deals` + `counterparties` | заказ клиента проходит offered→paid |
|
||||||
|
| 7 | `ledger` — энергия | результат пополняет энергию, дефицит замедляет тики |
|
||||||
|
|
||||||
|
Зависимости: 3 требует 1 и 2. 4 требует 3. 5 требует 2. 7 требует 3.
|
||||||
|
|
||||||
|
## 9. Что НЕ делаем
|
||||||
|
|
||||||
|
- Рекламные метрики (Директ/CPA) как основу — ушло из фокуса.
|
||||||
|
- Соцсеть/ленту/подписки внутри агента.
|
||||||
|
- Токены как валюту.
|
||||||
|
- Полный harness-реестр.
|
||||||
|
- «Суперразум из общества».
|
||||||
|
|
||||||
|
## 10. Открытые вопросы
|
||||||
|
|
||||||
|
1. **Свои сайты или клиентские заказы в первую очередь?** (влияет на приоритет
|
||||||
|
шага 2 vs 6; предлагаю сначала свои сайты — полный контроль над метриками).
|
||||||
|
2. **Кто ставит `horizon_days`** — агент предлагает, человек правит? (Мой дефолт.)
|
||||||
|
3. **Где предел «качества» текста** — только метрики или ещё экспертная оценка
|
||||||
|
Ника обязательна перед публикацией на tier<3.
|
||||||
@@ -1,3 +1,110 @@
|
|||||||
# spark-mind
|
# spark-mind
|
||||||
|
|
||||||
Self-hosted autonomous agent (heartbeat + memory + persona) on Spark X2.5-4B
|
Self-hosted автономный агент (MVP). Концепт позаимствован из
|
||||||
|
[Animus](https://github.com/Craigtut/animus) (Craigtut): heartbeat,
|
||||||
|
4 слоя памяти, persona в 8 слоёв. Движок — **Ornith-1.5-9B** (локально, self-hosted, Ollama).
|
||||||
|
|
||||||
|
## Почему Ornith-1.5-9B
|
||||||
|
|
||||||
|
- Локальный inference (Ollama), никаких облачных ключей
|
||||||
|
- Нативная поддержка tool-calling (саб-агенты) + thinking
|
||||||
|
- 262K контекст (для heartbeat держим 4K-16K + внешняя память)
|
||||||
|
- ~40-60 t/s на Q6_K
|
||||||
|
|
||||||
|
> ⚠️ Spark X2.5-4B не грузится в Ollama (unknown architecture 'spark2_5').
|
||||||
|
> Ornith-9B — рабочая замена: thinking + tools, дефицитный ресурс.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
heartbeat.ts — ядро: 5 триггеров тика, состояния Cold/Warm
|
||||||
|
memory.ts — 4 слоя памяти (short/working/core self/long-term) + SQLite
|
||||||
|
persona.ts — 8 слоёв → системный промпт
|
||||||
|
mind.ts — оркестратор: heartbeat + memory + persona + LLM
|
||||||
|
subagent.ts — SubAgent SDK: researcher, coder, analyst, content-writer
|
||||||
|
embedding.ts — bge-m3 embeddings (dim=1024), cosine similarity
|
||||||
|
database.ts — SQLite репозиторий: 5 таблиц, WAL-режим
|
||||||
|
telegram.ts — Telegram Channel Layer: MAX Messenger API
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (thoughts) | последние ~10 мыслей/опыт/сообщений |
|
||||||
|
| working | SQLite (notepads) | notepad по контактам |
|
||||||
|
| core self | SQLite (self_observations) | самосознательность |
|
||||||
|
| long-term | SQLite (long_term + embeddings) | факты/опыт/процедуры + bge-m3 векторы |
|
||||||
|
|
||||||
|
### Sub-Agents
|
||||||
|
|
||||||
|
Поддерживаемые агенты:
|
||||||
|
- **researcher** — сбор и анализ информации (web_search, file_read, web_extract)
|
||||||
|
- **coder** — написание и ревью кода (file_write, terminal, code_review)
|
||||||
|
- **analyst** — анализ данных и ситуаций (data_query, spreadsheet, chart_generator)
|
||||||
|
- **content-writer** — создание веб-контента (template_search, seo_check, grammar_check)
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build # tsc -p tsconfig.json (проверка типов)
|
||||||
|
npm start # node dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Ornith-1.5-9B должен быть доступен по OpenAI-совместимому API (Ollama):
|
||||||
|
- Ollama: `http://localhost:11434` (модель `ornith-1.5-9b:latest`)
|
||||||
|
|
||||||
|
### Настройка Telegram
|
||||||
|
|
||||||
|
Для отправки ответов в Telegram добавьте в `index.ts`:
|
||||||
|
```typescript
|
||||||
|
import { TelegramMessenger } from './src/telegram.js';
|
||||||
|
|
||||||
|
const telegram = new TelegramMessenger({
|
||||||
|
botUrl: 'https://bot.qw4.ru/bot/nik_bot',
|
||||||
|
userId: '140245519',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Примечание: если bot.qw4.ru недоступен (SSL-ошибка 'unrecognized name'),
|
||||||
|
> агент работает в console-only режиме.
|
||||||
|
|
||||||
|
## Статус P0–P5
|
||||||
|
|
||||||
|
| Priority | Компонент | Статус |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| P0 | heartbeat → mind wiring | ✅ |
|
||||||
|
| P1 | structured JSON output (parseSparkOutput) | ✅ |
|
||||||
|
| P2 | bge-m3 embeddings + cosine search | ✅ |
|
||||||
|
| P3 | SubAgent SDK (researcher, coder, analyst, content-writer) | ✅ |
|
||||||
|
| P4 | SQLite persistence (5 таблиц, WAL) | ✅ |
|
||||||
|
| P5 | Telegram Channel Layer (MAX API + fallback) | ✅ |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- **План** — [DESIGN.md](./DESIGN.md): автономный контент-продюсер с заземлением
|
||||||
|
- **Чек-лист** — [TODO.md](./TODO.md)
|
||||||
|
|
||||||
|
Суть: агент производит контент (текст, изображение, подкаст, видео), публикует
|
||||||
|
и через заданный горизонт **замеряет, что сработало** — показы, позиции, CTR,
|
||||||
|
дочитывания, прослушивания, переходы (метрики Метрики/GSC/Webmaster/Wordstat).
|
||||||
|
Качество оценивается не рассуждением агента, а этими метриками. Права (автономия)
|
||||||
|
выдаются по подтверждённому трек-рекорду: доказал рост метрик — получил свободу
|
||||||
|
публиковать без спроса. Скиллы агента = существующий производственный стек
|
||||||
|
(editor-pro, ComfyUI, Silero TTS, video-pipeline, Wordstat, Astro).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — концепт вдохновлён Animus (MIT, Craig Tuttle).
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# spark-mind — Оставшиеся TODO
|
||||||
|
|
||||||
|
Полный план — в [DESIGN.md](./DESIGN.md): **автономный контент-продюсер
|
||||||
|
с заземлением на метриках и автономией по заслугам.**
|
||||||
|
|
||||||
|
Статус: P0–P5 базовые завершены (heartbeat→mind, JSON output, bge-m3 embeddings,
|
||||||
|
sub-agents, SQLite, Telegram+fallback). Далее — программа контент-производства.
|
||||||
|
|
||||||
|
Порядок обязателен: есть зависимости между шагами.
|
||||||
|
|
||||||
|
## Шаг 0 — Основа
|
||||||
|
- [ ] P5: `decision.noAction: true` — агент может решить, что действие не нужно,
|
||||||
|
и не писать thought (сейчас heartbeat всегда «работает» → шум в памяти).
|
||||||
|
- [ ] Тонкий `Channel`: `ConsoleChannel`, `TelegramChannel`, `FileChannel`.
|
||||||
|
`sendReply` сейчас жёстко зашит на Telegram+console.
|
||||||
|
|
||||||
|
## Шаг 1 — Планировщик (нужен для pipeline/measure/reflection)
|
||||||
|
- [ ] Cron-задачи: триггер `{type:'task'}` объявлен, но никто не эмитит.
|
||||||
|
- [ ] Проверка due-записей в SQLite → эмит `{type:'pipeline'}` и `{type:'measure'}`.
|
||||||
|
- [ ] Ночной `{type:'reflection'}`.
|
||||||
|
- Приёмка: задача с датой выполняется в срок.
|
||||||
|
|
||||||
|
## Шаг 2 — Конвейер контента (центральный объект)
|
||||||
|
- [ ] Таблица `content_items` (kind: text|image|podcast|video; status:
|
||||||
|
brief→generated→optimized→published→measured→archived).
|
||||||
|
- [ ] Переходы статусов + журнал переходов.
|
||||||
|
- [ ] Привязка к `site` (свои сайты) и `deal_id` (клиентские заказы).
|
||||||
|
- Приёмка: item проходит brief→published, все переходы в журнале.
|
||||||
|
|
||||||
|
## Шаг 3 — Заземление: outcomes + measure
|
||||||
|
- [ ] Таблица `outcomes`: metric_kind (reach, engagement, position, traffic,
|
||||||
|
conversion, revenue, speed, quality), direction, horizon_days, due_at.
|
||||||
|
- [ ] Замер через `{type:'measure'}`: Метрика API / GSC / Yandex Webmaster / Wordstat.
|
||||||
|
- [ ] Нормализация в `score` [-1..+1]; базлайн области для нового контента.
|
||||||
|
- Приёмка: метрика замеряется через horizon, score пишется.
|
||||||
|
|
||||||
|
## Шаг 4 — Автономия по заслугам
|
||||||
|
- [ ] Таблица `capabilities` (тир на область: text, image, podcast, video,
|
||||||
|
seo_onpage, seo_semantics, publish).
|
||||||
|
- [ ] Тиры 0..4 (observer → suggest → ask → act+report → autonomous).
|
||||||
|
- [ ] +1 тир за 3 нетто-положительных; −1 за 1 отрицательный; затухание 90 дней.
|
||||||
|
- [ ] Границы persona `values` непреодолимы на любом тире.
|
||||||
|
- Приёмка: тир растёт после 3 успехов и падает после провала.
|
||||||
|
|
||||||
|
## Шаг 5 — Методы: skills + reflection
|
||||||
|
- [ ] Таблица `skills` (name, area, method_md, tool, validated, uses,
|
||||||
|
success_rate, embedding).
|
||||||
|
- [ ] Retrieval нужного метода по эмбеддингу bge-m3 (не грузить всё сразу).
|
||||||
|
- [ ] Наполнение методами из реального стека: editor-pro (text/seo_onpage),
|
||||||
|
ComfyUI (image), Silero TTS (podcast), video-pipeline (video),
|
||||||
|
Wordstat (seo_semantics), Astro (publish).
|
||||||
|
- [ ] `reflection`: ночной цикл обобщает items+outcomes+thoughts →
|
||||||
|
long-term и `self_observations` (**сейчас мёртвый слой**).
|
||||||
|
- [ ] Tool-runtime для под-агентов: tools сейчас — только строки-декларации.
|
||||||
|
|
||||||
|
## Шаг 6 — Заказы: deals + counterparties
|
||||||
|
- [ ] Таблицы `counterparties`, `deals`, `deal_events` (append-only).
|
||||||
|
- [ ] `trust_score` выводится из истории, не задаётся руками.
|
||||||
|
- [ ] Заказ клиента на контент: offered→accepted→delivered→paid|refused|defaulted.
|
||||||
|
- Приёмка: заказ проходит полный цикл.
|
||||||
|
|
||||||
|
## Шаг 7 — Энергия: ledger
|
||||||
|
- [ ] Таблица `ledger`.
|
||||||
|
- [ ] Расход: тик, генерация (текст/фото/аудио/видео), публикация, саб-агент.
|
||||||
|
- [ ] Приход: положительные outcomes + донат.
|
||||||
|
- [ ] Низкий баланс → интервал тиков растёт → Deep Rest.
|
||||||
|
- Приёмка: результат пополняет энергию, дефицит замедляет тики.
|
||||||
|
|
||||||
|
## Инфраструктура (по мере надобности)
|
||||||
|
- [ ] Конфиг / `.env`: URL-ы Ollama, dbPath, Telegram захардкожены в `index.ts`.
|
||||||
|
- [ ] Retry + timeout (AbortController) для LLM/embedding fetch.
|
||||||
|
- [ ] Тесты: `parseSparkOutput`, `Database` CRUD, cosine-поиск, нормализация score.
|
||||||
|
- [ ] Web UI (persona setup) вместо `DEFAULT_PERSONA` в коде.
|
||||||
|
|
||||||
|
## Каналы вывода
|
||||||
|
- [ ] Telegram: обход SSL-проблемы bot.qw4.ru (`tlsv1 unrecognized name`, серверная).
|
||||||
|
Сейчас console-only fallback.
|
||||||
|
|
||||||
|
## Движок
|
||||||
|
- [ ] Spark X2.5-4B: не грузится в Ollama (`unknown architecture 'spark2_5'`).
|
||||||
|
Варианты: совместимый GGUF / llama.cpp напрямую / остаться на Ornith-1.5-9B.
|
||||||
|
**Важно:** локальные модели пумолкают (пустой ответ), если `subAgent`
|
||||||
|
в JSON-схеме structured output.
|
||||||
|
|
||||||
|
## Косметика
|
||||||
|
- [ ] Устаревший doc-комментарий в `mind.ts` (класс `SparkLLMClient` описан как
|
||||||
|
«клиент для Spark X2.5-4B, в MVP заглушка»).
|
||||||
|
- [ ] Дефолт модели в конструкторе `SparkLLMClient` = `'spark-x2.5-4b'`.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
import { OllamaEmbeddingClient } from './src/embedding.js';
|
||||||
|
import { SubAgentFactory } from './src/subagent.js';
|
||||||
|
import { TelegramMessenger } from './src/telegram.js';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
|
||||||
|
const HEARTBEAT_MS = 5 * 60 * 1000; // 5 мин — idle-мысли
|
||||||
|
const DB_PATH = join(homedir(), '.local/share/spark-mind/spark-mind.db');
|
||||||
|
|
||||||
|
const heartbeat = new Heartbeat({ intervalMs: HEARTBEAT_MS });
|
||||||
|
// Embeddings: bge-m3 (dim=1024) — та же модель, что в content-atom
|
||||||
|
const embedding = new OllamaEmbeddingClient('http://localhost:11434');
|
||||||
|
const memory = new MemorySystem({ recentCount: 10, dbPath: DB_PATH }, embedding); // last 10 thoughts + SQLite
|
||||||
|
// Sub-agents: researcher, coder, analyst, content-writer
|
||||||
|
const agentFactory = new SubAgentFactory();
|
||||||
|
|
||||||
|
// Telegram: MAX Messenger API (nik_bot → user_id 140245519)
|
||||||
|
const telegramConfig: import('./src/telegram.js').TelegramConfig = {
|
||||||
|
botUrl: 'https://bot.qw4.ru/bot/nik_bot',
|
||||||
|
userId: '140245519',
|
||||||
|
};
|
||||||
|
const telegram = new TelegramMessenger(telegramConfig);
|
||||||
|
|
||||||
|
const mind = new Mind(
|
||||||
|
heartbeat,
|
||||||
|
memory,
|
||||||
|
DEFAULT_PERSONA,
|
||||||
|
new SparkLLMClient('http://localhost:11434', 'ornith-1.5-9b:latest'), // Ornith-9B — движок
|
||||||
|
agentFactory,
|
||||||
|
telegram,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Подключаем heartbeat к оркестратору — теперь тики реально обрабатываются
|
||||||
|
heartbeat.setOnTick((trigger) => mind.handleTick(trigger));
|
||||||
|
|
||||||
|
// Старт heartbeat
|
||||||
|
heartbeat.start();
|
||||||
|
console.log('[spark-mind] started — heartbeat every', HEARTBEAT_MS / 1000, 's');
|
||||||
|
|
||||||
|
// Демо: симулируем тик через 10 сек (в прода — реальные триггеры)
|
||||||
|
setTimeout(() => {
|
||||||
|
void mind.handleTick({ type: 'message', contactId: 'demo', text: 'Привет' });
|
||||||
|
}, 10_000);
|
||||||
Generated
+444
@@ -0,0 +1,444 @@
|
|||||||
|
{
|
||||||
|
"name": "spark-mind",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "spark-mind",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^9.6.0",
|
||||||
|
"@types/node": "^26.5.0",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/better-sqlite3": {
|
||||||
|
"version": "13.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
|
||||||
|
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||||
|
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "spark-mind",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Self-hosted autonomous agent (heartbeat + memory + persona) on Spark X2.5-4B",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "node --loader ts-node/esm index.ts",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^9.6.0",
|
||||||
|
"@types/node": "^26.5.0",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* SQLite репозиторий для системы памяти Spark Mind.
|
||||||
|
* 5 таблиц: thoughts (short-term), notepads (working),
|
||||||
|
* self_observations (core self), long_term (факты/опыт), long_term_embeddings (векторы).
|
||||||
|
*
|
||||||
|
* Данные хранятся в ~/.local/share/spark-mind/spark-mind.db.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sqlite3 from 'better-sqlite3';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
private db: sqlite3.Database;
|
||||||
|
|
||||||
|
constructor(dbPath?: string) {
|
||||||
|
const path = dbPath ?? join(homedir(), '.local/share/spark-mind/spark-mind.db');
|
||||||
|
mkdirSync(join(path, '..'), { recursive: true });
|
||||||
|
this.db = new sqlite3(path, { readonly: false });
|
||||||
|
this.db.pragma('journal_mode = WAL');
|
||||||
|
this.db.pragma('synchronous = normal');
|
||||||
|
this.db.pragma('foreign_keys = ON');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS thoughts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS notepads (
|
||||||
|
contact_id TEXT PRIMARY KEY,
|
||||||
|
block TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS self_observations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS long_term (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('fact', 'experience', 'procedure', 'outcome')),
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS long_term_embeddings (
|
||||||
|
long_term_id TEXT PRIMARY KEY REFERENCES long_term(id) ON DELETE CASCADE,
|
||||||
|
embedding BLOB NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Short-term (thoughts) ===
|
||||||
|
|
||||||
|
insertThought(id: string, text: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO thoughts (id, text, created_at) VALUES (?, ?, ?)').run(id, text, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentThoughts(limit: number): { id: string; text: string; created_at: number }[] {
|
||||||
|
return this.db.prepare(
|
||||||
|
'SELECT * FROM thoughts ORDER BY created_at DESC LIMIT ?',
|
||||||
|
).all(limit) as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Working (notepads) ===
|
||||||
|
|
||||||
|
upsertNotepad(contactId: string, block: string, updatedAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO notepads (contact_id, block, updated_at) VALUES (?, ?, ?)').run(contactId, block, updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotepad(contactId: string): string | null {
|
||||||
|
return (this.db.prepare('SELECT block FROM notepads WHERE contact_id = ?').get(contactId) as { block: string } | undefined)?.block ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Core self ===
|
||||||
|
|
||||||
|
insertSelfObservation(id: string, text: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO self_observations (id, text, created_at) VALUES (?, ?, ?)').run(id, text, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSelfObservations(): { id: string; text: string; created_at: number }[] {
|
||||||
|
return this.db.prepare('SELECT * FROM self_observations ORDER BY created_at DESC').all() as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Long-term ===
|
||||||
|
|
||||||
|
insertLongTerm(id: string, text: string, type: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO long_term (id, text, type, created_at) VALUES (?, ?, ?, ?)').run(id, text, type, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLongTerm(limit: number): { id: string; text: string; type: string; created_at: number }[] {
|
||||||
|
return this.db.prepare('SELECT * FROM long_term ORDER BY created_at DESC LIMIT ?').all(limit) as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
insertLongTermEmbedding(longTermId: string, embedding: number[]): void {
|
||||||
|
const blob = Buffer.from(new Float32Array(embedding).buffer);
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO long_term_embeddings (long_term_id, embedding) VALUES (?, ?)').run(longTermId, blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLongTermEmbedding(longTermId: string): Float32Array | null {
|
||||||
|
const row = this.db.prepare('SELECT embedding FROM long_term_embeddings WHERE long_term_id = ?').get(longTermId) as { embedding: Buffer } | undefined;
|
||||||
|
return row ? new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllLongTermWithVectors(): { id: string; text: string; type: string; created_at: number; embedding: Float32Array }[] {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT lt.id, lt.text, lt.type, lt.created_at, le.embedding
|
||||||
|
FROM long_term lt
|
||||||
|
JOIN long_term_embeddings le ON le.long_term_id = lt.id
|
||||||
|
`).all() as any[];
|
||||||
|
return rows.map(r => ({
|
||||||
|
...r,
|
||||||
|
embedding: new Float32Array(r.embedding.buffer, r.embedding.byteOffset, r.embedding.byteLength),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Embedding Client — локальные эмбеддинги через Ollama /api/embeddings.
|
||||||
|
*
|
||||||
|
* Модель: bge-m3 (dim=1024) — та же, что в content-atom.
|
||||||
|
* НЕ переключаться на nomic-embed-text (dim=768) — разный размер вектора
|
||||||
|
* = переиндексация Qdrant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface EmbeddingClient {
|
||||||
|
embed(text: string): Promise<number[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ollama EmbeddingClient — bge-m3. */
|
||||||
|
export class OllamaEmbeddingClient implements EmbeddingClient {
|
||||||
|
constructor(
|
||||||
|
private baseUrl: string,
|
||||||
|
private modelName = 'bge-m3',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async embed(text: string): Promise<number[]> {
|
||||||
|
const resp = await fetch(`${this.baseUrl}/api/embeddings`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: this.modelName, prompt: text }),
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`Embedding error: ${resp.status}`);
|
||||||
|
const data: any = await resp.json();
|
||||||
|
return data.embedding ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Векторная арифметика — cosine similarity.
|
||||||
|
* Держим здесь, чтобы memory.ts не зависел от внешних библиотек.
|
||||||
|
*/
|
||||||
|
export function cosineSimilarity(a: number[], b: number[]): number {
|
||||||
|
if (a.length === 0 || b.length === 0) return 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
/** Колбэк, который выполняется на каждом тике (в прода: mind.handleTick). */
|
||||||
|
private onTick?: (trigger: TickTrigger) => Promise<void>;
|
||||||
|
|
||||||
|
constructor(config: Partial<HeartbeatConfig> = {}) {
|
||||||
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подключает обработчик тика (оркестратор mind). */
|
||||||
|
setOnTick(fn: (trigger: TickTrigger) => Promise<void>) {
|
||||||
|
this.onTick = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Любой тик сбрасывает 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 — теперь реальный запуск оркестратора
|
||||||
|
if (this.onTick) {
|
||||||
|
await this.onTick(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Memory System — 4 слоя памяти (концепт Animus).
|
||||||
|
* Хранение: SQLite (5 таблиц) + bge-m3 эмбеддинги (cosine-поиск).
|
||||||
|
*
|
||||||
|
* Слои:
|
||||||
|
* short-term → thoughts (recent thoughts, last N)
|
||||||
|
* working → notepads (notepad per contact)
|
||||||
|
* core self → self_observations (накопленная самосознательность)
|
||||||
|
* long-term → long_term + long_term_embeddings (факты/опыт/процедуры/исходы)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EmbeddingClient, cosineSimilarity } from './embedding.js';
|
||||||
|
import { Database } from './database.js';
|
||||||
|
|
||||||
|
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 interface MemorySystemConfig {
|
||||||
|
recentCount?: number;
|
||||||
|
dbPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemorySystem {
|
||||||
|
private recentCount: number;
|
||||||
|
private db: Database;
|
||||||
|
private embeddingClient: EmbeddingClient | null;
|
||||||
|
|
||||||
|
constructor(config: MemorySystemConfig = {}, embeddingClient?: EmbeddingClient) {
|
||||||
|
this.recentCount = config.recentCount ?? 10;
|
||||||
|
this.db = new Database(config.dbPath);
|
||||||
|
this.embeddingClient = embeddingClient ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Short-term: последние мысли/опыт/сообщения ===
|
||||||
|
addThought(text: string): Thought {
|
||||||
|
const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() };
|
||||||
|
this.db.insertThought(t.id, t.text, t.createdAt);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
recentThoughts(): Thought[] {
|
||||||
|
const rows = this.db.getRecentThoughts(this.recentCount);
|
||||||
|
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Working memory: notepad по контактам ===
|
||||||
|
updateNotepad(contactId: string, block: string): void {
|
||||||
|
const ts = Date.now();
|
||||||
|
this.db.upsertNotepad(contactId, block, ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotepad(contactId: string): string {
|
||||||
|
return this.db.getNotepad(contactId) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Core self: накопленная самосознательность ===
|
||||||
|
addSelfObservation(text: string, tags: string[] = []): SelfObservation {
|
||||||
|
const o: SelfObservation = { id: `so-${Date.now()}`, text, createdAt: Date.now(), tags };
|
||||||
|
this.db.insertSelfObservation(o.id, o.text, o.createdAt);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSelfObservations(): SelfObservation[] {
|
||||||
|
const rows = this.db.getAllSelfObservations();
|
||||||
|
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at, tags: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Long-term: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
|
||||||
|
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
|
||||||
|
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
|
||||||
|
let embedding: number[] = [];
|
||||||
|
if (this.embeddingClient) {
|
||||||
|
embedding = await this.embeddingClient.embed(text);
|
||||||
|
}
|
||||||
|
// Сначала long_term (FK требует), потом embedding
|
||||||
|
this.db.insertLongTerm(m.id, m.text, m.type, m.createdAt);
|
||||||
|
if (embedding.length > 0) {
|
||||||
|
this.db.insertLongTermEmbedding(m.id, embedding);
|
||||||
|
}
|
||||||
|
m.embedding = embedding;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поиск по long-term по тексту через эмбедд (cosine similarity в SQLite). */
|
||||||
|
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
|
||||||
|
if (!this.embeddingClient) {
|
||||||
|
const rows = this.db.getLongTerm(this.recentCount);
|
||||||
|
return rows.map(r => ({ id: r.id, text: r.text, type: r.type as LongTermMemory['type'], createdAt: r.created_at }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = await this.embeddingClient.embed(query);
|
||||||
|
const all = this.db.getAllLongTermWithVectors();
|
||||||
|
const scored = all
|
||||||
|
.map(m => ({ m, score: cosineSimilarity(q, Array.from(m.embedding)) }))
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, topK)
|
||||||
|
.map(x => ({
|
||||||
|
id: x.m.id,
|
||||||
|
text: x.m.text,
|
||||||
|
type: x.m.type as LongTermMemory['type'],
|
||||||
|
createdAt: x.m.created_at,
|
||||||
|
embedding: Array.from(x.m.embedding),
|
||||||
|
}));
|
||||||
|
return scored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Консолидация: перенос недавних мыслей в long-term (по спецификации Animus). */
|
||||||
|
async consolidate(recent: Thought[]): Promise<void> {
|
||||||
|
for (const t of recent) {
|
||||||
|
// эвристика: только «значимые» мысли (по тегам/ключевым словам)
|
||||||
|
await this.addLongTerm(t.text, 'experience');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Mind — per-contact per-tick сессия (концепт Animus).
|
||||||
|
* Mind не выполняет долгую работу сам — он оркестратор:
|
||||||
|
* думает, чувствует, решает, делегирует саб-агентам.
|
||||||
|
*
|
||||||
|
* Ornith-1.5-9B как движок:
|
||||||
|
* - через Ollama (OpenAI-compatible /v1/server)
|
||||||
|
* - поддерживает tool-calling (саб-агенты)
|
||||||
|
* - 262K контекст, но для heartbeat держим 4K-16K + внешняя память
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Heartbeat, TickTrigger } from './heartbeat.js';
|
||||||
|
import { MemorySystem } from './memory.js';
|
||||||
|
import { Persona, compileSystemPrompt } from './persona.js';
|
||||||
|
import { SubAgent, SubAgentFactory, SubAgentResult } from './subagent.js';
|
||||||
|
import { TelegramMessenger } from './telegram.js';
|
||||||
|
|
||||||
|
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> {
|
||||||
|
// Инструкция structured output — только {thoughts, emotion, decision.reply}.
|
||||||
|
// subAgent НЕ включаем: gemma4:E4B (и другие локальные модели) пумолкают
|
||||||
|
// (пустой ответ), когда subAgent есть в JSON-схеме. subAgent — P3,
|
||||||
|
// появится в sub-agent SDK.
|
||||||
|
const structuredOutput = [
|
||||||
|
'Отвечай ТОЛЬКО валидным JSON без оградок и комментариев.',
|
||||||
|
'Структура:',
|
||||||
|
'{ "thoughts": "собственные мысли mind",',
|
||||||
|
' "emotion": { "valence": 0..1, "arousal": 0..1 },',
|
||||||
|
' "decision": { "reply": "текст ответа или null" }',
|
||||||
|
'}',
|
||||||
|
'thoughts и reply — на русском.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
response_format: { type: 'json_object' }, // structured output
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: `${systemPrompt}\n\n${structuredOutput}` },
|
||||||
|
{ 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 отдаёт JSON: {thoughts, emotion, decision}
|
||||||
|
return parseSparkOutput(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSparkOutput(content: string): LLMResult {
|
||||||
|
// 1. Убираем markdown-оградки (```json ... ```), если модель их добавила
|
||||||
|
const cleaned = content
|
||||||
|
.replace(/^```(?:json)?\s*/i, '')
|
||||||
|
.replace(/\s*```$/i, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// 2. Пробуем распарсить JSON (structured output)
|
||||||
|
let parsed: any = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(cleaned);
|
||||||
|
} catch {
|
||||||
|
// fallback: ищем первый {...} блок в тексте
|
||||||
|
const match = cleaned.match(/({[\s\S]*})/);
|
||||||
|
if (match) {
|
||||||
|
try { parsed = JSON.parse(match[1]); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
const thoughts = tryParseJson(parsed.thoughts);
|
||||||
|
const reply = tryParseJson(parsed.decision?.reply ?? parsed.reply);
|
||||||
|
return {
|
||||||
|
text: typeof parsed.text === 'string' ? parsed.text : '',
|
||||||
|
thoughts: typeof thoughts === 'string' ? thoughts : typeof thoughts === 'object' ? JSON.stringify(thoughts) : '',
|
||||||
|
emotion: {
|
||||||
|
valence: clampNum(parsed.emotion?.valence ?? parsed.valence, 0.5),
|
||||||
|
arousal: clampNum(parsed.emotion?.arousal ?? parsed.arousal, 0.5),
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
reply: typeof reply === 'string' ? reply : reply ? JSON.stringify(reply) : null,
|
||||||
|
subAgent: parsed.decision?.subAgent ?? parsed.subAgent ?? undefined,
|
||||||
|
action: parsed.decision?.action ?? parsed.action ?? undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback: raw-текст как reply
|
||||||
|
return {
|
||||||
|
text: content,
|
||||||
|
thoughts: content,
|
||||||
|
emotion: { valence: 0.5, arousal: 0.5 },
|
||||||
|
decision: { reply: content || null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampNum(v: number, fallback: number): number {
|
||||||
|
return typeof v === 'number' && !Number.isNaN(v) ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Если значение — строка с JSON (модель запарила вложенный объект), распарсим. */
|
||||||
|
function tryParseJson(v: unknown): unknown {
|
||||||
|
if (typeof v !== 'string') return v;
|
||||||
|
const trimmed = v.trim();
|
||||||
|
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return v;
|
||||||
|
try { return JSON.parse(trimmed); } catch { return v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Mind {
|
||||||
|
private heartbeat: Heartbeat;
|
||||||
|
private memory: MemorySystem;
|
||||||
|
private persona: Persona;
|
||||||
|
private llm: LLMClient;
|
||||||
|
/** Фабрика под-агентов. Если null — делегирование отключено. */
|
||||||
|
private agentFactory: SubAgentFactory | null;
|
||||||
|
/** Telegram-мессенджер. Если null — только console. */
|
||||||
|
private telegram: TelegramMessenger | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
heartbeat: Heartbeat,
|
||||||
|
memory: MemorySystem,
|
||||||
|
persona: Persona,
|
||||||
|
llm: LLMClient,
|
||||||
|
agentFactory?: SubAgentFactory,
|
||||||
|
telegram?: TelegramMessenger,
|
||||||
|
) {
|
||||||
|
this.heartbeat = heartbeat;
|
||||||
|
this.memory = memory;
|
||||||
|
this.persona = persona;
|
||||||
|
this.llm = llm;
|
||||||
|
this.agentFactory = agentFactory ?? null;
|
||||||
|
this.telegram = telegram ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Обработчик любого тика. */
|
||||||
|
async handleTick(trigger: TickTrigger): Promise<void> {
|
||||||
|
const systemPrompt = compileSystemPrompt(this.persona);
|
||||||
|
const context = this.buildContext(trigger);
|
||||||
|
|
||||||
|
const result = await this.generateTick(context, systemPrompt);
|
||||||
|
|
||||||
|
// 1. Short-term memory: мысли mind
|
||||||
|
this.memory.addThought(result.thoughts);
|
||||||
|
|
||||||
|
// 2. Emotion decay + consolidation (async — эмбеддится в long-term)
|
||||||
|
await this.consolidate();
|
||||||
|
|
||||||
|
// 3. Если mind решил ответить — отправляем (channel layer)
|
||||||
|
if (result.decision.reply) {
|
||||||
|
await this.sendReply(result.decision.reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Если делегировал саб-агенту — запускаем (с task из решения)
|
||||||
|
if (result.decision.subAgent) {
|
||||||
|
await this.runSubAgent(result.decision.subAgent, result.decision.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Основной цикл генерации (вызывается и для под-агентов). */
|
||||||
|
private async generateTick(context: string, systemPrompt: string): Promise<LLMResult> {
|
||||||
|
return this.llm.generate(context, systemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 async consolidate(): Promise<void> {
|
||||||
|
await this.memory.consolidate(this.memory.recentThoughts());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Channel layer — отправка ответа в Telegram (если настроен) + console.
|
||||||
|
* Fallback: если Telegram недоступен — только console.
|
||||||
|
*/
|
||||||
|
private async sendReply(text: string): Promise<void> {
|
||||||
|
// Всегда выводить в консоль
|
||||||
|
console.log('[reply]', text);
|
||||||
|
|
||||||
|
// Если Telegram настроен — попробовать отправить
|
||||||
|
if (this.telegram) {
|
||||||
|
try {
|
||||||
|
const result = await this.telegram.sendMessage(text);
|
||||||
|
console.log('[telegram] sent:', result.messageId, 'at', new Date(result.sentAt).toISOString());
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[telegram] send failed, using console-only:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-agent — делегирование тяжёлого reasoning.
|
||||||
|
* Создаёт под-агента, выполняет задачу, сохраняет результат.
|
||||||
|
*/
|
||||||
|
private async runSubAgent(type: string, task?: string): Promise<void> {
|
||||||
|
if (!this.agentFactory) {
|
||||||
|
console.log('[sub-agent] disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const agent = SubAgentFactory.create(type);
|
||||||
|
const agentTask = task || `Автоматически выполни задачу: ${type}`;
|
||||||
|
|
||||||
|
console.log(`[sub-agent] ${type}: ${agentTask}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await agent.execute(agentTask, this.llm);
|
||||||
|
console.log(`[sub-agent ${type}] done (${result.durationMs}ms):`, result.output.slice(0, 200));
|
||||||
|
|
||||||
|
// Сохраняем результат в memory
|
||||||
|
this.memory.addThought(`[sub-agent ${type}] ${result.output.slice(0, 150)}`);
|
||||||
|
await this.memory.addLongTerm(`[sub-agent ${type}] ${result.output.slice(0, 200)}`, 'experience');
|
||||||
|
|
||||||
|
// Уведомляем heartbeat о завершении
|
||||||
|
this.heartbeat.tick({ type: 'subagent-complete', subagentId: type, result: { output: result.output, durationMs: result.durationMs } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[sub-agent ${type}] error:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ?? [];
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* SubAgent SDK — делегирование задач Ornith-9B.
|
||||||
|
*
|
||||||
|
* Mind делегирует подзадачи агенту (например, «проанализируй сайт», «напиши код»).
|
||||||
|
* Ornith-9B работает автономно, возвращает результат.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LLMClient } from './mind.js';
|
||||||
|
|
||||||
|
export interface SubAgentConfig {
|
||||||
|
/** Название агента (например, 'researcher', 'coder', 'analyst') */
|
||||||
|
name: string;
|
||||||
|
/** Ролевое описание агента (system prompt) */
|
||||||
|
role: string;
|
||||||
|
/** Доступные инструменты (если нужны) */
|
||||||
|
tools?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentResult {
|
||||||
|
agent: string;
|
||||||
|
output: string;
|
||||||
|
/** Время выполнения в мс */
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubAgent — делегирует задачу отдельному LLM-клиенту.
|
||||||
|
* Поддерживает: researcher, coder, analyst (расширяется).
|
||||||
|
*/
|
||||||
|
export class SubAgent {
|
||||||
|
private agent: string;
|
||||||
|
private role: string;
|
||||||
|
private tools: string[];
|
||||||
|
|
||||||
|
constructor(config: SubAgentConfig) {
|
||||||
|
this.agent = config.name;
|
||||||
|
this.role = config.role;
|
||||||
|
this.tools = config.tools ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Выполнить задачу, вернуть результат. */
|
||||||
|
async execute(task: string, llmClient: LLMClient): Promise<SubAgentResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const systemPrompt = [
|
||||||
|
this.role,
|
||||||
|
'',
|
||||||
|
'Выполняй задачу прямо и конкретно.',
|
||||||
|
this.tools.length > 0
|
||||||
|
? `Доступные инструменты: ${this.tools.join(', ')}. Используй их при необходимости.`
|
||||||
|
: 'Работай автономно.',
|
||||||
|
'Верни результат в формате: { result: "ответ на задачу" }',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = await llmClient.generate(task, systemPrompt);
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
agent: this.agent,
|
||||||
|
output: typeof result.text === 'string' ? result.text : JSON.stringify(result.text),
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo(): { name: string; role: string; tools: string[] } {
|
||||||
|
return { name: this.agent, role: this.role, tools: this.tools };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubAgentFactory — создаёт агентов по типу.
|
||||||
|
* Поддерживает: researcher, coder, analyst, content-writer.
|
||||||
|
*/
|
||||||
|
export class SubAgentFactory {
|
||||||
|
static create(type: string, customRole?: string): SubAgent {
|
||||||
|
const role = customRole ?? this.defaultRoles[type];
|
||||||
|
if (!role) {
|
||||||
|
throw new Error(`Unknown sub-agent type: ${type}. Available: ${Object.keys(this.defaultRoles).join(', ')}`);
|
||||||
|
}
|
||||||
|
return new SubAgent({
|
||||||
|
name: type,
|
||||||
|
role,
|
||||||
|
tools: this.availableTools[type],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static defaultRoles: Record<string, string> = {
|
||||||
|
researcher: `Ты исследователь. Собираешь и анализируешь информацию по запросу.
|
||||||
|
- Формулируй гипотезы, ищи подтверждающие/опровергающие данные
|
||||||
|
- Давай структурированный ответ с выводами
|
||||||
|
- Указывай степень уверенности (высокая/средняя/низкая)`,
|
||||||
|
|
||||||
|
coder: `Ты разработчик. Пишешь и ревьюишь код.
|
||||||
|
- Следуй best practices (TypeScript, Python, Rust)
|
||||||
|
- Пиши тестируемый, чистый код с комментариями
|
||||||
|
- Учитывай контекст проекта`,
|
||||||
|
|
||||||
|
analyst: `Ты аналитик. Анализируешь данные и ситуации.
|
||||||
|
- Разбирай проблемы на составляющие
|
||||||
|
- Давай количественную и качественную оценку
|
||||||
|
- Предлагай рекомендации на основе фактов`,
|
||||||
|
|
||||||
|
'content-writer': `Ты контент-писатель. Создаёшь тексты для веба.
|
||||||
|
- Пиши ясно, точно, по делу
|
||||||
|
- Используй подзаголовки и списки для читаемости
|
||||||
|
- Учитывай SEO-рекомендации`,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static availableTools: Record<string, string[]> = {
|
||||||
|
researcher: ['web_search', 'file_read', 'web_extract'],
|
||||||
|
coder: ['file_write', 'terminal', 'code_review'],
|
||||||
|
analyst: ['data_query', 'spreadsheet', 'chart_generator'],
|
||||||
|
'content-writer': ['template_search', 'seo_check', 'grammar_check'],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Telegram Channel Layer — отправка ответов через MAX Messenger API.
|
||||||
|
*
|
||||||
|
* Бот: nik_bot (user_id: 140245519).
|
||||||
|
* API endpoint: https://bot.qw4.ru/bot/nik_bot/message
|
||||||
|
*
|
||||||
|
* FIX: SNI issue с Node.js — используем https модуль с правильными опциями.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'node:https';
|
||||||
|
|
||||||
|
export interface TelegramConfig {
|
||||||
|
botUrl: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageResult {
|
||||||
|
messageId: string;
|
||||||
|
sentAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram Messenger — отправка сообщений через MAX API.
|
||||||
|
* Поддержка SNI для HTTPS запросов.
|
||||||
|
*/
|
||||||
|
export class TelegramMessenger {
|
||||||
|
private botUrl: string;
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
constructor(config: TelegramConfig) {
|
||||||
|
this.botUrl = config.botUrl;
|
||||||
|
this.userId = config.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Отправить сообщение через https module (SNI fix). */
|
||||||
|
async sendMessage(text: string): Promise<MessageResult> {
|
||||||
|
// Парсим URL
|
||||||
|
const url = new URL(this.botUrl);
|
||||||
|
const path = `/message`;
|
||||||
|
|
||||||
|
const postData = JSON.stringify({
|
||||||
|
user_id: this.userId,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: 443,
|
||||||
|
path: path,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(postData),
|
||||||
|
'SNI': url.hostname, // Explicit SNI
|
||||||
|
},
|
||||||
|
// SNI fix для Node.js
|
||||||
|
servername: url.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk: Buffer) => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`Telegram send error: ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data: any = JSON.parse(body);
|
||||||
|
resolve({
|
||||||
|
messageId: data.message_id ?? `msg-${Date.now()}`,
|
||||||
|
sentAt: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(postData);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": ["es2020"],
|
||||||
|
"types": ["node"],
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "index.ts"]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user