agenticow 0.1.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/src/index.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ // Type declarations for agenticow — Git for Agent Memory.
2
+
3
+ export interface OpenOptions {
4
+ /** Vector dimension. Required when creating a new memory file. */
5
+ dimension?: number;
6
+ /** Distance metric. Default: "cosine". */
7
+ metric?: string;
8
+ /** HNSW M parameter (optional, create only). */
9
+ m?: number;
10
+ /** HNSW efConstruction (optional, create only). */
11
+ efConstruction?: number;
12
+ /** Keep an in-memory edit log enabling diff()/promote(). Default: true. */
13
+ track?: boolean;
14
+ }
15
+
16
+ export interface MemoryDiff {
17
+ added: number[];
18
+ overridden: number[];
19
+ deleted: number[];
20
+ }
21
+
22
+ export interface IngestResult {
23
+ accepted: number;
24
+ rejected: number;
25
+ epoch: number;
26
+ }
27
+
28
+ export interface QueryOptions {
29
+ /** HNSW efSearch passed to each store in the lineage chain. */
30
+ efSearch?: number;
31
+ /** Candidates to over-fetch per store before exact merge. Default: k*4. */
32
+ overscan?: number;
33
+ }
34
+
35
+ export interface QueryHit {
36
+ id: number;
37
+ distance: number;
38
+ /** label/id of the lineage node the hit came from (which "wins"). */
39
+ branch: string;
40
+ }
41
+
42
+ export interface CheckpointDescriptor {
43
+ id: string;
44
+ label: string;
45
+ path: string;
46
+ depth: number;
47
+ }
48
+
49
+ export interface LineageNode {
50
+ role: 'working' | 'checkpoint' | 'base';
51
+ id: string;
52
+ label: string | null;
53
+ path: string;
54
+ tombstones: number;
55
+ }
56
+
57
+ export interface MemoryStatus {
58
+ totalVectors: number;
59
+ totalSegments: number;
60
+ fileSize: number;
61
+ currentEpoch: number;
62
+ profileId: number;
63
+ compactionState: string;
64
+ deadSpaceRatio: number;
65
+ readOnly: boolean;
66
+ chainDepth: number;
67
+ dimension: number;
68
+ metric: string;
69
+ }
70
+
71
+ export type IngestRecord = { id: number; vector: number[] | Float32Array };
72
+
73
+ export class AgenticMemory {
74
+ static open(filePath: string, opts?: OpenOptions): AgenticMemory;
75
+ readonly dimension: number;
76
+ ingest(records: IngestRecord[]): IngestResult;
77
+ ingest(vectors: Float32Array, ids: number[]): IngestResult;
78
+ delete(ids: number[]): { deleted: number; tombstoned: number };
79
+ query(vector: number[] | Float32Array, k?: number, opts?: QueryOptions): QueryHit[];
80
+ branch(label?: string, filePath?: string): AgenticMemory;
81
+ fork(label?: string, filePath?: string): AgenticMemory;
82
+ diff(): MemoryDiff;
83
+ promote(target: AgenticMemory): { ingested: number; deleted: number };
84
+ checkpoint(label?: string): CheckpointDescriptor;
85
+ rollback(checkpointId?: string): { restoredTo: string; depth: number };
86
+ lineage(): LineageNode[];
87
+ status(): MemoryStatus;
88
+ save(manifestPath: string): string;
89
+ static load(manifestPath: string): AgenticMemory;
90
+ close(): void;
91
+ }
92
+
93
+ export function open(filePath: string, opts?: OpenOptions): AgenticMemory;
94
+
95
+ declare const _default: {
96
+ open: typeof open;
97
+ AgenticMemory: typeof AgenticMemory;
98
+ };
99
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,465 @@
1
+ // agenticow — Git for Agent Memory
2
+ // Copy-On-Write vector branching for embedded multi-agent memory.
3
+ //
4
+ // Built on ruvector's RVF format via @ruvector/rvf-node. The headline capability
5
+ // is base-size-independent COW branch creation: deriving a branch off a base
6
+ // memory costs ~0.5 ms and ~162 bytes regardless of how big the base is —
7
+ // proven 83x faster and ~3000x smaller than full-copy snapshots at 1M vectors.
8
+ //
9
+ // Honest scope:
10
+ // - Branch/checkpoint CREATE is O(edits), O(1) in base size. PROVEN.
11
+ // - query() is an EXACT read-through: parent ∪ child-edits, child wins on an
12
+ // id collision, deletes are honored. It works by merging each store in the
13
+ // lineage chain (child -> ... -> base). Each store answers with its own
14
+ // native index; agenticow merges + re-ranks exactly across the boundary.
15
+ // - A single ANN/HNSW index that SPANS the COW boundary is NOT shipped — that
16
+ // is roadmap. Native cluster-level read-through (branch()) landed in
17
+ // ruvnet/RuVector PR #617; until it is published, agenticow implements the
18
+ // read-through in this wrapper over the shipped derive() primitive.
19
+
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+ import crypto from 'node:crypto';
24
+ import pkg from '@ruvector/rvf-node';
25
+
26
+ const { RvfDatabase } = pkg;
27
+
28
+ const DEFAULT_METRIC = 'cosine';
29
+
30
+ /** @param {number[]|Float32Array} v */
31
+ function toF32(v) {
32
+ return v instanceof Float32Array ? v : Float32Array.from(v);
33
+ }
34
+
35
+ // L2-normalize a copy of v. Used when metric is cosine so that ranking is
36
+ // identical whether the engine scores with cosine or L2 — important because the
37
+ // shipped rvf-node binding reopens files with the l2 metric (the cosine setting
38
+ // is not persisted). On unit vectors, L2 distance is monotonic with cosine
39
+ // distance, so top-K is preserved either way.
40
+ function l2normalize(src) {
41
+ const v = src instanceof Float32Array ? Float32Array.from(src) : Float32Array.from(src);
42
+ let n = 0;
43
+ for (let i = 0; i < v.length; i++) n += v[i] * v[i];
44
+ n = Math.sqrt(n);
45
+ if (n > 0) for (let i = 0; i < v.length; i++) v[i] /= n;
46
+ return v;
47
+ }
48
+
49
+ function tmpChildPath(base, label) {
50
+ const slug = (label || 'branch').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24) || 'branch';
51
+ const rand = crypto.randomBytes(4).toString('hex');
52
+ const dir = path.dirname(base);
53
+ const stem = path.basename(base).replace(/\.rvf$/i, '');
54
+ return path.join(dir, `${stem}.${slug}-${rand}.rvf`);
55
+ }
56
+
57
+ /**
58
+ * One node in the COW lineage chain. `db` is an open RvfDatabase handle.
59
+ * `tombstones` are ids deleted at this node (hide the same id in ancestors).
60
+ */
61
+ class Node {
62
+ constructor(db, nodePath, label) {
63
+ this.db = db;
64
+ this.path = nodePath;
65
+ this.label = label || null;
66
+ this.id = db.fileId();
67
+ this.tombstones = new Set();
68
+ // Edit log for diff()/promote(). Cheap on small branches; disabled on huge
69
+ // bases via { track:false } in open().
70
+ this.editIds = new Set();
71
+ this.editVecs = new Map();
72
+ }
73
+ }
74
+
75
+ export class AgenticMemory {
76
+ /** @private */
77
+ constructor(workingNode, ancestors, dim, metric, track = true) {
78
+ /** @type {Node} */
79
+ this._working = workingNode;
80
+ /** @type {Node[]} ancestors newest -> oldest (base last) */
81
+ this._ancestors = ancestors;
82
+ this._dim = dim;
83
+ this._metric = metric;
84
+ this._track = track;
85
+ this._normalize = String(metric).toLowerCase() === 'cosine';
86
+ this._closed = false;
87
+ }
88
+
89
+ /**
90
+ * Open an existing memory file, or create one if it does not exist.
91
+ * @param {string} filePath
92
+ * @param {{dimension?:number, metric?:string, m?:number, efConstruction?:number, track?:boolean}} [opts]
93
+ * track (default true): keep an in-memory edit log enabling diff()/promote().
94
+ * Set false on very large bases to avoid caching their vectors.
95
+ * @returns {AgenticMemory}
96
+ */
97
+ static open(filePath, opts = {}) {
98
+ let db, dim, metric;
99
+ if (fs.existsSync(filePath)) {
100
+ db = RvfDatabase.open(filePath);
101
+ dim = db.dimension();
102
+ metric = db.metric ? db.metric() : (opts.metric || DEFAULT_METRIC);
103
+ } else {
104
+ if (!opts.dimension) {
105
+ throw new Error('agenticow: dimension is required when creating a new memory file');
106
+ }
107
+ dim = opts.dimension;
108
+ metric = opts.metric || DEFAULT_METRIC;
109
+ db = RvfDatabase.create(filePath, {
110
+ dimension: dim,
111
+ metric,
112
+ ...(opts.m ? { m: opts.m } : {}),
113
+ ...(opts.efConstruction ? { efConstruction: opts.efConstruction } : {}),
114
+ });
115
+ }
116
+ return new AgenticMemory(new Node(db, filePath, 'base'), [], dim, metric, opts.track !== false);
117
+ }
118
+
119
+ /** Full lineage chain, working node first. @returns {Node[]} */
120
+ _chain() {
121
+ return [this._working, ...this._ancestors];
122
+ }
123
+
124
+ _deriveOpts() {
125
+ return { dimension: this._dim, metric: this._metric };
126
+ }
127
+
128
+ _assertOpen() {
129
+ if (this._closed) throw new Error('agenticow: memory is closed');
130
+ }
131
+
132
+ /**
133
+ * Ingest vectors into the current working node.
134
+ * Forms:
135
+ * ingest([{ id, vector }, ...])
136
+ * ingest(Float32Array, number[]) // flat batch, fastest
137
+ * @returns {{accepted:number, rejected:number, epoch:number}}
138
+ */
139
+ ingest(records, ids) {
140
+ this._assertOpen();
141
+ let flat, idArr;
142
+ if (records instanceof Float32Array) {
143
+ flat = this._normalize ? Float32Array.from(records) : records;
144
+ idArr = ids;
145
+ } else if (Array.isArray(records)) {
146
+ idArr = records.map((r) => r.id);
147
+ flat = new Float32Array(records.length * this._dim);
148
+ for (let i = 0; i < records.length; i++) flat.set(toF32(records[i].vector), i * this._dim);
149
+ } else {
150
+ throw new Error('agenticow: ingest expects an array of {id,vector} or (Float32Array, ids)');
151
+ }
152
+ if (this._normalize) {
153
+ // normalize each row in place (flat is already a private copy here)
154
+ for (let i = 0; i < idArr.length; i++) {
155
+ const o = i * this._dim;
156
+ let n = 0;
157
+ for (let j = 0; j < this._dim; j++) n += flat[o + j] * flat[o + j];
158
+ n = Math.sqrt(n);
159
+ if (n > 0) for (let j = 0; j < this._dim; j++) flat[o + j] /= n;
160
+ }
161
+ }
162
+ // A re-ingested id is no longer a delete in this node.
163
+ for (const id of idArr) this._working.tombstones.delete(id);
164
+ const res = this._working.db.ingestBatch(flat, idArr);
165
+ if (this._track) {
166
+ for (let i = 0; i < idArr.length; i++) {
167
+ const id = idArr[i];
168
+ this._working.editIds.add(id);
169
+ this._working.editVecs.set(id, flat.slice(i * this._dim, (i + 1) * this._dim));
170
+ }
171
+ }
172
+ return res;
173
+ }
174
+
175
+ /**
176
+ * Delete ids from the current branch's view (COW tombstone). The id stays in
177
+ * the ancestor on disk, but is hidden from this branch's reads.
178
+ * @param {number[]} ids
179
+ */
180
+ delete(ids) {
181
+ this._assertOpen();
182
+ let deleted = 0;
183
+ try {
184
+ const res = this._working.db.delete(ids);
185
+ deleted = res?.deleted ?? 0;
186
+ } catch {
187
+ /* id may live only in an ancestor; fall through to tombstone */
188
+ }
189
+ for (const id of ids) {
190
+ this._working.tombstones.add(id);
191
+ this._working.editIds.delete(id);
192
+ this._working.editVecs.delete(id);
193
+ }
194
+ return { deleted, tombstoned: ids.length };
195
+ }
196
+
197
+ /**
198
+ * EXACT read-through k-NN: parent ∪ child-edits, child wins on id collision,
199
+ * deletes honored. Merges every node in the lineage chain and re-ranks.
200
+ * @param {number[]|Float32Array} vector
201
+ * @param {number} [k=10]
202
+ * @param {{efSearch?:number, overscan?:number}} [opts]
203
+ * @returns {{id:number, distance:number, branch:string}[]}
204
+ */
205
+ query(vector, k = 10, opts = {}) {
206
+ this._assertOpen();
207
+ const qv = this._normalize ? l2normalize(vector) : toF32(vector);
208
+ const fetch = Math.max(k, opts.overscan || k * 4);
209
+ const resolved = new Map(); // id -> {id, distance, branch}
210
+ const hidden = new Set(); // ids tombstoned by a nearer descendant
211
+ const qopts = opts.efSearch ? { efSearch: opts.efSearch } : undefined;
212
+ for (const node of this._chain()) {
213
+ for (const t of node.tombstones) hidden.add(t);
214
+ let hits = [];
215
+ try {
216
+ hits = node.db.query(qv, fetch, qopts);
217
+ } catch {
218
+ hits = [];
219
+ }
220
+ for (const h of hits) {
221
+ if (resolved.has(h.id) || hidden.has(h.id)) continue; // nearer node wins
222
+ resolved.set(h.id, { id: h.id, distance: h.distance, branch: node.label || node.id });
223
+ }
224
+ }
225
+ return [...resolved.values()].sort((a, b) => a.distance - b.distance).slice(0, k);
226
+ }
227
+
228
+ /**
229
+ * Create an isolated COW branch (a parallel fork of this memory). O(1) in base
230
+ * size — ~0.5 ms / 162 bytes. The branch sees everything this memory currently
231
+ * has via read-through, plus its own future edits. Mutations are isolated:
232
+ * neither side sees the other's later writes.
233
+ * @param {string} [label]
234
+ * @param {string} [filePath]
235
+ * @returns {AgenticMemory}
236
+ */
237
+ branch(label, filePath) {
238
+ this._assertOpen();
239
+ // Freeze the current working node so BOTH sides derive off the same
240
+ // immutable snapshot — this is what guarantees mutation isolation (neither
241
+ // the parent nor the branch sees the other's later writes).
242
+ const frozen = this._working;
243
+ const parentChildPath = tmpChildPath(frozen.path, 'work');
244
+ const parentChildDb = frozen.db.derive(parentChildPath, this._deriveOpts());
245
+ const childPath = filePath || tmpChildPath(frozen.path, label);
246
+ const childDb = frozen.db.derive(childPath, this._deriveOpts());
247
+ // Parent continues, transparently, in its own fresh child.
248
+ this._ancestors = [frozen, ...this._ancestors];
249
+ this._working = new Node(parentChildDb, parentChildPath, 'working');
250
+ // Branch shares the frozen snapshot + all older ancestors.
251
+ const branchNode = new Node(childDb, childPath, label || 'branch');
252
+ return new AgenticMemory(branchNode, [...this._ancestors], this._dim, this._metric, this._track);
253
+ }
254
+
255
+ /**
256
+ * Lightweight fork: derive a child WITHOUT re-pointing this memory. Use this to
257
+ * fan out many branches off a base you will not mutate again (e.g. spawn 1,000
258
+ * per-user branches off one shared base). One derive() per fork — ~0.5 ms /
259
+ * 162 bytes each, O(1) in base size. Read-through isolation holds as long as
260
+ * the parent base stays read-only after forking.
261
+ * @param {string} [label]
262
+ * @param {string} [filePath]
263
+ * @returns {AgenticMemory}
264
+ */
265
+ fork(label, filePath) {
266
+ this._assertOpen();
267
+ const childPath = filePath || tmpChildPath(this._working.path, label);
268
+ const childDb = this._working.db.derive(childPath, this._deriveOpts());
269
+ const childNode = new Node(childDb, childPath, label || 'fork');
270
+ return new AgenticMemory(
271
+ childNode,
272
+ [this._working, ...this._ancestors],
273
+ this._dim,
274
+ this._metric,
275
+ this._track
276
+ );
277
+ }
278
+
279
+ /**
280
+ * Git-style diff of this branch's working node against its nearest ancestor:
281
+ * which ids were added, overridden, or tombstoned. Requires edit tracking
282
+ * (open with track !== false).
283
+ * @returns {{added:number[], overridden:number[], deleted:number[]}}
284
+ */
285
+ diff() {
286
+ this._assertOpen();
287
+ const ancestorIds = new Set();
288
+ for (const a of this._ancestors) for (const id of a.editIds) ancestorIds.add(id);
289
+ const added = [];
290
+ const overridden = [];
291
+ for (const id of this._working.editIds) {
292
+ (ancestorIds.has(id) ? overridden : added).push(id);
293
+ }
294
+ return {
295
+ added: added.sort((a, b) => a - b),
296
+ overridden: overridden.sort((a, b) => a - b),
297
+ deleted: [...this._working.tombstones].sort((a, b) => a - b),
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Promote (merge) this branch's recorded edits onto a target memory — the
303
+ * Git-style "branch -> reviewed -> production" workflow. Replays the working
304
+ * node's ingested vectors and tombstones into `target`. Requires edit tracking.
305
+ * @param {AgenticMemory} target
306
+ * @returns {{ingested:number, deleted:number}}
307
+ */
308
+ promote(target) {
309
+ this._assertOpen();
310
+ if (!(target instanceof AgenticMemory)) {
311
+ throw new Error('agenticow: promote target must be an AgenticMemory');
312
+ }
313
+ const ids = [...this._working.editIds];
314
+ if (ids.length && this._working.editVecs.size === 0) {
315
+ throw new Error('agenticow: promote needs tracked edit vectors (open with track:true)');
316
+ }
317
+ if (ids.length) {
318
+ const flat = new Float32Array(ids.length * this._dim);
319
+ for (let i = 0; i < ids.length; i++) flat.set(this._working.editVecs.get(ids[i]), i * this._dim);
320
+ target.ingest(flat, ids);
321
+ }
322
+ const tomb = [...this._working.tombstones];
323
+ if (tomb.length) target.delete(tomb);
324
+ return { ingested: ids.length, deleted: tomb.length };
325
+ }
326
+
327
+ /**
328
+ * Freeze the current state as an immutable restore point and keep working in a
329
+ * fresh COW child. O(1) in base size. Returns the checkpoint descriptor.
330
+ * @param {string} [label]
331
+ * @returns {{id:string, label:string, path:string, depth:number}}
332
+ */
333
+ checkpoint(label) {
334
+ this._assertOpen();
335
+ const frozen = this._working;
336
+ frozen.label = label || frozen.label || `ckpt-${this._ancestors.length + 1}`;
337
+ const childPath = tmpChildPath(frozen.path, 'work');
338
+ const childDb = frozen.db.derive(childPath, this._deriveOpts());
339
+ const childNode = new Node(childDb, childPath, 'working');
340
+ this._ancestors = [frozen, ...this._ancestors];
341
+ this._working = childNode;
342
+ return {
343
+ id: frozen.id,
344
+ label: frozen.label,
345
+ path: frozen.path,
346
+ depth: this._ancestors.length,
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Discard all edits since a checkpoint and resume from it. With no argument,
352
+ * rolls back to the most recent checkpoint. Abandons the poisoned working
353
+ * child and derives a fresh writable child off the chosen checkpoint.
354
+ * @param {string} [checkpointId] fileId of the checkpoint to return to
355
+ * @returns {{restoredTo:string, depth:number}}
356
+ */
357
+ rollback(checkpointId) {
358
+ this._assertOpen();
359
+ if (this._ancestors.length === 0) {
360
+ throw new Error('agenticow: nothing to roll back to (no checkpoints)');
361
+ }
362
+ let idx = 0; // default: most recent checkpoint
363
+ if (checkpointId) {
364
+ idx = this._ancestors.findIndex((n) => n.id === checkpointId);
365
+ if (idx === -1) throw new Error(`agenticow: checkpoint ${checkpointId} not found`);
366
+ }
367
+ // Discard the current poisoned working child and any checkpoints newer than target.
368
+ try {
369
+ this._working.db.close();
370
+ } catch { /* ignore */ }
371
+ try {
372
+ fs.rmSync(this._working.path, { force: true });
373
+ } catch { /* ignore */ }
374
+ const target = this._ancestors[idx];
375
+ const newAncestors = this._ancestors.slice(idx); // target + older
376
+ const childPath = tmpChildPath(target.path, 'work');
377
+ const childDb = target.db.derive(childPath, this._deriveOpts());
378
+ this._working = new Node(childDb, childPath, 'working');
379
+ this._ancestors = newAncestors;
380
+ return { restoredTo: target.id, depth: this._ancestors.length };
381
+ }
382
+
383
+ /** Lineage chain metadata, working node first. */
384
+ lineage() {
385
+ return this._chain().map((n, i) => ({
386
+ role: i === 0 ? 'working' : (i === this._chain().length - 1 ? 'base' : 'checkpoint'),
387
+ id: n.id,
388
+ label: n.label,
389
+ path: n.path,
390
+ tombstones: n.tombstones.size,
391
+ }));
392
+ }
393
+
394
+ /** Status of the working node plus chain depth. */
395
+ status() {
396
+ this._assertOpen();
397
+ const s = this._working.db.status();
398
+ return {
399
+ ...s,
400
+ chainDepth: this._chain().length,
401
+ dimension: this._dim,
402
+ metric: this._metric,
403
+ };
404
+ }
405
+
406
+ /** dimension of stored vectors */
407
+ get dimension() {
408
+ return this._dim;
409
+ }
410
+
411
+ /**
412
+ * Persist the lineage to a small JSON manifest so the CLI (or another process)
413
+ * can reopen the exact chain. Vector data stays in the .rvf files; the manifest
414
+ * holds the chain order, labels, tombstones and (for diff/promote) the working
415
+ * node's recorded edits.
416
+ * @param {string} manifestPath
417
+ */
418
+ save(manifestPath) {
419
+ this._assertOpen();
420
+ const nodes = this._chain().map((n, i) => ({
421
+ path: path.resolve(n.path),
422
+ label: n.label,
423
+ tombstones: [...n.tombstones],
424
+ // only the working node (i===0) needs its edit vectors for promote()
425
+ editIds: i === 0 ? [...n.editIds] : [],
426
+ editVecs: i === 0 ? Object.fromEntries([...n.editVecs].map(([id, v]) => [id, Array.from(v)])) : {},
427
+ }));
428
+ fs.writeFileSync(manifestPath, JSON.stringify({ v: 1, dim: this._dim, metric: this._metric, track: this._track, nodes }, null, 2));
429
+ return manifestPath;
430
+ }
431
+
432
+ /**
433
+ * Reconstruct an AgenticMemory from a manifest written by save().
434
+ * @param {string} manifestPath
435
+ * @returns {AgenticMemory}
436
+ */
437
+ static load(manifestPath) {
438
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
439
+ const nodes = m.nodes.map((nm, i) => {
440
+ const db = i === 0 ? RvfDatabase.open(nm.path) : RvfDatabase.openReadonly(nm.path);
441
+ const node = new Node(db, nm.path, nm.label);
442
+ node.tombstones = new Set(nm.tombstones || []);
443
+ node.editIds = new Set(nm.editIds || []);
444
+ node.editVecs = new Map(Object.entries(nm.editVecs || {}).map(([id, v]) => [Number(id), Float32Array.from(v)]));
445
+ return node;
446
+ });
447
+ return new AgenticMemory(nodes[0], nodes.slice(1), m.dim, m.metric, m.track !== false);
448
+ }
449
+
450
+ /** Close all open handles in the chain. */
451
+ close() {
452
+ if (this._closed) return;
453
+ for (const n of this._chain()) {
454
+ try { n.db.close(); } catch { /* ignore */ }
455
+ }
456
+ this._closed = true;
457
+ }
458
+ }
459
+
460
+ /** Convenience: open or create a memory. @see AgenticMemory.open */
461
+ export function open(filePath, opts) {
462
+ return AgenticMemory.open(filePath, opts);
463
+ }
464
+
465
+ export default { open, AgenticMemory };