morpheus-cli 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -273
- package/dist/cli/commands/doctor.js +36 -1
- package/dist/cli/commands/init.js +92 -0
- package/dist/cli/commands/start.js +2 -1
- package/dist/cli/index.js +1 -17
- package/dist/cli/utils/render.js +2 -1
- package/dist/cli/utils/version.js +16 -0
- package/dist/config/manager.js +16 -0
- package/dist/config/schemas.js +15 -8
- package/dist/http/api.js +111 -0
- package/dist/runtime/__tests__/manual_santi_verify.js +55 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/memory/sati/__tests__/repository.test.js +71 -0
- package/dist/runtime/memory/sati/__tests__/service.test.js +99 -0
- package/dist/runtime/memory/sati/index.js +58 -0
- package/dist/runtime/memory/sati/repository.js +226 -0
- package/dist/runtime/memory/sati/service.js +142 -0
- package/dist/runtime/memory/sati/system-prompts.js +42 -0
- package/dist/runtime/memory/sati/types.js +1 -0
- package/dist/runtime/memory/sqlite.js +5 -1
- package/dist/runtime/migration.js +53 -1
- package/dist/runtime/oracle.js +32 -7
- package/dist/runtime/santi/contracts.js +1 -0
- package/dist/runtime/santi/middleware.js +61 -0
- package/dist/runtime/santi/santi.js +109 -0
- package/dist/runtime/santi/store.js +158 -0
- package/dist/runtime/tools/factory.js +31 -25
- package/dist/types/config.js +1 -0
- package/dist/ui/assets/index-BLLLlr0w.css +1 -0
- package/dist/ui/assets/index-Ccml5qIL.js +50 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-AEbYNHuy.css +0 -1
- package/dist/ui/assets/index-BjnI8c1U.js +0 -50
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
2
|
+
import { SatiService } from "./service.js";
|
|
3
|
+
import { DisplayManager } from "../../display.js";
|
|
4
|
+
const display = DisplayManager.getInstance();
|
|
5
|
+
export class SatiMemoryMiddleware {
|
|
6
|
+
service;
|
|
7
|
+
static instance;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.service = SatiService.getInstance();
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!SatiMemoryMiddleware.instance) {
|
|
13
|
+
SatiMemoryMiddleware.instance = new SatiMemoryMiddleware();
|
|
14
|
+
}
|
|
15
|
+
return SatiMemoryMiddleware.instance;
|
|
16
|
+
}
|
|
17
|
+
async beforeAgent(currentMessage, history) {
|
|
18
|
+
try {
|
|
19
|
+
// Extract recent messages content strings for context
|
|
20
|
+
const recentText = history.slice(-10).map(m => m.content.toString());
|
|
21
|
+
display.log(`[Sati] Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
|
|
22
|
+
const result = await this.service.recover(currentMessage, recentText);
|
|
23
|
+
if (result.relevant_memories.length === 0) {
|
|
24
|
+
display.log('[Sati] No relevant memories found', { source: 'Sati' });
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const memoryContext = result.relevant_memories
|
|
28
|
+
.map(m => `- [${m.category.toUpperCase()}] ${m.summary}`)
|
|
29
|
+
.join('\n');
|
|
30
|
+
display.log(`[Sati] Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
|
|
31
|
+
return new AIMessage(`
|
|
32
|
+
### LONG-TERM MEMORY (SATI)
|
|
33
|
+
The following information was retrieved from previous sessions. Use it if relevant:
|
|
34
|
+
|
|
35
|
+
${memoryContext}
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
display.log(`[SatiMiddleware] Error in beforeAgent: ${error}`, { source: 'Sati' });
|
|
40
|
+
// Fail open: return null so execution continues without memory
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async afterAgent(generatedResponse, history) {
|
|
45
|
+
try {
|
|
46
|
+
await this.service.evaluateAndPersist([
|
|
47
|
+
...history.slice(-5).map(m => ({
|
|
48
|
+
role: m._getType() === 'human' ? 'user' : 'assistant',
|
|
49
|
+
content: m.content.toString()
|
|
50
|
+
})),
|
|
51
|
+
{ role: 'assistant', content: generatedResponse }
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error('[SatiMiddleware] Error in afterAgent:', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { randomUUID } from 'crypto'; // Available in recent Node versions
|
|
6
|
+
export class SatiRepository {
|
|
7
|
+
db = null;
|
|
8
|
+
dbPath;
|
|
9
|
+
static instance;
|
|
10
|
+
constructor(dbPath) {
|
|
11
|
+
this.dbPath = dbPath || path.join(homedir(), '.morpheus', 'memory', 'santi-memory.db');
|
|
12
|
+
}
|
|
13
|
+
static getInstance(dbPath) {
|
|
14
|
+
if (!SatiRepository.instance) {
|
|
15
|
+
SatiRepository.instance = new SatiRepository(dbPath);
|
|
16
|
+
}
|
|
17
|
+
return SatiRepository.instance;
|
|
18
|
+
}
|
|
19
|
+
initialize() {
|
|
20
|
+
try {
|
|
21
|
+
// Ensure directory exists
|
|
22
|
+
fs.ensureDirSync(path.dirname(this.dbPath));
|
|
23
|
+
// Connect to database
|
|
24
|
+
this.db = new Database(this.dbPath, { timeout: 5000 });
|
|
25
|
+
this.db.pragma('journal_mode = WAL');
|
|
26
|
+
// Create schema
|
|
27
|
+
this.createSchema();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`[SatiRepository] Failed to initialize database: ${error}`);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
createSchema() {
|
|
35
|
+
if (!this.db)
|
|
36
|
+
throw new Error("DB not initialized");
|
|
37
|
+
this.db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS long_term_memory (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
category TEXT NOT NULL,
|
|
41
|
+
importance TEXT NOT NULL,
|
|
42
|
+
summary TEXT NOT NULL,
|
|
43
|
+
details TEXT,
|
|
44
|
+
hash TEXT NOT NULL UNIQUE,
|
|
45
|
+
source TEXT,
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
updated_at TEXT NOT NULL,
|
|
48
|
+
last_accessed_at TEXT,
|
|
49
|
+
access_count INTEGER DEFAULT 0,
|
|
50
|
+
version INTEGER DEFAULT 1,
|
|
51
|
+
archived INTEGER DEFAULT 0
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_memory_category ON long_term_memory(category);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_memory_importance ON long_term_memory(importance);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_memory_archived ON long_term_memory(archived);
|
|
57
|
+
|
|
58
|
+
-- FTS5 Virtual Table for semantic-like keyword search
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(summary, content='long_term_memory', content_rowid='rowid');
|
|
60
|
+
|
|
61
|
+
-- Triggers to sync FTS
|
|
62
|
+
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON long_term_memory BEGIN
|
|
63
|
+
INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
|
|
64
|
+
END;
|
|
65
|
+
CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON long_term_memory BEGIN
|
|
66
|
+
INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
|
|
67
|
+
END;
|
|
68
|
+
CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON long_term_memory BEGIN
|
|
69
|
+
INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
|
|
70
|
+
INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
|
|
71
|
+
END;
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
async save(record) {
|
|
75
|
+
if (!this.db)
|
|
76
|
+
this.initialize();
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
const fullRecord = {
|
|
79
|
+
id: randomUUID(),
|
|
80
|
+
...record,
|
|
81
|
+
created_at: new Date(now),
|
|
82
|
+
updated_at: new Date(now),
|
|
83
|
+
access_count: 0,
|
|
84
|
+
version: 1,
|
|
85
|
+
archived: false
|
|
86
|
+
};
|
|
87
|
+
const stmt = this.db.prepare(`
|
|
88
|
+
INSERT INTO long_term_memory (
|
|
89
|
+
id, category, importance, summary, details, hash, source,
|
|
90
|
+
created_at, updated_at, last_accessed_at, access_count, version, archived
|
|
91
|
+
) VALUES (
|
|
92
|
+
@id, @category, @importance, @summary, @details, @hash, @source,
|
|
93
|
+
@created_at, @updated_at, @last_accessed_at, @access_count, @version, @archived
|
|
94
|
+
)
|
|
95
|
+
ON CONFLICT(hash) DO UPDATE SET
|
|
96
|
+
importance = excluded.importance,
|
|
97
|
+
access_count = long_term_memory.access_count + 1,
|
|
98
|
+
last_accessed_at = excluded.updated_at,
|
|
99
|
+
updated_at = excluded.updated_at,
|
|
100
|
+
details = excluded.details
|
|
101
|
+
`);
|
|
102
|
+
// SQLite expects 0/1 for boolean and NULL for undefined
|
|
103
|
+
const params = {
|
|
104
|
+
...fullRecord,
|
|
105
|
+
details: fullRecord.details || null,
|
|
106
|
+
source: fullRecord.source || null,
|
|
107
|
+
created_at: now,
|
|
108
|
+
updated_at: now,
|
|
109
|
+
last_accessed_at: now, // Set accessing time on save/update
|
|
110
|
+
archived: 0
|
|
111
|
+
};
|
|
112
|
+
stmt.run(params);
|
|
113
|
+
return fullRecord;
|
|
114
|
+
}
|
|
115
|
+
findByHash(hash) {
|
|
116
|
+
if (!this.db)
|
|
117
|
+
this.initialize();
|
|
118
|
+
const row = this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(hash);
|
|
119
|
+
return row ? this.mapRowToRecord(row) : null;
|
|
120
|
+
}
|
|
121
|
+
search(query, limit = 5) {
|
|
122
|
+
if (!this.db)
|
|
123
|
+
this.initialize();
|
|
124
|
+
// Sanitize query for FTS5: remove characters that break FTS5 syntax
|
|
125
|
+
// Keep only alphanumeric, spaces, and safe punctuation (comma, period, hyphen)
|
|
126
|
+
// const safeQuery = query.replace(/[^a-zA-Z0-9\s,.\-]/g, "").trim();
|
|
127
|
+
// if (!safeQuery) {
|
|
128
|
+
// console.warn('[SatiRepository] Empty query after sanitization');
|
|
129
|
+
// return this.getFallbackMemories(limit);
|
|
130
|
+
// }
|
|
131
|
+
// try {
|
|
132
|
+
// // Try FTS5 search first
|
|
133
|
+
// const stmt = this.db!.prepare(`
|
|
134
|
+
// SELECT m.*
|
|
135
|
+
// FROM long_term_memory m
|
|
136
|
+
// JOIN memory_fts f ON m.rowid = f.rowid
|
|
137
|
+
// WHERE memory_fts MATCH ? AND m.archived = 0
|
|
138
|
+
// ORDER BY rank
|
|
139
|
+
// LIMIT ?
|
|
140
|
+
// `);
|
|
141
|
+
// const rows = stmt.all(safeQuery, limit) as any[];
|
|
142
|
+
// if (rows.length > 0) {
|
|
143
|
+
// console.log(`[SatiRepository] FTS5 found ${rows.length} memories for: "${safeQuery}"`);
|
|
144
|
+
// return rows.map(this.mapRowToRecord);
|
|
145
|
+
// }
|
|
146
|
+
// // Fallback: try LIKE search
|
|
147
|
+
// console.log(`[SatiRepository] FTS5 returned no results, trying LIKE search for: "${safeQuery}"`);
|
|
148
|
+
// const likeStmt = this.db!.prepare(`
|
|
149
|
+
// SELECT * FROM long_term_memory
|
|
150
|
+
// WHERE (summary LIKE ? OR details LIKE ?)
|
|
151
|
+
// AND archived = 0
|
|
152
|
+
// ORDER BY importance DESC, access_count DESC
|
|
153
|
+
// LIMIT ?
|
|
154
|
+
// `);
|
|
155
|
+
// const likePattern = `%${safeQuery}%`;
|
|
156
|
+
// const likeRows = likeStmt.all(likePattern, likePattern, limit) as any[];
|
|
157
|
+
// if (likeRows.length > 0) {
|
|
158
|
+
// console.log(`[SatiRepository] LIKE search found ${likeRows.length} memories`);
|
|
159
|
+
// return likeRows.map(this.mapRowToRecord);
|
|
160
|
+
// }
|
|
161
|
+
// // Final fallback: return most important/accessed memories
|
|
162
|
+
// console.log('[SatiRepository] No search results, returning most important memories');
|
|
163
|
+
// return this.getFallbackMemories(limit);
|
|
164
|
+
// } catch (e) {
|
|
165
|
+
// console.warn(`[SatiRepository] Search failed for query "${query}": ${e}`);
|
|
166
|
+
// return this.getFallbackMemories(limit);
|
|
167
|
+
// }
|
|
168
|
+
return this.getFallbackMemories(limit);
|
|
169
|
+
}
|
|
170
|
+
getFallbackMemories(limit) {
|
|
171
|
+
if (!this.db)
|
|
172
|
+
return [];
|
|
173
|
+
const stmt = this.db.prepare(`
|
|
174
|
+
SELECT * FROM long_term_memory
|
|
175
|
+
WHERE archived = 0
|
|
176
|
+
ORDER BY
|
|
177
|
+
CASE importance
|
|
178
|
+
WHEN 'critical' THEN 1
|
|
179
|
+
WHEN 'high' THEN 2
|
|
180
|
+
WHEN 'medium' THEN 3
|
|
181
|
+
WHEN 'low' THEN 4
|
|
182
|
+
END,
|
|
183
|
+
access_count DESC,
|
|
184
|
+
created_at DESC
|
|
185
|
+
LIMIT ?
|
|
186
|
+
`);
|
|
187
|
+
const rows = stmt.all(limit);
|
|
188
|
+
return rows.map(this.mapRowToRecord);
|
|
189
|
+
}
|
|
190
|
+
getAllMemories() {
|
|
191
|
+
if (!this.db)
|
|
192
|
+
this.initialize();
|
|
193
|
+
const rows = this.db.prepare('SELECT * FROM long_term_memory WHERE archived = 0 ORDER BY created_at DESC').all();
|
|
194
|
+
return rows.map(this.mapRowToRecord);
|
|
195
|
+
}
|
|
196
|
+
mapRowToRecord(row) {
|
|
197
|
+
return {
|
|
198
|
+
id: row.id,
|
|
199
|
+
category: row.category,
|
|
200
|
+
importance: row.importance,
|
|
201
|
+
summary: row.summary,
|
|
202
|
+
details: row.details,
|
|
203
|
+
hash: row.hash,
|
|
204
|
+
source: row.source,
|
|
205
|
+
created_at: new Date(row.created_at),
|
|
206
|
+
updated_at: new Date(row.updated_at),
|
|
207
|
+
last_accessed_at: row.last_accessed_at ? new Date(row.last_accessed_at) : undefined,
|
|
208
|
+
access_count: row.access_count,
|
|
209
|
+
version: row.version,
|
|
210
|
+
archived: Boolean(row.archived)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
close() {
|
|
214
|
+
if (this.db) {
|
|
215
|
+
this.db.close();
|
|
216
|
+
this.db = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
archiveMemory(id) {
|
|
220
|
+
if (!this.db)
|
|
221
|
+
this.initialize();
|
|
222
|
+
const stmt = this.db.prepare('UPDATE long_term_memory SET archived = 1 WHERE id = ?');
|
|
223
|
+
const result = stmt.run(id);
|
|
224
|
+
return result.changes > 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { SatiRepository } from './repository.js';
|
|
2
|
+
import { ConfigManager } from '../../../config/manager.js';
|
|
3
|
+
import { ProviderFactory } from '../../providers/factory.js';
|
|
4
|
+
import { SystemMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
|
|
5
|
+
import { SATI_EVALUATION_PROMPT } from './system-prompts.js';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { DisplayManager } from '../../display.js';
|
|
8
|
+
import { SQLiteChatMessageHistory } from '../sqlite.js';
|
|
9
|
+
const display = DisplayManager.getInstance();
|
|
10
|
+
export class SatiService {
|
|
11
|
+
repository;
|
|
12
|
+
static instance;
|
|
13
|
+
constructor() {
|
|
14
|
+
this.repository = SatiRepository.getInstance();
|
|
15
|
+
}
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!SatiService.instance) {
|
|
18
|
+
SatiService.instance = new SatiService();
|
|
19
|
+
}
|
|
20
|
+
return SatiService.instance;
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
this.repository.initialize();
|
|
24
|
+
}
|
|
25
|
+
async recover(currentMessage, recentMessages) {
|
|
26
|
+
const santiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
27
|
+
const memoryLimit = santiConfig.memory_limit || 1000;
|
|
28
|
+
// Use the current message as the primary search query
|
|
29
|
+
// We could enhance this by extracting keywords from the last few messages
|
|
30
|
+
// but for FR-004 we start with user input.
|
|
31
|
+
const memories = this.repository.search(currentMessage, memoryLimit);
|
|
32
|
+
return {
|
|
33
|
+
relevant_memories: memories.map(m => ({
|
|
34
|
+
summary: m.summary,
|
|
35
|
+
category: m.category,
|
|
36
|
+
importance: m.importance
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async evaluateAndPersist(conversation) {
|
|
41
|
+
try {
|
|
42
|
+
const santiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
43
|
+
if (!santiConfig)
|
|
44
|
+
return;
|
|
45
|
+
// Use the main provider factory to get an agent (Reusing Zion configuration)
|
|
46
|
+
// We pass empty tools as Sati is a pure reasoning agent here
|
|
47
|
+
const agent = await ProviderFactory.create(santiConfig, []);
|
|
48
|
+
// Get existing memories for context (Simulated "Working Memory" or full list if small)
|
|
49
|
+
const allMemories = this.repository.getAllMemories();
|
|
50
|
+
const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
|
|
51
|
+
// Map conversation to strict types and sanitize
|
|
52
|
+
const recentConversation = conversation.map(c => ({
|
|
53
|
+
role: (c.role === 'human' ? 'user' : c.role),
|
|
54
|
+
content: c.content
|
|
55
|
+
}));
|
|
56
|
+
const inputPayload = {
|
|
57
|
+
recent_conversation: recentConversation,
|
|
58
|
+
existing_memory_summaries: existingSummaries
|
|
59
|
+
};
|
|
60
|
+
const messages = [
|
|
61
|
+
new SystemMessage(SATI_EVALUATION_PROMPT),
|
|
62
|
+
new HumanMessage(JSON.stringify(inputPayload, null, 2))
|
|
63
|
+
];
|
|
64
|
+
const history = new SQLiteChatMessageHistory({ sessionId: 'sati-evaluation' });
|
|
65
|
+
try {
|
|
66
|
+
const inputMsg = new ToolMessage({
|
|
67
|
+
content: JSON.stringify(inputPayload, null, 2),
|
|
68
|
+
tool_call_id: `sati-input-${Date.now()}`,
|
|
69
|
+
name: 'sati_evaluation_input'
|
|
70
|
+
});
|
|
71
|
+
inputMsg.provider_metadata = {
|
|
72
|
+
provider: santiConfig.provider,
|
|
73
|
+
model: santiConfig.model
|
|
74
|
+
};
|
|
75
|
+
await history.addMessage(inputMsg);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.warn('[SatiService] Failed to persist input log:', e);
|
|
79
|
+
}
|
|
80
|
+
const response = await agent.invoke({ messages });
|
|
81
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
82
|
+
let content = lastMessage.content.toString();
|
|
83
|
+
try {
|
|
84
|
+
const outputToolMsg = new ToolMessage({
|
|
85
|
+
content: content,
|
|
86
|
+
tool_call_id: `sati-output-${Date.now()}`,
|
|
87
|
+
name: 'sati_evaluation_output'
|
|
88
|
+
});
|
|
89
|
+
if (lastMessage.usage_metadata) {
|
|
90
|
+
outputToolMsg.usage_metadata = lastMessage.usage_metadata;
|
|
91
|
+
}
|
|
92
|
+
outputToolMsg.provider_metadata = {
|
|
93
|
+
provider: santiConfig.provider,
|
|
94
|
+
model: santiConfig.model
|
|
95
|
+
};
|
|
96
|
+
await history.addMessage(outputToolMsg);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn('[SatiService] Failed to persist output log:', e);
|
|
100
|
+
}
|
|
101
|
+
// Safe JSON parsing (handle markdown blocks if LLM wraps output)
|
|
102
|
+
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = JSON.parse(content);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.warn('[SatiService] Failed to parse JSON response:', content);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (result.should_store && result.summary && result.category && result.importance) {
|
|
112
|
+
display.log(`Persisting new memory: [${result.category.toUpperCase()}] ${result.summary}`, { source: 'Sati' });
|
|
113
|
+
try {
|
|
114
|
+
await this.repository.save({
|
|
115
|
+
summary: result.summary,
|
|
116
|
+
category: result.category,
|
|
117
|
+
importance: result.importance,
|
|
118
|
+
details: result.reason,
|
|
119
|
+
hash: this.generateHash(result.summary),
|
|
120
|
+
source: 'conversation' // Could track actual session ID here if available
|
|
121
|
+
});
|
|
122
|
+
// Quiet success - logging handled by repository/middleware if needed, or verbose debug
|
|
123
|
+
}
|
|
124
|
+
catch (saveError) {
|
|
125
|
+
if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
|
|
126
|
+
// Duplicate detected by DB (Hash collision)
|
|
127
|
+
// This is expected given T012 logic
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw saveError;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('[SatiService] Evaluation failed:', error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
generateHash(content) {
|
|
140
|
+
return createHash('sha256').update(content.trim().toLowerCase()).digest('hex');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const SATI_EVALUATION_PROMPT = `You are **Sati**, an autonomous background memory manager for an AI assistant.
|
|
2
|
+
Your goal is to analyze the conversation interaction and decide if any **Persistent Long-Term Memory** should be stored.
|
|
3
|
+
|
|
4
|
+
### INPUT DATA
|
|
5
|
+
You will receive:
|
|
6
|
+
1. A list of recent messages (USER and ASSISTANT).
|
|
7
|
+
2. A list of ALREADY EXISTING memory summaries (to avoid duplicates).
|
|
8
|
+
|
|
9
|
+
### MEMORY CATEGORIES
|
|
10
|
+
Classify any new memory into one of these types:
|
|
11
|
+
- **preference**: User preferences (e.g., "I like dark mode", "Use TypeScript").
|
|
12
|
+
- **project**: Details about the user's projects, architecture, or tech stack.
|
|
13
|
+
- **identity**: Facts about the user's identity, role, or background.
|
|
14
|
+
- **constraint**: Hard rules the user wants you to follow (e.g., "Never use single quotes").
|
|
15
|
+
- **context**: General context that is useful for the long term.
|
|
16
|
+
- **personal_data**: Non-sensitive personal info (e.g., birthday, location).
|
|
17
|
+
- **languages**: User's spoken or programming languages.
|
|
18
|
+
- **favorite_things**: Favorites (movies, books, etc.).
|
|
19
|
+
- **relationships**: Mention of colleagues, family, or friends.
|
|
20
|
+
- **pets**: Info about user's pets.
|
|
21
|
+
- **naming**: Naming conventions the user prefers.
|
|
22
|
+
- **professional_profile**: Job title, industry, skills.
|
|
23
|
+
|
|
24
|
+
### CRITICAL RULES
|
|
25
|
+
1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens. If found, ignore them explicitly.
|
|
26
|
+
2. **NO DUPLICATES**: If the information is already covered by the \`existing_memory_summaries\`, DO NOT store it again.
|
|
27
|
+
3. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
|
|
28
|
+
4. **IMPORTANCE**: Assign 'low', 'medium', or 'high' importance. Store only 'medium' or 'high' unless it's a specific user preference (which is always important).
|
|
29
|
+
|
|
30
|
+
### TOP IMPORTANT GUIDELINES
|
|
31
|
+
5. **OBEY THE USER**: If the user explicitly states something should be remembered, it must be stored with at least 'medium' importance.
|
|
32
|
+
|
|
33
|
+
### OUTPUT FORMAT
|
|
34
|
+
You MUST respond with a valid JSON object matching the \`ISatiEvaluationOutput\` interface:
|
|
35
|
+
{
|
|
36
|
+
"should_store": boolean,
|
|
37
|
+
"category": "category_name" | null,
|
|
38
|
+
"importance": "low" | "medium" | "high" | null,
|
|
39
|
+
"summary": "Concise factual statement" | null,
|
|
40
|
+
"reason": "Why you decided to store or not store"
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -140,7 +140,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
140
140
|
async getMessages() {
|
|
141
141
|
try {
|
|
142
142
|
// Fetch new columns
|
|
143
|
-
const stmt = this.db.prepare(
|
|
143
|
+
const stmt = this.db.prepare(`SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model
|
|
144
|
+
FROM messages
|
|
145
|
+
WHERE session_id = ?
|
|
146
|
+
ORDER BY id DESC
|
|
147
|
+
LIMIT ?`);
|
|
144
148
|
const rows = stmt.all(this.sessionId, this.limit);
|
|
145
149
|
return rows.map((row) => {
|
|
146
150
|
let msg;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
3
4
|
import { PATHS } from '../config/paths.js';
|
|
4
5
|
import { DisplayManager } from './display.js';
|
|
5
6
|
export async function migrateConfigFile() {
|
|
@@ -17,7 +18,6 @@ export async function migrateConfigFile() {
|
|
|
17
18
|
catch (err) {
|
|
18
19
|
display.log(`Failed to migrate config.yaml to zaion.yaml: ${err.message}`, { source: 'Zaion', level: 'warning' });
|
|
19
20
|
}
|
|
20
|
-
return;
|
|
21
21
|
}
|
|
22
22
|
if (legacyExists && newExists) {
|
|
23
23
|
display.log('Both config.yaml and zaion.yaml exist. Using zaion.yaml and leaving config.yaml in place.', {
|
|
@@ -25,4 +25,56 @@ export async function migrateConfigFile() {
|
|
|
25
25
|
level: 'warning'
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
+
// Migrate memory.limit to llm.context_window
|
|
29
|
+
await migrateContextWindow();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Migrates memory.limit to llm.context_window
|
|
33
|
+
* Creates backup before modifying config
|
|
34
|
+
*/
|
|
35
|
+
async function migrateContextWindow() {
|
|
36
|
+
const display = DisplayManager.getInstance();
|
|
37
|
+
const configPath = PATHS.config;
|
|
38
|
+
try {
|
|
39
|
+
// Check if config file exists
|
|
40
|
+
if (!await fs.pathExists(configPath)) {
|
|
41
|
+
return; // No config to migrate
|
|
42
|
+
}
|
|
43
|
+
// Read current config
|
|
44
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
45
|
+
const config = yaml.load(configContent);
|
|
46
|
+
// Check if migration is needed
|
|
47
|
+
const hasOldField = config?.memory?.limit !== undefined;
|
|
48
|
+
const hasNewField = config?.llm?.context_window !== undefined;
|
|
49
|
+
// Already migrated or nothing to migrate
|
|
50
|
+
if (!hasOldField || hasNewField) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Create backup before migration
|
|
54
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
55
|
+
const backupPath = `${configPath}.backup-${timestamp}`;
|
|
56
|
+
await fs.copy(configPath, backupPath);
|
|
57
|
+
display.log(`Created config backup: ${backupPath}`, { source: 'Migration', level: 'info' });
|
|
58
|
+
// Perform migration
|
|
59
|
+
if (!config.llm) {
|
|
60
|
+
config.llm = {};
|
|
61
|
+
}
|
|
62
|
+
config.llm.context_window = config.memory.limit;
|
|
63
|
+
delete config.memory.limit;
|
|
64
|
+
// If memory object is now empty, keep it but with undefined limit for backward compat
|
|
65
|
+
if (Object.keys(config.memory || {}).length === 0) {
|
|
66
|
+
config.memory = {};
|
|
67
|
+
}
|
|
68
|
+
// Write migrated config
|
|
69
|
+
const migratedYaml = yaml.dump(config);
|
|
70
|
+
await fs.writeFile(configPath, migratedYaml, 'utf8');
|
|
71
|
+
display.log('Migrated memory.limit → llm.context_window', { source: 'Migration', level: 'info' });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Fail open: log error but don't crash
|
|
75
|
+
display.log(`Config migration failed: ${error.message}. System will use defaults.`, {
|
|
76
|
+
source: 'Migration',
|
|
77
|
+
level: 'warning'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
28
80
|
}
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -5,12 +5,14 @@ import { ConfigManager } from "../config/manager.js";
|
|
|
5
5
|
import { ProviderError } from "./errors.js";
|
|
6
6
|
import { DisplayManager } from "./display.js";
|
|
7
7
|
import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
|
|
8
|
+
import { SatiMemoryMiddleware } from "./memory/sati/index.js";
|
|
8
9
|
export class Oracle {
|
|
9
10
|
provider;
|
|
10
11
|
config;
|
|
11
12
|
history;
|
|
12
13
|
display = DisplayManager.getInstance();
|
|
13
14
|
databasePath;
|
|
15
|
+
satiMiddleware = SatiMemoryMiddleware.getInstance();
|
|
14
16
|
constructor(config, overrides) {
|
|
15
17
|
this.config = config || ConfigManager.getInstance().get();
|
|
16
18
|
this.databasePath = overrides?.databasePath;
|
|
@@ -32,10 +34,12 @@ export class Oracle {
|
|
|
32
34
|
throw new Error("Provider factory returned undefined");
|
|
33
35
|
}
|
|
34
36
|
// Initialize persistent memory with SQLite
|
|
37
|
+
const contextWindow = this.config.llm?.context_window ?? this.config.memory?.limit ?? 100;
|
|
38
|
+
this.display.log(`Using context window: ${contextWindow} messages`, { source: 'Oracle' });
|
|
35
39
|
this.history = new SQLiteChatMessageHistory({
|
|
36
40
|
sessionId: "default",
|
|
37
41
|
databasePath: this.databasePath,
|
|
38
|
-
limit:
|
|
42
|
+
limit: contextWindow,
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
catch (err) {
|
|
@@ -181,13 +185,29 @@ You do not speculate when verification is possible.
|
|
|
181
185
|
You maintain intent until resolution.
|
|
182
186
|
|
|
183
187
|
`);
|
|
184
|
-
// Load existing history from database
|
|
185
|
-
|
|
188
|
+
// Load existing history from database in reverse order (most recent first)
|
|
189
|
+
let previousMessages = await this.history.getMessages();
|
|
190
|
+
previousMessages = previousMessages.reverse();
|
|
191
|
+
// Sati Middleware: Retrieval
|
|
192
|
+
let memoryMessage = null;
|
|
193
|
+
try {
|
|
194
|
+
memoryMessage = await this.satiMiddleware.beforeAgent(message, previousMessages);
|
|
195
|
+
if (memoryMessage) {
|
|
196
|
+
this.display.log('Sati memory retrieved.', { source: 'Sati' });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
// Fail open - do not disrupt main flow
|
|
201
|
+
this.display.log(`Sati memory retrieval failed: ${e.message}`, { source: 'Sati' });
|
|
202
|
+
}
|
|
186
203
|
const messages = [
|
|
187
|
-
systemMessage
|
|
188
|
-
...previousMessages,
|
|
189
|
-
userMessage
|
|
204
|
+
systemMessage
|
|
190
205
|
];
|
|
206
|
+
if (memoryMessage) {
|
|
207
|
+
messages.push(memoryMessage);
|
|
208
|
+
}
|
|
209
|
+
messages.push(...previousMessages);
|
|
210
|
+
messages.push(userMessage);
|
|
191
211
|
const response = await this.provider.invoke({ messages });
|
|
192
212
|
// Identify new messages generated during the interaction
|
|
193
213
|
// The `messages` array passed to invoke had length `messages.length`
|
|
@@ -195,6 +215,7 @@ You maintain intent until resolution.
|
|
|
195
215
|
// New messages start after the inputs.
|
|
196
216
|
const startNewMessagesIndex = messages.length;
|
|
197
217
|
const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
|
|
218
|
+
// console.log('New generated messages', newGeneratedMessages);
|
|
198
219
|
// Persist User Message first
|
|
199
220
|
await this.history.addMessage(userMessage);
|
|
200
221
|
// Persist all new intermediate tool calls and responses
|
|
@@ -208,7 +229,11 @@ You maintain intent until resolution.
|
|
|
208
229
|
}
|
|
209
230
|
this.display.log('Response generated.', { source: 'Oracle' });
|
|
210
231
|
const lastMessage = response.messages[response.messages.length - 1];
|
|
211
|
-
|
|
232
|
+
const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
233
|
+
// Sati Middleware: Evaluation (Fire and forget)
|
|
234
|
+
this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage])
|
|
235
|
+
.catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
|
|
236
|
+
return responseContent;
|
|
212
237
|
}
|
|
213
238
|
catch (err) {
|
|
214
239
|
throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|