@pyxmate/memory 0.14.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MemoryClient
3
- } from "./chunk-QNRIO462.mjs";
3
+ } from "./chunk-W4LB326D.mjs";
4
4
 
5
5
  // ../dashboard/src/aggregations/consolidation-analytics.ts
6
6
  function analyzeConsolidationLog(entries) {
@@ -112,47 +112,26 @@ var MemoryClient = class {
112
112
  const qs = searchParams.toString();
113
113
  return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
114
114
  }
115
- // --- Additional endpoints ---
115
+ // --- File ingest ---
116
116
  /**
117
- * Ingest a file with optional two-phase enrichment.
117
+ * Native streaming file ingest. Yields typed {@link IngestEvent}s as the
118
+ * server (parsing/storing) and the SDK (enrichment/result) make progress.
118
119
  *
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}.
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.
126
124
  *
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.
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.
144
129
  *
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.
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).
154
133
  */
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.
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.
156
135
  async *ingestFileEvents(file, options) {
157
136
  const controller = new AbortController();
158
137
  const relayAbort = () => controller.abort(options?.signal?.reason);
@@ -162,9 +141,7 @@ var MemoryClient = class {
162
141
  const formData = new FormData();
163
142
  formData.append("file", file);
164
143
  if (options?.description) formData.append("description", options.description);
165
- const wantsTextWindows = Boolean(
166
- options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
167
- );
144
+ const wantsTextWindows = Boolean(options?.enrichment?.extractEntitiesV2);
168
145
  const headers = {
169
146
  Accept: "application/x-ndjson",
170
147
  ...this._authHeaders
@@ -187,12 +164,13 @@ var MemoryClient = class {
187
164
  }
188
165
  const contentType = res.headers.get("content-type") ?? "";
189
166
  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
- }
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
+ );
196
174
  return;
197
175
  }
198
176
  if (!res.body) {
@@ -275,7 +253,7 @@ var MemoryClient = class {
275
253
  * {@link IngestResultEvent} at the end. Skips work cleanly when the
276
254
  * server emitted no enrichment block or the caller wired no callbacks.
277
255
  */
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.
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.
279
257
  async *completeIngestFileEvents(file, result, options, signal) {
280
258
  try {
281
259
  if (!result.enrichment || !options?.enrichment) {
@@ -346,24 +324,6 @@ var MemoryClient = class {
346
324
  );
347
325
  entities = extracted.entities;
348
326
  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
327
  }
368
328
  const hasGraph = (entities?.length ?? 0) > 0;
369
329
  const hasImages = descriptions.length > 0;
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-JBKJDTFO.mjs";
15
- import "./chunk-QNRIO462.mjs";
14
+ } from "./chunk-K7JZXUBN.mjs";
15
+ import "./chunk-W4LB326D.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 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';
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, 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 {
@@ -72,15 +72,14 @@ interface ExtendedMemoryInterface extends MemoryInterface {
72
72
  }
73
73
 
74
74
  /**
75
- * Callbacks for two-phase file enrichment. All callbacks are optional so the
76
- * SDK can support every combination of the v2 flow:
75
+ * Callbacks for two-phase file enrichment. All callbacks are optional:
77
76
  * - Image-rich PDF + describeImage only → describes images, no entity extraction
78
- * - Image-rich PDF + describeImage + extractEntities[V2] → describes + extracts
79
- * - Text-only file + extractEntities[V2] only → extracts from textWindows
80
- * - Mixed file + all three → describes + extracts from both sources
77
+ * - Image-rich PDF + describeImage + extractEntitiesV2 → describes + extracts
78
+ * - Text-only file + extractEntitiesV2 only → extracts from textWindows
79
+ * - Mixed file + both → describes images + extracts from both sources
81
80
  *
82
- * Without ANY callback, ingestFile returns the parsed result without running
83
- * Phase 2/3 — caller opted out of enrichment entirely.
81
+ * Without any callback, the ingest stream emits the server result with no
82
+ * SDK-side enrichment — caller opted out entirely.
84
83
  */
85
84
  interface EnrichmentCallbacks {
86
85
  /**
@@ -90,21 +89,10 @@ interface EnrichmentCallbacks {
90
89
  */
91
90
  describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta$1) => Promise<string>;
92
91
  /**
93
- * Legacy entity-extraction callback. Receives a flat string array which
94
- * the SDK constructs as `[...textWindows, ...imageDescriptions]` callers
95
- * cannot distinguish the two sources. Kept for backwards compatibility;
96
- * new code should prefer {@link extractEntitiesV2}.
97
- */
98
- extractEntities?: (descriptions: string[]) => Promise<{
99
- entities: IngestEntity$1[];
100
- relationships: IngestRelationship$1[];
101
- }>;
102
- /**
103
- * v2 entity-extraction callback. Receives text windows and image
104
- * descriptions separately so callers can apply different prompts per
105
- * source. Takes precedence over {@link extractEntities} when both are
106
- * defined. Triggers the `X-Pyx-Enrichment-Capabilities: text_windows_v1`
107
- * negotiation header on ingest.
92
+ * Entity-extraction callback. Receives text windows and image descriptions
93
+ * separately so callers can apply different prompts per source. Triggers
94
+ * the `X-Pyx-Enrichment-Capabilities: text_windows_v1` negotiation header
95
+ * on ingest.
108
96
  */
109
97
  extractEntitiesV2?: (input: {
110
98
  textWindows: string[];
@@ -117,9 +105,8 @@ interface EnrichmentCallbacks {
117
105
  }>;
118
106
  }
119
107
  /**
120
- * Options for {@link MemoryClient.ingestFile} and
121
- * {@link MemoryClient.ingestFileEvents}. `signal` lets long-running ingests
122
- * (LLM enrichment + graph writes) be cancelled cleanly.
108
+ * Options for {@link MemoryClient.ingestFileEvents}. `signal` lets
109
+ * long-running ingests (LLM enrichment + graph writes) be cancelled cleanly.
123
110
  */
124
111
  interface IngestFileOptions {
125
112
  description?: string;
@@ -171,35 +158,22 @@ declare class MemoryClient implements ExtendedMemoryInterface {
171
158
  shutdown(): Promise<void>;
172
159
  list(params?: MemoryListParams): Promise<MemoryListResult>;
173
160
  /**
174
- * Ingest a file with optional two-phase enrichment.
161
+ * Native streaming file ingest. Yields typed {@link IngestEvent}s as the
162
+ * server (parsing/storing) and the SDK (enrichment/result) make progress.
175
163
  *
176
- * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
177
- * With enrichment callbacks: fetches extracted images, calls describeImage for each,
178
- * optionally extracts entities, then submits enrichment data back to the server.
179
- */
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.
164
+ * Wire contract: SDK POSTs with `Accept: application/x-ndjson`; the server
165
+ * MUST respond with NDJSON. There is no JSON fallback — older servers
166
+ * that emit `application/json` for this endpoint are not supported, and
167
+ * the SDK yields a terminal `error` event in that case.
193
168
  *
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.
169
+ * After the server's terminal `result`, the SDK runs its own enrichment
170
+ * phase (image-describe entity-extract `/enrich` POST), emitting
171
+ * progress + heartbeat events around each step, then yields the single
172
+ * terminal `result` event with the merged SDK + server result.
199
173
  *
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.
174
+ * Promise-shaped consumers should iterate the returned AsyncIterable and
175
+ * collect the terminal event; there is no separate `ingestFile()` Promise
176
+ * method by design (one wire format, one SDK method).
203
177
  */
204
178
  ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent$1>;
205
179
  /**
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryClient,
3
3
  MemoryServerError
4
- } from "./chunk-QNRIO462.mjs";
4
+ } from "./chunk-W4LB326D.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-JBKJDTFO.mjs";
15
- import "./chunk-QNRIO462.mjs";
14
+ } from "./chunk-K7JZXUBN.mjs";
15
+ import "./chunk-W4LB326D.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.14.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -207,7 +207,7 @@ pyx-memory has two auth tiers: `API_KEY` (read + write) and `ADMIN_API_KEY` (des
207
207
 
208
208
  | Key | Allowed operations |
209
209
  |-----|-------------------|
210
- | `API_KEY` | All read operations (search, get, list, stats) + write operations (store, ingestFile) |
210
+ | `API_KEY` | All read operations (search, get, list, stats) + write operations (store, ingestFileEvents) |
211
211
  | `ADMIN_API_KEY` | Destructive operations: DELETE, forget, decay, consolidate, reindex, deleteBySource |
212
212
 
213
213
  ```bash
@@ -42,18 +42,24 @@ if (memory) {
42
42
  const nodes = await memory.graphNodes();
43
43
  const traversal = await memory.graphQuery({ nodeId: 'node-1', depth: 2 });
44
44
 
45
- // File ingestion — also available on MemoryClient
45
+ // File ingestion — native NDJSON streaming via ingestFileEvents()
46
46
  const file = new File([buffer], 'report.pdf', { type: 'application/pdf' });
47
- const result = await memory.ingestFile(file);
47
+ for await (const event of memory.ingestFileEvents(file)) {
48
+ if (event.type === 'progress') console.log(`[${event.stage}] ${event.message ?? ''}`);
49
+ if (event.type === 'error') throw new Error(event.message ?? event.error);
50
+ // event.type === 'result' carries the terminal { filename, chunks, entryIds, ... }
51
+ }
48
52
 
49
53
  // Image ingestion with AI description
50
54
  // pyx-memory saves the original image to {DATA_DIR}/files/ and indexes the
51
55
  // description in vector + SQLite for semantic search. Without a description,
52
56
  // images get a minimal placeholder ("[Image] filename (size KB)").
53
57
  const image = new File([imgBuffer], 'photo.png', { type: 'image/png' });
54
- const imgResult = await memory.ingestFile(image, {
58
+ for await (const _ of memory.ingestFileEvents(image, {
55
59
  description: 'A screenshot of the login page showing an error dialog',
56
- });
60
+ })) {
61
+ /* drain to terminal */
62
+ }
57
63
 
58
64
  // Lifecycle
59
65
  await memory.consolidate();
@@ -1,7 +1,7 @@
1
1
  # Pattern: File Uploads
2
2
 
3
3
  This is consumer-side guidance: how to decide whether to forward a file
4
- straight to `ingestFile()` or to pre-extract its text upstream first.
4
+ straight to `ingestFileEvents()` or to pre-extract its text upstream first.
5
5
 
6
6
  ## TL;DR
7
7
 
@@ -90,53 +90,42 @@ For deterministic peak memory or timing (production UX), consumers should pre-ex
90
90
 
91
91
  **Image ingestion**: Images cannot have text extracted. Pass a `description` field with a natural-language description (e.g., from an LLM with vision). Without a description, images get a minimal placeholder (`[Image] filename (size KB)`).
92
92
 
93
- **PDF enrichment**: When a PDF contains images (≥50x50px), the response includes an `enrichment` block. The SDK's `ingestFile()` with `EnrichmentCallbacks` handles the full flow automatically — see [sdk-guide.md](sdk-guide.md#two-phase-pdf-enrichment).
93
+ **PDF enrichment**: When a PDF contains images (≥50x50px), the server emits an `enrichment` block on the terminal `result` event. The SDK's `ingestFileEvents()` with `EnrichmentCallbacks` handles the full flow automatically — see [sdk-guide.md](sdk-guide.md#two-phase-pdf-enrichment).
94
94
 
95
- ### Streaming Progress (NDJSON)
95
+ ### NDJSON Wire Format (the only response shape)
96
96
 
97
- For real-time ingestion progress, request NDJSON streaming by setting the `Accept` header or a query parameter:
98
-
99
- ```bash
100
- # Via Accept header
101
- curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
102
- -H "Authorization: Bearer {{API_KEY}}" \
103
- -H "Accept: application/x-ndjson" \
104
- -F "file=@large-report.pdf"
105
-
106
- # Via query parameter
107
- curl -X POST "{{ENDPOINT}}/api/memory/ingest/file?stream=true" \
108
- -H "Authorization: Bearer {{API_KEY}}" \
109
- -F "file=@large-report.pdf"
110
- ```
111
-
112
- The response is streamed as newline-delimited JSON (`Content-Type: application/x-ndjson`). Each line is a JSON object:
97
+ `POST /api/memory/ingest/file` always streams `Content-Type: application/x-ndjson`. Each line is one typed event with `schemaVersion: 1`. The terminal event is exactly one of `result` (success) or `error` (failure):
113
98
 
114
99
  ```
115
- {"type":"progress","stage":"parsing","filename":"large-report.pdf","message":"Extracting text..."}
116
- {"type":"progress","stage":"storing","filename":"large-report.pdf","chunksStored":10,"totalCharacters":4800,"message":"Storing chunk 10..."}
117
- {"type":"progress","stage":"storing","filename":"large-report.pdf","chunksStored":20,"totalCharacters":9600,"message":"Storing chunk 20..."}
118
- {"type":"progress","stage":"complete","filename":"large-report.pdf","chunksStored":24,"totalCharacters":11520,"message":"Ingestion complete"}
119
- {"type":"result","filename":"large-report.pdf","fileType":".pdf","chunks":24,"entryIds":[...],"totalCharacters":11520}
100
+ {"schemaVersion":1,"type":"progress","stage":"parsing","filename":"large-report.pdf","message":"Extracting text..."}
101
+ {"schemaVersion":1,"type":"progress","stage":"storing","filename":"large-report.pdf","chunksStored":10,"totalCharacters":4800}
102
+ {"schemaVersion":1,"type":"heartbeat","stage":"storing","message":"File ingest still running"}
103
+ {"schemaVersion":1,"type":"progress","stage":"enrichment","filename":"large-report.pdf"}
104
+ {"schemaVersion":1,"type":"result","stage":"complete","filename":"large-report.pdf","fileType":".pdf","chunks":24,"entryIds":["…"],"totalCharacters":11520}
120
105
  ```
121
106
 
122
- **Progress stages**: `parsing` (text extraction), `storing` (chunk storage emitted every batch), `enrichment` (PDF image extraction), `complete` (done).
107
+ **Stable stages**: `parsing` (text/image extraction), `storing` (chunk + vector writes), `enrichment` (LLM image-describe + entity-extract + `/enrich` POST), `complete` (terminal result only). Finer detail goes in optional counters (`chunksStored`, `totalCharacters`) and `message`, not in new stage names.
108
+
109
+ **Heartbeat**: emitted every 20s while the pipeline is doing slow work. Sized below undici's default 300s `headersTimeout` and Envoy's default 5m stream-idle so an LLM call or graph batch never trips an upstream socket timeout.
123
110
 
124
- The last line always has `"type":"result"` and contains the full `IngestionResult` (same shape as the non-streaming JSON response). On error, the last line has `"type":"error"` with an `error` message string.
111
+ **Errors before the stream starts** (multipart parse, file size cap, validation) come back as a single-line NDJSON `error` event with the appropriate HTTP status code on the response.
125
112
 
126
- Without the `Accept: application/x-ndjson` header (or `?stream=true`), the endpoint behaves exactly as beforereturning a single JSON response after ingestion completes.
113
+ Pre-v0.15.0 servers returned `application/json` for this endpoint. v0.15.0 SDK consumers will see a terminal `error` event with the message `Memory server returned application/json instead of application/x-ndjson server is older than v0.15.0` if they hit such a server; upgrade the server.
127
114
 
128
115
  ### Example: ingest a document
129
116
 
130
117
  ```bash
131
- curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
118
+ curl -N -X POST {{ENDPOINT}}/api/memory/ingest/file \
132
119
  -H "Authorization: Bearer {{API_KEY}}" \
133
120
  -F "file=@report.pdf"
134
121
  ```
135
122
 
123
+ `-N` disables curl buffering so events appear as the server emits them.
124
+
136
125
  ### Example: ingest an image with description
137
126
 
138
127
  ```bash
139
- curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
128
+ curl -N -X POST {{ENDPOINT}}/api/memory/ingest/file \
140
129
  -H "Authorization: Bearer {{API_KEY}}" \
141
130
  -F "file=@screenshot.png" \
142
131
  -F "description=A screenshot of the dashboard showing memory usage at 85%"
@@ -145,29 +134,45 @@ curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
145
134
  ### SDK usage
146
135
 
147
136
  ```typescript
137
+ import { MemoryClient, type FileIngestResult } from '@pyxmate/memory';
138
+
139
+ // Iterate the AsyncIterable. The terminal event is `result` (success) or `error` (failure).
140
+ async function ingestAndCollect(memory: MemoryClient, file: File): Promise<FileIngestResult> {
141
+ for await (const event of memory.ingestFileEvents(file)) {
142
+ if (event.type === 'progress' || event.type === 'heartbeat') continue;
143
+ if (event.type === 'error') throw new Error(event.message ?? event.error);
144
+ // event.type === 'result'
145
+ const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...result } = event;
146
+ return result;
147
+ }
148
+ throw new Error('memory ingest stream ended without a terminal event');
149
+ }
150
+
148
151
  // Document (text-only)
149
152
  const doc = new File([buffer], 'report.pdf', { type: 'application/pdf' });
150
- await memory.ingestFile(doc);
153
+ await ingestAndCollect(memory, doc);
151
154
 
152
155
  // Image with description
153
156
  const img = new File([imgBuffer], 'photo.png', { type: 'image/png' });
154
- await memory.ingestFile(img, {
157
+ await ingestAndCollect(memory, img); // pass via options when needed
158
+ for await (const _ of memory.ingestFileEvents(img, {
155
159
  description: 'A photo of the whiteboard from the architecture meeting',
156
- });
160
+ })) { /* drain */ }
157
161
 
158
162
  // PDF with two-phase enrichment (images described via LLM vision)
159
- await memory.ingestFile(pdfFile, {
163
+ for await (const event of memory.ingestFileEvents(pdfFile, {
160
164
  enrichment: {
161
- describeImage: async (buffer, meta) => {
162
- // Call your LLM vision model to describe the image
163
- return 'A bar chart showing Q4 revenue growth of 23%';
164
- },
165
- extractEntities: async (descriptions) => ({
165
+ describeImage: async (buffer, meta) => 'A bar chart showing Q4 revenue growth of 23%',
166
+ extractEntitiesV2: async ({ textWindows, imageDescriptions, filename, mimeType }) => ({
166
167
  entities: [{ name: 'Q4 Revenue', type: 'METRIC' }],
167
168
  relationships: [],
168
169
  }),
169
170
  },
170
- });
171
+ })) {
172
+ if (event.type === 'progress') {
173
+ console.log(`[${event.stage}] ${event.message ?? ''}`);
174
+ }
175
+ }
171
176
  ```
172
177
 
173
178
  ## File Downloads
@@ -201,13 +206,13 @@ const buffer = await response.arrayBuffer();
201
206
 
202
207
  ## Two-Phase PDF Enrichment
203
208
 
204
- When a PDF contains extractable images, the server returns an `enrichment` block from `POST /api/memory/ingest/file`. The SDK wraps the full three-phase flow into a single `ingestFile()` call with `EnrichmentCallbacks`.
209
+ When a PDF contains extractable images, the server's terminal `result` event carries an `enrichment` block. The SDK's `ingestFileEvents()` with `EnrichmentCallbacks` runs the full three-phase flow automatically, emitting its own `enrichment` progress + heartbeat events while it works.
205
210
 
206
211
  ### Flow
207
212
 
208
213
  ```
209
- Phase 1: POST /api/memory/ingest/file → text stored immediately
210
- Response: { ...result, enrichment: { fileId, token, expiresAt, images: [...] } }
214
+ Phase 1: POST /api/memory/ingest/file → text stored immediately, enrichment block emitted on terminal `result`
215
+ Result event: { schemaVersion: 1, type: 'result', stage: 'complete', ...ingestResult, enrichment: { fileId, token, expiresAt, images: [...] } }
211
216
 
212
217
  Phase 2: GET /api/memory/files/{fileId}/images/{imageId}?token=... → binary image
213
218
  (SDK fetches each image, calls describeImage callback)
@@ -179,36 +179,44 @@ All from `@pyx-memory/core` except `MemoryServerError` from `@pyx-memory/client`
179
179
 
180
180
  ## Two-Phase PDF Enrichment
181
181
 
182
- When ingesting PDFs that contain images, `ingestFile()` supports a two-phase enrichment flow via `EnrichmentCallbacks`. The SDK handles all three phases automatically:
182
+ When ingesting PDFs that contain images, `ingestFileEvents()` runs a three-phase enrichment flow via `EnrichmentCallbacks`. The SDK handles all phases automatically and emits typed `IngestEvent`s as it goes:
183
183
 
184
- 1. **Upload** — text extracted and stored immediately
185
- 2. **Image description** — each extracted image is fetched and described via your LLM vision callback (batched, 5 concurrent)
186
- 3. **Enrichment** — descriptions + entities submitted back to the server
184
+ 1. **Server upload** — text extracted and stored immediately; server emits `progress` events with stage `parsing` then `storing`
185
+ 2. **Image description** — each extracted image is fetched and described via your LLM vision callback (batched, 5 concurrent); SDK emits `progress`/`heartbeat` events with stage `enrichment`
186
+ 3. **Enrichment write** — descriptions + entities POSTed back to the server's `/enrich` endpoint; SDK emits the terminal `result` event
187
187
 
188
188
  ```typescript
189
- import { MemoryClient, type EnrichmentCallbacks } from '@pyx-memory/client';
189
+ import { MemoryClient, type EnrichmentCallbacks, type FileIngestResult } from '@pyxmate/memory';
190
190
 
191
191
  const memory = new MemoryClient('http://localhost:7822', apiKey);
192
192
 
193
193
  const enrichment: EnrichmentCallbacks = {
194
- // Required: describe each image using LLM vision
194
+ // Optional: describe each image using LLM vision
195
195
  describeImage: async (imageBuffer, meta) => {
196
196
  // imageBuffer is ArrayBuffer of the extracted PNG/JPEG
197
197
  // meta has: imageId, pageNumber, width, height, mimeType
198
- const description = await myVisionModel.describe(imageBuffer);
199
- return description;
198
+ return await myVisionModel.describe(imageBuffer);
200
199
  },
201
- // Optional: extract entities from all descriptions at once
202
- extractEntities: async (descriptions) => ({
200
+ // Optional: extract entities from text windows + image descriptions
201
+ extractEntitiesV2: async ({ textWindows, imageDescriptions, filename, mimeType }) => ({
203
202
  entities: [{ name: 'Revenue', type: 'METRIC' }],
204
203
  relationships: [{ source: 'Revenue', target: 'Q4', type: 'RELATED_TO' }],
205
204
  }),
206
205
  };
207
206
 
208
- const result = await memory.ingestFile(pdfFile, { enrichment });
209
- // result.enrichment is defined when PDF had images
210
- // Text chunks are already stored from Phase 1
211
- // Image descriptions are stored from Phase 3
207
+ let result: FileIngestResult | null = null;
208
+ for await (const event of memory.ingestFileEvents(pdfFile, { enrichment })) {
209
+ if (event.type === 'progress') {
210
+ console.log(`[${event.stage}] ${event.message ?? ''}`);
211
+ continue;
212
+ }
213
+ if (event.type === 'error') throw new Error(event.message ?? event.error);
214
+ if (event.type === 'result') {
215
+ const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...payload } = event;
216
+ result = payload;
217
+ }
218
+ }
219
+ if (!result) throw new Error('memory ingest stream ended without a result');
212
220
  ```
213
221
 
214
- Without `enrichment` callbacks, `ingestFile()` behaves identically to beforefully backwards compatible.
222
+ Without `enrichment` callbacks, the stream still emits `parsing` `storing` progress events and a terminal `result` the SDK just skips Phase 2/3.
@@ -107,13 +107,11 @@ class MemoryClient implements ExtendedMemoryInterface {
107
107
  graphEdges(): Promise<{ stats: { nodeCount: number; edgeCount: number } }>;
108
108
  graphQuery(query: { nodeId: string; depth?: number }): Promise<GraphTraversalResult>;
109
109
 
110
- // File ingestion with optional two-phase enrichment
111
- // Without callbacks: standard single-phase ingest (backwards compatible)
112
- // With callbacks: fetches extracted images, calls describeImage, submits enrichment
113
- ingestFile(file: File, options?: {
114
- description?: string;
115
- enrichment?: EnrichmentCallbacks;
116
- }): Promise<FileIngestResult>;
110
+ // File ingestion native NDJSON streaming (v0.15.0+)
111
+ // Yields typed IngestEvents (progress / heartbeat / result / error).
112
+ // With enrichment callbacks: fetches extracted images, calls describeImage,
113
+ // calls extractEntitiesV2, posts /enrich, then yields the terminal `result`.
114
+ ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent>;
117
115
 
118
116
  // Bi-temporal queries
119
117
  queryAsOf(asOf: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;