P3 - SubAgent SDK: - src/subagent.ts: researcher, coder, analyst, content-writer - Mind.runSubAgent() → SubAgentFactory.create() → execute() - Результаты сохраняются в long-term memory - Heartbeat.notify(subagent-complete) после завершения P4 - SQLite persistence (5 таблиц, WAL): - src/database.ts: thoughts, notepads, self_observations, long_term, embeddings - memory.ts → Database (полная замена in-memory) - index.ts: dbPath → ~/.local/share/spark-mind/spark-mind.db P5 - Telegram Channel Layer: - src/telegram.ts: https module (SNI fix), MAX Messenger API - mind.ts: sendReply() → Telegram + console fallback - index.ts: TelegramMessenger(config) → Mind Fixes: - FK constraint: long_term INSERT → long_term_embeddings INSERT - JSON parse: nested objects, markdown fences - P5 telegram: graceful degradation on SSL error
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
/**
|
|
* 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');
|
|
}
|
|
}
|
|
}
|