@memstack/core 0.2.0 → 0.5.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/README.md +321 -69
- package/dist/index.cjs +2480 -269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +537 -27
- package/dist/index.d.ts +537 -27
- package/dist/index.js +2470 -267
- package/dist/index.js.map +1 -1
- package/package.json +27 -24
- package/src/adapters/embedding/cohere.ts +0 -77
- package/src/adapters/embedding/openai.ts +0 -65
- package/src/adapters/llm/anthropic.ts +0 -69
- package/src/adapters/llm/groq.ts +0 -185
- package/src/adapters/llm/ollama.ts +0 -85
- package/src/adapters/llm/openai.ts +0 -178
- package/src/adapters/storage/disk.ts +0 -334
- package/src/adapters/storage/memory.ts +0 -147
- package/src/adapters/storage/postgres.ts +0 -310
- package/src/adapters/storage/redis.ts +0 -274
- package/src/client.ts +0 -236
- package/src/errors.ts +0 -47
- package/src/index.ts +0 -52
- package/src/interfaces.ts +0 -154
- package/src/memory/ContextCompiler.ts +0 -158
- package/src/memory/MemoryStore.ts +0 -266
- package/src/memory/Pruner.ts +0 -66
- package/src/memory/Summarizer.ts +0 -46
- package/src/types.ts +0 -50
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import type { LLMProvider } from "../../interfaces.js";
|
|
2
|
-
import { MemStackError } from "../../errors.js";
|
|
3
|
-
|
|
4
|
-
export interface OpenAILLMConfig {
|
|
5
|
-
apiKey: string;
|
|
6
|
-
baseURL?: string;
|
|
7
|
-
defaultModel?: string;
|
|
8
|
-
defaultTemperature?: number;
|
|
9
|
-
defaultMaxTokens?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class OpenAILLMAdapter implements LLMProvider {
|
|
13
|
-
private apiKey: string;
|
|
14
|
-
private baseURL: string;
|
|
15
|
-
private defaultModel: string;
|
|
16
|
-
private defaultTemperature: number;
|
|
17
|
-
private defaultMaxTokens: number;
|
|
18
|
-
|
|
19
|
-
constructor(config: OpenAILLMConfig) {
|
|
20
|
-
this.apiKey = config.apiKey;
|
|
21
|
-
this.baseURL = config.baseURL ?? "https://api.openai.com/v1";
|
|
22
|
-
this.defaultModel = config.defaultModel ?? "gpt-4o-mini";
|
|
23
|
-
this.defaultTemperature = config.defaultTemperature ?? 0.7;
|
|
24
|
-
this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async complete(request: {
|
|
28
|
-
system: string;
|
|
29
|
-
user: string;
|
|
30
|
-
model?: string;
|
|
31
|
-
temperature?: number;
|
|
32
|
-
maxTokens?: number;
|
|
33
|
-
}): Promise<{ text: string; tokens: { prompt: number; completion: number; total: number } }> {
|
|
34
|
-
const model = request.model ?? this.defaultModel;
|
|
35
|
-
const temperature = request.temperature ?? this.defaultTemperature;
|
|
36
|
-
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
40
|
-
method: "POST",
|
|
41
|
-
headers: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
model,
|
|
47
|
-
messages: [
|
|
48
|
-
{ role: "system", content: request.system },
|
|
49
|
-
{ role: "user", content: request.user },
|
|
50
|
-
],
|
|
51
|
-
temperature,
|
|
52
|
-
max_tokens: maxTokens,
|
|
53
|
-
}),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
const err = await response.text();
|
|
58
|
-
throw new MemStackError("LLM_ERROR", `OpenAI API error: ${response.status} ${err}`, {
|
|
59
|
-
retryable: response.status >= 500,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const data = (await response.json()) as {
|
|
64
|
-
choices: { message: { content: string } }[];
|
|
65
|
-
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
text: data.choices[0]?.message?.content ?? "",
|
|
70
|
-
tokens: {
|
|
71
|
-
prompt: data.usage?.prompt_tokens ?? 0,
|
|
72
|
-
completion: data.usage?.completion_tokens ?? 0,
|
|
73
|
-
total: data.usage?.total_tokens ?? 0,
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
} catch (err) {
|
|
77
|
-
if (err instanceof MemStackError) throw err;
|
|
78
|
-
throw new MemStackError("LLM_ERROR", `LLM request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
79
|
-
retryable: true,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async *completeStream(request: {
|
|
85
|
-
system: string;
|
|
86
|
-
user: string;
|
|
87
|
-
model?: string;
|
|
88
|
-
temperature?: number;
|
|
89
|
-
maxTokens?: number;
|
|
90
|
-
}): AsyncIterable<{ text: string; tokens: { prompt: number; completion: number; total: number } }> {
|
|
91
|
-
const model = request.model ?? this.defaultModel;
|
|
92
|
-
const temperature = request.temperature ?? this.defaultTemperature;
|
|
93
|
-
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
94
|
-
|
|
95
|
-
let response: Response;
|
|
96
|
-
try {
|
|
97
|
-
response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: {
|
|
100
|
-
"Content-Type": "application/json",
|
|
101
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
102
|
-
},
|
|
103
|
-
body: JSON.stringify({
|
|
104
|
-
model,
|
|
105
|
-
messages: [
|
|
106
|
-
{ role: "system", content: request.system },
|
|
107
|
-
{ role: "user", content: request.user },
|
|
108
|
-
],
|
|
109
|
-
temperature,
|
|
110
|
-
max_tokens: maxTokens,
|
|
111
|
-
stream: true,
|
|
112
|
-
stream_options: { include_usage: true },
|
|
113
|
-
}),
|
|
114
|
-
});
|
|
115
|
-
} catch (err) {
|
|
116
|
-
throw new MemStackError("LLM_ERROR", `OpenAI streaming request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
117
|
-
retryable: true,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!response.ok) {
|
|
122
|
-
const err = await response.text();
|
|
123
|
-
throw new MemStackError("LLM_ERROR", `OpenAI streaming error: ${response.status} ${err}`, {
|
|
124
|
-
retryable: response.status >= 500,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const reader = response.body?.getReader();
|
|
129
|
-
if (!reader) throw new MemStackError("LLM_ERROR", "No response body for streaming");
|
|
130
|
-
|
|
131
|
-
const decoder = new TextDecoder();
|
|
132
|
-
let buffer = "";
|
|
133
|
-
let promptTokens = 0;
|
|
134
|
-
let completionTokens = 0;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
while (true) {
|
|
138
|
-
const { done, value } = await reader.read();
|
|
139
|
-
if (done) break;
|
|
140
|
-
|
|
141
|
-
buffer += decoder.decode(value, { stream: true });
|
|
142
|
-
const lines = buffer.split("\n");
|
|
143
|
-
buffer = lines.pop() ?? "";
|
|
144
|
-
|
|
145
|
-
for (const line of lines) {
|
|
146
|
-
const trimmed = line.trim();
|
|
147
|
-
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
148
|
-
const data = trimmed.slice(6);
|
|
149
|
-
if (data === "[DONE]") continue;
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const parsed = JSON.parse(data) as {
|
|
153
|
-
choices?: { delta?: { content?: string } }[];
|
|
154
|
-
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const content = parsed.choices?.[0]?.delta?.content ?? "";
|
|
158
|
-
if (parsed.usage) {
|
|
159
|
-
promptTokens = parsed.usage.prompt_tokens;
|
|
160
|
-
completionTokens = parsed.usage.completion_tokens;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (content) {
|
|
164
|
-
yield {
|
|
165
|
-
text: content,
|
|
166
|
-
tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens },
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Skip unparseable chunks
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} finally {
|
|
175
|
-
reader.releaseLock();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { Memory, MemoryType } from "../../types.js";
|
|
4
|
-
import type { StorageProvider, MemoryStoreInput, MemoryRetrieveQuery, MemoryCountFilter } from "../../interfaces.js";
|
|
5
|
-
import { storageError, notFound } from "../../errors.js";
|
|
6
|
-
|
|
7
|
-
interface MemoryRecord extends Memory {
|
|
8
|
-
_touchedAt: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface DiskStorageConfig {
|
|
12
|
-
/** Directory to store memory files. Default: "./memstack-data" */
|
|
13
|
-
storageDir?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class DiskStorage implements StorageProvider {
|
|
17
|
-
private dir: string;
|
|
18
|
-
private _writeLocks = new Map<string, Promise<void>>();
|
|
19
|
-
|
|
20
|
-
constructor(config: DiskStorageConfig = {}) {
|
|
21
|
-
this.dir = config.storageDir ?? "./memstack-data";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async initialize(): Promise<void> {
|
|
25
|
-
await mkdir(this.dir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
generateId(): string {
|
|
29
|
-
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async store(input: MemoryStoreInput): Promise<Memory> {
|
|
33
|
-
const now = new Date();
|
|
34
|
-
const memory: MemoryRecord = {
|
|
35
|
-
id: input.id ?? this.generateId(),
|
|
36
|
-
actorId: input.actorId,
|
|
37
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
38
|
-
content: input.content,
|
|
39
|
-
importance: input.importance ?? 0.5,
|
|
40
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
41
|
-
tags: input.tags ?? [],
|
|
42
|
-
embedding: input.embedding,
|
|
43
|
-
sourceId: input.sourceId,
|
|
44
|
-
metadata: input.metadata ?? {},
|
|
45
|
-
expiresAt: input.expiresAt,
|
|
46
|
-
createdAt: now,
|
|
47
|
-
_touchedAt: now.toISOString(),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
await this._withWriteLock(input.actorId, async () => {
|
|
51
|
-
const records = await this._readFile(input.actorId);
|
|
52
|
-
const existingIdx = records.findIndex((r) => r.id === memory.id);
|
|
53
|
-
if (existingIdx !== -1) {
|
|
54
|
-
records[existingIdx] = memory;
|
|
55
|
-
} else {
|
|
56
|
-
records.push(memory);
|
|
57
|
-
}
|
|
58
|
-
await this._writeFile(input.actorId, records);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return this._toExternal(memory);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> {
|
|
65
|
-
const byActor = new Map<string, MemoryRecord[]>();
|
|
66
|
-
const now = new Date();
|
|
67
|
-
const all: MemoryRecord[] = [];
|
|
68
|
-
|
|
69
|
-
for (const input of inputs) {
|
|
70
|
-
const memory: MemoryRecord = {
|
|
71
|
-
id: input.id ?? this.generateId(),
|
|
72
|
-
actorId: input.actorId,
|
|
73
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
74
|
-
content: input.content,
|
|
75
|
-
importance: input.importance ?? 0.5,
|
|
76
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
77
|
-
tags: input.tags ?? [],
|
|
78
|
-
embedding: input.embedding,
|
|
79
|
-
sourceId: input.sourceId,
|
|
80
|
-
metadata: input.metadata ?? {},
|
|
81
|
-
expiresAt: input.expiresAt,
|
|
82
|
-
createdAt: now,
|
|
83
|
-
_touchedAt: now.toISOString(),
|
|
84
|
-
};
|
|
85
|
-
all.push(memory);
|
|
86
|
-
const group = byActor.get(input.actorId) ?? [];
|
|
87
|
-
group.push(memory);
|
|
88
|
-
byActor.set(input.actorId, group);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
for (const [actorId, group] of byActor) {
|
|
92
|
-
await this._withWriteLock(actorId, async () => {
|
|
93
|
-
const existing = await this._readFile(actorId);
|
|
94
|
-
for (const mem of group) {
|
|
95
|
-
const existingIdx = existing.findIndex((r) => r.id === mem.id);
|
|
96
|
-
if (existingIdx !== -1) {
|
|
97
|
-
existing[existingIdx] = mem;
|
|
98
|
-
} else {
|
|
99
|
-
existing.push(mem);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
await this._writeFile(actorId, existing);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return all.map((m) => this._toExternal(m));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async get(id: string): Promise<Memory | null> {
|
|
110
|
-
const actorIds = await this._listActors();
|
|
111
|
-
for (const actorId of actorIds) {
|
|
112
|
-
const records = await this._readFile(actorId);
|
|
113
|
-
const record = records.find((r) => r.id === id);
|
|
114
|
-
if (record) {
|
|
115
|
-
if (record.expiresAt && new Date(record.expiresAt) <= new Date()) return null;
|
|
116
|
-
return this._toExternal(record);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async delete(id: string): Promise<void> {
|
|
123
|
-
const actorIds = await this._listActors();
|
|
124
|
-
for (const actorId of actorIds) {
|
|
125
|
-
const records = await this._readFile(actorId);
|
|
126
|
-
const index = records.findIndex((r) => r.id === id);
|
|
127
|
-
if (index !== -1) {
|
|
128
|
-
await this._withWriteLock(actorId, async () => {
|
|
129
|
-
const fresh = await this._readFile(actorId);
|
|
130
|
-
const i = fresh.findIndex((r) => r.id === id);
|
|
131
|
-
if (i !== -1) {
|
|
132
|
-
fresh.splice(i, 1);
|
|
133
|
-
await this._writeFile(actorId, fresh);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
throw notFound("Memory", id);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async deleteMany(ids: string[]): Promise<number> {
|
|
143
|
-
const idSet = new Set(ids);
|
|
144
|
-
let count = 0;
|
|
145
|
-
const actorIds = await this._listActors();
|
|
146
|
-
|
|
147
|
-
for (const actorId of actorIds) {
|
|
148
|
-
await this._withWriteLock(actorId, async () => {
|
|
149
|
-
const records = await this._readFile(actorId);
|
|
150
|
-
const before = records.length;
|
|
151
|
-
const filtered = records.filter((r) => !idSet.has(r.id));
|
|
152
|
-
count += before - filtered.length;
|
|
153
|
-
if (before !== filtered.length) {
|
|
154
|
-
await this._writeFile(actorId, filtered);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return count;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async retrieve(query: MemoryRetrieveQuery, _embedding?: number[]): Promise<Memory[]> {
|
|
163
|
-
const actorIds = query.actorId ? [query.actorId] : await this._listActors();
|
|
164
|
-
let results: MemoryRecord[] = [];
|
|
165
|
-
|
|
166
|
-
for (const actorId of actorIds) {
|
|
167
|
-
let records = await this._readFile(actorId);
|
|
168
|
-
const now = new Date();
|
|
169
|
-
records = records.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
170
|
-
|
|
171
|
-
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
172
|
-
records = records.filter((r) => query.memoryTypes!.includes(r.memoryType));
|
|
173
|
-
}
|
|
174
|
-
if (query.tags && query.tags.length > 0) {
|
|
175
|
-
records = records.filter((r) => r.tags?.some((t) => query.tags!.includes(t)));
|
|
176
|
-
}
|
|
177
|
-
if (query.query) {
|
|
178
|
-
const q = query.query.toLowerCase();
|
|
179
|
-
records = records.filter((r) => r.content.toLowerCase().includes(q));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
results.push(...records);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
switch (query.strategy) {
|
|
186
|
-
case "recent":
|
|
187
|
-
results.sort((a, b) => new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime());
|
|
188
|
-
break;
|
|
189
|
-
case "important":
|
|
190
|
-
results.sort((a, b) => b.importance - a.importance);
|
|
191
|
-
break;
|
|
192
|
-
case "hybrid":
|
|
193
|
-
case "semantic":
|
|
194
|
-
default:
|
|
195
|
-
results.sort(
|
|
196
|
-
(a, b) =>
|
|
197
|
-
b.importance - a.importance || new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime()
|
|
198
|
-
);
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Touch records on retrieval
|
|
203
|
-
const nowStr = new Date().toISOString();
|
|
204
|
-
const toTouch = results.slice(0, query.limit ?? 10);
|
|
205
|
-
const touchedActors = new Set<string>();
|
|
206
|
-
for (const r of toTouch) {
|
|
207
|
-
r._touchedAt = nowStr;
|
|
208
|
-
touchedActors.add(r.actorId);
|
|
209
|
-
}
|
|
210
|
-
// Persist touches back to disk
|
|
211
|
-
for (const actorId of touchedActors) {
|
|
212
|
-
await this._withWriteLock(actorId, async () => {
|
|
213
|
-
const records = await this._readFile(actorId);
|
|
214
|
-
let changed = false;
|
|
215
|
-
for (const r of records) {
|
|
216
|
-
const touched = toTouch.find((t) => t.id === r.id);
|
|
217
|
-
if (touched) { r._touchedAt = nowStr; changed = true; }
|
|
218
|
-
}
|
|
219
|
-
if (changed) await this._writeFile(actorId, records);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const limit = query.limit ?? 10;
|
|
224
|
-
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async count(filter?: MemoryCountFilter): Promise<number> {
|
|
228
|
-
const actorIds = filter?.actorId ? [filter.actorId] : await this._listActors();
|
|
229
|
-
let total = 0;
|
|
230
|
-
const now = new Date();
|
|
231
|
-
|
|
232
|
-
for (const actorId of actorIds) {
|
|
233
|
-
let records = await this._readFile(actorId);
|
|
234
|
-
records = records.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
235
|
-
|
|
236
|
-
if (filter?.memoryType) {
|
|
237
|
-
records = records.filter((r) => r.memoryType === filter.memoryType);
|
|
238
|
-
}
|
|
239
|
-
if (filter?.minImportance !== undefined) {
|
|
240
|
-
records = records.filter((r) => r.importance >= filter.minImportance!);
|
|
241
|
-
}
|
|
242
|
-
total += records.length;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return total;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async touch(id: string): Promise<void> {
|
|
249
|
-
const actorIds = await this._listActors();
|
|
250
|
-
for (const actorId of actorIds) {
|
|
251
|
-
const records = await this._readFile(actorId);
|
|
252
|
-
const found = records.find((r) => r.id === id);
|
|
253
|
-
if (found) {
|
|
254
|
-
const nowStr = new Date().toISOString();
|
|
255
|
-
await this._withWriteLock(actorId, async () => {
|
|
256
|
-
const fresh = await this._readFile(actorId);
|
|
257
|
-
const match = fresh.find((r) => r.id === id);
|
|
258
|
-
if (match) {
|
|
259
|
-
match._touchedAt = nowStr;
|
|
260
|
-
await this._writeFile(actorId, fresh);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
throw notFound("Memory", id);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async close(): Promise<void> {
|
|
270
|
-
this._writeLocks.clear();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ── Internal ──
|
|
274
|
-
|
|
275
|
-
private _filePath(actorId: string): string {
|
|
276
|
-
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
277
|
-
return join(this.dir, `${safe}.json`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async _readFile(actorId: string): Promise<MemoryRecord[]> {
|
|
281
|
-
try {
|
|
282
|
-
const raw = await readFile(this._filePath(actorId), "utf-8");
|
|
283
|
-
const parsed = JSON.parse(raw);
|
|
284
|
-
return Array.isArray(parsed) ? (parsed as MemoryRecord[]) : [];
|
|
285
|
-
} catch (err: unknown) {
|
|
286
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
287
|
-
throw storageError(`Failed to read memories for actor ${actorId}: ${(err as Error).message}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private async _writeFile(actorId: string, records: MemoryRecord[]): Promise<void> {
|
|
292
|
-
try {
|
|
293
|
-
await writeFile(this._filePath(actorId), JSON.stringify(records, null, 2), "utf-8");
|
|
294
|
-
} catch (err) {
|
|
295
|
-
throw storageError(`Failed to write memories for actor ${actorId}: ${(err as Error).message}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private async _withWriteLock(actorId: string, fn: () => Promise<void>): Promise<void> {
|
|
300
|
-
const prev = this._writeLocks.get(actorId) ?? Promise.resolve();
|
|
301
|
-
let resolve!: () => void;
|
|
302
|
-
const next = new Promise<void>((r) => { resolve = r; });
|
|
303
|
-
this._writeLocks.set(actorId, next);
|
|
304
|
-
await prev;
|
|
305
|
-
try {
|
|
306
|
-
await fn();
|
|
307
|
-
} finally {
|
|
308
|
-
resolve();
|
|
309
|
-
// Clean up: only remove this entry if it hasn't been replaced by a newer lock
|
|
310
|
-
if (this._writeLocks.get(actorId) === next) {
|
|
311
|
-
this._writeLocks.delete(actorId);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private async _listActors(): Promise<string[]> {
|
|
317
|
-
try {
|
|
318
|
-
const entries = await readdir(this.dir);
|
|
319
|
-
return entries.filter((e) => e.endsWith(".json")).map((e) => e.slice(0, -5));
|
|
320
|
-
} catch (err: unknown) {
|
|
321
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
322
|
-
throw storageError(`Failed to list actors: ${(err as Error).message}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private _toExternal(record: MemoryRecord): Memory {
|
|
327
|
-
const { _touchedAt: _, ...rest } = record;
|
|
328
|
-
return {
|
|
329
|
-
...rest,
|
|
330
|
-
createdAt: new Date(record.createdAt),
|
|
331
|
-
expiresAt: record.expiresAt ? new Date(record.expiresAt) : undefined,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { Memory, MemoryType } from "../../types.js";
|
|
2
|
-
import type { StorageProvider, MemoryStoreInput, MemoryRetrieveQuery, MemoryCountFilter } from "../../interfaces.js";
|
|
3
|
-
import { storageError, notFound } from "../../errors.js";
|
|
4
|
-
|
|
5
|
-
interface MemoryRecord extends Memory {
|
|
6
|
-
_touchedAt: Date;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class InMemoryStorage implements StorageProvider {
|
|
10
|
-
private memories: Map<string, MemoryRecord> = new Map();
|
|
11
|
-
|
|
12
|
-
async initialize(): Promise<void> {}
|
|
13
|
-
|
|
14
|
-
generateId(): string {
|
|
15
|
-
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async store(input: MemoryStoreInput): Promise<Memory> {
|
|
19
|
-
const now = new Date();
|
|
20
|
-
const memory: MemoryRecord = {
|
|
21
|
-
id: input.id ?? this.generateId(),
|
|
22
|
-
actorId: input.actorId,
|
|
23
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
24
|
-
content: input.content,
|
|
25
|
-
importance: input.importance ?? 0.5,
|
|
26
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
27
|
-
tags: input.tags ?? [],
|
|
28
|
-
embedding: input.embedding,
|
|
29
|
-
sourceId: input.sourceId,
|
|
30
|
-
metadata: input.metadata ?? {},
|
|
31
|
-
expiresAt: input.expiresAt,
|
|
32
|
-
createdAt: now,
|
|
33
|
-
_touchedAt: now,
|
|
34
|
-
};
|
|
35
|
-
this.memories.set(memory.id, memory);
|
|
36
|
-
return memory;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> {
|
|
40
|
-
const results: Memory[] = [];
|
|
41
|
-
for (const input of inputs) {
|
|
42
|
-
results.push(await this.store(input));
|
|
43
|
-
}
|
|
44
|
-
return results;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async get(id: string): Promise<Memory | null> {
|
|
48
|
-
const memory = this.memories.get(id);
|
|
49
|
-
if (!memory) return null;
|
|
50
|
-
if (memory.expiresAt && memory.expiresAt <= new Date()) {
|
|
51
|
-
this.memories.delete(id);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return memory;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async delete(id: string): Promise<void> {
|
|
58
|
-
if (!this.memories.has(id)) throw notFound("Memory", id);
|
|
59
|
-
this.memories.delete(id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async deleteMany(ids: string[]): Promise<number> {
|
|
63
|
-
let count = 0;
|
|
64
|
-
for (const id of ids) {
|
|
65
|
-
if (this.memories.has(id)) {
|
|
66
|
-
this.memories.delete(id);
|
|
67
|
-
count++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return count;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async retrieve(query: MemoryRetrieveQuery, _embedding?: number[]): Promise<Memory[]> {
|
|
74
|
-
let results = Array.from(this.memories.values());
|
|
75
|
-
|
|
76
|
-
// Filter out expired
|
|
77
|
-
const now = new Date();
|
|
78
|
-
results = results.filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
79
|
-
|
|
80
|
-
if (query.actorId) {
|
|
81
|
-
results = results.filter((m) => m.actorId === query.actorId);
|
|
82
|
-
}
|
|
83
|
-
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
84
|
-
results = results.filter((m) => query.memoryTypes!.includes(m.memoryType));
|
|
85
|
-
}
|
|
86
|
-
if (query.tags && query.tags.length > 0) {
|
|
87
|
-
results = results.filter((m) => m.tags?.some((t) => query.tags!.includes(t)));
|
|
88
|
-
}
|
|
89
|
-
if (query.query) {
|
|
90
|
-
const q = query.query.toLowerCase();
|
|
91
|
-
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
switch (query.strategy) {
|
|
95
|
-
case "recent":
|
|
96
|
-
results.sort((a, b) => b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
97
|
-
break;
|
|
98
|
-
case "important":
|
|
99
|
-
results.sort((a, b) => b.importance - a.importance);
|
|
100
|
-
break;
|
|
101
|
-
case "hybrid":
|
|
102
|
-
case "semantic":
|
|
103
|
-
default:
|
|
104
|
-
results.sort((a, b) => b.importance - a.importance || b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Touch records on retrieval (for recency tracking in context assembly)
|
|
109
|
-
for (const r of results.slice(0, query.limit ?? 10)) {
|
|
110
|
-
r._touchedAt = new Date();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const limit = query.limit ?? 10;
|
|
114
|
-
return results.slice(0, limit);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async count(filter?: MemoryCountFilter): Promise<number> {
|
|
118
|
-
const now = new Date();
|
|
119
|
-
let results = Array.from(this.memories.values())
|
|
120
|
-
.filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
121
|
-
|
|
122
|
-
if (filter?.actorId) {
|
|
123
|
-
results = results.filter((m) => m.actorId === filter.actorId);
|
|
124
|
-
}
|
|
125
|
-
if (filter?.memoryType) {
|
|
126
|
-
results = results.filter((m) => m.memoryType === filter.memoryType);
|
|
127
|
-
}
|
|
128
|
-
if (filter?.minImportance !== undefined) {
|
|
129
|
-
results = results.filter((m) => m.importance >= filter.minImportance!);
|
|
130
|
-
}
|
|
131
|
-
return results.length;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async touch(id: string): Promise<void> {
|
|
135
|
-
const memory = this.memories.get(id);
|
|
136
|
-
if (!memory) throw notFound("Memory", id);
|
|
137
|
-
memory._touchedAt = new Date();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async close(): Promise<void> {
|
|
141
|
-
this.memories.clear();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
getAllMemories(): Memory[] {
|
|
145
|
-
return Array.from(this.memories.values());
|
|
146
|
-
}
|
|
147
|
-
}
|