@pyxmate/memory 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DZZHJ66P.mjs → chunk-K7JZXUBN.mjs} +1 -1
- package/dist/chunk-W4LB326D.mjs +618 -0
- package/dist/dashboard.mjs +2 -2
- package/dist/index.d.ts +158 -33
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +2 -2
- package/package.json +1 -1
- package/skills/pyx-memory/patterns/access-control.md +1 -1
- package/skills/pyx-memory/patterns/consumer.md +10 -4
- package/skills/pyx-memory/patterns/file-uploads.md +1 -1
- package/skills/pyx-memory/reference/http-api.md +46 -41
- package/skills/pyx-memory/reference/sdk-guide.md +23 -15
- package/skills/pyx-memory/reference/types.md +5 -7
- package/dist/chunk-4YIKI2BA.mjs +0 -404
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// ../client/src/memory-client.ts
|
|
2
|
+
var MemoryServerError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "MemoryServerError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
/** True when the server returned HTTP 404 (not found). */
|
|
10
|
+
get isNotFound() {
|
|
11
|
+
return this.status === 404;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
15
|
+
var INGEST_EVENT_HEARTBEAT_MS = 2e4;
|
|
16
|
+
var MemoryClient = class {
|
|
17
|
+
baseUrl;
|
|
18
|
+
_authHeaders;
|
|
19
|
+
_requestTimeoutMs;
|
|
20
|
+
constructor(memoryUrl, apiKeyOrOptions) {
|
|
21
|
+
this.baseUrl = memoryUrl.replace(/\/$/, "");
|
|
22
|
+
let apiKey;
|
|
23
|
+
let defaultHeaders = {};
|
|
24
|
+
let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
25
|
+
if (typeof apiKeyOrOptions === "string") {
|
|
26
|
+
apiKey = apiKeyOrOptions;
|
|
27
|
+
} else if (apiKeyOrOptions) {
|
|
28
|
+
apiKey = apiKeyOrOptions.apiKey;
|
|
29
|
+
defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
|
|
30
|
+
if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
|
|
31
|
+
requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const trimmed = apiKey?.trim();
|
|
35
|
+
this._authHeaders = {
|
|
36
|
+
...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
|
|
37
|
+
...defaultHeaders
|
|
38
|
+
};
|
|
39
|
+
this._requestTimeoutMs = requestTimeoutMs;
|
|
40
|
+
}
|
|
41
|
+
/** Encode a path segment to prevent URL injection */
|
|
42
|
+
encodePathSegment(segment) {
|
|
43
|
+
return encodeURIComponent(segment);
|
|
44
|
+
}
|
|
45
|
+
async initialize() {
|
|
46
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
47
|
+
headers: this._authHeaders
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Memory server not reachable at ${this.baseUrl}: ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async store(entry) {
|
|
54
|
+
return this.fetchApi("/api/memory/ingest", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify(entry)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async search(params) {
|
|
60
|
+
const searchParams = new URLSearchParams({ query: params.query });
|
|
61
|
+
if (params.limit) searchParams.set("limit", String(params.limit));
|
|
62
|
+
if (params.type) searchParams.set("type", params.type);
|
|
63
|
+
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
64
|
+
if (params.strategy) searchParams.set("strategy", params.strategy);
|
|
65
|
+
if (params.eventTimeRange) {
|
|
66
|
+
searchParams.set("eventTimeStart", params.eventTimeRange[0]);
|
|
67
|
+
searchParams.set("eventTimeEnd", params.eventTimeRange[1]);
|
|
68
|
+
}
|
|
69
|
+
if (params.asOf) searchParams.set("asOf", params.asOf);
|
|
70
|
+
if (params.abstentionThreshold != null)
|
|
71
|
+
searchParams.set("abstentionThreshold", String(params.abstentionThreshold));
|
|
72
|
+
return this.fetchApi(`/api/memory/search?${searchParams}`);
|
|
73
|
+
}
|
|
74
|
+
async get(id) {
|
|
75
|
+
try {
|
|
76
|
+
return await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async delete(id) {
|
|
83
|
+
try {
|
|
84
|
+
await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`, {
|
|
85
|
+
method: "DELETE"
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async clearSession(sessionId) {
|
|
94
|
+
const result = await this.fetchApi(
|
|
95
|
+
`/api/memory/sessions/${this.encodePathSegment(sessionId)}`,
|
|
96
|
+
{ method: "DELETE" }
|
|
97
|
+
);
|
|
98
|
+
return result.cleared;
|
|
99
|
+
}
|
|
100
|
+
async stats() {
|
|
101
|
+
const stats = await this.fetchApi("/api/memory/stats");
|
|
102
|
+
return { ...stats, connected: true };
|
|
103
|
+
}
|
|
104
|
+
async shutdown() {
|
|
105
|
+
}
|
|
106
|
+
async list(params = {}) {
|
|
107
|
+
const searchParams = new URLSearchParams();
|
|
108
|
+
if (params.page != null) searchParams.set("page", String(params.page));
|
|
109
|
+
if (params.limit != null) searchParams.set("limit", String(params.limit));
|
|
110
|
+
if (params.type) searchParams.set("type", params.type);
|
|
111
|
+
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
112
|
+
const qs = searchParams.toString();
|
|
113
|
+
return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
// --- File ingest ---
|
|
116
|
+
/**
|
|
117
|
+
* Native streaming file ingest. Yields typed {@link IngestEvent}s as the
|
|
118
|
+
* server (parsing/storing) and the SDK (enrichment/result) make progress.
|
|
119
|
+
*
|
|
120
|
+
* Wire contract: SDK POSTs with `Accept: application/x-ndjson`; the server
|
|
121
|
+
* MUST respond with NDJSON. There is no JSON fallback — older servers
|
|
122
|
+
* that emit `application/json` for this endpoint are not supported, and
|
|
123
|
+
* the SDK yields a terminal `error` event in that case.
|
|
124
|
+
*
|
|
125
|
+
* After the server's terminal `result`, the SDK runs its own enrichment
|
|
126
|
+
* phase (image-describe → entity-extract → `/enrich` POST), emitting
|
|
127
|
+
* progress + heartbeat events around each step, then yields the single
|
|
128
|
+
* terminal `result` event with the merged SDK + server result.
|
|
129
|
+
*
|
|
130
|
+
* Promise-shaped consumers should iterate the returned AsyncIterable and
|
|
131
|
+
* collect the terminal event; there is no separate `ingestFile()` Promise
|
|
132
|
+
* method by design (one wire format, one SDK method).
|
|
133
|
+
*/
|
|
134
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch + SDK enrichment fan-out is two cooperating paths; documented inline and tested in memory-client-events.test.ts.
|
|
135
|
+
async *ingestFileEvents(file, options) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const relayAbort = () => controller.abort(options?.signal?.reason);
|
|
138
|
+
if (options?.signal?.aborted) relayAbort();
|
|
139
|
+
options?.signal?.addEventListener("abort", relayAbort, { once: true });
|
|
140
|
+
try {
|
|
141
|
+
const formData = new FormData();
|
|
142
|
+
formData.append("file", file);
|
|
143
|
+
if (options?.description) formData.append("description", options.description);
|
|
144
|
+
const wantsTextWindows = Boolean(options?.enrichment?.extractEntitiesV2);
|
|
145
|
+
const headers = {
|
|
146
|
+
Accept: "application/x-ndjson",
|
|
147
|
+
...this._authHeaders
|
|
148
|
+
};
|
|
149
|
+
if (wantsTextWindows) headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
|
|
150
|
+
let res;
|
|
151
|
+
try {
|
|
152
|
+
res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: formData,
|
|
155
|
+
headers,
|
|
156
|
+
signal: controller.signal
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
yield this.ingestErrorEvent(
|
|
160
|
+
this.translateFetchError(err, "/api/memory/ingest/file"),
|
|
161
|
+
"parsing"
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
166
|
+
if (!contentType.includes("application/x-ndjson")) {
|
|
167
|
+
yield this.ingestErrorEvent(
|
|
168
|
+
new MemoryServerError(
|
|
169
|
+
`Memory server returned ${contentType || "unknown content-type"} instead of application/x-ndjson \u2014 server is older than v0.15.0`,
|
|
170
|
+
res.status
|
|
171
|
+
),
|
|
172
|
+
"parsing"
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!res.body) {
|
|
177
|
+
yield this.ingestErrorEvent(
|
|
178
|
+
new MemoryServerError("Memory server returned an empty stream", res.status),
|
|
179
|
+
"parsing"
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const reader = res.body.getReader();
|
|
184
|
+
const decoder = new TextDecoder();
|
|
185
|
+
let buffer = "";
|
|
186
|
+
let currentStage = "parsing";
|
|
187
|
+
let serverResult = null;
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
const { done, value } = await reader.read();
|
|
191
|
+
if (done) break;
|
|
192
|
+
buffer += decoder.decode(value, { stream: true });
|
|
193
|
+
const lines = buffer.split("\n");
|
|
194
|
+
buffer = lines.pop() ?? "";
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (!line.trim()) continue;
|
|
197
|
+
const raw = JSON.parse(line);
|
|
198
|
+
const type = raw.type;
|
|
199
|
+
if (type === "progress" || type === "heartbeat") {
|
|
200
|
+
const stage = this.normalizeActiveIngestStage(raw.stage);
|
|
201
|
+
if (!stage) continue;
|
|
202
|
+
currentStage = stage;
|
|
203
|
+
yield { ...raw, schemaVersion: 1, type, stage };
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (type === "result") {
|
|
207
|
+
serverResult = this.fileIngestResultFromEvent({
|
|
208
|
+
...raw,
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
type: "result",
|
|
211
|
+
stage: "complete"
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (type === "error") {
|
|
216
|
+
yield {
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
type: "error",
|
|
219
|
+
stage: this.normalizeActiveIngestStage(raw.stage) ?? currentStage,
|
|
220
|
+
error: typeof raw.error === "string" ? raw.error : "File ingest failed",
|
|
221
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
222
|
+
code: typeof raw.code === "string" || typeof raw.code === "number" ? raw.code : void 0,
|
|
223
|
+
status: typeof raw.status === "number" ? raw.status : void 0
|
|
224
|
+
};
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (serverResult) break;
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
yield this.ingestErrorEvent(err, currentStage);
|
|
232
|
+
return;
|
|
233
|
+
} finally {
|
|
234
|
+
reader.releaseLock();
|
|
235
|
+
}
|
|
236
|
+
if (!serverResult) {
|
|
237
|
+
yield this.ingestErrorEvent(
|
|
238
|
+
new MemoryServerError("File ingest stream ended without a server result", 0),
|
|
239
|
+
currentStage
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
yield* this.completeIngestFileEvents(file, serverResult, options, controller.signal);
|
|
244
|
+
} finally {
|
|
245
|
+
options?.signal?.removeEventListener("abort", relayAbort);
|
|
246
|
+
controller.abort();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Run the SDK-side enrichment phase (image-describe → entity-extract →
|
|
251
|
+
* `/enrich` POST) for a server result, emitting progress + heartbeat
|
|
252
|
+
* events around the slow steps and yielding the single terminal
|
|
253
|
+
* {@link IngestResultEvent} at the end. Skips work cleanly when the
|
|
254
|
+
* server emitted no enrichment block or the caller wired no callbacks.
|
|
255
|
+
*/
|
|
256
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / no-op are documented decision branches; the fan-out covers the v0.15.0 enrichment phase + heartbeat plumbing.
|
|
257
|
+
async *completeIngestFileEvents(file, result, options, signal) {
|
|
258
|
+
try {
|
|
259
|
+
if (!result.enrichment || !options?.enrichment) {
|
|
260
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const enrichment = result.enrichment;
|
|
264
|
+
const { fileId, token, expiresAt, images } = enrichment;
|
|
265
|
+
const isV2 = "version" in enrichment;
|
|
266
|
+
const textWindows = isV2 ? enrichment.textWindows : [];
|
|
267
|
+
const descriptions = [];
|
|
268
|
+
const describeImage = options.enrichment.describeImage;
|
|
269
|
+
if (describeImage && images.length > 0) {
|
|
270
|
+
yield {
|
|
271
|
+
schemaVersion: 1,
|
|
272
|
+
type: "progress",
|
|
273
|
+
stage: "enrichment",
|
|
274
|
+
filename: file.name,
|
|
275
|
+
message: "Describing extracted images"
|
|
276
|
+
};
|
|
277
|
+
const CONCURRENCY = 5;
|
|
278
|
+
for (let i = 0; i < images.length; i += CONCURRENCY) {
|
|
279
|
+
const batch = images.slice(i, i + CONCURRENCY);
|
|
280
|
+
const batchResults = yield* this.withSdkHeartbeats(
|
|
281
|
+
"enrichment",
|
|
282
|
+
Promise.all(
|
|
283
|
+
batch.map(async (imageMeta) => {
|
|
284
|
+
const imageRes = await fetch(
|
|
285
|
+
`${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
|
|
286
|
+
{ headers: this._authHeaders, signal }
|
|
287
|
+
);
|
|
288
|
+
if (!imageRes.ok) {
|
|
289
|
+
throw new MemoryServerError(
|
|
290
|
+
`Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
|
|
291
|
+
imageRes.status
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const imageBuffer = await imageRes.arrayBuffer();
|
|
295
|
+
const description = await describeImage(imageBuffer, imageMeta);
|
|
296
|
+
return { imageId: imageMeta.imageId, description };
|
|
297
|
+
})
|
|
298
|
+
),
|
|
299
|
+
signal
|
|
300
|
+
);
|
|
301
|
+
descriptions.push(...batchResults);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let entities;
|
|
305
|
+
let relationships;
|
|
306
|
+
const imageDescriptionTexts = descriptions.map((d) => d.description);
|
|
307
|
+
if (options.enrichment.extractEntitiesV2 && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
|
|
308
|
+
yield {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
type: "progress",
|
|
311
|
+
stage: "enrichment",
|
|
312
|
+
filename: file.name,
|
|
313
|
+
message: "Extracting entities"
|
|
314
|
+
};
|
|
315
|
+
const extracted = yield* this.withSdkHeartbeats(
|
|
316
|
+
"enrichment",
|
|
317
|
+
options.enrichment.extractEntitiesV2({
|
|
318
|
+
textWindows,
|
|
319
|
+
imageDescriptions: imageDescriptionTexts,
|
|
320
|
+
mimeType: file.type,
|
|
321
|
+
filename: file.name
|
|
322
|
+
}),
|
|
323
|
+
signal
|
|
324
|
+
);
|
|
325
|
+
entities = extracted.entities;
|
|
326
|
+
relationships = extracted.relationships;
|
|
327
|
+
}
|
|
328
|
+
const hasGraph = (entities?.length ?? 0) > 0;
|
|
329
|
+
const hasImages = descriptions.length > 0;
|
|
330
|
+
if (!hasGraph && !hasImages) {
|
|
331
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
yield {
|
|
335
|
+
schemaVersion: 1,
|
|
336
|
+
type: "progress",
|
|
337
|
+
stage: "enrichment",
|
|
338
|
+
filename: file.name,
|
|
339
|
+
message: "Persisting enrichment"
|
|
340
|
+
};
|
|
341
|
+
const enrichRes = yield* this.withSdkHeartbeats(
|
|
342
|
+
"enrichment",
|
|
343
|
+
fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
"X-Enrichment-Token": `${token}:${expiresAt}`,
|
|
348
|
+
...this._authHeaders
|
|
349
|
+
},
|
|
350
|
+
signal,
|
|
351
|
+
body: JSON.stringify({ imageDescriptions: descriptions, entities, relationships })
|
|
352
|
+
}),
|
|
353
|
+
signal
|
|
354
|
+
);
|
|
355
|
+
if (!enrichRes.ok) {
|
|
356
|
+
const body = await enrichRes.json().catch(() => ({}));
|
|
357
|
+
throw new MemoryServerError(
|
|
358
|
+
body.error ?? `Enrichment failed: ${enrichRes.status}`,
|
|
359
|
+
enrichRes.status
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const enrichData = await this.parseApiResponse(enrichRes);
|
|
363
|
+
result.entryIds.push(...enrichData.entryIds);
|
|
364
|
+
result.chunks += descriptions.length;
|
|
365
|
+
result.totalCharacters += descriptions.reduce((sum, d) => sum + d.description.length, 0);
|
|
366
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
367
|
+
} catch (err) {
|
|
368
|
+
yield this.ingestErrorEvent(err, "enrichment");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Race a Promise against a periodic heartbeat tick. Yields a heartbeat
|
|
373
|
+
* IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
|
|
374
|
+
* settles, then returns the resolved value (or rethrows). Lets callers
|
|
375
|
+
* keep upstream sockets alive through long LLM/HTTP work without
|
|
376
|
+
* coupling the heartbeat cadence to the work itself.
|
|
377
|
+
*/
|
|
378
|
+
async *withSdkHeartbeats(stage, work, signal) {
|
|
379
|
+
let settled = false;
|
|
380
|
+
let value;
|
|
381
|
+
let thrown;
|
|
382
|
+
const done = work.then(
|
|
383
|
+
(v) => {
|
|
384
|
+
settled = true;
|
|
385
|
+
value = v;
|
|
386
|
+
},
|
|
387
|
+
(err) => {
|
|
388
|
+
settled = true;
|
|
389
|
+
thrown = err;
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
while (!settled) {
|
|
393
|
+
let timer;
|
|
394
|
+
const tick = new Promise((resolve) => {
|
|
395
|
+
timer = setTimeout(() => resolve("tick"), INGEST_EVENT_HEARTBEAT_MS);
|
|
396
|
+
});
|
|
397
|
+
let cleanupAbort = () => {
|
|
398
|
+
};
|
|
399
|
+
const abort = new Promise((resolve) => {
|
|
400
|
+
if (!signal) return;
|
|
401
|
+
if (signal.aborted) return resolve("abort");
|
|
402
|
+
const onAbort = () => resolve("abort");
|
|
403
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
404
|
+
cleanupAbort = () => signal.removeEventListener("abort", onAbort);
|
|
405
|
+
});
|
|
406
|
+
const winner = await Promise.race([done.then(() => "done"), tick, abort]);
|
|
407
|
+
if (timer) clearTimeout(timer);
|
|
408
|
+
cleanupAbort();
|
|
409
|
+
if (winner === "abort") throw new DOMException("File ingest aborted", "AbortError");
|
|
410
|
+
if (winner === "tick" && !settled) {
|
|
411
|
+
yield {
|
|
412
|
+
schemaVersion: 1,
|
|
413
|
+
type: "heartbeat",
|
|
414
|
+
stage,
|
|
415
|
+
message: "SDK enrichment still running"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await done;
|
|
420
|
+
if (thrown) throw thrown;
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
normalizeActiveIngestStage(stage) {
|
|
424
|
+
if (stage === "parsing" || stage === "storing" || stage === "enrichment") return stage;
|
|
425
|
+
if (stage === "chunking") return "storing";
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
fileIngestResultFromEvent(event) {
|
|
429
|
+
const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...result } = event;
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
ingestErrorEvent(error, stage) {
|
|
433
|
+
const status = error instanceof MemoryServerError ? error.status : error instanceof Error && error.name === "AbortError" ? 499 : void 0;
|
|
434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
435
|
+
return {
|
|
436
|
+
schemaVersion: 1,
|
|
437
|
+
type: "error",
|
|
438
|
+
stage,
|
|
439
|
+
error: message,
|
|
440
|
+
message,
|
|
441
|
+
...status != null ? { status, code: status } : {}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get the download URL for an uploaded file.
|
|
446
|
+
* Returns a URL that serves the original file binary with proper Content-Type.
|
|
447
|
+
*/
|
|
448
|
+
getFileDownloadUrl(filename) {
|
|
449
|
+
return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Download an uploaded file by filename.
|
|
453
|
+
* Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
|
|
454
|
+
*/
|
|
455
|
+
async downloadFile(filename) {
|
|
456
|
+
const url = this.getFileDownloadUrl(filename);
|
|
457
|
+
const res = await fetch(url, { headers: this._authHeaders });
|
|
458
|
+
if (!res.ok) {
|
|
459
|
+
throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
|
|
460
|
+
}
|
|
461
|
+
return res;
|
|
462
|
+
}
|
|
463
|
+
/** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
|
|
464
|
+
async listEntries(params) {
|
|
465
|
+
const result = await this.list(params);
|
|
466
|
+
return result.entries;
|
|
467
|
+
}
|
|
468
|
+
async graphNodes() {
|
|
469
|
+
const result = await this.fetchApi(
|
|
470
|
+
"/api/memory/graph/nodes"
|
|
471
|
+
);
|
|
472
|
+
return result.nodes;
|
|
473
|
+
}
|
|
474
|
+
async graphEdges() {
|
|
475
|
+
return this.fetchApi(
|
|
476
|
+
"/api/memory/graph/edges"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
async graphQuery(query) {
|
|
480
|
+
return this.fetchApi("/api/memory/graph/query", {
|
|
481
|
+
method: "POST",
|
|
482
|
+
body: JSON.stringify(query)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// --- ExtendedMemoryInterface methods ---
|
|
486
|
+
async consolidate() {
|
|
487
|
+
return this.fetchApi("/api/memory/consolidate", {
|
|
488
|
+
method: "POST"
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async forget(id, reason) {
|
|
492
|
+
try {
|
|
493
|
+
await this.fetchApi(`/api/memory/forget/${this.encodePathSegment(id)}`, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
body: JSON.stringify({ reason })
|
|
496
|
+
});
|
|
497
|
+
return true;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async summarizeSession(sessionId) {
|
|
504
|
+
try {
|
|
505
|
+
return await this.fetchApi(
|
|
506
|
+
`/api/memory/sessions/${this.encodePathSegment(sessionId)}/summarize`,
|
|
507
|
+
{ method: "POST" }
|
|
508
|
+
);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async runDecay() {
|
|
515
|
+
const result = await this.fetchApi("/api/memory/decay", {
|
|
516
|
+
method: "POST"
|
|
517
|
+
});
|
|
518
|
+
return result.archived;
|
|
519
|
+
}
|
|
520
|
+
async reindex() {
|
|
521
|
+
await this.fetchApi("/api/memory/reindex", { method: "POST" });
|
|
522
|
+
}
|
|
523
|
+
async clearGraph() {
|
|
524
|
+
const result = await this.fetchApi("/api/memory/graph/clear", {
|
|
525
|
+
method: "POST"
|
|
526
|
+
});
|
|
527
|
+
return result.deleted;
|
|
528
|
+
}
|
|
529
|
+
async deleteBySource(source) {
|
|
530
|
+
const result = await this.fetchApi(
|
|
531
|
+
`/api/memory/source/${this.encodePathSegment(source)}`,
|
|
532
|
+
{ method: "DELETE" }
|
|
533
|
+
);
|
|
534
|
+
return result.deleted;
|
|
535
|
+
}
|
|
536
|
+
async queryAsOf(asOfDate, filters = {}) {
|
|
537
|
+
const params = new URLSearchParams({ asOf: asOfDate });
|
|
538
|
+
if (filters.type) params.set("type", filters.type);
|
|
539
|
+
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
540
|
+
if (filters.source) params.set("source", filters.source);
|
|
541
|
+
if (filters.limit) params.set("limit", String(filters.limit));
|
|
542
|
+
const result = await this.fetchApi(
|
|
543
|
+
`/api/memory/query-as-of?${params}`
|
|
544
|
+
);
|
|
545
|
+
return result.entries;
|
|
546
|
+
}
|
|
547
|
+
async queryByEventTime(startTime, endTime, filters = {}) {
|
|
548
|
+
const params = new URLSearchParams({ startTime, endTime });
|
|
549
|
+
if (filters.type) params.set("type", filters.type);
|
|
550
|
+
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
551
|
+
if (filters.source) params.set("source", filters.source);
|
|
552
|
+
if (filters.limit) params.set("limit", String(filters.limit));
|
|
553
|
+
const result = await this.fetchApi(
|
|
554
|
+
`/api/memory/query-by-event-time?${params}`
|
|
555
|
+
);
|
|
556
|
+
return result.entries;
|
|
557
|
+
}
|
|
558
|
+
async fetchApi(path, options) {
|
|
559
|
+
const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
|
|
560
|
+
let res;
|
|
561
|
+
try {
|
|
562
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
563
|
+
...options,
|
|
564
|
+
headers: {
|
|
565
|
+
"Content-Type": "application/json",
|
|
566
|
+
...options?.headers,
|
|
567
|
+
...this._authHeaders
|
|
568
|
+
},
|
|
569
|
+
signal
|
|
570
|
+
});
|
|
571
|
+
} catch (err) {
|
|
572
|
+
throw this.translateFetchError(err, path);
|
|
573
|
+
}
|
|
574
|
+
return this.parseApiResponse(res);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Map fetch-layer rejections into a typed `MemoryServerError` so callers
|
|
578
|
+
* can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
|
|
579
|
+
* caller's signal generally fires an `AbortError`. Anything else (DNS,
|
|
580
|
+
* TCP reset, TLS) becomes a wrapped error with status 0.
|
|
581
|
+
*/
|
|
582
|
+
translateFetchError(err, path) {
|
|
583
|
+
if (err instanceof Error) {
|
|
584
|
+
if (err.name === "TimeoutError") {
|
|
585
|
+
return new MemoryServerError(
|
|
586
|
+
`Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
|
|
587
|
+
504
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (err.name === "AbortError") {
|
|
591
|
+
return new MemoryServerError(`Memory server request aborted (${path})`, 499);
|
|
592
|
+
}
|
|
593
|
+
return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
|
|
594
|
+
}
|
|
595
|
+
return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
|
|
596
|
+
}
|
|
597
|
+
/** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
|
|
598
|
+
async parseApiResponse(res) {
|
|
599
|
+
let body;
|
|
600
|
+
try {
|
|
601
|
+
body = await res.json();
|
|
602
|
+
} catch {
|
|
603
|
+
throw new MemoryServerError(
|
|
604
|
+
`Memory server error: invalid JSON response (${res.status})`,
|
|
605
|
+
res.status
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (!body?.success || body.data == null) {
|
|
609
|
+
throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
|
|
610
|
+
}
|
|
611
|
+
return body.data;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
export {
|
|
616
|
+
MemoryServerError,
|
|
617
|
+
MemoryClient
|
|
618
|
+
};
|
package/dist/dashboard.mjs
CHANGED