@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.
- package/LICENSE +201 -0
- package/README.md +196 -0
- package/SECURITY.md +12 -0
- package/dist/memory.cjs +1672 -0
- package/dist/memory.d.ts +7601 -0
- package/dist/memory.mjs +1631 -0
- package/package.json +46 -0
package/dist/memory.mjs
ADDED
|
@@ -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 };
|