@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/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, 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
  /**
@@ -88,23 +87,12 @@ interface EnrichmentCallbacks {
88
87
  * metadata. Required for image-bearing files; safe to omit for text-only
89
88
  * uploads where the server emits zero images.
90
89
  */
91
- describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
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[];
@@ -116,6 +104,15 @@ interface EnrichmentCallbacks {
116
104
  relationships: IngestRelationship$1[];
117
105
  }>;
118
106
  }
107
+ /**
108
+ * Options for {@link MemoryClient.ingestFileEvents}. `signal` lets
109
+ * long-running ingests (LLM enrichment + graph writes) be cancelled cleanly.
110
+ */
111
+ interface IngestFileOptions {
112
+ description?: string;
113
+ enrichment?: EnrichmentCallbacks;
114
+ signal?: AbortSignal;
115
+ }
119
116
  /** Error thrown by MemoryClient when the server returns a non-success response. */
120
117
  declare class MemoryServerError extends Error {
121
118
  readonly status: number;
@@ -161,16 +158,43 @@ declare class MemoryClient implements ExtendedMemoryInterface {
161
158
  shutdown(): Promise<void>;
162
159
  list(params?: MemoryListParams): Promise<MemoryListResult>;
163
160
  /**
164
- * 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.
163
+ *
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.
165
168
  *
166
- * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
167
- * With enrichment callbacks: fetches extracted images, calls describeImage for each,
168
- * optionally extracts entities, then submits enrichment data back to the server.
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.
173
+ *
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).
177
+ */
178
+ ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent$1>;
179
+ /**
180
+ * Run the SDK-side enrichment phase (image-describe → entity-extract →
181
+ * `/enrich` POST) for a server result, emitting progress + heartbeat
182
+ * events around the slow steps and yielding the single terminal
183
+ * {@link IngestResultEvent} at the end. Skips work cleanly when the
184
+ * server emitted no enrichment block or the caller wired no callbacks.
185
+ */
186
+ private completeIngestFileEvents;
187
+ /**
188
+ * Race a Promise against a periodic heartbeat tick. Yields a heartbeat
189
+ * IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
190
+ * settles, then returns the resolved value (or rethrows). Lets callers
191
+ * keep upstream sockets alive through long LLM/HTTP work without
192
+ * coupling the heartbeat cadence to the work itself.
169
193
  */
170
- ingestFile(file: File, options?: {
171
- description?: string;
172
- enrichment?: EnrichmentCallbacks;
173
- }): Promise<FileIngestResult>;
194
+ private withSdkHeartbeats;
195
+ private normalizeActiveIngestStage;
196
+ private fileIngestResultFromEvent;
197
+ private ingestErrorEvent;
174
198
  /**
175
199
  * Get the download URL for an uploaded file.
176
200
  * Returns a URL that serves the original file binary with proper Content-Type.
@@ -492,4 +516,105 @@ interface GraphTraversalResult {
492
516
  }>;
493
517
  }
494
518
 
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 };
519
+ /** Metadata for a single image extracted from a PDF. */
520
+ interface ExtractedImageMeta {
521
+ imageId: string;
522
+ pageNumber: number;
523
+ width: number;
524
+ height: number;
525
+ mimeType: 'image/png' | 'image/jpeg';
526
+ }
527
+ /**
528
+ * Legacy Phase 1 enrichment block (v1). Returned by servers running the
529
+ * pre-Patch-#2 pipeline OR by Patch-#2+ servers when the client does NOT
530
+ * signal `text_windows_v1` capability via the
531
+ * `X-Pyx-Enrichment-Capabilities` request header.
532
+ */
533
+ interface EnrichmentPendingV1 {
534
+ fileId: string;
535
+ token: string;
536
+ expiresAt: string;
537
+ images: ExtractedImageMeta[];
538
+ }
539
+ /** Truncation report for v2 text-window emission. Always present in v2. */
540
+ interface EnrichmentTextWindowsTruncation {
541
+ /** Total characters seen by the parser, including content beyond the cap. */
542
+ originalChars: number;
543
+ /** Sum of emitted window lengths. <= originalChars. */
544
+ includedChars: number;
545
+ windowCount: number;
546
+ truncated: boolean;
547
+ reason: 'maxWindows' | 'maxTotalChars' | null;
548
+ }
549
+ /**
550
+ * v2 enrichment block. Returned by Patch-#2+ servers when the client signals
551
+ * `text_windows_v1` capability. Adds `textWindows` so SDK callers that bring
552
+ * their own LLM can extract entities from any text-bearing file (text PDFs,
553
+ * .txt, .md, .docx, .pptx, .xlsx, …) — closing the structural gap that left
554
+ * Knowledge Graph + Vector Map blank for text-only uploads.
555
+ */
556
+ interface EnrichmentPendingV2 {
557
+ /** Discriminator. v1 omits this field. */
558
+ version: 2;
559
+ fileId: string;
560
+ token: string;
561
+ expiresAt: string;
562
+ images: ExtractedImageMeta[];
563
+ /** UTF-16 text windows for entity extraction. Empty when no text content. */
564
+ textWindows: string[];
565
+ textWindowsTruncation: EnrichmentTextWindowsTruncation;
566
+ }
567
+ /**
568
+ * Discriminated union over enrichment shapes. Narrow with `'version' in prep`
569
+ * before accessing v2-only fields.
570
+ */
571
+ type EnrichmentPending = EnrichmentPendingV1 | EnrichmentPendingV2;
572
+ /** Extended ingestion result when PDF contains images or v2 text windows. */
573
+ interface FileIngestResult {
574
+ filename: string;
575
+ fileType: string;
576
+ chunks: number;
577
+ entryIds: string[];
578
+ totalCharacters: number;
579
+ /** Present when images were extracted (v1) OR text windows / images were emitted (v2). */
580
+ enrichment?: EnrichmentPending;
581
+ }
582
+ /** Coarse pipeline stages. Stable vocabulary — finer detail goes in counters/message. */
583
+ type IngestStage = 'parsing' | 'storing' | 'enrichment' | 'complete';
584
+ /** Mid-stream progress notification — never carries the terminal result. */
585
+ interface IngestProgressEvent {
586
+ schemaVersion: 1;
587
+ type: 'progress';
588
+ stage: Exclude<IngestStage, 'complete'>;
589
+ filename?: string;
590
+ chunksStored?: number;
591
+ totalCharacters?: number;
592
+ message?: string;
593
+ }
594
+ /** Keepalive emitted while a stage is doing slow work (LLM, graph batch, etc). */
595
+ interface IngestHeartbeatEvent {
596
+ schemaVersion: 1;
597
+ type: 'heartbeat';
598
+ stage: Exclude<IngestStage, 'complete'>;
599
+ message?: string;
600
+ }
601
+ /** Terminal success — exactly one per stream, carries the full FileIngestResult. */
602
+ interface IngestResultEvent extends FileIngestResult {
603
+ schemaVersion: 1;
604
+ type: 'result';
605
+ stage: 'complete';
606
+ message?: string;
607
+ }
608
+ /** Terminal failure — exactly one per stream, mutually exclusive with result. */
609
+ interface IngestErrorEvent {
610
+ schemaVersion: 1;
611
+ type: 'error';
612
+ stage: IngestStage;
613
+ error: string;
614
+ message?: string;
615
+ code?: string | number;
616
+ status?: number;
617
+ }
618
+ type IngestEvent = IngestProgressEvent | IngestHeartbeatEvent | IngestResultEvent | IngestErrorEvent;
619
+
620
+ 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-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-DZZHJ66P.mjs";
15
- import "./chunk-4YIKI2BA.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.13.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[]>;