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