skyloom 1.4.0
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/.github/workflows/ci.yml +36 -0
- package/CONVERSION_PLAN.md +191 -0
- package/README.md +67 -0
- package/dist/agents/dew.d.ts +15 -0
- package/dist/agents/dew.d.ts.map +1 -0
- package/dist/agents/dew.js +74 -0
- package/dist/agents/dew.js.map +1 -0
- package/dist/agents/fair.d.ts +15 -0
- package/dist/agents/fair.d.ts.map +1 -0
- package/dist/agents/fair.js +106 -0
- package/dist/agents/fair.js.map +1 -0
- package/dist/agents/fog.d.ts +15 -0
- package/dist/agents/fog.d.ts.map +1 -0
- package/dist/agents/fog.js +52 -0
- package/dist/agents/fog.js.map +1 -0
- package/dist/agents/frost.d.ts +15 -0
- package/dist/agents/frost.d.ts.map +1 -0
- package/dist/agents/frost.js +54 -0
- package/dist/agents/frost.js.map +1 -0
- package/dist/agents/rain.d.ts +15 -0
- package/dist/agents/rain.d.ts.map +1 -0
- package/dist/agents/rain.js +54 -0
- package/dist/agents/rain.js.map +1 -0
- package/dist/agents/snow.d.ts +27 -0
- package/dist/agents/snow.d.ts.map +1 -0
- package/dist/agents/snow.js +226 -0
- package/dist/agents/snow.js.map +1 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +402 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mode.d.ts +17 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +56 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/core/agent.d.ts +174 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1332 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agent_helpers.d.ts +51 -0
- package/dist/core/agent_helpers.d.ts.map +1 -0
- package/dist/core/agent_helpers.js +477 -0
- package/dist/core/agent_helpers.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +191 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cache.d.ts +63 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checkpoint.d.ts +19 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +120 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/circuit_breaker.d.ts +46 -0
- package/dist/core/circuit_breaker.d.ts.map +1 -0
- package/dist/core/circuit_breaker.js +99 -0
- package/dist/core/circuit_breaker.js.map +1 -0
- package/dist/core/config.d.ts +97 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +281 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +78 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +84 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/factory.d.ts +63 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +537 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/icons.d.ts +28 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +86 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +54 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm.d.ts +121 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +532 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/logger.d.ts +57 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +122 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mcp.d.ts +190 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +822 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/mcp_server.d.ts +26 -0
- package/dist/core/mcp_server.d.ts.map +1 -0
- package/dist/core/mcp_server.js +211 -0
- package/dist/core/mcp_server.js.map +1 -0
- package/dist/core/memory.d.ts +190 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +988 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/middleware.d.ts +114 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +248 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/pipelines.d.ts +87 -0
- package/dist/core/pipelines.d.ts.map +1 -0
- package/dist/core/pipelines.js +301 -0
- package/dist/core/pipelines.js.map +1 -0
- package/dist/core/profile.d.ts +23 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +289 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/router.d.ts +24 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +111 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schemas.d.ts +82 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +200 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.d.ts.map +1 -0
- package/dist/core/semantic.js +175 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/skill.d.ts +68 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +350 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/tool.d.ts +99 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +341 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tool_router.d.ts +29 -0
- package/dist/core/tool_router.d.ts.map +1 -0
- package/dist/core/tool_router.js +172 -0
- package/dist/core/tool_router.js.map +1 -0
- package/dist/core/workspace.d.ts +48 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +179 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/plugins/loader.d.ts +17 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +96 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +78 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tools/builtin.d.ts +10 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +414 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/computer.d.ts +12 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +326 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/delegate.d.ts +10 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +647 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/tts.d.ts +33 -0
- package/dist/web/tts.d.ts.map +1 -0
- package/dist/web/tts.js +69 -0
- package/dist/web/tts.js.map +1 -0
- package/package.json +60 -0
- package/scripts/install.js +48 -0
- package/scripts/link.js +10 -0
- package/setup.bat +79 -0
- package/skill-test-ty2fOA/test.md +10 -0
- package/src/agents/dew.ts +70 -0
- package/src/agents/fair.ts +102 -0
- package/src/agents/fog.ts +48 -0
- package/src/agents/frost.ts +50 -0
- package/src/agents/rain.ts +50 -0
- package/src/agents/snow.ts +239 -0
- package/src/cli/main.ts +405 -0
- package/src/cli/mode.ts +58 -0
- package/src/core/agent.ts +1506 -0
- package/src/core/agent_helpers.ts +461 -0
- package/src/core/bus.ts +221 -0
- package/src/core/cache.ts +153 -0
- package/src/core/checkpoint.ts +94 -0
- package/src/core/circuit_breaker.ts +119 -0
- package/src/core/config.ts +341 -0
- package/src/core/constants.ts +95 -0
- package/src/core/factory.ts +627 -0
- package/src/core/icons.ts +53 -0
- package/src/core/index.ts +31 -0
- package/src/core/llm.ts +724 -0
- package/src/core/logger.ts +144 -0
- package/src/core/mcp.ts +953 -0
- package/src/core/mcp_server.ts +176 -0
- package/src/core/memory.ts +1169 -0
- package/src/core/middleware.ts +350 -0
- package/src/core/pipelines.ts +424 -0
- package/src/core/profile.ts +255 -0
- package/src/core/router.ts +124 -0
- package/src/core/schemas.ts +282 -0
- package/src/core/semantic.ts +211 -0
- package/src/core/skill.ts +342 -0
- package/src/core/tool.ts +427 -0
- package/src/core/tool_router.ts +193 -0
- package/src/core/workspace.ts +150 -0
- package/src/plugins/loader.ts +66 -0
- package/src/skills/loader.ts +46 -0
- package/src/sql.js.d.ts +29 -0
- package/src/tools/builtin.ts +382 -0
- package/src/tools/computer.ts +269 -0
- package/src/tools/delegate.ts +49 -0
- package/src/web/server.ts +634 -0
- package/src/web/tts.ts +93 -0
- package/tests/bus.test.ts +121 -0
- package/tests/icons.test.ts +45 -0
- package/tests/router.test.ts +86 -0
- package/tests/schemas.test.ts +51 -0
- package/tests/semantic.test.ts +83 -0
- package/tests/setup.ts +10 -0
- package/tests/skill.test.ts +172 -0
- package/tests/tool.test.ts +108 -0
- package/tests/tool_router.test.ts +71 -0
- package/tsconfig.json +37 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Memory system: short-term context, long-term persistence, working memory.
|
|
4
|
+
*
|
|
5
|
+
* Three-layer memory for each agent with SQLite persistence.
|
|
6
|
+
* - Short-term: conversation context (persisted to SQLite)
|
|
7
|
+
* - Working: in-memory task-scoped state
|
|
8
|
+
* - Long-term: persistent key-value storage with search
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
44
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.Memory = void 0;
|
|
48
|
+
exports.getShortTermLock = getShortTermLock;
|
|
49
|
+
const sql_js_1 = __importDefault(require("sql.js"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const logger_1 = require("./logger");
|
|
54
|
+
const semantic_1 = require("./semantic");
|
|
55
|
+
const logger = (0, logger_1.getLogger)('memory');
|
|
56
|
+
// Token extraction patterns for fact-recall queries
|
|
57
|
+
const ASCII_TOKEN_RE = /[A-Za-z][A-Za-z0-9_+-]+/g;
|
|
58
|
+
const CJK_RUN_RE = /[一-鿿]+/g;
|
|
59
|
+
/**
|
|
60
|
+
* Simple inline Mutex implementation (replaces async-lock dependency).
|
|
61
|
+
*/
|
|
62
|
+
class SimpleMutex {
|
|
63
|
+
constructor() {
|
|
64
|
+
this._locked = false;
|
|
65
|
+
this._queue = [];
|
|
66
|
+
}
|
|
67
|
+
async lock(fn) {
|
|
68
|
+
await new Promise((resolve) => {
|
|
69
|
+
if (!this._locked) {
|
|
70
|
+
this._locked = true;
|
|
71
|
+
resolve();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this._queue.push(resolve);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
return await fn();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (this._queue.length > 0) {
|
|
82
|
+
const next = this._queue.shift();
|
|
83
|
+
next();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this._locked = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Three-layer memory system for agents.
|
|
93
|
+
*/
|
|
94
|
+
class Memory {
|
|
95
|
+
constructor(config, agentName) {
|
|
96
|
+
this.shortTerm = [];
|
|
97
|
+
this.working = {};
|
|
98
|
+
this.db = null;
|
|
99
|
+
this.SQL = null;
|
|
100
|
+
this.loaded = false;
|
|
101
|
+
this.pendingPersists = new Set();
|
|
102
|
+
this.activeSession = null;
|
|
103
|
+
// short_term is mutated from both main chat loop and handlers
|
|
104
|
+
// All mutations go through a short critical section
|
|
105
|
+
this.shortTermLock = new SimpleMutex();
|
|
106
|
+
this.config = config;
|
|
107
|
+
this.agentName = agentName;
|
|
108
|
+
const base = expandUserPath(config.dbPath);
|
|
109
|
+
this.dbPath = path.join(path.dirname(base), `${agentName}.db`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Initialize the database and load persistent data.
|
|
113
|
+
*/
|
|
114
|
+
async initDb() {
|
|
115
|
+
if (this.db !== null) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Initialize sql.js
|
|
119
|
+
this.SQL = await (0, sql_js_1.default)();
|
|
120
|
+
// Create directory if it doesn't exist
|
|
121
|
+
const dbDir = path.dirname(this.dbPath);
|
|
122
|
+
if (!fs.existsSync(dbDir)) {
|
|
123
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
// Load existing database or create new
|
|
126
|
+
let dbBuffer = null;
|
|
127
|
+
if (fs.existsSync(this.dbPath)) {
|
|
128
|
+
try {
|
|
129
|
+
dbBuffer = fs.readFileSync(this.dbPath);
|
|
130
|
+
}
|
|
131
|
+
catch { /* read failed, start fresh */ }
|
|
132
|
+
}
|
|
133
|
+
this.db = new this.SQL.Database(dbBuffer || undefined);
|
|
134
|
+
this.db.run('PRAGMA journal_mode = MEMORY');
|
|
135
|
+
this.db.run('PRAGMA busy_timeout = 100');
|
|
136
|
+
// Create tables
|
|
137
|
+
this.db.run(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
139
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
140
|
+
agent TEXT NOT NULL,
|
|
141
|
+
key TEXT NOT NULL,
|
|
142
|
+
value TEXT NOT NULL,
|
|
143
|
+
category TEXT DEFAULT 'general',
|
|
144
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
145
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
146
|
+
)
|
|
147
|
+
`);
|
|
148
|
+
// Migrate existing DBs
|
|
149
|
+
try {
|
|
150
|
+
this.db.run("ALTER TABLE memories ADD COLUMN category TEXT DEFAULT 'general'");
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Column already exists
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
this.db.run('ALTER TABLE memories ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Column already exists
|
|
160
|
+
}
|
|
161
|
+
// Ensure unique index
|
|
162
|
+
try {
|
|
163
|
+
this.db.run('DROP INDEX IF EXISTS idx_agent_key');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Ignore
|
|
167
|
+
}
|
|
168
|
+
this.db.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_key ON memories(agent, key)');
|
|
169
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_agent_category ON memories(agent, category)');
|
|
170
|
+
this.db.run(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
172
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
173
|
+
agent TEXT NOT NULL,
|
|
174
|
+
role TEXT NOT NULL,
|
|
175
|
+
content TEXT NOT NULL,
|
|
176
|
+
name TEXT,
|
|
177
|
+
tool_call_id TEXT,
|
|
178
|
+
tool_calls TEXT,
|
|
179
|
+
reasoning_content TEXT,
|
|
180
|
+
session_id TEXT,
|
|
181
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
182
|
+
)
|
|
183
|
+
`);
|
|
184
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent, created_at)');
|
|
185
|
+
try {
|
|
186
|
+
this.db.run('ALTER TABLE messages ADD COLUMN tool_calls TEXT');
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Column already exists
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
this.db.run('ALTER TABLE messages ADD COLUMN reasoning_content TEXT');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Column already exists
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
this.db.run('ALTER TABLE messages ADD COLUMN session_id TEXT');
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Column already exists
|
|
202
|
+
}
|
|
203
|
+
this.db.run(`
|
|
204
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
205
|
+
id TEXT PRIMARY KEY,
|
|
206
|
+
agent TEXT NOT NULL,
|
|
207
|
+
name TEXT,
|
|
208
|
+
preview TEXT DEFAULT '',
|
|
209
|
+
message_count INTEGER DEFAULT 0,
|
|
210
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
211
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
212
|
+
)
|
|
213
|
+
`);
|
|
214
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent, updated_at DESC)');
|
|
215
|
+
this.db.run(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS working_data (
|
|
217
|
+
agent TEXT NOT NULL,
|
|
218
|
+
key TEXT NOT NULL,
|
|
219
|
+
value TEXT NOT NULL,
|
|
220
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
221
|
+
PRIMARY KEY (agent, key)
|
|
222
|
+
)
|
|
223
|
+
`);
|
|
224
|
+
this.loadShortTerm();
|
|
225
|
+
this.loadWorking();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Persist the database to disk.
|
|
229
|
+
*/
|
|
230
|
+
persistDb() {
|
|
231
|
+
if (!this.db)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
const data = this.db.export();
|
|
235
|
+
fs.writeFileSync(this.dbPath, Buffer.from(data));
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
logger.warn('persist_db_failed', { path: this.dbPath, error: String(err) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Execute a SELECT query and return array of row objects.
|
|
243
|
+
*/
|
|
244
|
+
dbAll(sql, params) {
|
|
245
|
+
if (!this.db)
|
|
246
|
+
return [];
|
|
247
|
+
try {
|
|
248
|
+
const stmt = this.db.prepare(sql);
|
|
249
|
+
if (params)
|
|
250
|
+
stmt.bind(params);
|
|
251
|
+
const rows = [];
|
|
252
|
+
while (stmt.step()) {
|
|
253
|
+
rows.push(stmt.getAsObject());
|
|
254
|
+
}
|
|
255
|
+
stmt.free();
|
|
256
|
+
return rows;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Execute a SELECT query and return first row object.
|
|
264
|
+
*/
|
|
265
|
+
dbGet(sql, params) {
|
|
266
|
+
if (!this.db)
|
|
267
|
+
return null;
|
|
268
|
+
try {
|
|
269
|
+
const stmt = this.db.prepare(sql);
|
|
270
|
+
if (params)
|
|
271
|
+
stmt.bind(params);
|
|
272
|
+
let row = null;
|
|
273
|
+
if (stmt.step()) {
|
|
274
|
+
row = stmt.getAsObject();
|
|
275
|
+
}
|
|
276
|
+
stmt.free();
|
|
277
|
+
return row;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Execute a statement and return this for chaining.
|
|
285
|
+
*/
|
|
286
|
+
dbRun(sql, params) {
|
|
287
|
+
if (!this.db)
|
|
288
|
+
return;
|
|
289
|
+
try {
|
|
290
|
+
this.db.run(sql, params);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
logger.warn('db_run_failed', { sql: sql.slice(0, 80), error: String(err) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Load short-term memory from database.
|
|
298
|
+
*/
|
|
299
|
+
async loadShortTerm() {
|
|
300
|
+
if (!this.db || this.loaded) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
let rows = [];
|
|
304
|
+
if (this.activeSession) {
|
|
305
|
+
rows = this.dbAll(`SELECT role, content, name, tool_call_id, tool_calls, reasoning_content, created_at
|
|
306
|
+
FROM messages
|
|
307
|
+
WHERE agent = ? AND session_id = ?
|
|
308
|
+
ORDER BY id DESC LIMIT ?`, [this.agentName, this.activeSession, this.config.shortTermLimit]);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
this.loaded = true;
|
|
312
|
+
this.pruneDanglingToolCalls();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Truncate at timestamp gap
|
|
316
|
+
const gapSeconds = parseFloat(process.env.WA_RESUME_GAP_SECONDS || '14400'); // 4h default
|
|
317
|
+
rows = Memory.truncateAtTimestampGap(rows, gapSeconds);
|
|
318
|
+
for (const row of rows.reverse()) {
|
|
319
|
+
let toolCalls = null;
|
|
320
|
+
if (row.tool_calls) {
|
|
321
|
+
try {
|
|
322
|
+
toolCalls = JSON.parse(row.tool_calls);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Invalid JSON, skip
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.shortTerm.push({
|
|
329
|
+
role: row.role,
|
|
330
|
+
content: row.content,
|
|
331
|
+
name: row.name,
|
|
332
|
+
toolCallId: row.tool_call_id,
|
|
333
|
+
toolCalls,
|
|
334
|
+
reasoningContent: row.reasoning_content,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
this.loaded = true;
|
|
338
|
+
this.pruneDanglingToolCalls();
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Load working memory from database.
|
|
342
|
+
*/
|
|
343
|
+
async loadWorking() {
|
|
344
|
+
if (!this.db) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const rows = this.dbAll('SELECT key, value FROM working_data WHERE agent = ?', [this.agentName]);
|
|
348
|
+
for (const row of rows) {
|
|
349
|
+
try {
|
|
350
|
+
this.working[row.key] = JSON.parse(row.value);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Skip invalid JSON
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Keep only the contiguous tail of rows where consecutive timestamps
|
|
359
|
+
* are within gap_seconds of each other.
|
|
360
|
+
*/
|
|
361
|
+
static truncateAtTimestampGap(rows, gapSeconds) {
|
|
362
|
+
if (rows.length === 0 || gapSeconds <= 0) {
|
|
363
|
+
return rows;
|
|
364
|
+
}
|
|
365
|
+
const parseTs = (raw) => {
|
|
366
|
+
if (!raw)
|
|
367
|
+
return null;
|
|
368
|
+
if (raw instanceof Date)
|
|
369
|
+
return raw;
|
|
370
|
+
try {
|
|
371
|
+
return new Date(raw);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const keep = [rows[0]];
|
|
378
|
+
let prevTs = parseTs(rows[0].created_at);
|
|
379
|
+
for (let i = 1; i < rows.length; i++) {
|
|
380
|
+
const curTs = parseTs(rows[i].created_at);
|
|
381
|
+
if (prevTs && curTs) {
|
|
382
|
+
const delta = Math.abs((prevTs.getTime() - curTs.getTime()) / 1000);
|
|
383
|
+
if (delta > gapSeconds) {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
keep.push(rows[i]);
|
|
388
|
+
if (curTs) {
|
|
389
|
+
prevTs = curTs;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return keep;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Remove orphaned tool_calls/tool message pairs from short-term memory.
|
|
396
|
+
*/
|
|
397
|
+
pruneDanglingToolCalls() {
|
|
398
|
+
if (this.shortTerm.length === 0) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const n = this.shortTerm.length;
|
|
402
|
+
const remove = new Array(n).fill(false);
|
|
403
|
+
// Pass 1: position-aware matching
|
|
404
|
+
const waiting = {};
|
|
405
|
+
for (let i = 0; i < this.shortTerm.length; i++) {
|
|
406
|
+
const msg = this.shortTerm[i];
|
|
407
|
+
if (msg.role === 'assistant' && msg.toolCalls) {
|
|
408
|
+
for (const tc of msg.toolCalls) {
|
|
409
|
+
const tid = tc.id;
|
|
410
|
+
if (tid) {
|
|
411
|
+
if (!waiting[tid])
|
|
412
|
+
waiting[tid] = [];
|
|
413
|
+
waiting[tid].push(i);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (msg.role === 'tool' && msg.toolCallId) {
|
|
418
|
+
const tid = msg.toolCallId;
|
|
419
|
+
if (waiting[tid] && waiting[tid].length > 0) {
|
|
420
|
+
waiting[tid].pop();
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
remove[i] = true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Any assistant indices still in waiting stacks are orphaned
|
|
428
|
+
for (const indices of Object.values(waiting)) {
|
|
429
|
+
for (const i of indices) {
|
|
430
|
+
remove[i] = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!remove.some(r => r)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const kept = this.shortTerm.filter((_, i) => !remove[i]);
|
|
437
|
+
// Pass 2: remove tool messages with no preceding assistant
|
|
438
|
+
const seenTcIds = new Set();
|
|
439
|
+
const sanitized = [];
|
|
440
|
+
for (const msg of kept) {
|
|
441
|
+
if (msg.role === 'assistant' && msg.toolCalls) {
|
|
442
|
+
for (const tc of msg.toolCalls) {
|
|
443
|
+
const tid = tc.id;
|
|
444
|
+
if (tid) {
|
|
445
|
+
seenTcIds.add(tid);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else if (msg.role === 'tool' && msg.toolCallId && !seenTcIds.has(msg.toolCallId)) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
sanitized.push(msg);
|
|
453
|
+
}
|
|
454
|
+
this.shortTerm = sanitized;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Public wrapper around pruneDanglingToolCalls.
|
|
458
|
+
*/
|
|
459
|
+
pruneToolMessages() {
|
|
460
|
+
this.pruneDanglingToolCalls();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Flush pending operations and close database.
|
|
464
|
+
*/
|
|
465
|
+
async close() {
|
|
466
|
+
if (this.db) {
|
|
467
|
+
await this.flushPending();
|
|
468
|
+
this.db.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Wait for all pending persist operations to complete.
|
|
473
|
+
*/
|
|
474
|
+
async flushPending() {
|
|
475
|
+
if (this.pendingPersists.size > 0) {
|
|
476
|
+
const results = await Promise.allSettled(Array.from(this.pendingPersists));
|
|
477
|
+
for (const result of results) {
|
|
478
|
+
if (result.status === 'rejected') {
|
|
479
|
+
logger.warn('flush_persist_failed', { agent: this.agentName, error: String(result.reason) });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
this.pendingPersists.clear();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// -- Short-term memory --
|
|
486
|
+
/**
|
|
487
|
+
* Add a message to short-term memory.
|
|
488
|
+
*/
|
|
489
|
+
addMessage(role, content, kwargs) {
|
|
490
|
+
const msg = {
|
|
491
|
+
role,
|
|
492
|
+
content,
|
|
493
|
+
name: kwargs?.name,
|
|
494
|
+
toolCallId: kwargs?.toolCallId,
|
|
495
|
+
toolCalls: kwargs?.toolCalls,
|
|
496
|
+
reasoningContent: kwargs?.reasoningContent,
|
|
497
|
+
};
|
|
498
|
+
const ephemeral = kwargs?.ephemeral ?? false;
|
|
499
|
+
// Mutex-locked section
|
|
500
|
+
this.shortTermLock.lock(async () => {
|
|
501
|
+
this.shortTerm.push(msg);
|
|
502
|
+
if (this.shortTerm.length > this.config.shortTermLimit) {
|
|
503
|
+
const systemMsgs = this.shortTerm.filter(m => m.role === 'system');
|
|
504
|
+
const otherMsgs = this.shortTerm.filter(m => m.role !== 'system');
|
|
505
|
+
const keep = Math.max(0, this.config.shortTermLimit - systemMsgs.length);
|
|
506
|
+
this.shortTerm = systemMsgs.concat(keep > 0 ? otherMsgs.slice(-keep) : []);
|
|
507
|
+
this.pruneDanglingToolCalls();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
// Persist message if not ephemeral
|
|
511
|
+
if (this.db && role !== 'system' && !ephemeral) {
|
|
512
|
+
const toolCallsJson = msg.toolCalls ? JSON.stringify(msg.toolCalls) : null;
|
|
513
|
+
const sessionId = this.activeSession;
|
|
514
|
+
const promise = this.persistMessage(role, content, msg.name || null, msg.toolCallId || null, toolCallsJson, msg.reasoningContent || null, sessionId);
|
|
515
|
+
this.pendingPersists.add(promise);
|
|
516
|
+
promise.then(() => {
|
|
517
|
+
this.pendingPersists.delete(promise);
|
|
518
|
+
}).catch(() => {
|
|
519
|
+
this.pendingPersists.delete(promise);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Persist a message to database.
|
|
525
|
+
*/
|
|
526
|
+
async persistMessage(role, content, name, toolCallId, toolCalls = null, reasoningContent = null, sessionId = null) {
|
|
527
|
+
if (!this.db) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
this.dbRun(`INSERT INTO messages (agent, role, content, name, tool_call_id, tool_calls, reasoning_content, session_id)
|
|
532
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [this.agentName, role, content, name, toolCallId, toolCalls, reasoningContent, sessionId]);
|
|
533
|
+
if (sessionId) {
|
|
534
|
+
this.dbRun('UPDATE sessions SET message_count = message_count + 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [sessionId]);
|
|
535
|
+
}
|
|
536
|
+
// Auto-prune old messages
|
|
537
|
+
const maxPersisted = this.config.maxPersistedMessages || 1000;
|
|
538
|
+
if (sessionId && maxPersisted > 0) {
|
|
539
|
+
const row = this.dbGet('SELECT COUNT(*) as count FROM messages WHERE agent = ? AND session_id = ? AND role != ?', [this.agentName, sessionId, 'system']);
|
|
540
|
+
if (row && row.count > maxPersisted) {
|
|
541
|
+
const excess = row.count - maxPersisted;
|
|
542
|
+
this.dbRun(`DELETE FROM messages WHERE id IN (
|
|
543
|
+
SELECT id FROM messages WHERE agent = ? AND session_id = ? AND role != ?
|
|
544
|
+
ORDER BY id ASC LIMIT ?
|
|
545
|
+
)`, [this.agentName, sessionId, 'system', excess]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
logger.warn('persist_message_failed', { agent: this.agentName, error: String(err) });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get messages as a list of dicts compatible with LLM API.
|
|
555
|
+
*/
|
|
556
|
+
getMessages() {
|
|
557
|
+
this.pruneDanglingToolCalls();
|
|
558
|
+
const msgs = [];
|
|
559
|
+
for (const m of this.shortTerm) {
|
|
560
|
+
const d = { role: m.role, content: m.content };
|
|
561
|
+
if (m.name)
|
|
562
|
+
d.name = m.name;
|
|
563
|
+
if (m.toolCallId)
|
|
564
|
+
d.tool_call_id = m.toolCallId;
|
|
565
|
+
if (m.toolCalls)
|
|
566
|
+
d.tool_calls = m.toolCalls;
|
|
567
|
+
if (m.reasoningContent)
|
|
568
|
+
d.reasoning_content = m.reasoningContent;
|
|
569
|
+
msgs.push(d);
|
|
570
|
+
}
|
|
571
|
+
return msgs;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Return stats about current memory usage.
|
|
575
|
+
*/
|
|
576
|
+
getContextWindowUsage() {
|
|
577
|
+
let totalChars = 0;
|
|
578
|
+
let cjk = 0;
|
|
579
|
+
for (const m of this.shortTerm) {
|
|
580
|
+
totalChars += m.content.length;
|
|
581
|
+
for (const c of m.content) {
|
|
582
|
+
const code = c.charCodeAt(0);
|
|
583
|
+
if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x303f)) {
|
|
584
|
+
cjk++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const other = totalChars - cjk;
|
|
589
|
+
return {
|
|
590
|
+
messageCount: this.shortTerm.length,
|
|
591
|
+
totalChars,
|
|
592
|
+
estimatedTokens: Math.max(1, cjk * 2 + Math.floor(other / 4)),
|
|
593
|
+
limit: this.config.shortTermLimit,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Clear in-memory short-term memory.
|
|
598
|
+
*/
|
|
599
|
+
async clearShortTerm() {
|
|
600
|
+
const systemMsgs = this.shortTerm.filter(m => m.role === 'system');
|
|
601
|
+
this.shortTerm = systemMsgs;
|
|
602
|
+
if (!this.db) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (this.activeSession !== null) {
|
|
606
|
+
this.dbRun('DELETE FROM messages WHERE agent = ? AND role != ? AND session_id = ?', [this.agentName, 'system', this.activeSession]);
|
|
607
|
+
this.dbRun('UPDATE sessions SET message_count = 0 WHERE id = ?', [this.activeSession]);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
this.dbRun('DELETE FROM messages WHERE agent = ? AND role != ? AND session_id IS NULL', [this.agentName, 'system']);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// -- Working memory --
|
|
614
|
+
/**
|
|
615
|
+
* Set a working memory value.
|
|
616
|
+
*/
|
|
617
|
+
setWorking(key, value) {
|
|
618
|
+
this.working[key] = value;
|
|
619
|
+
this.schedulePersistWorking();
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Get a working memory value.
|
|
623
|
+
*/
|
|
624
|
+
getWorking(key, defaultValue = null) {
|
|
625
|
+
return this.working[key] ?? defaultValue;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Clear all working memory.
|
|
629
|
+
*/
|
|
630
|
+
clearWorking() {
|
|
631
|
+
this.working = {};
|
|
632
|
+
this.schedulePersistWorking();
|
|
633
|
+
}
|
|
634
|
+
schedulePersistWorking() {
|
|
635
|
+
if (!this.db) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const promise = this.persistWorking();
|
|
639
|
+
this.pendingPersists.add(promise);
|
|
640
|
+
promise.then(() => {
|
|
641
|
+
this.pendingPersists.delete(promise);
|
|
642
|
+
}).catch(() => {
|
|
643
|
+
this.pendingPersists.delete(promise);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async persistWorking() {
|
|
647
|
+
if (!this.db) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
this.dbRun('DELETE FROM working_data WHERE agent = ?', [this.agentName]);
|
|
652
|
+
for (const [key, value] of Object.entries(this.working)) {
|
|
653
|
+
this.dbRun('INSERT INTO working_data (agent, key, value) VALUES (?, ?, ?)', [this.agentName, key, JSON.stringify(value)]);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
logger.warn('persist_working_failed', { agent: this.agentName, error: String(err) });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// -- Long-term memory --
|
|
661
|
+
/**
|
|
662
|
+
* Store a long-term memory fact.
|
|
663
|
+
*/
|
|
664
|
+
async remember(key, value, category = 'general') {
|
|
665
|
+
if (!this.db) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
this.dbRun(`INSERT INTO memories (agent, key, value, category) VALUES (?, ?, ?, ?)
|
|
669
|
+
ON CONFLICT(agent, key) DO UPDATE SET value = excluded.value, category = excluded.category, updated_at = CURRENT_TIMESTAMP`, [this.agentName, key, JSON.stringify(value), category]);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Recall long-term memories.
|
|
673
|
+
*/
|
|
674
|
+
async recall(key, category, limit = 20) {
|
|
675
|
+
if (!this.db) {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
let query = 'SELECT key, value, category FROM memories WHERE agent = ?';
|
|
679
|
+
const params = [this.agentName];
|
|
680
|
+
if (key) {
|
|
681
|
+
query += ' AND key LIKE ?';
|
|
682
|
+
params.push(`%${key}%`);
|
|
683
|
+
}
|
|
684
|
+
if (category) {
|
|
685
|
+
query += ' AND category = ?';
|
|
686
|
+
params.push(category);
|
|
687
|
+
}
|
|
688
|
+
query += ' ORDER BY updated_at DESC LIMIT ?';
|
|
689
|
+
params.push(limit);
|
|
690
|
+
const rows = this.dbAll(query, params);
|
|
691
|
+
return rows.map((r) => ({
|
|
692
|
+
key: r.key,
|
|
693
|
+
value: JSON.parse(r.value),
|
|
694
|
+
category: r.category,
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Forget a memory.
|
|
699
|
+
*/
|
|
700
|
+
async forget(key) {
|
|
701
|
+
if (!this.db) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.dbRun('DELETE FROM memories WHERE agent = ? AND key = ?', [this.agentName, key]);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Tokenize query for recall.
|
|
708
|
+
*/
|
|
709
|
+
static tokenizeForRecall(query) {
|
|
710
|
+
const out = new Set();
|
|
711
|
+
const text = query || '';
|
|
712
|
+
// ASCII tokens first
|
|
713
|
+
let match;
|
|
714
|
+
while ((match = ASCII_TOKEN_RE.exec(text)) !== null) {
|
|
715
|
+
out.add(match[0]);
|
|
716
|
+
}
|
|
717
|
+
// CJK tokens (n-grams)
|
|
718
|
+
for (const run of text.match(CJK_RUN_RE) || []) {
|
|
719
|
+
for (const size of [3, 2]) {
|
|
720
|
+
if (run.length < size)
|
|
721
|
+
continue;
|
|
722
|
+
for (let i = 0; i <= run.length - size; i++) {
|
|
723
|
+
out.add(run.slice(i, i + size));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return Array.from(out);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Recall for injection into prompts.
|
|
731
|
+
*/
|
|
732
|
+
async recallForInjection(query, limit = 3) {
|
|
733
|
+
if (!this.db || !query) {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
const tokens = Memory.tokenizeForRecall(query).slice(0, 24);
|
|
737
|
+
const likeHits = [];
|
|
738
|
+
// Pass 1: LIKE token scan
|
|
739
|
+
if (tokens.length > 0) {
|
|
740
|
+
const likeClauses = tokens.map(() => 'key LIKE ? OR value LIKE ?').join(' OR ');
|
|
741
|
+
const params = [this.agentName];
|
|
742
|
+
for (const tok of tokens) {
|
|
743
|
+
const pattern = `%${tok}%`;
|
|
744
|
+
params.push(pattern, pattern);
|
|
745
|
+
}
|
|
746
|
+
params.push(limit * 2);
|
|
747
|
+
const sql = `SELECT key, value, category, updated_at FROM memories
|
|
748
|
+
WHERE agent = ? AND (${likeClauses})
|
|
749
|
+
ORDER BY updated_at DESC LIMIT ?`;
|
|
750
|
+
const rows = this.dbAll(sql, params);
|
|
751
|
+
for (const r of rows) {
|
|
752
|
+
try {
|
|
753
|
+
const val = JSON.parse(r.value);
|
|
754
|
+
likeHits.push({
|
|
755
|
+
key: r.key,
|
|
756
|
+
value: val,
|
|
757
|
+
category: r.category,
|
|
758
|
+
updatedAt: r.updated_at,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
likeHits.push({
|
|
763
|
+
key: r.key,
|
|
764
|
+
value: r.value,
|
|
765
|
+
category: r.category,
|
|
766
|
+
updatedAt: r.updated_at,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Pass 2: Semantic scoring (best-effort)
|
|
772
|
+
const semanticHits = [];
|
|
773
|
+
try {
|
|
774
|
+
const seenKeys = new Set(likeHits.map(h => h.key));
|
|
775
|
+
const rows = this.dbAll('SELECT key, value, category, updated_at FROM memories WHERE agent = ? ORDER BY updated_at DESC LIMIT 200', [this.agentName]);
|
|
776
|
+
const candidates = [];
|
|
777
|
+
for (const r of rows) {
|
|
778
|
+
if (seenKeys.has(r.key))
|
|
779
|
+
continue;
|
|
780
|
+
try {
|
|
781
|
+
const val = JSON.parse(r.value);
|
|
782
|
+
candidates.push({
|
|
783
|
+
key: r.key,
|
|
784
|
+
value: val,
|
|
785
|
+
category: r.category,
|
|
786
|
+
updatedAt: r.updated_at,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
candidates.push({
|
|
791
|
+
key: r.key,
|
|
792
|
+
value: r.value,
|
|
793
|
+
category: r.category,
|
|
794
|
+
updatedAt: r.updated_at,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const scorer = (0, semantic_1.getScorer)();
|
|
799
|
+
if (scorer) {
|
|
800
|
+
const ranked = scorer.rank(query, candidates, 'value', limit, 0.03);
|
|
801
|
+
for (const [_score, c] of ranked) {
|
|
802
|
+
semanticHits.push(c);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
// Semantic pass is best-effort
|
|
808
|
+
logger.debug('recall_semantic_failed', { error: String(err) });
|
|
809
|
+
}
|
|
810
|
+
// Merge: LIKE first, then semantic
|
|
811
|
+
const seen = new Set();
|
|
812
|
+
const merged = [];
|
|
813
|
+
for (const src of [likeHits, semanticHits]) {
|
|
814
|
+
for (const item of src) {
|
|
815
|
+
const k = item.key;
|
|
816
|
+
if (seen.has(k))
|
|
817
|
+
continue;
|
|
818
|
+
seen.add(k);
|
|
819
|
+
merged.push({ key: k, value: item.value, category: item.category });
|
|
820
|
+
if (merged.length >= limit) {
|
|
821
|
+
return merged;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return merged;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Format facts as a markdown block for prompt injection.
|
|
829
|
+
*/
|
|
830
|
+
static formatFactsBlock(facts) {
|
|
831
|
+
if (!facts || facts.length === 0) {
|
|
832
|
+
return '';
|
|
833
|
+
}
|
|
834
|
+
const lines = ['## 相关记忆'];
|
|
835
|
+
for (const f of facts) {
|
|
836
|
+
let v = f.value;
|
|
837
|
+
if (typeof v !== 'string') {
|
|
838
|
+
try {
|
|
839
|
+
v = JSON.stringify(v);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
v = String(v);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
lines.push(`- **${f.key}**: ${v}`);
|
|
846
|
+
}
|
|
847
|
+
return lines.join('\n');
|
|
848
|
+
}
|
|
849
|
+
// -- Session management --
|
|
850
|
+
/**
|
|
851
|
+
* Get active session ID.
|
|
852
|
+
*/
|
|
853
|
+
getActiveSession() {
|
|
854
|
+
return this.activeSession;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Create a new session.
|
|
858
|
+
*/
|
|
859
|
+
async createSession(name) {
|
|
860
|
+
const sessionId = Math.random().toString(36).slice(2, 14);
|
|
861
|
+
const preview = name || '';
|
|
862
|
+
if (this.db) {
|
|
863
|
+
this.dbRun('INSERT INTO sessions (id, agent, name, preview) VALUES (?, ?, ?, ?)', [sessionId, this.agentName, name, preview]);
|
|
864
|
+
}
|
|
865
|
+
this.activeSession = sessionId;
|
|
866
|
+
this.shortTerm = this.shortTerm.filter(m => m.role === 'system');
|
|
867
|
+
this.loaded = true;
|
|
868
|
+
return sessionId;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* List all sessions.
|
|
872
|
+
*/
|
|
873
|
+
async listSessions() {
|
|
874
|
+
if (!this.db) {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
const rows = this.dbAll('SELECT id, agent, name, preview, message_count, created_at, updated_at FROM sessions WHERE agent = ? ORDER BY updated_at DESC LIMIT 50', [this.agentName]);
|
|
878
|
+
return rows.map((r) => ({
|
|
879
|
+
id: r.id,
|
|
880
|
+
agent: r.agent,
|
|
881
|
+
name: r.name,
|
|
882
|
+
preview: r.preview,
|
|
883
|
+
messageCount: r.message_count,
|
|
884
|
+
createdAt: r.created_at,
|
|
885
|
+
updatedAt: r.updated_at,
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Resume the latest session.
|
|
890
|
+
*/
|
|
891
|
+
async resumeLatestSession() {
|
|
892
|
+
if (!this.db || this.activeSession) {
|
|
893
|
+
return this.activeSession;
|
|
894
|
+
}
|
|
895
|
+
const row = this.dbGet('SELECT id FROM sessions WHERE agent = ? ORDER BY updated_at DESC LIMIT 1', [this.agentName]);
|
|
896
|
+
if (!row) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
this.activeSession = row.id;
|
|
900
|
+
this.loaded = false;
|
|
901
|
+
this.shortTerm = this.shortTerm.filter(m => m.role === 'system');
|
|
902
|
+
await this.loadShortTerm();
|
|
903
|
+
return this.activeSession;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Load a specific session.
|
|
907
|
+
*/
|
|
908
|
+
async loadSession(sessionId) {
|
|
909
|
+
if (!this.db) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
const row = this.dbGet('SELECT id FROM sessions WHERE id = ? AND agent = ?', [sessionId, this.agentName]);
|
|
913
|
+
if (!row) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
this.activeSession = sessionId;
|
|
917
|
+
this.shortTerm = this.shortTerm.filter(m => m.role === 'system');
|
|
918
|
+
this.loaded = false;
|
|
919
|
+
await this.loadShortTerm();
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Delete a session.
|
|
924
|
+
*/
|
|
925
|
+
async deleteSession(sessionId) {
|
|
926
|
+
if (!this.db) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
const row = this.dbGet('SELECT id FROM sessions WHERE id = ? AND agent = ?', [sessionId, this.agentName]);
|
|
930
|
+
if (!row) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
this.dbRun('DELETE FROM messages WHERE agent = ? AND session_id = ?', [this.agentName, sessionId]);
|
|
934
|
+
this.dbRun('DELETE FROM sessions WHERE id = ? AND agent = ?', [sessionId, this.agentName]);
|
|
935
|
+
if (this.activeSession === sessionId) {
|
|
936
|
+
this.activeSession = null;
|
|
937
|
+
this.shortTerm = this.shortTerm.filter(m => m.role === 'system');
|
|
938
|
+
}
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Update session preview.
|
|
943
|
+
*/
|
|
944
|
+
async updateSessionPreview() {
|
|
945
|
+
if (!this.db || !this.activeSession) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const row = this.dbGet('SELECT content FROM messages WHERE agent = ? AND session_id = ? AND role = ? ORDER BY id ASC LIMIT 1', [this.agentName, this.activeSession, 'user']);
|
|
949
|
+
if (row) {
|
|
950
|
+
const preview = row.content.slice(0, 80);
|
|
951
|
+
this.dbRun('UPDATE sessions SET preview = ? WHERE id = ?', [preview, this.activeSession]);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Get memory statistics.
|
|
956
|
+
*/
|
|
957
|
+
async getMemoryStats() {
|
|
958
|
+
if (!this.db) {
|
|
959
|
+
return { total: 0, categories: {} };
|
|
960
|
+
}
|
|
961
|
+
const rows = this.dbAll('SELECT category, COUNT(*) as count FROM memories WHERE agent = ? GROUP BY category', [this.agentName]);
|
|
962
|
+
const categories = {};
|
|
963
|
+
for (const row of rows) {
|
|
964
|
+
categories[row.category] = row.count;
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
total: Object.values(categories).reduce((a, b) => a + b, 0),
|
|
968
|
+
categories,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
exports.Memory = Memory;
|
|
973
|
+
/**
|
|
974
|
+
* Helper for expanding home directory paths.
|
|
975
|
+
*/
|
|
976
|
+
function expandUserPath(filePath) {
|
|
977
|
+
if (filePath.startsWith('~')) {
|
|
978
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
979
|
+
}
|
|
980
|
+
return filePath;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* ShortTermLock helper - exposes the lock for use by agent.ts
|
|
984
|
+
*/
|
|
985
|
+
function getShortTermLock(memory) {
|
|
986
|
+
return memory.shortTermLock;
|
|
987
|
+
}
|
|
988
|
+
//# sourceMappingURL=memory.js.map
|