@pyxmate/memory 0.13.0 → 0.14.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MemoryClient
3
- } from "./chunk-4YIKI2BA.mjs";
3
+ } from "./chunk-QNRIO462.mjs";
4
4
 
5
5
  // ../dashboard/src/aggregations/consolidation-analytics.ts
6
6
  function analyzeConsolidationLog(entries) {
@@ -0,0 +1,658 @@
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
+ // --- Additional endpoints ---
116
+ /**
117
+ * Ingest a file with optional two-phase enrichment.
118
+ *
119
+ * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
120
+ * With enrichment callbacks: fetches extracted images, calls describeImage for each,
121
+ * optionally extracts entities, then submits enrichment data back to the server.
122
+ */
123
+ /**
124
+ * Ingest a file end-to-end: server upload + (optionally) SDK-side LLM
125
+ * enrichment + graph write. Returns the final {@link FileIngestResult}.
126
+ *
127
+ * This is the Promise-shaped convenience that consumes
128
+ * {@link MemoryClient.ingestFileEvents} internally — every consumer that
129
+ * doesn't need progress observability stays on this exact signature.
130
+ * Throws {@link MemoryServerError} on terminal error events.
131
+ */
132
+ async ingestFile(file, options) {
133
+ for await (const event of this.ingestFileEvents(file, options)) {
134
+ if (event.type === "result") return this.fileIngestResultFromEvent(event);
135
+ if (event.type === "error") {
136
+ throw new MemoryServerError(event.error, event.status ?? 500);
137
+ }
138
+ }
139
+ throw new MemoryServerError("File ingest stream ended without a terminal event", 0);
140
+ }
141
+ /**
142
+ * Native streaming ingest. Yields typed {@link IngestEvent}s as the server
143
+ * (parsing/storing) and the SDK (enrichment/result) make progress.
144
+ *
145
+ * Wire layout: SDK POSTs with `Accept: application/x-ndjson`; the server
146
+ * returns NDJSON `progress`/`heartbeat`/`result` events. After the server's
147
+ * terminal event, the SDK runs its own enrichment phase (image-describe,
148
+ * entity-extract, `/enrich` POST), emitting its own progress + heartbeat
149
+ * events around each step, then yields the single terminal `result` event.
150
+ *
151
+ * On older servers that don't honor `Accept: application/x-ndjson`, the
152
+ * SDK falls back to JSON parsing and synthesizes the SDK-side events as
153
+ * if it had streamed — same observable contract for callers either way.
154
+ */
155
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch + JSON fallback + SDK enrichment fan-out is genuinely 3 cooperating paths; each is documented inline and tested in memory-client-events.test.ts.
156
+ async *ingestFileEvents(file, options) {
157
+ const controller = new AbortController();
158
+ const relayAbort = () => controller.abort(options?.signal?.reason);
159
+ if (options?.signal?.aborted) relayAbort();
160
+ options?.signal?.addEventListener("abort", relayAbort, { once: true });
161
+ try {
162
+ const formData = new FormData();
163
+ formData.append("file", file);
164
+ if (options?.description) formData.append("description", options.description);
165
+ const wantsTextWindows = Boolean(
166
+ options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
167
+ );
168
+ const headers = {
169
+ Accept: "application/x-ndjson",
170
+ ...this._authHeaders
171
+ };
172
+ if (wantsTextWindows) headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
173
+ let res;
174
+ try {
175
+ res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
176
+ method: "POST",
177
+ body: formData,
178
+ headers,
179
+ signal: controller.signal
180
+ });
181
+ } catch (err) {
182
+ yield this.ingestErrorEvent(
183
+ this.translateFetchError(err, "/api/memory/ingest/file"),
184
+ "parsing"
185
+ );
186
+ return;
187
+ }
188
+ const contentType = res.headers.get("content-type") ?? "";
189
+ if (!contentType.includes("application/x-ndjson")) {
190
+ try {
191
+ const result = await this.parseApiResponse(res);
192
+ yield* this.completeIngestFileEvents(file, result, options, controller.signal);
193
+ } catch (err) {
194
+ yield this.ingestErrorEvent(err, "parsing");
195
+ }
196
+ return;
197
+ }
198
+ if (!res.body) {
199
+ yield this.ingestErrorEvent(
200
+ new MemoryServerError("Memory server returned an empty stream", res.status),
201
+ "parsing"
202
+ );
203
+ return;
204
+ }
205
+ const reader = res.body.getReader();
206
+ const decoder = new TextDecoder();
207
+ let buffer = "";
208
+ let currentStage = "parsing";
209
+ let serverResult = null;
210
+ try {
211
+ while (true) {
212
+ const { done, value } = await reader.read();
213
+ if (done) break;
214
+ buffer += decoder.decode(value, { stream: true });
215
+ const lines = buffer.split("\n");
216
+ buffer = lines.pop() ?? "";
217
+ for (const line of lines) {
218
+ if (!line.trim()) continue;
219
+ const raw = JSON.parse(line);
220
+ const type = raw.type;
221
+ if (type === "progress" || type === "heartbeat") {
222
+ const stage = this.normalizeActiveIngestStage(raw.stage);
223
+ if (!stage) continue;
224
+ currentStage = stage;
225
+ yield { ...raw, schemaVersion: 1, type, stage };
226
+ continue;
227
+ }
228
+ if (type === "result") {
229
+ serverResult = this.fileIngestResultFromEvent({
230
+ ...raw,
231
+ schemaVersion: 1,
232
+ type: "result",
233
+ stage: "complete"
234
+ });
235
+ break;
236
+ }
237
+ if (type === "error") {
238
+ yield {
239
+ schemaVersion: 1,
240
+ type: "error",
241
+ stage: this.normalizeActiveIngestStage(raw.stage) ?? currentStage,
242
+ error: typeof raw.error === "string" ? raw.error : "File ingest failed",
243
+ message: typeof raw.message === "string" ? raw.message : void 0,
244
+ code: typeof raw.code === "string" || typeof raw.code === "number" ? raw.code : void 0,
245
+ status: typeof raw.status === "number" ? raw.status : void 0
246
+ };
247
+ return;
248
+ }
249
+ }
250
+ if (serverResult) break;
251
+ }
252
+ } catch (err) {
253
+ yield this.ingestErrorEvent(err, currentStage);
254
+ return;
255
+ } finally {
256
+ reader.releaseLock();
257
+ }
258
+ if (!serverResult) {
259
+ yield this.ingestErrorEvent(
260
+ new MemoryServerError("File ingest stream ended without a server result", 0),
261
+ currentStage
262
+ );
263
+ return;
264
+ }
265
+ yield* this.completeIngestFileEvents(file, serverResult, options, controller.signal);
266
+ } finally {
267
+ options?.signal?.removeEventListener("abort", relayAbort);
268
+ controller.abort();
269
+ }
270
+ }
271
+ /**
272
+ * Run the SDK-side enrichment phase (image-describe → entity-extract →
273
+ * `/enrich` POST) for a server result, emitting progress + heartbeat
274
+ * events around the slow steps and yielding the single terminal
275
+ * {@link IngestResultEvent} at the end. Skips work cleanly when the
276
+ * server emitted no enrichment block or the caller wired no callbacks.
277
+ */
278
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / extract-entities-v1 / no-op are documented decision branches; this is the same fan-out the original ingestFile() had, plus heartbeat plumbing.
279
+ async *completeIngestFileEvents(file, result, options, signal) {
280
+ try {
281
+ if (!result.enrichment || !options?.enrichment) {
282
+ yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
283
+ return;
284
+ }
285
+ const enrichment = result.enrichment;
286
+ const { fileId, token, expiresAt, images } = enrichment;
287
+ const isV2 = "version" in enrichment;
288
+ const textWindows = isV2 ? enrichment.textWindows : [];
289
+ const descriptions = [];
290
+ const describeImage = options.enrichment.describeImage;
291
+ if (describeImage && images.length > 0) {
292
+ yield {
293
+ schemaVersion: 1,
294
+ type: "progress",
295
+ stage: "enrichment",
296
+ filename: file.name,
297
+ message: "Describing extracted images"
298
+ };
299
+ const CONCURRENCY = 5;
300
+ for (let i = 0; i < images.length; i += CONCURRENCY) {
301
+ const batch = images.slice(i, i + CONCURRENCY);
302
+ const batchResults = yield* this.withSdkHeartbeats(
303
+ "enrichment",
304
+ Promise.all(
305
+ batch.map(async (imageMeta) => {
306
+ const imageRes = await fetch(
307
+ `${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
308
+ { headers: this._authHeaders, signal }
309
+ );
310
+ if (!imageRes.ok) {
311
+ throw new MemoryServerError(
312
+ `Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
313
+ imageRes.status
314
+ );
315
+ }
316
+ const imageBuffer = await imageRes.arrayBuffer();
317
+ const description = await describeImage(imageBuffer, imageMeta);
318
+ return { imageId: imageMeta.imageId, description };
319
+ })
320
+ ),
321
+ signal
322
+ );
323
+ descriptions.push(...batchResults);
324
+ }
325
+ }
326
+ let entities;
327
+ let relationships;
328
+ const imageDescriptionTexts = descriptions.map((d) => d.description);
329
+ if (options.enrichment.extractEntitiesV2 && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
330
+ yield {
331
+ schemaVersion: 1,
332
+ type: "progress",
333
+ stage: "enrichment",
334
+ filename: file.name,
335
+ message: "Extracting entities"
336
+ };
337
+ const extracted = yield* this.withSdkHeartbeats(
338
+ "enrichment",
339
+ options.enrichment.extractEntitiesV2({
340
+ textWindows,
341
+ imageDescriptions: imageDescriptionTexts,
342
+ mimeType: file.type,
343
+ filename: file.name
344
+ }),
345
+ signal
346
+ );
347
+ entities = extracted.entities;
348
+ relationships = extracted.relationships;
349
+ } else if (options.enrichment.extractEntities) {
350
+ const allInputs = [...textWindows, ...imageDescriptionTexts];
351
+ if (allInputs.length > 0) {
352
+ yield {
353
+ schemaVersion: 1,
354
+ type: "progress",
355
+ stage: "enrichment",
356
+ filename: file.name,
357
+ message: "Extracting entities"
358
+ };
359
+ const extracted = yield* this.withSdkHeartbeats(
360
+ "enrichment",
361
+ options.enrichment.extractEntities(allInputs),
362
+ signal
363
+ );
364
+ entities = extracted.entities;
365
+ relationships = extracted.relationships;
366
+ }
367
+ }
368
+ const hasGraph = (entities?.length ?? 0) > 0;
369
+ const hasImages = descriptions.length > 0;
370
+ if (!hasGraph && !hasImages) {
371
+ yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
372
+ return;
373
+ }
374
+ yield {
375
+ schemaVersion: 1,
376
+ type: "progress",
377
+ stage: "enrichment",
378
+ filename: file.name,
379
+ message: "Persisting enrichment"
380
+ };
381
+ const enrichRes = yield* this.withSdkHeartbeats(
382
+ "enrichment",
383
+ fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
384
+ method: "POST",
385
+ headers: {
386
+ "Content-Type": "application/json",
387
+ "X-Enrichment-Token": `${token}:${expiresAt}`,
388
+ ...this._authHeaders
389
+ },
390
+ signal,
391
+ body: JSON.stringify({ imageDescriptions: descriptions, entities, relationships })
392
+ }),
393
+ signal
394
+ );
395
+ if (!enrichRes.ok) {
396
+ const body = await enrichRes.json().catch(() => ({}));
397
+ throw new MemoryServerError(
398
+ body.error ?? `Enrichment failed: ${enrichRes.status}`,
399
+ enrichRes.status
400
+ );
401
+ }
402
+ const enrichData = await this.parseApiResponse(enrichRes);
403
+ result.entryIds.push(...enrichData.entryIds);
404
+ result.chunks += descriptions.length;
405
+ result.totalCharacters += descriptions.reduce((sum, d) => sum + d.description.length, 0);
406
+ yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
407
+ } catch (err) {
408
+ yield this.ingestErrorEvent(err, "enrichment");
409
+ }
410
+ }
411
+ /**
412
+ * Race a Promise against a periodic heartbeat tick. Yields a heartbeat
413
+ * IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
414
+ * settles, then returns the resolved value (or rethrows). Lets callers
415
+ * keep upstream sockets alive through long LLM/HTTP work without
416
+ * coupling the heartbeat cadence to the work itself.
417
+ */
418
+ async *withSdkHeartbeats(stage, work, signal) {
419
+ let settled = false;
420
+ let value;
421
+ let thrown;
422
+ const done = work.then(
423
+ (v) => {
424
+ settled = true;
425
+ value = v;
426
+ },
427
+ (err) => {
428
+ settled = true;
429
+ thrown = err;
430
+ }
431
+ );
432
+ while (!settled) {
433
+ let timer;
434
+ const tick = new Promise((resolve) => {
435
+ timer = setTimeout(() => resolve("tick"), INGEST_EVENT_HEARTBEAT_MS);
436
+ });
437
+ let cleanupAbort = () => {
438
+ };
439
+ const abort = new Promise((resolve) => {
440
+ if (!signal) return;
441
+ if (signal.aborted) return resolve("abort");
442
+ const onAbort = () => resolve("abort");
443
+ signal.addEventListener("abort", onAbort, { once: true });
444
+ cleanupAbort = () => signal.removeEventListener("abort", onAbort);
445
+ });
446
+ const winner = await Promise.race([done.then(() => "done"), tick, abort]);
447
+ if (timer) clearTimeout(timer);
448
+ cleanupAbort();
449
+ if (winner === "abort") throw new DOMException("File ingest aborted", "AbortError");
450
+ if (winner === "tick" && !settled) {
451
+ yield {
452
+ schemaVersion: 1,
453
+ type: "heartbeat",
454
+ stage,
455
+ message: "SDK enrichment still running"
456
+ };
457
+ }
458
+ }
459
+ await done;
460
+ if (thrown) throw thrown;
461
+ return value;
462
+ }
463
+ normalizeActiveIngestStage(stage) {
464
+ if (stage === "parsing" || stage === "storing" || stage === "enrichment") return stage;
465
+ if (stage === "chunking") return "storing";
466
+ return null;
467
+ }
468
+ fileIngestResultFromEvent(event) {
469
+ const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...result } = event;
470
+ return result;
471
+ }
472
+ ingestErrorEvent(error, stage) {
473
+ const status = error instanceof MemoryServerError ? error.status : error instanceof Error && error.name === "AbortError" ? 499 : void 0;
474
+ const message = error instanceof Error ? error.message : String(error);
475
+ return {
476
+ schemaVersion: 1,
477
+ type: "error",
478
+ stage,
479
+ error: message,
480
+ message,
481
+ ...status != null ? { status, code: status } : {}
482
+ };
483
+ }
484
+ /**
485
+ * Get the download URL for an uploaded file.
486
+ * Returns a URL that serves the original file binary with proper Content-Type.
487
+ */
488
+ getFileDownloadUrl(filename) {
489
+ return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
490
+ }
491
+ /**
492
+ * Download an uploaded file by filename.
493
+ * Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
494
+ */
495
+ async downloadFile(filename) {
496
+ const url = this.getFileDownloadUrl(filename);
497
+ const res = await fetch(url, { headers: this._authHeaders });
498
+ if (!res.ok) {
499
+ throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
500
+ }
501
+ return res;
502
+ }
503
+ /** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
504
+ async listEntries(params) {
505
+ const result = await this.list(params);
506
+ return result.entries;
507
+ }
508
+ async graphNodes() {
509
+ const result = await this.fetchApi(
510
+ "/api/memory/graph/nodes"
511
+ );
512
+ return result.nodes;
513
+ }
514
+ async graphEdges() {
515
+ return this.fetchApi(
516
+ "/api/memory/graph/edges"
517
+ );
518
+ }
519
+ async graphQuery(query) {
520
+ return this.fetchApi("/api/memory/graph/query", {
521
+ method: "POST",
522
+ body: JSON.stringify(query)
523
+ });
524
+ }
525
+ // --- ExtendedMemoryInterface methods ---
526
+ async consolidate() {
527
+ return this.fetchApi("/api/memory/consolidate", {
528
+ method: "POST"
529
+ });
530
+ }
531
+ async forget(id, reason) {
532
+ try {
533
+ await this.fetchApi(`/api/memory/forget/${this.encodePathSegment(id)}`, {
534
+ method: "POST",
535
+ body: JSON.stringify({ reason })
536
+ });
537
+ return true;
538
+ } catch (error) {
539
+ if (error instanceof MemoryServerError && error.isNotFound) return false;
540
+ throw error;
541
+ }
542
+ }
543
+ async summarizeSession(sessionId) {
544
+ try {
545
+ return await this.fetchApi(
546
+ `/api/memory/sessions/${this.encodePathSegment(sessionId)}/summarize`,
547
+ { method: "POST" }
548
+ );
549
+ } catch (error) {
550
+ if (error instanceof MemoryServerError && error.isNotFound) return null;
551
+ throw error;
552
+ }
553
+ }
554
+ async runDecay() {
555
+ const result = await this.fetchApi("/api/memory/decay", {
556
+ method: "POST"
557
+ });
558
+ return result.archived;
559
+ }
560
+ async reindex() {
561
+ await this.fetchApi("/api/memory/reindex", { method: "POST" });
562
+ }
563
+ async clearGraph() {
564
+ const result = await this.fetchApi("/api/memory/graph/clear", {
565
+ method: "POST"
566
+ });
567
+ return result.deleted;
568
+ }
569
+ async deleteBySource(source) {
570
+ const result = await this.fetchApi(
571
+ `/api/memory/source/${this.encodePathSegment(source)}`,
572
+ { method: "DELETE" }
573
+ );
574
+ return result.deleted;
575
+ }
576
+ async queryAsOf(asOfDate, filters = {}) {
577
+ const params = new URLSearchParams({ asOf: asOfDate });
578
+ if (filters.type) params.set("type", filters.type);
579
+ if (filters.agentId) params.set("agentId", filters.agentId);
580
+ if (filters.source) params.set("source", filters.source);
581
+ if (filters.limit) params.set("limit", String(filters.limit));
582
+ const result = await this.fetchApi(
583
+ `/api/memory/query-as-of?${params}`
584
+ );
585
+ return result.entries;
586
+ }
587
+ async queryByEventTime(startTime, endTime, filters = {}) {
588
+ const params = new URLSearchParams({ startTime, endTime });
589
+ if (filters.type) params.set("type", filters.type);
590
+ if (filters.agentId) params.set("agentId", filters.agentId);
591
+ if (filters.source) params.set("source", filters.source);
592
+ if (filters.limit) params.set("limit", String(filters.limit));
593
+ const result = await this.fetchApi(
594
+ `/api/memory/query-by-event-time?${params}`
595
+ );
596
+ return result.entries;
597
+ }
598
+ async fetchApi(path, options) {
599
+ const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
600
+ let res;
601
+ try {
602
+ res = await fetch(`${this.baseUrl}${path}`, {
603
+ ...options,
604
+ headers: {
605
+ "Content-Type": "application/json",
606
+ ...options?.headers,
607
+ ...this._authHeaders
608
+ },
609
+ signal
610
+ });
611
+ } catch (err) {
612
+ throw this.translateFetchError(err, path);
613
+ }
614
+ return this.parseApiResponse(res);
615
+ }
616
+ /**
617
+ * Map fetch-layer rejections into a typed `MemoryServerError` so callers
618
+ * can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
619
+ * caller's signal generally fires an `AbortError`. Anything else (DNS,
620
+ * TCP reset, TLS) becomes a wrapped error with status 0.
621
+ */
622
+ translateFetchError(err, path) {
623
+ if (err instanceof Error) {
624
+ if (err.name === "TimeoutError") {
625
+ return new MemoryServerError(
626
+ `Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
627
+ 504
628
+ );
629
+ }
630
+ if (err.name === "AbortError") {
631
+ return new MemoryServerError(`Memory server request aborted (${path})`, 499);
632
+ }
633
+ return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
634
+ }
635
+ return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
636
+ }
637
+ /** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
638
+ async parseApiResponse(res) {
639
+ let body;
640
+ try {
641
+ body = await res.json();
642
+ } catch {
643
+ throw new MemoryServerError(
644
+ `Memory server error: invalid JSON response (${res.status})`,
645
+ res.status
646
+ );
647
+ }
648
+ if (!body?.success || body.data == null) {
649
+ throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
650
+ }
651
+ return body.data;
652
+ }
653
+ };
654
+
655
+ export {
656
+ MemoryServerError,
657
+ MemoryClient
658
+ };
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-DZZHJ66P.mjs";
15
- import "./chunk-4YIKI2BA.mjs";
14
+ } from "./chunk-JBKJDTFO.mjs";
15
+ import "./chunk-QNRIO462.mjs";
16
16
  export {
17
17
  DashboardClient,
18
18
  Poller,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StoreInput as StoreInput$1, MemoryEntry as MemoryEntry$1, MemorySearchParams as MemorySearchParams$1, MemorySearchResult as MemorySearchResult$1, MemoryType as MemoryType$1, MemoryStats as MemoryStats$1, ExtractedImageMeta, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1, FileIngestResult, GraphNode as GraphNode$1, GraphTraversalResult as GraphTraversalResult$1 } from '@pyx-memory/shared';
1
+ import { StoreInput as StoreInput$1, MemoryEntry as MemoryEntry$1, MemorySearchParams as MemorySearchParams$1, MemorySearchResult as MemorySearchResult$1, MemoryType as MemoryType$1, MemoryStats as MemoryStats$1, ExtractedImageMeta as ExtractedImageMeta$1, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1, FileIngestResult as FileIngestResult$1, IngestEvent as IngestEvent$1, GraphNode as GraphNode$1, GraphTraversalResult as GraphTraversalResult$1 } from '@pyx-memory/shared';
2
2
 
3
3
  /** Parameters for paginated entry listing. */
4
4
  interface MemoryListParams {
@@ -88,7 +88,7 @@ interface EnrichmentCallbacks {
88
88
  * metadata. Required for image-bearing files; safe to omit for text-only
89
89
  * uploads where the server emits zero images.
90
90
  */
91
- describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
91
+ describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta$1) => Promise<string>;
92
92
  /**
93
93
  * Legacy entity-extraction callback. Receives a flat string array which
94
94
  * the SDK constructs as `[...textWindows, ...imageDescriptions]` — callers
@@ -116,6 +116,16 @@ interface EnrichmentCallbacks {
116
116
  relationships: IngestRelationship$1[];
117
117
  }>;
118
118
  }
119
+ /**
120
+ * Options for {@link MemoryClient.ingestFile} and
121
+ * {@link MemoryClient.ingestFileEvents}. `signal` lets long-running ingests
122
+ * (LLM enrichment + graph writes) be cancelled cleanly.
123
+ */
124
+ interface IngestFileOptions {
125
+ description?: string;
126
+ enrichment?: EnrichmentCallbacks;
127
+ signal?: AbortSignal;
128
+ }
119
129
  /** Error thrown by MemoryClient when the server returns a non-success response. */
120
130
  declare class MemoryServerError extends Error {
121
131
  readonly status: number;
@@ -167,10 +177,50 @@ declare class MemoryClient implements ExtendedMemoryInterface {
167
177
  * With enrichment callbacks: fetches extracted images, calls describeImage for each,
168
178
  * optionally extracts entities, then submits enrichment data back to the server.
169
179
  */
170
- ingestFile(file: File, options?: {
171
- description?: string;
172
- enrichment?: EnrichmentCallbacks;
173
- }): Promise<FileIngestResult>;
180
+ /**
181
+ * Ingest a file end-to-end: server upload + (optionally) SDK-side LLM
182
+ * enrichment + graph write. Returns the final {@link FileIngestResult}.
183
+ *
184
+ * This is the Promise-shaped convenience that consumes
185
+ * {@link MemoryClient.ingestFileEvents} internally — every consumer that
186
+ * doesn't need progress observability stays on this exact signature.
187
+ * Throws {@link MemoryServerError} on terminal error events.
188
+ */
189
+ ingestFile(file: File, options?: IngestFileOptions): Promise<FileIngestResult$1>;
190
+ /**
191
+ * Native streaming ingest. Yields typed {@link IngestEvent}s as the server
192
+ * (parsing/storing) and the SDK (enrichment/result) make progress.
193
+ *
194
+ * Wire layout: SDK POSTs with `Accept: application/x-ndjson`; the server
195
+ * returns NDJSON `progress`/`heartbeat`/`result` events. After the server's
196
+ * terminal event, the SDK runs its own enrichment phase (image-describe,
197
+ * entity-extract, `/enrich` POST), emitting its own progress + heartbeat
198
+ * events around each step, then yields the single terminal `result` event.
199
+ *
200
+ * On older servers that don't honor `Accept: application/x-ndjson`, the
201
+ * SDK falls back to JSON parsing and synthesizes the SDK-side events as
202
+ * if it had streamed — same observable contract for callers either way.
203
+ */
204
+ ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent$1>;
205
+ /**
206
+ * Run the SDK-side enrichment phase (image-describe → entity-extract →
207
+ * `/enrich` POST) for a server result, emitting progress + heartbeat
208
+ * events around the slow steps and yielding the single terminal
209
+ * {@link IngestResultEvent} at the end. Skips work cleanly when the
210
+ * server emitted no enrichment block or the caller wired no callbacks.
211
+ */
212
+ private completeIngestFileEvents;
213
+ /**
214
+ * Race a Promise against a periodic heartbeat tick. Yields a heartbeat
215
+ * IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
216
+ * settles, then returns the resolved value (or rethrows). Lets callers
217
+ * keep upstream sockets alive through long LLM/HTTP work without
218
+ * coupling the heartbeat cadence to the work itself.
219
+ */
220
+ private withSdkHeartbeats;
221
+ private normalizeActiveIngestStage;
222
+ private fileIngestResultFromEvent;
223
+ private ingestErrorEvent;
174
224
  /**
175
225
  * Get the download URL for an uploaded file.
176
226
  * Returns a URL that serves the original file binary with proper Content-Type.
@@ -492,4 +542,105 @@ interface GraphTraversalResult {
492
542
  }>;
493
543
  }
494
544
 
495
- export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestRelationship, type IngestionResult, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, SensitivityLevel, type StoreInput, StoreTarget, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, VectorProvider };
545
+ /** Metadata for a single image extracted from a PDF. */
546
+ interface ExtractedImageMeta {
547
+ imageId: string;
548
+ pageNumber: number;
549
+ width: number;
550
+ height: number;
551
+ mimeType: 'image/png' | 'image/jpeg';
552
+ }
553
+ /**
554
+ * Legacy Phase 1 enrichment block (v1). Returned by servers running the
555
+ * pre-Patch-#2 pipeline OR by Patch-#2+ servers when the client does NOT
556
+ * signal `text_windows_v1` capability via the
557
+ * `X-Pyx-Enrichment-Capabilities` request header.
558
+ */
559
+ interface EnrichmentPendingV1 {
560
+ fileId: string;
561
+ token: string;
562
+ expiresAt: string;
563
+ images: ExtractedImageMeta[];
564
+ }
565
+ /** Truncation report for v2 text-window emission. Always present in v2. */
566
+ interface EnrichmentTextWindowsTruncation {
567
+ /** Total characters seen by the parser, including content beyond the cap. */
568
+ originalChars: number;
569
+ /** Sum of emitted window lengths. <= originalChars. */
570
+ includedChars: number;
571
+ windowCount: number;
572
+ truncated: boolean;
573
+ reason: 'maxWindows' | 'maxTotalChars' | null;
574
+ }
575
+ /**
576
+ * v2 enrichment block. Returned by Patch-#2+ servers when the client signals
577
+ * `text_windows_v1` capability. Adds `textWindows` so SDK callers that bring
578
+ * their own LLM can extract entities from any text-bearing file (text PDFs,
579
+ * .txt, .md, .docx, .pptx, .xlsx, …) — closing the structural gap that left
580
+ * Knowledge Graph + Vector Map blank for text-only uploads.
581
+ */
582
+ interface EnrichmentPendingV2 {
583
+ /** Discriminator. v1 omits this field. */
584
+ version: 2;
585
+ fileId: string;
586
+ token: string;
587
+ expiresAt: string;
588
+ images: ExtractedImageMeta[];
589
+ /** UTF-16 text windows for entity extraction. Empty when no text content. */
590
+ textWindows: string[];
591
+ textWindowsTruncation: EnrichmentTextWindowsTruncation;
592
+ }
593
+ /**
594
+ * Discriminated union over enrichment shapes. Narrow with `'version' in prep`
595
+ * before accessing v2-only fields.
596
+ */
597
+ type EnrichmentPending = EnrichmentPendingV1 | EnrichmentPendingV2;
598
+ /** Extended ingestion result when PDF contains images or v2 text windows. */
599
+ interface FileIngestResult {
600
+ filename: string;
601
+ fileType: string;
602
+ chunks: number;
603
+ entryIds: string[];
604
+ totalCharacters: number;
605
+ /** Present when images were extracted (v1) OR text windows / images were emitted (v2). */
606
+ enrichment?: EnrichmentPending;
607
+ }
608
+ /** Coarse pipeline stages. Stable vocabulary — finer detail goes in counters/message. */
609
+ type IngestStage = 'parsing' | 'storing' | 'enrichment' | 'complete';
610
+ /** Mid-stream progress notification — never carries the terminal result. */
611
+ interface IngestProgressEvent {
612
+ schemaVersion: 1;
613
+ type: 'progress';
614
+ stage: Exclude<IngestStage, 'complete'>;
615
+ filename?: string;
616
+ chunksStored?: number;
617
+ totalCharacters?: number;
618
+ message?: string;
619
+ }
620
+ /** Keepalive emitted while a stage is doing slow work (LLM, graph batch, etc). */
621
+ interface IngestHeartbeatEvent {
622
+ schemaVersion: 1;
623
+ type: 'heartbeat';
624
+ stage: Exclude<IngestStage, 'complete'>;
625
+ message?: string;
626
+ }
627
+ /** Terminal success — exactly one per stream, carries the full FileIngestResult. */
628
+ interface IngestResultEvent extends FileIngestResult {
629
+ schemaVersion: 1;
630
+ type: 'result';
631
+ stage: 'complete';
632
+ message?: string;
633
+ }
634
+ /** Terminal failure — exactly one per stream, mutually exclusive with result. */
635
+ interface IngestErrorEvent {
636
+ schemaVersion: 1;
637
+ type: 'error';
638
+ stage: IngestStage;
639
+ error: string;
640
+ message?: string;
641
+ code?: string | number;
642
+ status?: number;
643
+ }
644
+ type IngestEvent = IngestProgressEvent | IngestHeartbeatEvent | IngestResultEvent | IngestErrorEvent;
645
+
646
+ export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, SensitivityLevel, type StoreInput, StoreTarget, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, VectorProvider };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryClient,
3
3
  MemoryServerError
4
- } from "./chunk-4YIKI2BA.mjs";
4
+ } from "./chunk-QNRIO462.mjs";
5
5
 
6
6
  // ../shared/src/constants/defaults.ts
7
7
  var DEFAULTS = {
package/dist/react.mjs CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-DZZHJ66P.mjs";
15
- import "./chunk-4YIKI2BA.mjs";
14
+ } from "./chunk-JBKJDTFO.mjs";
15
+ import "./chunk-QNRIO462.mjs";
16
16
 
17
17
  // ../dashboard/src/hooks/use-consolidation-log.ts
18
18
  import { useCallback as useCallback2, useMemo } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -1,404 +0,0 @@
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 MemoryClient = class {
16
- baseUrl;
17
- _authHeaders;
18
- _requestTimeoutMs;
19
- constructor(memoryUrl, apiKeyOrOptions) {
20
- this.baseUrl = memoryUrl.replace(/\/$/, "");
21
- let apiKey;
22
- let defaultHeaders = {};
23
- let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
24
- if (typeof apiKeyOrOptions === "string") {
25
- apiKey = apiKeyOrOptions;
26
- } else if (apiKeyOrOptions) {
27
- apiKey = apiKeyOrOptions.apiKey;
28
- defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
29
- if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
30
- requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
31
- }
32
- }
33
- const trimmed = apiKey?.trim();
34
- this._authHeaders = {
35
- ...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
36
- ...defaultHeaders
37
- };
38
- this._requestTimeoutMs = requestTimeoutMs;
39
- }
40
- /** Encode a path segment to prevent URL injection */
41
- encodePathSegment(segment) {
42
- return encodeURIComponent(segment);
43
- }
44
- async initialize() {
45
- const response = await fetch(`${this.baseUrl}/health`, {
46
- headers: this._authHeaders
47
- });
48
- if (!response.ok) {
49
- throw new Error(`Memory server not reachable at ${this.baseUrl}: ${response.status}`);
50
- }
51
- }
52
- async store(entry) {
53
- return this.fetchApi("/api/memory/ingest", {
54
- method: "POST",
55
- body: JSON.stringify(entry)
56
- });
57
- }
58
- async search(params) {
59
- const searchParams = new URLSearchParams({ query: params.query });
60
- if (params.limit) searchParams.set("limit", String(params.limit));
61
- if (params.type) searchParams.set("type", params.type);
62
- if (params.agentId) searchParams.set("agentId", params.agentId);
63
- if (params.strategy) searchParams.set("strategy", params.strategy);
64
- if (params.eventTimeRange) {
65
- searchParams.set("eventTimeStart", params.eventTimeRange[0]);
66
- searchParams.set("eventTimeEnd", params.eventTimeRange[1]);
67
- }
68
- if (params.asOf) searchParams.set("asOf", params.asOf);
69
- if (params.abstentionThreshold != null)
70
- searchParams.set("abstentionThreshold", String(params.abstentionThreshold));
71
- return this.fetchApi(`/api/memory/search?${searchParams}`);
72
- }
73
- async get(id) {
74
- try {
75
- return await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`);
76
- } catch (error) {
77
- if (error instanceof MemoryServerError && error.isNotFound) return null;
78
- throw error;
79
- }
80
- }
81
- async delete(id) {
82
- try {
83
- await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`, {
84
- method: "DELETE"
85
- });
86
- return true;
87
- } catch (error) {
88
- if (error instanceof MemoryServerError && error.isNotFound) return false;
89
- throw error;
90
- }
91
- }
92
- async clearSession(sessionId) {
93
- const result = await this.fetchApi(
94
- `/api/memory/sessions/${this.encodePathSegment(sessionId)}`,
95
- { method: "DELETE" }
96
- );
97
- return result.cleared;
98
- }
99
- async stats() {
100
- const stats = await this.fetchApi("/api/memory/stats");
101
- return { ...stats, connected: true };
102
- }
103
- async shutdown() {
104
- }
105
- async list(params = {}) {
106
- const searchParams = new URLSearchParams();
107
- if (params.page != null) searchParams.set("page", String(params.page));
108
- if (params.limit != null) searchParams.set("limit", String(params.limit));
109
- if (params.type) searchParams.set("type", params.type);
110
- if (params.agentId) searchParams.set("agentId", params.agentId);
111
- const qs = searchParams.toString();
112
- return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
113
- }
114
- // --- Additional endpoints ---
115
- /**
116
- * Ingest a file with optional two-phase enrichment.
117
- *
118
- * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
119
- * With enrichment callbacks: fetches extracted images, calls describeImage for each,
120
- * optionally extracts entities, then submits enrichment data back to the server.
121
- */
122
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: two-phase enrichment fans out across image-describe + entity-extract + v1/v2 negotiation; each branch maps to a documented case in memory-client-enrichment.test.ts
123
- async ingestFile(file, options) {
124
- const formData = new FormData();
125
- formData.append("file", file);
126
- if (options?.description) {
127
- formData.append("description", options.description);
128
- }
129
- const wantsTextWindows = Boolean(
130
- options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
131
- );
132
- const headers = { ...this._authHeaders };
133
- if (wantsTextWindows) {
134
- headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
135
- }
136
- const res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
137
- method: "POST",
138
- body: formData,
139
- headers
140
- });
141
- const result = await this.parseApiResponse(res);
142
- if (!result.enrichment || !options?.enrichment) {
143
- return result;
144
- }
145
- const enrichment = result.enrichment;
146
- const { fileId, token, expiresAt, images } = enrichment;
147
- const isV2 = "version" in enrichment;
148
- const textWindows = isV2 ? enrichment.textWindows : [];
149
- const descriptions = [];
150
- const describeImage = options.enrichment.describeImage;
151
- if (describeImage && images.length > 0) {
152
- const CONCURRENCY = 5;
153
- for (let i = 0; i < images.length; i += CONCURRENCY) {
154
- const batch = images.slice(i, i + CONCURRENCY);
155
- const batchResults = await Promise.all(
156
- batch.map(async (imageMeta) => {
157
- const imageRes = await fetch(
158
- `${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
159
- { headers: this._authHeaders }
160
- );
161
- if (!imageRes.ok) {
162
- throw new MemoryServerError(
163
- `Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
164
- imageRes.status
165
- );
166
- }
167
- const imageBuffer = await imageRes.arrayBuffer();
168
- const description = await describeImage(imageBuffer, imageMeta);
169
- return { imageId: imageMeta.imageId, description };
170
- })
171
- );
172
- descriptions.push(...batchResults);
173
- }
174
- }
175
- let entities;
176
- let relationships;
177
- const v2Callback = options.enrichment.extractEntitiesV2;
178
- const legacyCallback = options.enrichment.extractEntities;
179
- const imageDescriptionTexts = descriptions.map((d) => d.description);
180
- if (v2Callback && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
181
- const extracted = await v2Callback({
182
- textWindows,
183
- imageDescriptions: imageDescriptionTexts,
184
- mimeType: file.type,
185
- filename: file.name
186
- });
187
- entities = extracted.entities;
188
- relationships = extracted.relationships;
189
- } else if (legacyCallback) {
190
- const allInputs = [...textWindows, ...imageDescriptionTexts];
191
- if (allInputs.length > 0) {
192
- const extracted = await legacyCallback(allInputs);
193
- entities = extracted.entities;
194
- relationships = extracted.relationships;
195
- }
196
- }
197
- const hasGraph = (entities?.length ?? 0) > 0;
198
- const hasImages = descriptions.length > 0;
199
- if (!hasGraph && !hasImages) {
200
- return result;
201
- }
202
- const enrichTokenHeader = `${token}:${expiresAt}`;
203
- const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
204
- method: "POST",
205
- headers: {
206
- "Content-Type": "application/json",
207
- "X-Enrichment-Token": enrichTokenHeader,
208
- ...this._authHeaders
209
- },
210
- body: JSON.stringify({
211
- imageDescriptions: descriptions,
212
- entities,
213
- relationships
214
- })
215
- });
216
- if (!enrichRes.ok) {
217
- const body = await enrichRes.json().catch(() => ({}));
218
- throw new MemoryServerError(
219
- body.error ?? `Enrichment failed: ${enrichRes.status}`,
220
- enrichRes.status
221
- );
222
- }
223
- const enrichData = await this.parseApiResponse(enrichRes);
224
- const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
225
- result.entryIds.push(...enrichData.entryIds);
226
- result.chunks += descriptions.length;
227
- result.totalCharacters += enrichCharacters;
228
- return result;
229
- }
230
- /**
231
- * Get the download URL for an uploaded file.
232
- * Returns a URL that serves the original file binary with proper Content-Type.
233
- */
234
- getFileDownloadUrl(filename) {
235
- return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
236
- }
237
- /**
238
- * Download an uploaded file by filename.
239
- * Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
240
- */
241
- async downloadFile(filename) {
242
- const url = this.getFileDownloadUrl(filename);
243
- const res = await fetch(url, { headers: this._authHeaders });
244
- if (!res.ok) {
245
- throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
246
- }
247
- return res;
248
- }
249
- /** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
250
- async listEntries(params) {
251
- const result = await this.list(params);
252
- return result.entries;
253
- }
254
- async graphNodes() {
255
- const result = await this.fetchApi(
256
- "/api/memory/graph/nodes"
257
- );
258
- return result.nodes;
259
- }
260
- async graphEdges() {
261
- return this.fetchApi(
262
- "/api/memory/graph/edges"
263
- );
264
- }
265
- async graphQuery(query) {
266
- return this.fetchApi("/api/memory/graph/query", {
267
- method: "POST",
268
- body: JSON.stringify(query)
269
- });
270
- }
271
- // --- ExtendedMemoryInterface methods ---
272
- async consolidate() {
273
- return this.fetchApi("/api/memory/consolidate", {
274
- method: "POST"
275
- });
276
- }
277
- async forget(id, reason) {
278
- try {
279
- await this.fetchApi(`/api/memory/forget/${this.encodePathSegment(id)}`, {
280
- method: "POST",
281
- body: JSON.stringify({ reason })
282
- });
283
- return true;
284
- } catch (error) {
285
- if (error instanceof MemoryServerError && error.isNotFound) return false;
286
- throw error;
287
- }
288
- }
289
- async summarizeSession(sessionId) {
290
- try {
291
- return await this.fetchApi(
292
- `/api/memory/sessions/${this.encodePathSegment(sessionId)}/summarize`,
293
- { method: "POST" }
294
- );
295
- } catch (error) {
296
- if (error instanceof MemoryServerError && error.isNotFound) return null;
297
- throw error;
298
- }
299
- }
300
- async runDecay() {
301
- const result = await this.fetchApi("/api/memory/decay", {
302
- method: "POST"
303
- });
304
- return result.archived;
305
- }
306
- async reindex() {
307
- await this.fetchApi("/api/memory/reindex", { method: "POST" });
308
- }
309
- async clearGraph() {
310
- const result = await this.fetchApi("/api/memory/graph/clear", {
311
- method: "POST"
312
- });
313
- return result.deleted;
314
- }
315
- async deleteBySource(source) {
316
- const result = await this.fetchApi(
317
- `/api/memory/source/${this.encodePathSegment(source)}`,
318
- { method: "DELETE" }
319
- );
320
- return result.deleted;
321
- }
322
- async queryAsOf(asOfDate, filters = {}) {
323
- const params = new URLSearchParams({ asOf: asOfDate });
324
- if (filters.type) params.set("type", filters.type);
325
- if (filters.agentId) params.set("agentId", filters.agentId);
326
- if (filters.source) params.set("source", filters.source);
327
- if (filters.limit) params.set("limit", String(filters.limit));
328
- const result = await this.fetchApi(
329
- `/api/memory/query-as-of?${params}`
330
- );
331
- return result.entries;
332
- }
333
- async queryByEventTime(startTime, endTime, filters = {}) {
334
- const params = new URLSearchParams({ startTime, endTime });
335
- if (filters.type) params.set("type", filters.type);
336
- if (filters.agentId) params.set("agentId", filters.agentId);
337
- if (filters.source) params.set("source", filters.source);
338
- if (filters.limit) params.set("limit", String(filters.limit));
339
- const result = await this.fetchApi(
340
- `/api/memory/query-by-event-time?${params}`
341
- );
342
- return result.entries;
343
- }
344
- async fetchApi(path, options) {
345
- const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
346
- let res;
347
- try {
348
- res = await fetch(`${this.baseUrl}${path}`, {
349
- ...options,
350
- headers: {
351
- "Content-Type": "application/json",
352
- ...options?.headers,
353
- ...this._authHeaders
354
- },
355
- signal
356
- });
357
- } catch (err) {
358
- throw this.translateFetchError(err, path);
359
- }
360
- return this.parseApiResponse(res);
361
- }
362
- /**
363
- * Map fetch-layer rejections into a typed `MemoryServerError` so callers
364
- * can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
365
- * caller's signal generally fires an `AbortError`. Anything else (DNS,
366
- * TCP reset, TLS) becomes a wrapped error with status 0.
367
- */
368
- translateFetchError(err, path) {
369
- if (err instanceof Error) {
370
- if (err.name === "TimeoutError") {
371
- return new MemoryServerError(
372
- `Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
373
- 504
374
- );
375
- }
376
- if (err.name === "AbortError") {
377
- return new MemoryServerError(`Memory server request aborted (${path})`, 499);
378
- }
379
- return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
380
- }
381
- return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
382
- }
383
- /** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
384
- async parseApiResponse(res) {
385
- let body;
386
- try {
387
- body = await res.json();
388
- } catch {
389
- throw new MemoryServerError(
390
- `Memory server error: invalid JSON response (${res.status})`,
391
- res.status
392
- );
393
- }
394
- if (!body?.success || body.data == null) {
395
- throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
396
- }
397
- return body.data;
398
- }
399
- };
400
-
401
- export {
402
- MemoryServerError,
403
- MemoryClient
404
- };