@amemhq/core 1.0.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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/index.cjs +2369 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +502 -0
- package/dist/index.d.ts +502 -0
- package/dist/index.js +2298 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
declare function configure(opts: {
|
|
2
|
+
dataDir?: string;
|
|
3
|
+
}): void;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* embedding.ts — Local ONNX embedding via @huggingface/transformers
|
|
7
|
+
* Matches Python: SentenceTransformer.encode(text, normalize_embeddings=True)
|
|
8
|
+
*
|
|
9
|
+
* The model is selectable because the default is not a good retrieval model: it
|
|
10
|
+
* caps at 128 tokens, so anything longer is truncated before it reaches the
|
|
11
|
+
* vector. Changing it is a breaking change whenever the dimension differs —
|
|
12
|
+
* Qdrant fixes a collection's vector size at creation — so the default stays put
|
|
13
|
+
* and the switch is opt-in. See docs/reference/embedding-models.md.
|
|
14
|
+
*/
|
|
15
|
+
/** The model shipped since the beginning. Not changed here on purpose. */
|
|
16
|
+
declare const DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
17
|
+
/** Which model this process embeds with. */
|
|
18
|
+
declare function getEmbeddingModel(): string;
|
|
19
|
+
/**
|
|
20
|
+
* The vector width this model produces, measured rather than looked up.
|
|
21
|
+
*
|
|
22
|
+
* A hardcoded table would be wrong the moment someone points AMEM_EMBED_MODEL at
|
|
23
|
+
* something not in it, and wrong silently — the collection would be created with
|
|
24
|
+
* the wrong size and every insert would fail. Encoding one short string costs one
|
|
25
|
+
* forward pass on a model that has to load anyway, and is right for any model.
|
|
26
|
+
*/
|
|
27
|
+
declare function getEmbeddingDim(): Promise<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Encode text to 384-dim normalized embedding vector.
|
|
30
|
+
* Singleton model, loaded once and reused.
|
|
31
|
+
*/
|
|
32
|
+
declare function encode(text: string): Promise<number[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Load the model now rather than on the first encode(). A long-lived service
|
|
35
|
+
* pays the download at startup, not on a user's first write.
|
|
36
|
+
*/
|
|
37
|
+
declare function loadModel(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the model is resident. Synchronous and I/O-free — unlike encode() it
|
|
40
|
+
* can never trigger the several-hundred-megabyte download, so a health check is
|
|
41
|
+
* free to poll it.
|
|
42
|
+
*/
|
|
43
|
+
declare function isModelLoaded(): boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* storage.ts — Qdrant vector storage for A-MEM
|
|
47
|
+
* Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26
|
|
48
|
+
* Collection: amem_notes, 384-dim cosine, with agent_id isolation
|
|
49
|
+
*/
|
|
50
|
+
/** Per-agent override config. If collection is set, mode B (isolated collection) is used. */
|
|
51
|
+
interface AgentAmemConfig {
|
|
52
|
+
agentId?: string;
|
|
53
|
+
collection?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Top-level plugin config shape (superset — existing fields preserved). */
|
|
56
|
+
interface AmemPluginConfig {
|
|
57
|
+
agentId?: string;
|
|
58
|
+
collection?: string;
|
|
59
|
+
topK?: number;
|
|
60
|
+
/** Per-agent overrides keyed by agentId */
|
|
61
|
+
agents?: Record<string, AgentAmemConfig>;
|
|
62
|
+
llmProvider?: string;
|
|
63
|
+
llmModel?: string;
|
|
64
|
+
llmBaseURL?: string;
|
|
65
|
+
llmStrongProvider?: string;
|
|
66
|
+
llmStrongModel?: string;
|
|
67
|
+
llmStrongBaseURL?: string;
|
|
68
|
+
/** Which tier the agent_end CRUD decision runs on: `fast` (default) or `strong`. */
|
|
69
|
+
llmCrudRole?: 'fast' | 'strong';
|
|
70
|
+
/** Story 43: run the nightly contradiction sweep. Default true. */
|
|
71
|
+
conflictSweep?: boolean;
|
|
72
|
+
/** Similarity floor for accepting an LLM-chosen UPDATE target. Raise it for
|
|
73
|
+
* cheaper models — a rejected update is stored as a new memory, never lost. */
|
|
74
|
+
crudUpdateMinSim?: number;
|
|
75
|
+
}
|
|
76
|
+
/** One entry in a note's evolution history (Story 13-B) */
|
|
77
|
+
interface EvolutionEntry {
|
|
78
|
+
triggeredBy: string;
|
|
79
|
+
triggeredAt: string;
|
|
80
|
+
oldContext: string;
|
|
81
|
+
newContext: string;
|
|
82
|
+
oldTags: string[];
|
|
83
|
+
newTags: string[];
|
|
84
|
+
action?: 'update_neighbor' | 'strengthen' | 'consolidate' | 'crud_update';
|
|
85
|
+
/** Story 41: the content this entry replaced, so an overwrite stays recoverable. */
|
|
86
|
+
oldContent?: string;
|
|
87
|
+
suggestedConnections?: string[];
|
|
88
|
+
tagsUpdated?: string[];
|
|
89
|
+
}
|
|
90
|
+
interface MemoryNote {
|
|
91
|
+
id: string;
|
|
92
|
+
content: string;
|
|
93
|
+
keywords: string[];
|
|
94
|
+
tags: string[];
|
|
95
|
+
context: string;
|
|
96
|
+
links: string[];
|
|
97
|
+
embedding: number[];
|
|
98
|
+
timestamp: string;
|
|
99
|
+
agent_id: string;
|
|
100
|
+
hash: string;
|
|
101
|
+
retrieval_count: number;
|
|
102
|
+
last_accessed: string;
|
|
103
|
+
evolution_history: EvolutionEntry[];
|
|
104
|
+
category: string;
|
|
105
|
+
is_active: boolean;
|
|
106
|
+
note_type: 'memory' | 'knowledge';
|
|
107
|
+
topics: string[];
|
|
108
|
+
pending_merge: boolean;
|
|
109
|
+
evolution_type?: 'EVOLVE' | 'CONFLICT' | 'EXPAND' | 'NEW';
|
|
110
|
+
conflict: boolean;
|
|
111
|
+
conflicts_with?: string[];
|
|
112
|
+
conflict_reason?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Story 43: when this note was last included in a contradiction scan.
|
|
115
|
+
* Absent = never scanned. Lets the sweep skip batches it has already judged,
|
|
116
|
+
* which is what makes a daily run cost one or two calls instead of re-reading
|
|
117
|
+
* the whole store every night.
|
|
118
|
+
*/
|
|
119
|
+
conflict_scanned_at?: string;
|
|
120
|
+
subjects: string[];
|
|
121
|
+
ephemeral: boolean;
|
|
122
|
+
low_quality: boolean;
|
|
123
|
+
owner: string;
|
|
124
|
+
readers: string[];
|
|
125
|
+
writers: string[];
|
|
126
|
+
}
|
|
127
|
+
interface QueryResult {
|
|
128
|
+
note: MemoryNote;
|
|
129
|
+
score: number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Raised when the configured model's vector width does not match the collection
|
|
133
|
+
* that already exists. Its own class so the plugin can log it loudly instead of
|
|
134
|
+
* as one more startup warning — this one needs the operator to act.
|
|
135
|
+
*/
|
|
136
|
+
declare class EmbeddingDimensionMismatchError extends Error {
|
|
137
|
+
readonly collection: string;
|
|
138
|
+
readonly collectionDim: number;
|
|
139
|
+
readonly modelDim: number;
|
|
140
|
+
readonly model: string;
|
|
141
|
+
constructor(collection: string, collectionDim: number, modelDim: number, model: string);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Ask Qdrant whether it can serve, right now.
|
|
145
|
+
*
|
|
146
|
+
* `ensureCollection()` cannot answer this: it latches `_collectionReady` and
|
|
147
|
+
* short-circuits on every later call, so once it has succeeded it keeps
|
|
148
|
+
* reporting success long after Qdrant has gone away. `/readyz` answers in plain
|
|
149
|
+
* text, so it deliberately bypasses the JSON-parsing `qdrant()` helper above.
|
|
150
|
+
*/
|
|
151
|
+
declare function pingQdrant(): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Ensure the given Qdrant collection exists with the correct schema.
|
|
154
|
+
* If collectionName is omitted, uses process.env.AMEM_COLLECTION (default: amem_notes).
|
|
155
|
+
* Mode B agents pass their dedicated collection name here.
|
|
156
|
+
*/
|
|
157
|
+
declare function ensureCollection(collectionName?: string): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Core CRUD implementation scoped to a specific collection and agent filter mode.
|
|
160
|
+
* collectionName: which Qdrant collection to operate on.
|
|
161
|
+
* modeBIsolated: if true, skip the "also include shared" filter in agentFilter
|
|
162
|
+
* (mode B collections are already per-agent, so no cross-agent filter needed).
|
|
163
|
+
*/
|
|
164
|
+
declare function makeCrud(collectionName: string, modeBIsolated?: boolean): {
|
|
165
|
+
addNote(note: MemoryNote): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Story 36: this is the one read that bypasses the agent filter — it fetches
|
|
168
|
+
* straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
|
|
169
|
+
* note comes back as `null` (indistinguishable from missing, so nothing leaks,
|
|
170
|
+
* and callers already handle null). Omitting it skips the check, preserving
|
|
171
|
+
* behaviour for internal callers that only ever hold their own ids.
|
|
172
|
+
*/
|
|
173
|
+
getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
|
|
174
|
+
updateNote(note: MemoryNote): Promise<void>;
|
|
175
|
+
findByHash(hash: string, agentId: string): Promise<MemoryNote | null>;
|
|
176
|
+
/**
|
|
177
|
+
* Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
|
|
178
|
+
* hold the note already should prefer checking `canWrite` themselves; this
|
|
179
|
+
* fetch-then-check path exists for callers that only have an id (the plugin's
|
|
180
|
+
* CRUD hook). Returns false — without writing — when the caller may not write.
|
|
181
|
+
* Omitting `callerAgentId` skips the check, preserving existing behaviour for
|
|
182
|
+
* internal callers that are already scoped to their own notes.
|
|
183
|
+
*/
|
|
184
|
+
updateNoteContent(id: string, content: string, embedding: number[], hash: string, callerAgentId?: string): Promise<boolean>;
|
|
185
|
+
queryByEmbedding(embedding: number[], topK: number, agentId: string, scoreThreshold?: number, subject?: string): Promise<QueryResult[]>;
|
|
186
|
+
listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
|
|
187
|
+
deleteNote(id: string): Promise<void>;
|
|
188
|
+
/** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
|
|
189
|
+
invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
|
|
190
|
+
getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]>;
|
|
191
|
+
countNotes(agentId?: string): Promise<number>;
|
|
192
|
+
updateNoteLinks(id: string, links: string[]): Promise<void>;
|
|
193
|
+
patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
|
|
194
|
+
replaceLinkReferences(oldId: string, newId: string, agentId: string): Promise<void>;
|
|
195
|
+
};
|
|
196
|
+
type StorageContext = ReturnType<typeof makeCrud>;
|
|
197
|
+
/**
|
|
198
|
+
* Create a StorageContext scoped to a specific collection (mode B) or the default collection (mode A).
|
|
199
|
+
* Mode A (same collection): pass collectionName = undefined → uses AMEM_COLLECTION env var.
|
|
200
|
+
* Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.
|
|
201
|
+
*/
|
|
202
|
+
declare function createStorageContext(collectionName?: string, modeBIsolated?: boolean): StorageContext;
|
|
203
|
+
declare function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
|
|
204
|
+
declare function updateNote(note: MemoryNote): Promise<void>;
|
|
205
|
+
declare function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
|
|
206
|
+
declare function deleteNote(id: string): Promise<void>;
|
|
207
|
+
declare function invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
|
|
208
|
+
declare function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* memory.ts — A-MEM core logic: addMemory, searchMemory, listMemories
|
|
212
|
+
* Full TypeScript port of amem_client.py
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
interface QualityCheckResult {
|
|
216
|
+
ok: boolean;
|
|
217
|
+
ephemeral: boolean;
|
|
218
|
+
reason?: string;
|
|
219
|
+
}
|
|
220
|
+
declare function checkQuality(content: string): QualityCheckResult;
|
|
221
|
+
declare function addMemory(content: string, agentId?: string, opts?: {
|
|
222
|
+
scope?: 'private' | 'shared';
|
|
223
|
+
storageCtx?: StorageContext;
|
|
224
|
+
/**
|
|
225
|
+
* Story 44: who this memory is about. Empty (the default) means it is about
|
|
226
|
+
* the world or the agent itself, and stays visible whoever is present.
|
|
227
|
+
*/
|
|
228
|
+
subjects?: string[];
|
|
229
|
+
}): Promise<string>;
|
|
230
|
+
/**
|
|
231
|
+
* The cheap write path: quality gate → embed the raw content → store.
|
|
232
|
+
*
|
|
233
|
+
* Deliberately skips LLM note construction, similarity dedup, link generation
|
|
234
|
+
* and evolution, so a real-time caller (a game brain logging events tick by
|
|
235
|
+
* tick) never pays for an LLM round-trip. Cost is one embed + one upsert.
|
|
236
|
+
*
|
|
237
|
+
* Episodic notes are an **append-only, faithful event log**: the same content
|
|
238
|
+
* written twice is two events, so there is no hash or vector dedup here.
|
|
239
|
+
* Evolution rewrites a note's context over time — precisely what you do not
|
|
240
|
+
* want for "remember the time the ender dragon killed us". The offline
|
|
241
|
+
* consolidation pass distils these raw events into long-term, linked notes.
|
|
242
|
+
*/
|
|
243
|
+
declare function addEpisodic(content: string, agentId?: string, opts?: {
|
|
244
|
+
scope?: 'private' | 'shared';
|
|
245
|
+
storageCtx?: StorageContext;
|
|
246
|
+
/** Story 44: who this episode is about. Empty = world/self. */
|
|
247
|
+
subjects?: string[];
|
|
248
|
+
}): Promise<string>;
|
|
249
|
+
interface SearchResult {
|
|
250
|
+
id: string;
|
|
251
|
+
content: string;
|
|
252
|
+
context: string;
|
|
253
|
+
tags: string[];
|
|
254
|
+
keywords: string[];
|
|
255
|
+
links: string[];
|
|
256
|
+
timestamp: string;
|
|
257
|
+
similarity: number;
|
|
258
|
+
rrf: number;
|
|
259
|
+
topics: string[];
|
|
260
|
+
note_type: 'memory' | 'knowledge';
|
|
261
|
+
}
|
|
262
|
+
declare function searchMemory(query: string, topK?: number, agentId?: string, opts?: {
|
|
263
|
+
useBfs?: boolean;
|
|
264
|
+
bfsSimThreshold?: number;
|
|
265
|
+
topicsFilter?: string[];
|
|
266
|
+
storageCtx?: StorageContext;
|
|
267
|
+
/**
|
|
268
|
+
* Story 44: scope retrieval to one person. Returns memories that name them
|
|
269
|
+
* plus memories that name nobody (world facts, facts about the agent).
|
|
270
|
+
* Omitted = no person scoping, i.e. today's behaviour.
|
|
271
|
+
*/
|
|
272
|
+
subject?: string;
|
|
273
|
+
}): Promise<SearchResult[]>;
|
|
274
|
+
declare function listMemories(agentId?: string, storageCtx?: StorageContext): Promise<{
|
|
275
|
+
count: number;
|
|
276
|
+
}>;
|
|
277
|
+
/**
|
|
278
|
+
* Merge semantically similar notes written today.
|
|
279
|
+
* Called asynchronously from agent_end hook; failures are silent.
|
|
280
|
+
* Returns the number of notes merged (deleted).
|
|
281
|
+
*
|
|
282
|
+
* Story 30: pending_merge=true notes are routed through LLM evolution judgment
|
|
283
|
+
* (EVOLVE/CONFLICT/EXPAND/NEW) instead of simple merge.
|
|
284
|
+
* Story 32: shared notes (agent_id='shared') are never merged/consolidated.
|
|
285
|
+
*/
|
|
286
|
+
declare function mergeSimilarNotes(agentId: string, storageCtx?: StorageContext): Promise<number>;
|
|
287
|
+
/**
|
|
288
|
+
* Consolidate semantically similar memories.
|
|
289
|
+
* Performs deep deduplication by category and similarity score.
|
|
290
|
+
* Story 32: shared notes (agent_id='shared') are skipped entirely.
|
|
291
|
+
*/
|
|
292
|
+
declare function consolidateMemories(agentId: string, logger?: any, storageCtx?: StorageContext): Promise<number>;
|
|
293
|
+
/**
|
|
294
|
+
* How a detected contradiction is handled.
|
|
295
|
+
*
|
|
296
|
+
* `review` (default) marks both notes and leaves the decision to a human — the
|
|
297
|
+
* safe option, because even a strong model is only around 55% accurate at
|
|
298
|
+
* spotting implicit contradictions.
|
|
299
|
+
*
|
|
300
|
+
* `auto` additionally retires the older note of each pair. It needs no human,
|
|
301
|
+
* but at that accuracy roughly two in five retirements will silence a memory
|
|
302
|
+
* that was still true. The retirement is a soft delete, so it is recoverable —
|
|
303
|
+
* but for a system answering in real time, "recoverable" only helps once someone
|
|
304
|
+
* notices. Documented as such; opt in deliberately.
|
|
305
|
+
*/
|
|
306
|
+
type ConflictMode = 'review' | 'auto';
|
|
307
|
+
interface ConflictSweepResult {
|
|
308
|
+
scanned: number;
|
|
309
|
+
pairsFound: number;
|
|
310
|
+
retired: number;
|
|
311
|
+
batchesScanned: number;
|
|
312
|
+
batchesSkipped: number;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Find memories that contradict each other and mark them.
|
|
316
|
+
*
|
|
317
|
+
* This is the cold half of the tiering split. The per-turn CRUD decision runs on
|
|
318
|
+
* a cheap model, which is safe (the update guard stops it writing to the wrong
|
|
319
|
+
* note) but dull — it misses contradictions it should have caught. This sweep is
|
|
320
|
+
* what catches them: it runs offline, in batches, on the strong tier, and it
|
|
321
|
+
* sees far more context than any single turn does.
|
|
322
|
+
*
|
|
323
|
+
* Batches by category and hands each batch to the model whole, rather than
|
|
324
|
+
* pairing by similarity — see llmConflictScan for why that distinction is the
|
|
325
|
+
* entire point.
|
|
326
|
+
*/
|
|
327
|
+
declare function conflictSweep(agentId: string, opts?: {
|
|
328
|
+
mode?: ConflictMode;
|
|
329
|
+
storageCtx?: StorageContext;
|
|
330
|
+
logger?: {
|
|
331
|
+
info: (m: string) => void;
|
|
332
|
+
};
|
|
333
|
+
/** Re-read every batch, including ones already judged. For a full re-sweep
|
|
334
|
+
* after changing the prompt or the model. */
|
|
335
|
+
force?: boolean;
|
|
336
|
+
}): Promise<ConflictSweepResult>;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* quality.ts — Memory quality scanning and review batch generation (Story 31)
|
|
340
|
+
*/
|
|
341
|
+
|
|
342
|
+
interface LowQualityItem {
|
|
343
|
+
note: MemoryNote;
|
|
344
|
+
reasons: LowQualityReason[];
|
|
345
|
+
}
|
|
346
|
+
type LowQualityReason = 'too_short' | 'expired_ephemeral' | 'pending_conflict';
|
|
347
|
+
declare function scanLowQuality(agentId: string): Promise<LowQualityItem[]>;
|
|
348
|
+
declare function generateReviewBatch(agentId: string, outputPath?: string): Promise<string>;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* auth.ts — write authorization for the Access Protocol (Story 33).
|
|
352
|
+
*
|
|
353
|
+
* Story 32 gave every note `owner` / `readers` / `writers` and enforced `readers`
|
|
354
|
+
* at query time. It deliberately left `writers` unenforced. The consequence: the
|
|
355
|
+
* agent filter matches `agent_id == caller OR agent_id == 'shared'`, so ANY query
|
|
356
|
+
* can return another agent's shared note — and every mutation then wrote to it
|
|
357
|
+
* unchecked. An audit found eight such write sites (dedup, link generation,
|
|
358
|
+
* evolution ×2, CRUD update/delete, quality scan, link rewriting).
|
|
359
|
+
*
|
|
360
|
+
* This is the one rule they all gate on. Kept pure and dependency-free so the
|
|
361
|
+
* policy is unit-testable on its own and identical everywhere it is applied.
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* May `callerAgentId` mutate `note`?
|
|
366
|
+
*
|
|
367
|
+
* True when the caller owns it, is listed in `writers`, or `writers` is open
|
|
368
|
+
* (`'*'`). Everything else — notably another agent's shared note, which is
|
|
369
|
+
* readable but not writable — is denied.
|
|
370
|
+
*/
|
|
371
|
+
declare function canWrite(note: Pick<MemoryNote, 'owner' | 'writers'>, callerAgentId: string): boolean;
|
|
372
|
+
/**
|
|
373
|
+
* May `callerAgentId` read `note`? (Story 36 — the read half of the protocol.)
|
|
374
|
+
*
|
|
375
|
+
* True when the caller owns it, is listed in `readers`, or the note is public
|
|
376
|
+
* (`readers` contains `'*'`, which is how a shared-scope write is stored).
|
|
377
|
+
*
|
|
378
|
+
* Queries already filter by `agent_id`, so list/search paths never surface an
|
|
379
|
+
* unreadable note. This guards the one primitive that bypasses that filter —
|
|
380
|
+
* `getNote(id)` fetches straight by UUID — and the link-neighbourhood walks that
|
|
381
|
+
* use it: a shared note's `links[]` can name its owner's PRIVATE notes, so
|
|
382
|
+
* following those links would otherwise read memory the caller may not see.
|
|
383
|
+
*/
|
|
384
|
+
declare function canRead(note: Pick<MemoryNote, 'owner' | 'readers'>, callerAgentId: string): boolean;
|
|
385
|
+
|
|
386
|
+
interface MigrateResult {
|
|
387
|
+
/** Points found in the source. */
|
|
388
|
+
total: number;
|
|
389
|
+
/** Notes whose derived fields were empty — the pre-pipeline cohort. */
|
|
390
|
+
missingDerived: number;
|
|
391
|
+
/** Notes whose fields were re-extracted. 0 unless refreshFields. */
|
|
392
|
+
refreshed: number;
|
|
393
|
+
/** Notes written into the target. 0 on a dry run. */
|
|
394
|
+
migrated: number;
|
|
395
|
+
sourceDim: number | null;
|
|
396
|
+
targetDim: number;
|
|
397
|
+
model: string;
|
|
398
|
+
dryRun: boolean;
|
|
399
|
+
}
|
|
400
|
+
declare function migrateCollection(opts: {
|
|
401
|
+
/** Source collection. Read-only; never modified. */
|
|
402
|
+
from: string;
|
|
403
|
+
/** Target collection. Created if absent; must not already hold points. */
|
|
404
|
+
to: string;
|
|
405
|
+
/** Re-extract keywords/tags/context for notes that never had them. Default true. */
|
|
406
|
+
refreshFields?: boolean;
|
|
407
|
+
/** Report what would happen and write nothing. Default TRUE — opt in to writing. */
|
|
408
|
+
dryRun?: boolean;
|
|
409
|
+
logger?: {
|
|
410
|
+
info: (m: string) => void;
|
|
411
|
+
warn: (m: string) => void;
|
|
412
|
+
};
|
|
413
|
+
}): Promise<MigrateResult>;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Similarity floor for accepting an UPDATE target.
|
|
417
|
+
*
|
|
418
|
+
* Heuristic, not empirically tuned: it sits just above the 0.3 bar the engine
|
|
419
|
+
* already uses for "these two notes are related at all", because a legitimate
|
|
420
|
+
* CRUD UPDATE is often a correction or contradiction ("drinks tea" → "switched to
|
|
421
|
+
* coffee") that is related but not near-identical. Set it too high and real
|
|
422
|
+
* corrections get downgraded; too low and the guard does nothing.
|
|
423
|
+
*
|
|
424
|
+
* Failing this check is SAFE by construction — the caller inserts the fact as a
|
|
425
|
+
* new memory instead of overwriting, and scheduled consolidation can merge later.
|
|
426
|
+
* So the cost of a false positive is a duplicate, and the cost of a false
|
|
427
|
+
* negative is a destroyed memory. Bias accordingly: raise it for cheaper models.
|
|
428
|
+
*/
|
|
429
|
+
declare const DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35;
|
|
430
|
+
/** Resolve the threshold: env var wins, then an explicit override, then default. */
|
|
431
|
+
declare function resolveCrudUpdateMinSim(override?: number): number;
|
|
432
|
+
/**
|
|
433
|
+
* May `newEmbedding`'s fact overwrite the memory `targetEmbedding` belongs to?
|
|
434
|
+
*
|
|
435
|
+
* True when the replacement is plausibly about the same thing. False means the
|
|
436
|
+
* LLM most likely named the wrong index — the caller should insert instead of
|
|
437
|
+
* overwrite, never throw.
|
|
438
|
+
*
|
|
439
|
+
* Both vectors are L2-normalized by `encode`, so this is a dot product.
|
|
440
|
+
*/
|
|
441
|
+
declare function isPlausibleUpdateTarget(newEmbedding: number[], targetEmbedding: number[], minSimilarity?: number): boolean;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* llm.ts — LLM helpers for A-MEM note construction, linking, evolution
|
|
445
|
+
*/
|
|
446
|
+
/**
|
|
447
|
+
* Which tier a call runs on (Story 42).
|
|
448
|
+
*
|
|
449
|
+
* The engine's calls split cleanly by how much model capability they actually
|
|
450
|
+
* need. Published results are consistent that memory quality is mostly
|
|
451
|
+
* architecture-bound — extraction differs ~2 points between a cheap and a strong
|
|
452
|
+
* model — with ONE exception: judging whether a new fact CONTRADICTS a stored
|
|
453
|
+
* one, where the gap is large. So the frequent, easy calls run `fast`, and the
|
|
454
|
+
* rare, genuinely hard judgements can run `strong` if the operator configures
|
|
455
|
+
* one. See docs/guide/design-rationale.md for the evidence.
|
|
456
|
+
*/
|
|
457
|
+
type LlmRole = 'fast' | 'strong';
|
|
458
|
+
/** Provider/model/endpoint for one role. */
|
|
459
|
+
interface LlmRoleConfig {
|
|
460
|
+
provider?: string;
|
|
461
|
+
model?: string;
|
|
462
|
+
baseURL?: string;
|
|
463
|
+
}
|
|
464
|
+
/** Runtime LLM settings a host may inject. See the precedence note above. */
|
|
465
|
+
interface LlmConfig extends LlmRoleConfig {
|
|
466
|
+
/** Per-request timeout in ms for the SDK client. Guards against a slow or
|
|
467
|
+
* stuck endpoint hanging the whole addMemory pipeline. Default 30000.
|
|
468
|
+
* Shared by both roles — it is a transport concern, not a tier one. */
|
|
469
|
+
timeoutMs?: number;
|
|
470
|
+
/**
|
|
471
|
+
* Optional `strong` tier. Each field falls back to the `fast` value
|
|
472
|
+
* INDIVIDUALLY, so all three useful shapes work:
|
|
473
|
+
* - only `model` → same endpoint, better model (gpt-4o-mini → gpt-4o)
|
|
474
|
+
* - all three → a wholly separate backend (local Ollama + cloud Claude)
|
|
475
|
+
* - nothing → strong IS fast, i.e. today's single-model behaviour
|
|
476
|
+
* There is deliberately no built-in strong default: inventing one would start
|
|
477
|
+
* spending an existing user's money on a pricier model without them asking.
|
|
478
|
+
*/
|
|
479
|
+
strong?: LlmRoleConfig;
|
|
480
|
+
/** Which role the agent_end CRUD decision uses. Default `fast`. */
|
|
481
|
+
crudRole?: LlmRole;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Point the engine's LLM calls at a provider/model/endpoint chosen by the host.
|
|
485
|
+
*
|
|
486
|
+
* Environment variables still win over anything passed here, and any field left
|
|
487
|
+
* undefined falls through to the default — so `configureLlm({ model })` changes
|
|
488
|
+
* only the model. Safe to call before or after the first LLM call.
|
|
489
|
+
*/
|
|
490
|
+
declare function configureLlm(cfg: LlmConfig): void;
|
|
491
|
+
interface MemoryOperation {
|
|
492
|
+
action: 'NEW' | 'UPDATE' | 'DELETE' | 'NONE';
|
|
493
|
+
fact: string;
|
|
494
|
+
existingIdx?: number;
|
|
495
|
+
reason?: string;
|
|
496
|
+
}
|
|
497
|
+
declare function llmCrudDecision(userText: string, assistantText: string, existingMemories: Array<{
|
|
498
|
+
idx: number;
|
|
499
|
+
content: string;
|
|
500
|
+
}>): Promise<MemoryOperation[]>;
|
|
501
|
+
|
|
502
|
+
export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, type EvolutionEntry, type LlmConfig, type LowQualityItem, type LowQualityReason, type MemoryNote, type MemoryOperation, type MigrateResult, type QueryResult, type SearchResult, type StorageContext, addEpisodic, addMemory, canRead, canWrite, checkQuality, configure, configureLlm, conflictSweep, consolidateMemories, createStorageContext, deleteNote, encode, ensureCollection, generateReviewBatch, getEmbeddingDim, getEmbeddingModel, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, updateNote };
|