@surrealdb/memory 1.0.0-alpha.9

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.
@@ -0,0 +1,1672 @@
1
+
2
+ //#region src/file-body.ts
3
+ /**
4
+ * Normalises a file-like input to a `Blob` for multipart uploads.
5
+ * `ReadableStream` inputs are buffered in full (suitable for typical document sizes).
6
+ */
7
+ async function agentMemoryFileInputToBlob(input, mimeType) {
8
+ if (typeof File !== "undefined" && input instanceof File) return input;
9
+ if (input instanceof Blob) return input;
10
+ if (input instanceof ArrayBuffer) return new Blob([input], { type: mimeType });
11
+ if (ArrayBuffer.isView(input)) return new Blob([input], { type: mimeType });
12
+ const stream = input;
13
+ const reader = stream.getReader();
14
+ const chunks = [];
15
+ try {
16
+ for (;;) {
17
+ const { done, value } = await reader.read();
18
+ if (done) break;
19
+ if (value) chunks.push(value);
20
+ }
21
+ } finally {
22
+ reader.releaseLock();
23
+ }
24
+ const total = chunks.reduce((a, c) => a + c.byteLength, 0);
25
+ const out = new Uint8Array(total);
26
+ let offset = 0;
27
+ for (const c of chunks) {
28
+ out.set(c, offset);
29
+ offset += c.byteLength;
30
+ }
31
+ return new Blob([out], { type: mimeType });
32
+ }
33
+
34
+ //#endregion
35
+ //#region src/pagination.ts
36
+ /**
37
+ * Copies the pagination parameters a caller supplied into a query object,
38
+ * leaving absent ones absent so the server applies its own defaults.
39
+ */
40
+ function addPageParams(query, options) {
41
+ if (options?.limit !== void 0) query.limit = options.limit;
42
+ if (options?.cursor !== void 0) query.cursor = options.cursor;
43
+ if (options?.count !== void 0) query.count = options.count;
44
+ }
45
+ /**
46
+ * Yields every page of a listing, following `page.nextCursor` to exhaustion.
47
+ *
48
+ * `fetchPage` receives the cursor to resume from — `undefined` on the first
49
+ * call — and must pass it through to the underlying request unchanged.
50
+ */
51
+ async function* walkPages(fetchPage, collection) {
52
+ let cursor;
53
+ const seen = /* @__PURE__ */ new Set();
54
+ for (;;) {
55
+ const response = await fetchPage(cursor);
56
+ if (response === null || response === void 0) return;
57
+ yield response;
58
+ const next = response.page?.nextCursor;
59
+ if (next === void 0 || next === null || next === "") return;
60
+ if (seen.has(next)) throw new Error(`Agent Memory returned a repeated pagination cursor while walking \`${collection}\`; the page walk cannot advance.`);
61
+ seen.add(next);
62
+ cursor = next;
63
+ }
64
+ }
65
+ /**
66
+ * Follows a listing's cursors to exhaustion and returns every row.
67
+ *
68
+ * This is what the Agent Memory CLI does for verbs that print a whole set, and
69
+ * what a caller wants when the collection is a tree or a filter source rather
70
+ * than a screenful. Without `max` it is an unbounded read by construction:
71
+ * prefer the page-at-a-time `list` for anything user-facing and large.
72
+ *
73
+ * `max` stops the walk once that many rows are in hand, so a caller that only
74
+ * ever renders the first N does not pay for the pages beyond them. The result
75
+ * can still overshoot `max` by up to one page, because pages arrive whole.
76
+ */
77
+ async function collectPages(fetchPage, collection, max) {
78
+ const rows = [];
79
+ for await (const page of walkPages(fetchPage, collection)) {
80
+ const items = page[collection];
81
+ if (items) rows.push(...items);
82
+ if (max !== void 0 && rows.length >= max) break;
83
+ }
84
+ return rows;
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/paths.ts
89
+ /**
90
+ * URL-encodes a single path segment (e.g. context id, entity name).
91
+ * @param value Raw segment value.
92
+ */
93
+ function encodePathSegment(value) {
94
+ return encodeURIComponent(value);
95
+ }
96
+ /**
97
+ * Returns the API path prefix for an Agent Memory context: `/api/v1/{contextId}`.
98
+ * @param contextId Context identifier.
99
+ */
100
+ function getContextApiPrefix(contextId) {
101
+ return `/api/v1/${encodePathSegment(contextId)}`;
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/scope.ts
106
+ /**
107
+ * Normalises a {@link Scope} input to the wire `ScopeSets` (a DNF selector,
108
+ * `string[][]`: an OR of clauses, each clause an AND of `key/value` slash-paths).
109
+ *
110
+ * A bare string becomes one single-path clause. Each element of the outer array
111
+ * becomes a clause: a string element is a one-path clause, an array element is an
112
+ * AND clause of its paths. Within each clause empty strings are dropped and paths
113
+ * are de-duplicated preserving first-seen order; a clause that ends up empty is
114
+ * dropped (empty clauses are rejected on the wire).
115
+ *
116
+ * @param scope Scope input in any accepted shape.
117
+ * @returns The normalised DNF selector, or `undefined` when no non-empty clause
118
+ * remains (so callers can omit the field entirely and use the key's default
119
+ * write region).
120
+ */
121
+ function normaliseScope(scope) {
122
+ if (scope === void 0 || scope === null) return void 0;
123
+ const clauses = typeof scope === "string" ? [scope] : scope;
124
+ const out = [];
125
+ for (const clause of clauses) {
126
+ const paths = typeof clause === "string" ? [clause] : clause;
127
+ const deduped = [...new Set(paths.filter((p) => p.length > 0))];
128
+ if (deduped.length > 0) out.push(deduped);
129
+ }
130
+ return out.length > 0 ? out : void 0;
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/components/documents.ts
135
+ /**
136
+ * Copies the deprecated offset parameters into a query object. Kept separate
137
+ * from {@link addPageParams} so the two never merge into one option bag: the
138
+ * server rejects `cursor` sent together with `page`.
139
+ */
140
+ function addOffsetParams(query, options) {
141
+ if (options?.page !== void 0) query.page = options.page;
142
+ if (options?.pageSize !== void 0) query.pageSize = options.pageSize;
143
+ }
144
+ async function buildUploadForm(options) {
145
+ const blob = await agentMemoryFileInputToBlob(options.file, options.contentType);
146
+ const form = new FormData();
147
+ const metadata = {};
148
+ if (options.title !== void 0) metadata.title = options.title;
149
+ if (options.source !== void 0) metadata.source = options.source;
150
+ const scopes = normaliseScope(options.scopes);
151
+ if (scopes) metadata.scopes = scopes;
152
+ if (options.labels !== void 0) metadata.labels = options.labels;
153
+ if (Object.keys(metadata).length > 0) form.append("metadata", JSON.stringify(metadata));
154
+ const name = options.filename ?? (typeof File !== "undefined" && options.file instanceof File ? options.file.name : "upload");
155
+ form.append("file", blob, name);
156
+ return form;
157
+ }
158
+ /** The per-request transport options an upload forwards from its own options. */
159
+ function sendOptions(options) {
160
+ return {
161
+ signal: options.signal,
162
+ onUploadProgress: options.onUploadProgress,
163
+ timeoutMs: options.timeoutMs
164
+ };
165
+ }
166
+ /** Keyword graph helpers for the document corpus. */
167
+ var DocumentKeywords = class {
168
+ transport;
169
+ contextId;
170
+ constructor(transport, contextId) {
171
+ this.transport = transport;
172
+ this.contextId = contextId;
173
+ }
174
+ get base() {
175
+ return `${getContextApiPrefix(this.contextId)}/documents/keywords`;
176
+ }
177
+ /** Lists one page of keywords with optional filters. */
178
+ async list(options) {
179
+ const query = {};
180
+ if (options?.q !== void 0) query.q = options.q;
181
+ if (options?.minDocumentCount !== void 0) query.minDocumentCount = options.minDocumentCount;
182
+ if (options?.sort !== void 0) query.sort = options.sort;
183
+ addPageParams(query, options);
184
+ addOffsetParams(query, options);
185
+ const body = await this.transport.requestJson("GET", this.base, { query });
186
+ return body;
187
+ }
188
+ /** Every matching keyword, following cursors to exhaustion. */
189
+ async listAll(options) {
190
+ return collectPages((cursor) => this.list({
191
+ ...options,
192
+ cursor
193
+ }), "keywords");
194
+ }
195
+ /** Vector search over keyword embeddings. */
196
+ async search(options) {
197
+ const payload = { query: options.query };
198
+ if (options.k !== void 0) payload.k = options.k;
199
+ if (options.threshold !== void 0) payload.threshold = options.threshold;
200
+ const body = await this.transport.requestJson("POST", `${this.base}/search`, { body: payload });
201
+ return body;
202
+ }
203
+ /** Gets one keyword by its normalised form. */
204
+ async get(normalised) {
205
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(normalised)}`);
206
+ return body;
207
+ }
208
+ /** Keywords linked to a document. */
209
+ async forDocument(documentId) {
210
+ const path = `${getContextApiPrefix(this.contextId)}/documents/${encodePathSegment(documentId)}/keywords`;
211
+ const body = await this.transport.requestJson("GET", path);
212
+ return body.keywords;
213
+ }
214
+ };
215
+ /** Document ingestion, retrieval, and corpus search. */
216
+ var Documents = class {
217
+ transport;
218
+ contextId;
219
+ /** Keyword graph for the document corpus. */
220
+ keywords;
221
+ constructor(transport, contextId) {
222
+ this.transport = transport;
223
+ this.contextId = contextId;
224
+ this.keywords = new DocumentKeywords(transport, contextId);
225
+ }
226
+ get base() {
227
+ return `${getContextApiPrefix(this.contextId)}/documents`;
228
+ }
229
+ /** Uploads a document (multipart). Returns the ingestion handle. */
230
+ async upload(options) {
231
+ const form = await buildUploadForm(options);
232
+ const body = await this.transport.requestJson("POST", this.base, {
233
+ body: form,
234
+ ...sendOptions(options)
235
+ });
236
+ return body;
237
+ }
238
+ /** Reprocesses an existing document with replacement bytes (multipart). */
239
+ async reprocess(documentId, options) {
240
+ const form = await buildUploadForm(options);
241
+ const path = `${this.base}/${encodePathSegment(documentId)}`;
242
+ const body = await this.transport.requestJson("PUT", path, {
243
+ body: form,
244
+ ...sendOptions(options)
245
+ });
246
+ if (body === null) return {
247
+ id: documentId,
248
+ status: "queued",
249
+ contentHash: "",
250
+ deduplicated: false
251
+ };
252
+ return body;
253
+ }
254
+ /** Metadata for one document. */
255
+ async get(documentId) {
256
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(documentId)}`);
257
+ return body;
258
+ }
259
+ /** Raw document bytes. */
260
+ async raw(documentId, options) {
261
+ return this.transport.requestBytes("GET", `${this.base}/${encodePathSegment(documentId)}/raw`, options?.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : void 0);
262
+ }
263
+ /** Lists one page of a document's text chunks, in document order. */
264
+ async chunks(documentId, options) {
265
+ const query = {};
266
+ addPageParams(query, options);
267
+ addOffsetParams(query, options);
268
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(documentId)}/chunks`, { query });
269
+ return body;
270
+ }
271
+ /**
272
+ * Every chunk of a document, following cursors to exhaustion.
273
+ *
274
+ * Reconstructing a document's text needs all of it, and `limit` is clamped
275
+ * at 500 server-side, so a single wide page silently truncates anything
276
+ * longer. Pass `max` when only the first N chunks are ever rendered, so a
277
+ * very long document does not cost a page fetch per hundred chunks.
278
+ */
279
+ async allChunks(documentId, options) {
280
+ return collectPages((cursor) => this.chunks(documentId, {
281
+ limit: options?.limit,
282
+ cursor
283
+ }), "chunks", options?.max);
284
+ }
285
+ /** Lists one page of documents with optional filters. */
286
+ async list(options) {
287
+ const query = {};
288
+ if (options?.status !== void 0) query.status = options.status;
289
+ if (options?.mimeType !== void 0) query.mimeType = options.mimeType;
290
+ addPageParams(query, options);
291
+ addOffsetParams(query, options);
292
+ const body = await this.transport.requestJson("GET", this.base, { query });
293
+ return body;
294
+ }
295
+ /** Every matching document, following cursors to exhaustion. */
296
+ async listAll(options) {
297
+ return collectPages((cursor) => this.list({
298
+ ...options,
299
+ cursor
300
+ }), "documents");
301
+ }
302
+ /**
303
+ * How many documents match, without fetching them.
304
+ *
305
+ * Asks for a single row with `count: true`, so the total is the only thing
306
+ * paid for beyond one page bound.
307
+ */
308
+ async count(options) {
309
+ const page = await this.list({
310
+ ...options,
311
+ limit: 1,
312
+ count: true
313
+ });
314
+ return page.page.totalSize ?? page.documents.length;
315
+ }
316
+ /** Deletes a document. */
317
+ async delete(documentId) {
318
+ await this.transport.requestJson("DELETE", `${this.base}/${encodePathSegment(documentId)}`);
319
+ }
320
+ /** Hybrid / vector / BM25 / graph search over the document corpus. */
321
+ async query(options) {
322
+ const body = await this.transport.requestJson("POST", `${this.base}/query`, { body: options });
323
+ return body;
324
+ }
325
+ /** Recomputes derived document↔keyword and document↔document links. */
326
+ async recomputeLinks() {
327
+ const body = await this.transport.requestJson("POST", `${this.base}/recompute-links`, { body: {} });
328
+ return body;
329
+ }
330
+ };
331
+
332
+ //#endregion
333
+ //#region src/components/entities.ts
334
+ /** Entity records, attributes, relations, and attribute history. */
335
+ var Entities = class {
336
+ transport;
337
+ contextId;
338
+ constructor(transport, contextId) {
339
+ this.transport = transport;
340
+ this.contextId = contextId;
341
+ }
342
+ get base() {
343
+ return `${getContextApiPrefix(this.contextId)}/entities`;
344
+ }
345
+ /** Lists one page of entities, optionally filtered by type. */
346
+ async list(options) {
347
+ const query = {};
348
+ if (options?.type !== void 0) query.type = options.type;
349
+ addPageParams(query, options);
350
+ const body = await this.transport.requestJson("GET", this.base, { query });
351
+ return body;
352
+ }
353
+ /** Every matching entity, following cursors to exhaustion. */
354
+ async listAll(options) {
355
+ return collectPages((cursor) => this.list({
356
+ type: options?.type,
357
+ limit: options?.limit,
358
+ cursor
359
+ }), "entities");
360
+ }
361
+ /**
362
+ * How many entities match, without fetching them.
363
+ *
364
+ * Asks for a single row with `count: true`, so the total is the only thing
365
+ * paid for beyond one page bound.
366
+ */
367
+ async count(options) {
368
+ const page = await this.list({
369
+ type: options?.type,
370
+ limit: 1,
371
+ count: true
372
+ });
373
+ return page.page.totalSize ?? page.entities.length;
374
+ }
375
+ /** Fetches a single entity by type and name, with its attributes and relations. */
376
+ async get(entityType, name) {
377
+ const path = `${this.base}/${encodePathSegment(entityType)}/${encodePathSegment(name)}`;
378
+ const body = await this.transport.requestJson("GET", path);
379
+ return body;
380
+ }
381
+ /** Returns the supersession history for one attribute key. */
382
+ async history(entityType, name, key) {
383
+ const path = `${this.base}/${encodePathSegment(entityType)}/${encodePathSegment(name)}/history/${encodePathSegment(key)}`;
384
+ const body = await this.transport.requestJson("GET", path);
385
+ return body.history;
386
+ }
387
+ /** Soft-deletes an entity (sets valid-until). */
388
+ async delete(entityType, name) {
389
+ const path = `${this.base}/${encodePathSegment(entityType)}/${encodePathSegment(name)}`;
390
+ await this.transport.requestJson("DELETE", path);
391
+ }
392
+ };
393
+
394
+ //#endregion
395
+ //#region src/components/keys.ts
396
+ /** Self-service API keys for this context (requires the `manage` grant). */
397
+ var Keys = class {
398
+ transport;
399
+ contextId;
400
+ constructor(transport, contextId) {
401
+ this.transport = transport;
402
+ this.contextId = contextId;
403
+ }
404
+ get base() {
405
+ return `${getContextApiPrefix(this.contextId)}/keys`;
406
+ }
407
+ /**
408
+ * Mints a new key. The full secret is returned once in
409
+ * {@link MintedKeyJson.key} and cannot be retrieved again.
410
+ */
411
+ async create(options) {
412
+ const payload = {};
413
+ if (options?.name !== void 0) payload.name = options.name;
414
+ if (options?.grants !== void 0) payload.grants = options.grants;
415
+ const body = await this.transport.requestJson("POST", this.base, {
416
+ body: Object.keys(payload).length > 0 ? payload : void 0,
417
+ query: { ttlSeconds: options?.ttlSeconds }
418
+ });
419
+ return body;
420
+ }
421
+ /**
422
+ * Lists one page of key metadata for the context (secrets are never
423
+ * included).
424
+ *
425
+ * `keys` can hold fewer than `limit` entries while `page.hasMore` is still
426
+ * true: a non-administrator sees only the key they authenticated with, so
427
+ * the page is bounded in the database and then filtered. Terminate a walk on
428
+ * `page.nextCursor`, never on a short page.
429
+ */
430
+ async list(options) {
431
+ const query = {};
432
+ addPageParams(query, options);
433
+ const body = await this.transport.requestJson("GET", this.base, { query });
434
+ return body;
435
+ }
436
+ /** Every key the caller can see, following cursors to exhaustion. */
437
+ async listAll(options) {
438
+ return collectPages((cursor) => this.list({
439
+ limit: options?.limit,
440
+ cursor
441
+ }), "keys");
442
+ }
443
+ /** Revokes a key by name. */
444
+ async delete(keyName) {
445
+ await this.transport.requestJson("DELETE", `${this.base}/${encodePathSegment(keyName)}`);
446
+ }
447
+ /** Rotates a key, returning a fresh secret in {@link MintedKeyJson.key}. */
448
+ async rotate(keyName, options) {
449
+ const body = await this.transport.requestJson("POST", `${this.base}/${encodePathSegment(keyName)}/rotate`, { query: { ttlSeconds: options?.ttlSeconds } });
450
+ return body;
451
+ }
452
+ };
453
+
454
+ //#endregion
455
+ //#region src/components/lifecycle.ts
456
+ /** Operator lifecycle sweeps (expiry and decay). */
457
+ var Lifecycle = class {
458
+ transport;
459
+ contextId;
460
+ constructor(transport, contextId) {
461
+ this.transport = transport;
462
+ this.contextId = contextId;
463
+ }
464
+ get base() {
465
+ return `${getContextApiPrefix(this.contextId)}/lifecycle`;
466
+ }
467
+ /** Runs the context-category expiry sweep. Returns the number of affected rows. */
468
+ async expire() {
469
+ const body = await this.transport.requestJson("POST", `${this.base}/expire`, { body: {} });
470
+ return body;
471
+ }
472
+ /** Runs the importance decay sweep. Returns the number of affected rows. */
473
+ async decay() {
474
+ const body = await this.transport.requestJson("POST", `${this.base}/decay`, { body: {} });
475
+ return body;
476
+ }
477
+ };
478
+
479
+ //#endregion
480
+ //#region src/components/principals.ts
481
+ /** Principals and their scope grants (requires the `manage` grant). */
482
+ var Principals = class {
483
+ transport;
484
+ contextId;
485
+ constructor(transport, contextId) {
486
+ this.transport = transport;
487
+ this.contextId = contextId;
488
+ }
489
+ get base() {
490
+ return `${getContextApiPrefix(this.contextId)}/principals`;
491
+ }
492
+ /** Lists one page of the context's principals. */
493
+ async list(options) {
494
+ const query = {};
495
+ addPageParams(query, options);
496
+ const body = await this.transport.requestJson("GET", this.base, { query });
497
+ return body;
498
+ }
499
+ /** Every principal in the context, following cursors to exhaustion. */
500
+ async listAll(options) {
501
+ return collectPages((cursor) => this.list({
502
+ limit: options?.limit,
503
+ cursor
504
+ }), "principals");
505
+ }
506
+ /** Fetches a single principal and its declared grants. */
507
+ async get(principalId) {
508
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(principalId)}`);
509
+ return body;
510
+ }
511
+ /** Resolves the verbs a principal effectively holds at a scope path. */
512
+ async effective(principalId, options) {
513
+ const query = { path: options.path };
514
+ if (options.asOf !== void 0) query.asOf = options.asOf;
515
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(principalId)}/effective`, { query });
516
+ return body;
517
+ }
518
+ /** Grants a principal a set of verbs over a scope pattern. */
519
+ async grant(principalId, options) {
520
+ const body = await this.transport.requestJson("POST", `${this.base}/${encodePathSegment(principalId)}/grants`, { body: {
521
+ path: options.path,
522
+ verbs: options.verbs
523
+ } });
524
+ return body;
525
+ }
526
+ /** Revokes a set of verbs from a principal over a scope pattern. */
527
+ async revoke(principalId, options) {
528
+ const body = await this.transport.requestJson("DELETE", `${this.base}/${encodePathSegment(principalId)}/grants`, { body: {
529
+ path: options.path,
530
+ verbs: options.verbs
531
+ } });
532
+ return body;
533
+ }
534
+ };
535
+
536
+ //#endregion
537
+ //#region src/components/scopes.ts
538
+ /** The scope tree: register, list, delete, and forget scope subtrees. */
539
+ var Scopes = class {
540
+ transport;
541
+ contextId;
542
+ constructor(transport, contextId) {
543
+ this.transport = transport;
544
+ this.contextId = contextId;
545
+ }
546
+ get base() {
547
+ return `${getContextApiPrefix(this.contextId)}/scopes`;
548
+ }
549
+ /**
550
+ * Lists one page of registered scope nodes.
551
+ *
552
+ * `scopes` can hold fewer than `limit` entries while `page.hasMore` is still
553
+ * true: the server bounds the page in the database and then drops nodes the
554
+ * caller has no grant over, so invisible nodes consume page budget without
555
+ * appearing. Terminate a walk on `page.nextCursor`, never on a short page —
556
+ * or call {@link listAll}, which does it correctly.
557
+ *
558
+ * `page.totalSize` is always absent here: the endpoint takes no `count`, and
559
+ * a total counted before the visibility filter would not match what a walk
560
+ * returns anyway.
561
+ */
562
+ async list(options) {
563
+ const query = {};
564
+ addPageParams(query, options);
565
+ const body = await this.transport.requestJson("GET", this.base, { query });
566
+ return body;
567
+ }
568
+ /**
569
+ * Every registered scope node, following cursors to exhaustion.
570
+ *
571
+ * The scope tree is consumed whole — as a tree to render, or as the source
572
+ * for scope autocomplete and validation — so a single page of it is not
573
+ * useful.
574
+ */
575
+ async listAll(options) {
576
+ return collectPages((cursor) => this.list({
577
+ limit: options?.limit,
578
+ cursor
579
+ }), "scopes");
580
+ }
581
+ /** Registers a scope path with optional display metadata. */
582
+ async register(options) {
583
+ const payload = { path: options.path };
584
+ if (options.displayName !== void 0) payload.displayName = options.displayName;
585
+ if (options.description !== void 0) payload.description = options.description;
586
+ const body = await this.transport.requestJson("POST", this.base, { body: payload });
587
+ return body;
588
+ }
589
+ /** Deletes (tombstones) a scope node by path. */
590
+ async delete(path) {
591
+ await this.transport.requestJson("DELETE", this.base, { query: { path } });
592
+ }
593
+ /** Forgets (erases) a scope subtree. Returns the number of rows forgotten. */
594
+ async forget(options) {
595
+ const payload = {};
596
+ if (options?.path !== void 0) payload.path = options.path;
597
+ const body = await this.transport.requestJson("POST", `${this.base}/forget`, { body: payload });
598
+ return body;
599
+ }
600
+ };
601
+
602
+ //#endregion
603
+ //#region src/components/sessions.ts
604
+ /** An open conversation session within an Agent Memory context. */
605
+ var Session = class {
606
+ transport;
607
+ contextId;
608
+ /** Session id (API path segment). */
609
+ id;
610
+ /** Creation timestamp. */
611
+ createdAt;
612
+ /** DNF scope selector the session writes to (outer OR, inner AND). */
613
+ scopes;
614
+ constructor(transport, contextId, info) {
615
+ this.transport = transport;
616
+ this.contextId = contextId;
617
+ this.id = info.id;
618
+ this.createdAt = info.createdAt;
619
+ this.scopes = info.scopes;
620
+ }
621
+ get base() {
622
+ return `${getContextApiPrefix(this.contextId)}/sessions/${encodePathSegment(this.id)}`;
623
+ }
624
+ /** Deletes this session on the server. */
625
+ async close() {
626
+ await this.transport.requestJson("DELETE", this.base);
627
+ }
628
+ /** Lists one page of turns recorded against this session, oldest first. */
629
+ async turns(options) {
630
+ const query = {};
631
+ addPageParams(query, options);
632
+ const body = await this.transport.requestJson("GET", `${this.base}/turns`, { query });
633
+ return body;
634
+ }
635
+ /**
636
+ * Every turn in this session, following cursors to exhaustion.
637
+ *
638
+ * A transcript is read whole, so this is usually what a caller wants.
639
+ */
640
+ async allTurns(options) {
641
+ return collectPages((cursor) => this.turns({
642
+ limit: options?.limit,
643
+ cursor
644
+ }), "turns");
645
+ }
646
+ /** Retrieves session-scoped LLM context text for a query. */
647
+ async context(options) {
648
+ const body = await this.transport.requestJson("POST", `${this.base}/context`, { body: { query: options.query } });
649
+ return body;
650
+ }
651
+ };
652
+ /** Creates and manages conversation sessions for a context. */
653
+ var Sessions = class {
654
+ transport;
655
+ contextId;
656
+ constructor(transport, contextId) {
657
+ this.transport = transport;
658
+ this.contextId = contextId;
659
+ }
660
+ /** Opens a new session with an optional DNF scope selector and metadata. */
661
+ async create(options) {
662
+ const base = `${getContextApiPrefix(this.contextId)}/sessions`;
663
+ const payload = {};
664
+ const scopes = normaliseScope(options?.scopes);
665
+ if (scopes) payload.scopes = scopes;
666
+ if (options?.metadata !== void 0) payload.metadata = options.metadata;
667
+ const body = await this.transport.requestJson("POST", base, { body: payload });
668
+ return new Session(this.transport, this.contextId, body);
669
+ }
670
+ };
671
+
672
+ //#endregion
673
+ //#region src/components/traces.ts
674
+ /** Retrieval decision traces for a context. */
675
+ var Traces = class {
676
+ transport;
677
+ contextId;
678
+ constructor(transport, contextId) {
679
+ this.transport = transport;
680
+ this.contextId = contextId;
681
+ }
682
+ get base() {
683
+ return `${getContextApiPrefix(this.contextId)}/traces`;
684
+ }
685
+ /** Lists one page of trace records, newest first. */
686
+ async list(options) {
687
+ const query = {};
688
+ addPageParams(query, options);
689
+ const body = await this.transport.requestJson("GET", this.base, { query });
690
+ return body;
691
+ }
692
+ /** Every trace record, following cursors to exhaustion. */
693
+ async listAll(options) {
694
+ return collectPages((cursor) => this.list({
695
+ limit: options?.limit,
696
+ cursor
697
+ }), "traces");
698
+ }
699
+ /** Fetches one trace by id. */
700
+ async get(traceId) {
701
+ const body = await this.transport.requestJson("GET", `${this.base}/${encodePathSegment(traceId)}`);
702
+ return body;
703
+ }
704
+ /** Aggregate trace statistics over the recent window. */
705
+ async stats() {
706
+ const body = await this.transport.requestJson("GET", `${this.base}/stats`);
707
+ return body;
708
+ }
709
+ };
710
+
711
+ //#endregion
712
+ //#region src/streaming.ts
713
+ function frameToChunk(payload, done) {
714
+ const delta = typeof payload.delta === "string" ? payload.delta : typeof payload.token === "string" ? payload.token : "";
715
+ const traceId = typeof payload.traceId === "string" ? payload.traceId : typeof payload.trace_id === "string" ? payload.trace_id : void 0;
716
+ const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : typeof payload.session_id === "string" ? payload.session_id : void 0;
717
+ return {
718
+ delta,
719
+ traceId,
720
+ sessionId,
721
+ done,
722
+ raw: payload
723
+ };
724
+ }
725
+ /**
726
+ * Parses an SSE response body into {@link ChatChunk}s.
727
+ *
728
+ * Handles multi-line `data:` payloads, comment lines, and the terminal
729
+ * `[DONE]` sentinel.
730
+ *
731
+ * @param response A streaming `fetch` response with a readable body.
732
+ */
733
+ async function* parseChatStream(response) {
734
+ const body = response.body;
735
+ if (!body) return;
736
+ const reader = body.getReader();
737
+ const decoder = new TextDecoder();
738
+ let buffer = "";
739
+ try {
740
+ for (;;) {
741
+ const { done, value } = await reader.read();
742
+ if (value) buffer += decoder.decode(value, { stream: true });
743
+ if (done) buffer += decoder.decode();
744
+ let sep;
745
+ while ((sep = buffer.search(/\r?\n\r?\n/)) !== -1) {
746
+ const rawFrame = buffer.slice(0, sep);
747
+ buffer = buffer.slice(sep + (buffer[sep] === "\r" ? 4 : 2));
748
+ const dataLines = [];
749
+ for (const line of rawFrame.split(/\r?\n/)) {
750
+ if (line.startsWith(":")) continue;
751
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
752
+ }
753
+ if (dataLines.length === 0) continue;
754
+ const data = dataLines.join("\n");
755
+ if (data === "[DONE]") {
756
+ yield {
757
+ delta: "",
758
+ done: true,
759
+ raw: {}
760
+ };
761
+ return;
762
+ }
763
+ let payload;
764
+ try {
765
+ payload = JSON.parse(data);
766
+ } catch {
767
+ payload = { delta: data };
768
+ }
769
+ const isDone = payload.done === true;
770
+ yield frameToChunk(payload, isDone);
771
+ if (isDone) return;
772
+ }
773
+ if (done) break;
774
+ }
775
+ } finally {
776
+ reader.releaseLock();
777
+ }
778
+ }
779
+
780
+ //#endregion
781
+ //#region src/errors.ts
782
+ /** Base class for all Agent Memory client errors. */
783
+ var AgentMemoryError = class extends Error {
784
+ name = "AgentMemoryError";
785
+ /** HTTP status code, or `0` for connection failures. */
786
+ status;
787
+ /** Short error title from the API or a generic label. */
788
+ title;
789
+ /** Human-readable detail when provided. */
790
+ detail;
791
+ /** RFC 7807 `type` URI when provided. */
792
+ type;
793
+ /** RFC 7807 `instance` when provided. */
794
+ instance;
795
+ /** Additional problem-details fields. */
796
+ extensions;
797
+ constructor(options) {
798
+ const detail = options.detail ?? void 0;
799
+ let message = `[${options.status}] ${options.title}`;
800
+ if (detail) message += `: ${detail}`;
801
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
802
+ this.status = options.status;
803
+ this.title = options.title;
804
+ this.detail = detail;
805
+ this.type = options.type ?? void 0;
806
+ this.instance = options.instance ?? void 0;
807
+ this.extensions = options.extensions ?? {};
808
+ }
809
+ };
810
+ /** Missing or invalid bearer token (401). */
811
+ var AuthError = class extends AgentMemoryError {
812
+ name = "AuthError";
813
+ };
814
+ /** Principal or scope floor rejected the call (403). */
815
+ var ScopeError = class extends AgentMemoryError {
816
+ name = "ScopeError";
817
+ };
818
+ /** Resource not found (404). */
819
+ var NotFoundError = class extends AgentMemoryError {
820
+ name = "NotFoundError";
821
+ };
822
+ /** Invalid request body or parameters (400 / 422). */
823
+ var ValidationError = class extends AgentMemoryError {
824
+ name = "ValidationError";
825
+ };
826
+ /** Rate or token budget exceeded (429). */
827
+ var RateLimitError = class extends AgentMemoryError {
828
+ name = "RateLimitError";
829
+ /** Seconds from `Retry-After` when numeric. */
830
+ retryAfter;
831
+ constructor(options) {
832
+ super(options);
833
+ this.retryAfter = options.retryAfter ?? void 0;
834
+ }
835
+ };
836
+ /** Server error after retries exhausted (5xx). */
837
+ var ServerError = class extends AgentMemoryError {
838
+ name = "ServerError";
839
+ };
840
+ /** Network failure, timeout, or other non-HTTP error (status 0). */
841
+ var ConnectionError = class extends AgentMemoryError {
842
+ name = "ConnectionError";
843
+ };
844
+ /**
845
+ * The caller aborted the request through the `signal` they supplied (status 0).
846
+ *
847
+ * Distinct from {@link ConnectionError} so a deliberate cancellation can be told
848
+ * apart from a timeout or a network failure: only the latter two describe
849
+ * something that went wrong, and only they are worth reporting to a user.
850
+ */
851
+ var CancelledError = class extends AgentMemoryError {
852
+ name = "CancelledError";
853
+ };
854
+ const STATUS_MAP = {
855
+ 400: ValidationError,
856
+ 401: AuthError,
857
+ 403: ScopeError,
858
+ 404: NotFoundError,
859
+ 422: ValidationError
860
+ };
861
+ function parseRetryAfter(headers) {
862
+ const raw = headers.get("Retry-After") ?? headers.get("retry-after");
863
+ if (raw === null) return void 0;
864
+ const n = Number(raw);
865
+ return Number.isFinite(n) ? n : void 0;
866
+ }
867
+ /**
868
+ * Builds a typed error from an API error response body and headers.
869
+ * @param status HTTP status code.
870
+ * @param body Parsed JSON body or plain text, or null.
871
+ * @param headers Response headers (for `Retry-After` on 429).
872
+ */
873
+ function errorFromResponse(status, body, headers) {
874
+ const extensions = {};
875
+ let title = "Agent Memory request failed";
876
+ let detail;
877
+ let type;
878
+ let instance;
879
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
880
+ const o = body;
881
+ const t = o.title ?? o.message;
882
+ if (typeof t === "string") title = t;
883
+ if (typeof o.detail === "string") detail = o.detail;
884
+ if (typeof o.type === "string") type = o.type;
885
+ if (typeof o.instance === "string") instance = o.instance;
886
+ for (const [key, value] of Object.entries(o)) if (![
887
+ "status",
888
+ "title",
889
+ "detail",
890
+ "type",
891
+ "instance",
892
+ "message"
893
+ ].includes(key)) extensions[key] = value;
894
+ } else if (typeof body === "string" && body.length > 0) detail = body;
895
+ const base = {
896
+ status,
897
+ title,
898
+ detail,
899
+ type,
900
+ instance,
901
+ extensions
902
+ };
903
+ if (status >= 500) return new ServerError(base);
904
+ if (status === 429) return new RateLimitError({
905
+ ...base,
906
+ retryAfter: parseRetryAfter(headers)
907
+ });
908
+ const Ctor = STATUS_MAP[status];
909
+ if (Ctor) return new Ctor(base);
910
+ return new AgentMemoryError(base);
911
+ }
912
+
913
+ //#endregion
914
+ //#region src/idempotency.ts
915
+ /**
916
+ * Idempotency-key derivation for safe write retries.
917
+ *
918
+ * Mirrors the reference clients: the key is a SHA-256 digest of the request
919
+ * method, path, body, and a 30-second time bucket. Identical writes replayed
920
+ * within the same bucket collapse to a single server-side effect, which makes
921
+ * the `/facts` and `/facts/batch` writes safe to retry.
922
+ */
923
+ const BUCKET_SECONDS = 30;
924
+ function toHex(buffer) {
925
+ const bytes = new Uint8Array(buffer);
926
+ let out = "";
927
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
928
+ return out;
929
+ }
930
+ /**
931
+ * Computes an idempotency key for a write request.
932
+ *
933
+ * @param method HTTP method (e.g. `POST`).
934
+ * @param path Request path including the context prefix.
935
+ * @param body Serialised request body (empty string when none).
936
+ * @param now Current epoch milliseconds (injectable for tests).
937
+ * @returns A hex-encoded SHA-256 digest.
938
+ */
939
+ async function idempotencyKey(method, path, body, now = Date.now()) {
940
+ const bucket = Math.floor(now / 1e3 / BUCKET_SECONDS);
941
+ const material = `${method}\0${path}\0${body}\0${bucket}`;
942
+ const data = new TextEncoder().encode(material);
943
+ const digest = await crypto.subtle.digest("SHA-256", data);
944
+ return toHex(digest);
945
+ }
946
+
947
+ //#endregion
948
+ //#region src/retry.ts
949
+ const _BACKOFF_MS = [
950
+ 250,
951
+ 500,
952
+ 1e3
953
+ ];
954
+ /** Back-off delays (ms) used between retry attempts for idempotent reads. */
955
+ function backoffSchedule(maxRetries) {
956
+ const capped = Math.max(0, Math.min(maxRetries, _BACKOFF_MS.length));
957
+ return _BACKOFF_MS.slice(0, capped);
958
+ }
959
+ /**
960
+ * Whether a failed request should be retried.
961
+ *
962
+ * Retries apply to idempotent reads (`GET`/`HEAD`) and writes explicitly marked
963
+ * idempotent, on `5xx` responses or connection errors (`status === null`).
964
+ */
965
+ function shouldRetry(method, status, attempt, maxRetries, idempotent = false) {
966
+ if (attempt >= maxRetries) return false;
967
+ const m = method.toUpperCase();
968
+ if (m !== "GET" && m !== "HEAD" && !idempotent) return false;
969
+ if (status === null) return true;
970
+ return status >= 500;
971
+ }
972
+
973
+ //#endregion
974
+ //#region src/transport.ts
975
+ const DEFAULT_TIMEOUT_MS = 3e4;
976
+ const DEFAULT_MAX_RETRIES = 3;
977
+ function buildUrl(endpoint, path, query) {
978
+ const urlStr = path.startsWith("http://") || path.startsWith("https://") ? path : `${endpoint.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
979
+ if (!query || Object.keys(query).length === 0) return urlStr;
980
+ const url = new URL(urlStr);
981
+ for (const [k, v] of Object.entries(query)) {
982
+ if (v === void 0 || v === null) continue;
983
+ url.searchParams.set(k, String(v));
984
+ }
985
+ return url.toString();
986
+ }
987
+ function decodeBody(text) {
988
+ if (!text) return null;
989
+ try {
990
+ return JSON.parse(text);
991
+ } catch {
992
+ return text;
993
+ }
994
+ }
995
+ function sleep(ms) {
996
+ return new Promise((r) => setTimeout(r, ms));
997
+ }
998
+ /**
999
+ * Combines a caller's abort signal with an internal one.
1000
+ *
1001
+ * The internal signal carries the request timeout, so it cannot simply be
1002
+ * replaced by the caller's — both have to be able to abort the request.
1003
+ *
1004
+ * @returns A cleanup function that detaches the forwarding listener.
1005
+ */
1006
+ function forwardAbort(from, to) {
1007
+ if (!from) return () => {};
1008
+ if (from.aborted) {
1009
+ to.abort();
1010
+ return () => {};
1011
+ }
1012
+ const onAbort = () => to.abort();
1013
+ from.addEventListener("abort", onAbort, { once: true });
1014
+ return () => from.removeEventListener("abort", onAbort);
1015
+ }
1016
+ /**
1017
+ * Sends a multipart body with `XMLHttpRequest` so its progress can be observed.
1018
+ *
1019
+ * `fetch` cannot report how much of a request body has been sent in any browser,
1020
+ * which leaves a large upload indistinguishable from a stalled one. This path is
1021
+ * used only when a caller asks for progress; everything else stays on `fetch`.
1022
+ */
1023
+ function sendWithProgress(options) {
1024
+ const { url, method, headers, body, signal, onProgress } = options;
1025
+ return new Promise((resolve, reject) => {
1026
+ const request = new XMLHttpRequest();
1027
+ request.open(method, url, true);
1028
+ for (const [name, value] of Object.entries(headers)) request.setRequestHeader(name, value);
1029
+ request.upload.addEventListener("progress", (event) => {
1030
+ onProgress({
1031
+ loaded: event.loaded,
1032
+ total: event.lengthComputable ? event.total : void 0
1033
+ });
1034
+ });
1035
+ request.addEventListener("load", () => {
1036
+ resolve({
1037
+ status: request.status,
1038
+ text: request.responseText,
1039
+ headers: parseRawHeaders(request.getAllResponseHeaders())
1040
+ });
1041
+ });
1042
+ request.addEventListener("error", () => reject(/* @__PURE__ */ new TypeError("Network request failed")));
1043
+ request.addEventListener("abort", () => {
1044
+ const error = /* @__PURE__ */ new Error("Request aborted");
1045
+ error.name = "AbortError";
1046
+ reject(error);
1047
+ });
1048
+ const onAbort = () => request.abort();
1049
+ if (signal.aborted) request.abort();
1050
+ else signal.addEventListener("abort", onAbort, { once: true });
1051
+ request.addEventListener("loadend", () => signal.removeEventListener("abort", onAbort));
1052
+ request.send(body);
1053
+ });
1054
+ }
1055
+ function parseRawHeaders(raw) {
1056
+ const headers = new Headers();
1057
+ for (const line of raw.trim().split(/[\r\n]+/)) {
1058
+ const separator = line.indexOf(":");
1059
+ if (separator === -1) continue;
1060
+ headers.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
1061
+ }
1062
+ return headers;
1063
+ }
1064
+ /**
1065
+ * Performs authenticated HTTP requests with retries, idempotency keys, JSON
1066
+ * handling, and server-sent-event streaming.
1067
+ */
1068
+ var Transport = class Transport {
1069
+ endpoint;
1070
+ apiKey;
1071
+ timeoutMs;
1072
+ maxRetries;
1073
+ fetchImpl;
1074
+ onBehalfOf;
1075
+ constructor(options) {
1076
+ if (!options.endpoint) throw new TypeError("Agent Memory endpoint is required.");
1077
+ if (!options.apiKey) throw new TypeError("Agent Memory API key is required.");
1078
+ this.endpoint = options.endpoint.replace(/\/$/, "");
1079
+ this.apiKey = options.apiKey;
1080
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1081
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
1082
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
1083
+ this.onBehalfOf = options.onBehalfOf;
1084
+ }
1085
+ /**
1086
+ * Returns a copy of this transport that issues every request on behalf of
1087
+ * `principalId` (adds the `X-Spectron-On-Behalf-Of` header).
1088
+ */
1089
+ withOnBehalfOf(principalId) {
1090
+ return new Transport({
1091
+ endpoint: this.endpoint,
1092
+ apiKey: this.apiKey,
1093
+ timeoutMs: this.timeoutMs,
1094
+ maxRetries: this.maxRetries,
1095
+ fetchImpl: this.fetchImpl,
1096
+ onBehalfOf: principalId
1097
+ });
1098
+ }
1099
+ /** Builds the common request headers, including delegation when configured. */
1100
+ baseHeaders(accept) {
1101
+ const headers = {
1102
+ Accept: accept,
1103
+ Authorization: `Bearer ${this.apiKey}`,
1104
+ "User-Agent": `surrealdb-memory-js/1.0.0-alpha.9`
1105
+ };
1106
+ if (this.onBehalfOf) headers["X-Spectron-On-Behalf-Of"] = this.onBehalfOf;
1107
+ return headers;
1108
+ }
1109
+ /**
1110
+ * Sends a JSON or multipart request.
1111
+ * @returns Parsed JSON, or `null` for empty 204 responses.
1112
+ */
1113
+ async requestJson(method, path, init) {
1114
+ const methodUpper = method.toUpperCase();
1115
+ const url = buildUrl(this.endpoint, path, init?.query);
1116
+ const schedule = backoffSchedule(this.maxRetries);
1117
+ const headerObj = this.baseHeaders("application/json");
1118
+ let body;
1119
+ let serialisedBody = "";
1120
+ const bodyInput = init?.body;
1121
+ const isMultipart = bodyInput instanceof FormData;
1122
+ if (bodyInput !== void 0) if (isMultipart) body = bodyInput;
1123
+ else {
1124
+ serialisedBody = JSON.stringify(bodyInput);
1125
+ body = serialisedBody;
1126
+ headerObj["Content-Type"] = "application/json";
1127
+ }
1128
+ const timeoutMs = isMultipart ? init?.timeoutMs ?? 0 : init?.timeoutMs ?? this.timeoutMs;
1129
+ if (init?.idempotent) headerObj["Idempotency-Key"] = await idempotencyKey(methodUpper, path, serialisedBody);
1130
+ if (init?.signal?.aborted) throw new CancelledError({
1131
+ status: 0,
1132
+ title: "Request cancelled",
1133
+ detail: "The signal was already aborted"
1134
+ });
1135
+ const withProgress = isMultipart && init?.onUploadProgress !== void 0 && typeof XMLHttpRequest !== "undefined";
1136
+ let attempt = 0;
1137
+ for (;;) {
1138
+ const controller = new AbortController();
1139
+ const detachAbort = forwardAbort(init?.signal, controller);
1140
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
1141
+ const headersForFetch = { ...headerObj };
1142
+ if (body instanceof FormData) delete headersForFetch["Content-Type"];
1143
+ try {
1144
+ const response = withProgress ? await sendWithProgress({
1145
+ url,
1146
+ method: methodUpper,
1147
+ headers: headersForFetch,
1148
+ body,
1149
+ signal: controller.signal,
1150
+ onProgress: init.onUploadProgress
1151
+ }).then(({ status, text: text$1, headers }) => ({
1152
+ status,
1153
+ ok: status >= 200 && status < 300,
1154
+ headers,
1155
+ text: () => Promise.resolve(text$1)
1156
+ })) : await this.fetchImpl(url, {
1157
+ method: methodUpper,
1158
+ headers: headersForFetch,
1159
+ body: methodUpper === "GET" || methodUpper === "HEAD" ? void 0 : body,
1160
+ signal: controller.signal
1161
+ });
1162
+ clearTimeout(timer);
1163
+ detachAbort();
1164
+ if (response.status >= 400 && shouldRetry(methodUpper, response.status, attempt, this.maxRetries, init?.idempotent)) {
1165
+ await sleep(schedule[attempt] ?? 1e3);
1166
+ attempt += 1;
1167
+ continue;
1168
+ }
1169
+ const text = await response.text();
1170
+ if (!response.ok) throw errorFromResponse(response.status, decodeBody(text), response.headers);
1171
+ if (response.status === 204 || text.length === 0) return null;
1172
+ return decodeBody(text);
1173
+ } catch (e) {
1174
+ clearTimeout(timer);
1175
+ detachAbort();
1176
+ if (e instanceof Error && e.name === "AbortError") {
1177
+ if (init?.signal?.aborted) throw new CancelledError({
1178
+ status: 0,
1179
+ title: "Request cancelled",
1180
+ detail: "The request was aborted by the caller",
1181
+ cause: e
1182
+ });
1183
+ throw new ConnectionError({
1184
+ status: 0,
1185
+ title: "Request timed out",
1186
+ detail: `Exceeded ${timeoutMs}ms`,
1187
+ cause: e
1188
+ });
1189
+ }
1190
+ if (shouldRetry(methodUpper, null, attempt, this.maxRetries, init?.idempotent) && !(e instanceof Error && "status" in e)) {
1191
+ await sleep(schedule[attempt] ?? 1e3);
1192
+ attempt += 1;
1193
+ continue;
1194
+ }
1195
+ if (e && typeof e === "object" && "status" in e) throw e;
1196
+ throw new ConnectionError({
1197
+ status: 0,
1198
+ title: "Connection failed",
1199
+ detail: e instanceof Error ? e.message : String(e),
1200
+ cause: e
1201
+ });
1202
+ }
1203
+ }
1204
+ }
1205
+ /**
1206
+ * GET that returns raw bytes (e.g. document `raw`).
1207
+ */
1208
+ async requestBytes(method, path, init) {
1209
+ const methodUpper = method.toUpperCase();
1210
+ const url = buildUrl(this.endpoint, path, init?.query);
1211
+ const timeoutMs = init?.timeoutMs ?? this.timeoutMs;
1212
+ const schedule = backoffSchedule(this.maxRetries);
1213
+ const headers = this.baseHeaders("*/*");
1214
+ let attempt = 0;
1215
+ for (;;) {
1216
+ const controller = new AbortController();
1217
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1218
+ try {
1219
+ const response = await this.fetchImpl(url, {
1220
+ method: methodUpper,
1221
+ headers,
1222
+ signal: controller.signal
1223
+ });
1224
+ clearTimeout(timer);
1225
+ if (response.status >= 400 && shouldRetry(methodUpper, response.status, attempt, this.maxRetries)) {
1226
+ await sleep(schedule[attempt] ?? 1e3);
1227
+ attempt += 1;
1228
+ continue;
1229
+ }
1230
+ if (!response.ok) {
1231
+ const text = await response.text();
1232
+ throw errorFromResponse(response.status, decodeBody(text), response.headers);
1233
+ }
1234
+ return await response.arrayBuffer();
1235
+ } catch (e) {
1236
+ clearTimeout(timer);
1237
+ if (e instanceof Error && e.name === "AbortError") throw new ConnectionError({
1238
+ status: 0,
1239
+ title: "Request timed out",
1240
+ detail: `Exceeded ${timeoutMs}ms`,
1241
+ cause: e
1242
+ });
1243
+ if (shouldRetry(methodUpper, null, attempt, this.maxRetries)) {
1244
+ await sleep(schedule[attempt] ?? 1e3);
1245
+ attempt += 1;
1246
+ continue;
1247
+ }
1248
+ if (e && typeof e === "object" && "status" in e) throw e;
1249
+ throw new ConnectionError({
1250
+ status: 0,
1251
+ title: "Connection failed",
1252
+ detail: e instanceof Error ? e.message : String(e),
1253
+ cause: e
1254
+ });
1255
+ }
1256
+ }
1257
+ }
1258
+ /**
1259
+ * Opens a server-sent-event stream (e.g. streaming `chat`).
1260
+ *
1261
+ * Streams are not retried; the returned {@link Response} carries the raw SSE
1262
+ * body for the caller to parse.
1263
+ */
1264
+ async stream(method, path, init) {
1265
+ const methodUpper = method.toUpperCase();
1266
+ const url = buildUrl(this.endpoint, path, init?.query);
1267
+ const headers = this.baseHeaders("text/event-stream");
1268
+ let body;
1269
+ if (init?.body !== void 0) {
1270
+ body = JSON.stringify(init.body);
1271
+ headers["Content-Type"] = "application/json";
1272
+ }
1273
+ let response;
1274
+ try {
1275
+ response = await this.fetchImpl(url, {
1276
+ method: methodUpper,
1277
+ headers,
1278
+ body
1279
+ });
1280
+ } catch (e) {
1281
+ throw new ConnectionError({
1282
+ status: 0,
1283
+ title: "Connection failed",
1284
+ detail: e instanceof Error ? e.message : String(e),
1285
+ cause: e
1286
+ });
1287
+ }
1288
+ if (!response.ok) {
1289
+ const text = await response.text();
1290
+ throw errorFromResponse(response.status, decodeBody(text), response.headers);
1291
+ }
1292
+ return response;
1293
+ }
1294
+ };
1295
+
1296
+ //#endregion
1297
+ //#region src/client.ts
1298
+ function addDefined(target, key, value) {
1299
+ if (value !== void 0) target[key] = value;
1300
+ }
1301
+ /**
1302
+ * Typed client for the public Agent Memory API: memory writes and recall, document
1303
+ * ingestion, sessions, entities, lifecycle, traces, and scope administration.
1304
+ *
1305
+ * The client is pinned to a single `context`; every call targets
1306
+ * `/api/v1/{context}/…`.
1307
+ */
1308
+ var AgentMemory = class AgentMemory {
1309
+ transport;
1310
+ /** Agent Memory context id this client calls. */
1311
+ contextId;
1312
+ /** Document ingestion, retrieval, corpus search, and the keyword graph. */
1313
+ documents;
1314
+ /** Entity records, attributes, relations, and attribute history. */
1315
+ entities;
1316
+ /** Conversation sessions for this context. */
1317
+ sessions;
1318
+ /** Expiry and decay sweeps. */
1319
+ lifecycle;
1320
+ /** Retrieval trace tooling. */
1321
+ traces;
1322
+ /** Principals and their scope grants. */
1323
+ principals;
1324
+ /** The scope tree. */
1325
+ scopes;
1326
+ /** Self-service API keys for this context. */
1327
+ keys;
1328
+ constructor(options) {
1329
+ if (!options.context) throw new TypeError("Agent Memory context is required.");
1330
+ this.contextId = options.context;
1331
+ this.transport = new Transport({
1332
+ apiKey: options.apiKey,
1333
+ endpoint: options.endpoint,
1334
+ timeoutMs: options.timeout,
1335
+ maxRetries: options.maxRetries,
1336
+ fetchImpl: options.fetchImpl
1337
+ });
1338
+ const components = AgentMemory.buildComponents(this.transport, this.contextId);
1339
+ this.documents = components.documents;
1340
+ this.entities = components.entities;
1341
+ this.sessions = components.sessions;
1342
+ this.lifecycle = components.lifecycle;
1343
+ this.traces = components.traces;
1344
+ this.principals = components.principals;
1345
+ this.scopes = components.scopes;
1346
+ this.keys = components.keys;
1347
+ }
1348
+ static buildComponents(transport, contextId) {
1349
+ return {
1350
+ documents: new Documents(transport, contextId),
1351
+ entities: new Entities(transport, contextId),
1352
+ sessions: new Sessions(transport, contextId),
1353
+ lifecycle: new Lifecycle(transport, contextId),
1354
+ traces: new Traces(transport, contextId),
1355
+ principals: new Principals(transport, contextId),
1356
+ scopes: new Scopes(transport, contextId),
1357
+ keys: new Keys(transport, contextId)
1358
+ };
1359
+ }
1360
+ get base() {
1361
+ return getContextApiPrefix(this.contextId);
1362
+ }
1363
+ /**
1364
+ * Returns a client that issues every request on behalf of `principalId`,
1365
+ * sending the `X-Spectron-On-Behalf-Of` delegation header. Requires the
1366
+ * `manage` grant. The original client is left unchanged.
1367
+ */
1368
+ onBehalfOf(principalId) {
1369
+ if (!principalId) throw new TypeError("onBehalfOf requires a principal id.");
1370
+ const transport = this.transport.withOnBehalfOf(principalId);
1371
+ const delegate = Object.create(AgentMemory.prototype);
1372
+ return Object.assign(delegate, {
1373
+ contextId: this.contextId,
1374
+ transport,
1375
+ ...AgentMemory.buildComponents(transport, this.contextId)
1376
+ });
1377
+ }
1378
+ /**
1379
+ * Liveness probe for the API (`GET /api/v1/health`).
1380
+ * @throws {AgentMemoryError} When the service is unhealthy or unreachable.
1381
+ */
1382
+ async health() {
1383
+ await this.transport.requestJson("GET", "/api/v1/health");
1384
+ }
1385
+ /**
1386
+ * Persists facts from free-form text and/or caller-supplied triples
1387
+ * (`POST /facts`). Idempotent within a 30-second window.
1388
+ */
1389
+ async remember(text, options) {
1390
+ const payload = {};
1391
+ addDefined(payload, "text", text);
1392
+ addDefined(payload, "infer", options?.infer);
1393
+ addDefined(payload, "session_id", options?.sessionId);
1394
+ addDefined(payload, "scopes", normaliseScope(options?.scopes));
1395
+ addDefined(payload, "role", options?.role);
1396
+ addDefined(payload, "memory_category", options?.memoryCategory);
1397
+ addDefined(payload, "labels", options?.labels);
1398
+ addDefined(payload, "triples", options?.triples);
1399
+ const body = await this.transport.requestJson("POST", `${this.base}/facts`, {
1400
+ body: payload,
1401
+ idempotent: true
1402
+ });
1403
+ return body;
1404
+ }
1405
+ /**
1406
+ * Persists facts from a batch of conversation messages (`POST /facts/batch`).
1407
+ * Idempotent within a 30-second window.
1408
+ */
1409
+ async rememberMany(messages, options) {
1410
+ const payload = { messages };
1411
+ addDefined(payload, "session_id", options?.sessionId);
1412
+ addDefined(payload, "scopes", normaliseScope(options?.scopes));
1413
+ addDefined(payload, "extract", options?.extract);
1414
+ addDefined(payload, "infer", options?.infer);
1415
+ addDefined(payload, "labels", options?.labels);
1416
+ const body = await this.transport.requestJson("POST", `${this.base}/facts/batch`, {
1417
+ body: payload,
1418
+ idempotent: true
1419
+ });
1420
+ return body;
1421
+ }
1422
+ /** Semantic recall over memory for this context (`POST /query`). */
1423
+ async recall(query, options) {
1424
+ const payload = { query };
1425
+ addDefined(payload, "k", options?.k);
1426
+ addDefined(payload, "mode", options?.mode);
1427
+ addDefined(payload, "sessionId", options?.sessionId);
1428
+ addDefined(payload, "include", options?.include);
1429
+ addDefined(payload, "asOf", options?.asOf);
1430
+ addDefined(payload, "atInstant", options?.atInstant);
1431
+ addDefined(payload, "labels", options?.labels);
1432
+ addDefined(payload, "lens", normaliseScope(options?.lens));
1433
+ addDefined(payload, "scopeView", options?.scopeView);
1434
+ addDefined(payload, "validFrom", options?.validFrom);
1435
+ addDefined(payload, "validUntil", options?.validUntil);
1436
+ addDefined(payload, "source", options?.source);
1437
+ addDefined(payload, "location", options?.location);
1438
+ const body = await this.transport.requestJson("POST", `${this.base}/query`, { body: payload });
1439
+ return body;
1440
+ }
1441
+ /** Forgets memory matching a natural-language query (`POST /forget`). */
1442
+ async forget(query, options) {
1443
+ const payload = { query };
1444
+ if (options?.purge) payload.purge = true;
1445
+ const body = await this.transport.requestJson("POST", `${this.base}/forget`, { body: payload });
1446
+ return body;
1447
+ }
1448
+ async chat(message, options) {
1449
+ const payload = { message };
1450
+ addDefined(payload, "sessionId", options?.sessionId);
1451
+ addDefined(payload, "scopes", normaliseScope(options?.scopes));
1452
+ addDefined(payload, "model", options?.model);
1453
+ if (options?.bypassCache) payload.bypassCache = true;
1454
+ addDefined(payload, "labels", options?.labels);
1455
+ if (options?.stream) {
1456
+ payload.stream = true;
1457
+ const response = await this.transport.stream("POST", `${this.base}/chat`, { body: payload });
1458
+ return parseChatStream(response);
1459
+ }
1460
+ const body = await this.transport.requestJson("POST", `${this.base}/chat`, { body: payload });
1461
+ return body;
1462
+ }
1463
+ /** Retrieves LLM-facing context text for a query without a session (`POST /context`). */
1464
+ async context(query, options) {
1465
+ const payload = { query };
1466
+ addDefined(payload, "k", options?.k);
1467
+ addDefined(payload, "labels", options?.labels);
1468
+ addDefined(payload, "lens", normaliseScope(options?.lens));
1469
+ addDefined(payload, "scopeView", options?.scopeView);
1470
+ const body = await this.transport.requestJson("POST", `${this.base}/context`, { body: payload });
1471
+ return body;
1472
+ }
1473
+ /** Runs a reflection pass; may persist attributes when `persist` is true (`POST /reflect`). */
1474
+ async reflect(query, options) {
1475
+ const body = await this.transport.requestJson("POST", `${this.base}/reflect`, { body: {
1476
+ query,
1477
+ persist: options?.persist ?? false
1478
+ } });
1479
+ return body;
1480
+ }
1481
+ /** Consolidates accumulated observations into durable facts (`POST /consolidate`). */
1482
+ async consolidate(options) {
1483
+ const payload = {};
1484
+ if (options?.dryRun) payload.dryRun = true;
1485
+ addDefined(payload, "factLimit", options?.factLimit);
1486
+ addDefined(payload, "observationLimit", options?.observationLimit);
1487
+ const body = await this.transport.requestJson("POST", `${this.base}/consolidate`, { body: payload });
1488
+ return body;
1489
+ }
1490
+ /** Infers and emits new relation edges between entities (`POST /elaborate`). */
1491
+ async elaborate(options) {
1492
+ const payload = {};
1493
+ addDefined(payload, "entityRef", options?.entityRef);
1494
+ addDefined(payload, "budget", options?.budget);
1495
+ if (options?.sweep) payload.sweep = true;
1496
+ if (options?.dryRun) payload.dryRun = true;
1497
+ const body = await this.transport.requestJson("POST", `${this.base}/elaborate`, { body: payload });
1498
+ return body;
1499
+ }
1500
+ /** Runs an integrity check over the memory store (`POST /fsck`). */
1501
+ async fsck(options) {
1502
+ const payload = {};
1503
+ addDefined(payload, "check", options?.check);
1504
+ addDefined(payload, "duplicateThreshold", options?.duplicateThreshold);
1505
+ addDefined(payload, "maxResults", options?.maxResults);
1506
+ const body = await this.transport.requestJson("POST", `${this.base}/fsck`, { body: payload });
1507
+ return body;
1508
+ }
1509
+ /** Inspects an entity, attribute, or trace by reference (`GET /inspect`). */
1510
+ async inspect(ref, options) {
1511
+ const query = { ref };
1512
+ addDefined(query, "asOf", options?.asOf);
1513
+ addDefined(query, "atInstant", options?.atInstant);
1514
+ addDefined(query, "validFrom", options?.validFrom);
1515
+ addDefined(query, "validUntil", options?.validUntil);
1516
+ const body = await this.transport.requestJson("GET", `${this.base}/inspect`, { query });
1517
+ return body;
1518
+ }
1519
+ /** Lists one page of audit rows for write/recall activity (`GET /audit`). */
1520
+ async audit(options) {
1521
+ const query = {};
1522
+ addDefined(query, "principal", options?.principal);
1523
+ addDefined(query, "key", options?.key);
1524
+ addDefined(query, "kind", options?.kind);
1525
+ addDefined(query, "since", options?.since);
1526
+ addDefined(query, "until", options?.until);
1527
+ addPageParams(query, options);
1528
+ const body = await this.transport.requestJson("GET", `${this.base}/audit`, { query });
1529
+ return body;
1530
+ }
1531
+ /** Every matching audit row, following cursors to exhaustion. */
1532
+ async auditAll(options) {
1533
+ return collectPages((cursor) => this.audit({
1534
+ ...options,
1535
+ cursor
1536
+ }), "rows");
1537
+ }
1538
+ /**
1539
+ * Structured memory state snapshot (`GET /state`).
1540
+ *
1541
+ * A snapshot, not an export: this is a composite read over six tables, each
1542
+ * bounded by `limit` (default 100, max 500), and `truncated` reports which
1543
+ * of them had more rows.
1544
+ *
1545
+ * Four of those tables have their own collection endpoint to enumerate them
1546
+ * completely — entities (see {@link AgentMemory.entities}), attributes,
1547
+ * relations, and actions. The remaining two, `instructions` and `unknowns`,
1548
+ * have no such route: when `truncated` flags either, the omitted rows cannot
1549
+ * be recovered other than by raising `limit`.
1550
+ */
1551
+ async state(options) {
1552
+ const query = {};
1553
+ addDefined(query, "limit", options?.limit);
1554
+ const body = await this.transport.requestJson("GET", `${this.base}/state`, { query });
1555
+ return body;
1556
+ }
1557
+ /** Static and dynamic profile slices (`GET /profile`). */
1558
+ async profile() {
1559
+ const body = await this.transport.requestJson("GET", `${this.base}/profile`);
1560
+ return body;
1561
+ }
1562
+ /** The calling principal's identity and resolved grants (`GET /me`). */
1563
+ async whoami() {
1564
+ const body = await this.transport.requestJson("GET", `${this.base}/me`);
1565
+ return body;
1566
+ }
1567
+ };
1568
+
1569
+ //#endregion
1570
+ //#region src/types/domain.ts
1571
+ /** Inference mode for the `/facts` write API. */
1572
+ const InferMode = {
1573
+ full: "full",
1574
+ triples: "triples",
1575
+ preview: "preview",
1576
+ none: "none"
1577
+ };
1578
+ /** Bulk extraction strategy for `/facts/batch`. */
1579
+ const BatchExtractionMode = {
1580
+ per_message: "per_message",
1581
+ whole_conversation: "whole_conversation"
1582
+ };
1583
+ /** Memory category classification applied during extraction. */
1584
+ const MemoryCategory = {
1585
+ identity: "identity",
1586
+ knowledge: "knowledge",
1587
+ context: "context"
1588
+ };
1589
+ /** Role of a conversation turn participant. */
1590
+ const TurnRole = {
1591
+ user: "user",
1592
+ assistant: "assistant",
1593
+ system: "system",
1594
+ tool: "tool"
1595
+ };
1596
+ /** Chunk query mode for `/documents/query`. */
1597
+ const QueryMode = {
1598
+ hybrid: "hybrid",
1599
+ vector: "vector",
1600
+ bm25: "bm25",
1601
+ hybrid_graph: "hybrid_graph"
1602
+ };
1603
+ /** Grant verb in the scope permission model. */
1604
+ const Verb = {
1605
+ read: "read",
1606
+ write: "write",
1607
+ create_scope: "create_scope",
1608
+ delete_scope: "delete_scope",
1609
+ grant: "grant",
1610
+ manage: "manage",
1611
+ forget: "forget"
1612
+ };
1613
+ /** Scope read breadth for memory queries. */
1614
+ const ScopeView = {
1615
+ strict: "strict",
1616
+ merged: "merged",
1617
+ crossTeam: "crossTeam"
1618
+ };
1619
+ /** Document pipeline status values returned by the API. */
1620
+ const DocumentStatus = {
1621
+ queued: "queued",
1622
+ extracting: "extracting",
1623
+ chunking: "chunking",
1624
+ embedding: "embedding",
1625
+ keywording: "keywording",
1626
+ extracting_nodes: "extracting_nodes",
1627
+ ready: "ready",
1628
+ failed: "failed"
1629
+ };
1630
+
1631
+ //#endregion
1632
+ exports.AgentMemory = AgentMemory;
1633
+ exports.AgentMemoryError = AgentMemoryError;
1634
+ exports.AuthError = AuthError;
1635
+ exports.BatchExtractionMode = BatchExtractionMode;
1636
+ exports.CancelledError = CancelledError;
1637
+ exports.ConnectionError = ConnectionError;
1638
+ exports.DocumentKeywords = DocumentKeywords;
1639
+ exports.DocumentStatus = DocumentStatus;
1640
+ exports.Documents = Documents;
1641
+ exports.Entities = Entities;
1642
+ exports.InferMode = InferMode;
1643
+ exports.Keys = Keys;
1644
+ exports.Lifecycle = Lifecycle;
1645
+ exports.MemoryCategory = MemoryCategory;
1646
+ exports.NotFoundError = NotFoundError;
1647
+ exports.Principals = Principals;
1648
+ exports.QueryMode = QueryMode;
1649
+ exports.RateLimitError = RateLimitError;
1650
+ exports.ScopeError = ScopeError;
1651
+ exports.ScopeView = ScopeView;
1652
+ exports.Scopes = Scopes;
1653
+ exports.ServerError = ServerError;
1654
+ exports.Session = Session;
1655
+ exports.Sessions = Sessions;
1656
+ exports.Traces = Traces;
1657
+ exports.Transport = Transport;
1658
+ exports.TurnRole = TurnRole;
1659
+ exports.ValidationError = ValidationError;
1660
+ exports.Verb = Verb;
1661
+ exports.addPageParams = addPageParams;
1662
+ exports.agentMemoryFileInputToBlob = agentMemoryFileInputToBlob;
1663
+ exports.backoffSchedule = backoffSchedule;
1664
+ exports.collectPages = collectPages;
1665
+ exports.encodePathSegment = encodePathSegment;
1666
+ exports.errorFromResponse = errorFromResponse;
1667
+ exports.getContextApiPrefix = getContextApiPrefix;
1668
+ exports.idempotencyKey = idempotencyKey;
1669
+ exports.normaliseScope = normaliseScope;
1670
+ exports.parseChatStream = parseChatStream;
1671
+ exports.shouldRetry = shouldRetry;
1672
+ exports.walkPages = walkPages;