@maximem/synap-js-sdk 0.1.3 → 0.2.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/README.md CHANGED
@@ -18,10 +18,10 @@ npm install @maximem/synap-js-sdk
18
18
  Install the Python runtime used by the wrapper:
19
19
 
20
20
  ```bash
21
- npx synap-js-sdk setup --sdk-version 0.1.1
21
+ npx synap-js-sdk setup --sdk-version 0.2.0
22
22
  ```
23
23
 
24
- If you want the latest Python SDK version, omit `--sdk-version`.
24
+ If you want the latest Python SDK version, omit `--sdk-version` and pass `--upgrade`.
25
25
 
26
26
  ## Verify Runtime
27
27
 
@@ -65,16 +65,27 @@ async function run() {
65
65
 
66
66
  await synap.addMemory({
67
67
  userId: 'user-123',
68
+ customerId: 'customer-456',
69
+ conversationId: 'conv-123',
68
70
  messages: [{ role: 'user', content: 'My name is Alex and I live in Austin.' }],
69
71
  });
70
72
 
71
- const result = await synap.searchMemory({
73
+ const context = await synap.fetchUserContext({
72
74
  userId: 'user-123',
73
- query: 'Where does the user live?',
75
+ customerId: 'customer-456',
76
+ conversationId: 'conv-123',
77
+ searchQuery: ['Where does the user live?'],
74
78
  maxResults: 10,
75
79
  });
76
80
 
77
- console.log(result);
81
+ console.log(context.facts);
82
+
83
+ const promptContext = await synap.getContextForPrompt({
84
+ conversationId: 'conv-123',
85
+ style: 'structured',
86
+ });
87
+
88
+ console.log(promptContext.formattedContext);
78
89
  await synap.shutdown();
79
90
  }
80
91
 
@@ -97,9 +108,17 @@ This command can:
97
108
  ## Single-Flow Setup (JS + TS)
98
109
 
99
110
  ```bash
100
- npm install @maximem/synap-js-sdk && npx synap-js-sdk setup --sdk-version 0.1.1 && npx synap-js-sdk setup-ts
111
+ npm install @maximem/synap-js-sdk && npx synap-js-sdk setup --sdk-version 0.2.0 && npx synap-js-sdk setup-ts
101
112
  ```
102
113
 
114
+ ## API Notes
115
+
116
+ - `addMemory()` now requires `customerId` to match the Python SDK's explicit ingestion scope.
117
+ - `fetchUserContext()`, `fetchCustomerContext()`, and `fetchClientContext()` expose the structured Python `ContextResponse` surface in JS/TS.
118
+ - `getContextForPrompt()` exposes compacted context plus recent un-compacted messages.
119
+ - `searchMemory()` and `getMemories()` remain convenience helpers built on top of user-scoped context fetches.
120
+ - Temporal fields are exposed in JS/TS as `eventDate`, `validUntil`, `temporalCategory`, `temporalConfidence`, plus top-level `temporalEvents`.
121
+
103
122
  ## CLI Commands
104
123
 
105
124
  ```bash
@@ -5,7 +5,9 @@ Protocol:
5
5
  stdin -> {"id": 1, "method": "init", "params": {...}}\n
6
6
  stdout <- {"id": 1, "result": {...}, "error": null}\n
7
7
  Methods:
8
- init, add_memory, search_memory, get_memories, delete_memory, shutdown
8
+ init, add_memory, search_memory, get_memories, fetch_user_context,
9
+ fetch_customer_context, fetch_client_context, get_context_for_prompt,
10
+ delete_memory, shutdown
9
11
  """
10
12
 
11
13
  import asyncio
@@ -51,6 +53,92 @@ def write_response(obj: dict) -> None:
51
53
  sys.stdout.flush()
52
54
 
53
55
 
56
+ def tracking_key(user_id: str, customer_id: Optional[str]) -> str:
57
+ """Scope tracked memory IDs by both customer and user."""
58
+ return f"{customer_id or ''}::{user_id}"
59
+
60
+
61
+ def serialize_context_response(context) -> dict:
62
+ """Serialize a Python ContextResponse for the JS bridge."""
63
+ payload = context.model_dump(mode="json")
64
+ payload["raw_response"] = context.raw if hasattr(context, "raw") else {}
65
+ return payload
66
+
67
+
68
+ def serialize_context_for_prompt_response(response) -> dict:
69
+ """Serialize a Python ContextForPromptResponse for the JS bridge."""
70
+ return response.model_dump(mode="json")
71
+
72
+
73
+ def flatten_context_items(context) -> List[dict]:
74
+ """Convert typed context collections into a flat memory list."""
75
+ items: List[dict] = []
76
+ for fact in context.facts:
77
+ items.append({
78
+ "id": fact.id,
79
+ "memory": fact.content,
80
+ "score": fact.confidence,
81
+ "source": fact.source,
82
+ "metadata": fact.metadata,
83
+ "context_type": "fact",
84
+ "event_date": str(fact.event_date) if getattr(fact, "event_date", None) else None,
85
+ "valid_until": str(fact.valid_until) if getattr(fact, "valid_until", None) else None,
86
+ "temporal_category": getattr(fact, "temporal_category", None),
87
+ "temporal_confidence": getattr(fact, "temporal_confidence", 0.0),
88
+ })
89
+ for preference in context.preferences:
90
+ items.append({
91
+ "id": preference.id,
92
+ "memory": preference.content,
93
+ "score": preference.strength,
94
+ "source": getattr(preference, "source", ""),
95
+ "metadata": preference.metadata,
96
+ "context_type": "preference",
97
+ "event_date": str(preference.event_date) if getattr(preference, "event_date", None) else None,
98
+ "valid_until": str(preference.valid_until) if getattr(preference, "valid_until", None) else None,
99
+ "temporal_category": getattr(preference, "temporal_category", None),
100
+ "temporal_confidence": getattr(preference, "temporal_confidence", 0.0),
101
+ })
102
+ for episode in context.episodes:
103
+ items.append({
104
+ "id": episode.id,
105
+ "memory": episode.summary,
106
+ "score": episode.significance,
107
+ "metadata": episode.metadata,
108
+ "context_type": "episode",
109
+ "event_date": str(episode.event_date) if getattr(episode, "event_date", None) else None,
110
+ "valid_until": str(episode.valid_until) if getattr(episode, "valid_until", None) else None,
111
+ "temporal_category": getattr(episode, "temporal_category", None),
112
+ "temporal_confidence": getattr(episode, "temporal_confidence", 0.0),
113
+ })
114
+ for emotion in context.emotions:
115
+ items.append({
116
+ "id": emotion.id,
117
+ "memory": emotion.context,
118
+ "score": emotion.intensity,
119
+ "metadata": emotion.metadata,
120
+ "context_type": "emotion",
121
+ "event_date": str(emotion.event_date) if getattr(emotion, "event_date", None) else None,
122
+ "valid_until": str(emotion.valid_until) if getattr(emotion, "valid_until", None) else None,
123
+ "temporal_category": getattr(emotion, "temporal_category", None),
124
+ "temporal_confidence": getattr(emotion, "temporal_confidence", 0.0),
125
+ })
126
+ for event in getattr(context, "temporal_events", []):
127
+ items.append({
128
+ "id": event.id,
129
+ "memory": event.content,
130
+ "score": event.temporal_confidence,
131
+ "source": event.source,
132
+ "metadata": event.metadata,
133
+ "context_type": "temporal_event",
134
+ "event_date": str(event.event_date) if event.event_date else None,
135
+ "valid_until": str(event.valid_until) if event.valid_until else None,
136
+ "temporal_category": event.temporal_category,
137
+ "temporal_confidence": event.temporal_confidence,
138
+ })
139
+ return items
140
+
141
+
54
142
  def messages_to_text(messages: List[dict]) -> str:
55
143
  lines: List[str] = []
56
144
  for message in messages:
@@ -147,7 +235,13 @@ async def handle_add_memory(params: dict) -> dict:
147
235
  timings: List[dict] = []
148
236
 
149
237
  user_id = params["user_id"]
238
+ customer_id = params.get("customer_id")
150
239
  messages = params["messages"]
240
+ conversation_id = params.get("conversation_id")
241
+ session_id = params.get("session_id")
242
+
243
+ if not customer_id:
244
+ raise ValueError("customer_id is required")
151
245
 
152
246
  step = time.perf_counter()
153
247
  transcript = messages_to_text(messages)
@@ -168,12 +262,27 @@ async def handle_add_memory(params: dict) -> dict:
168
262
  start = time.perf_counter()
169
263
 
170
264
  step = time.perf_counter()
171
- create_result = await sdk.memories.create(
172
- document=transcript,
173
- document_type="ai-chat-conversation",
174
- user_id=user_id,
175
- mode="long-range",
176
- )
265
+ mode = params.get("mode", "long-range")
266
+ document_type = params.get("document_type", "ai-chat-conversation")
267
+ document_id = params.get("document_id")
268
+ document_created_at = params.get("document_created_at")
269
+ metadata = params.get("metadata")
270
+
271
+ create_kwargs: dict = {
272
+ "document": transcript,
273
+ "document_type": document_type,
274
+ "user_id": user_id,
275
+ "customer_id": customer_id,
276
+ "mode": mode,
277
+ }
278
+ if document_id is not None:
279
+ create_kwargs["document_id"] = document_id
280
+ if document_created_at is not None:
281
+ create_kwargs["document_created_at"] = document_created_at
282
+ if metadata is not None:
283
+ create_kwargs["metadata"] = metadata
284
+
285
+ create_result = await sdk.memories.create(**create_kwargs)
177
286
  append_step(timings, "memories_create", step)
178
287
 
179
288
  ingestion_id = create_result.ingestion_id
@@ -185,10 +294,16 @@ async def handle_add_memory(params: dict) -> dict:
185
294
  if not content:
186
295
  continue
187
296
  try:
297
+ role = message.get("role", "user")
188
298
  await sdk.instance.send_message(
189
299
  content=content,
190
- role=message.get("role", "user"),
300
+ role=role,
301
+ conversation_id=conversation_id,
191
302
  user_id=user_id,
303
+ customer_id=customer_id,
304
+ session_id=session_id,
305
+ event_type="assistant_message" if role == "assistant" else "user_message",
306
+ metadata=message.get("metadata"),
192
307
  )
193
308
  except Exception as exc:
194
309
  logger.debug("gRPC send_message failed (non-fatal): %s", exc)
@@ -217,7 +332,7 @@ async def handle_add_memory(params: dict) -> dict:
217
332
 
218
333
  memory_ids = [str(memory_id) for memory_id in (final_status.memory_ids or [])]
219
334
  if memory_ids:
220
- user_memory_ids.setdefault(user_id, []).extend(memory_ids)
335
+ user_memory_ids.setdefault(tracking_key(user_id, customer_id), []).extend(memory_ids)
221
336
 
222
337
  return {
223
338
  "success": final_status.status.value != "failed",
@@ -241,37 +356,29 @@ async def handle_search_memory(params: dict) -> dict:
241
356
  timings: List[dict] = []
242
357
 
243
358
  user_id = params["user_id"]
359
+ customer_id = params.get("customer_id")
244
360
  query = params["query"]
245
361
  max_results = params.get("max_results", 10)
362
+ mode = params.get("mode", "fast")
363
+ conversation_id = params.get("conversation_id")
364
+ types = params.get("types", ["all"])
246
365
 
247
366
  start = time.perf_counter()
248
367
 
249
368
  step = time.perf_counter()
250
369
  context = await sdk.user.context.fetch(
251
370
  user_id=user_id,
371
+ customer_id=customer_id,
372
+ conversation_id=conversation_id,
252
373
  search_query=[query],
253
374
  max_results=max_results,
254
- types=["all"],
255
- mode="fast",
375
+ types=types,
376
+ mode=mode,
256
377
  )
257
378
  append_step(timings, "context_fetch", step)
258
379
 
259
380
  step = time.perf_counter()
260
- results = []
261
- for fact in context.facts:
262
- results.append({"id": fact.id, "memory": fact.content, "score": fact.confidence})
263
- for preference in context.preferences:
264
- results.append(
265
- {"id": preference.id, "memory": preference.content, "score": preference.strength}
266
- )
267
- for episode in context.episodes:
268
- results.append(
269
- {"id": episode.id, "memory": episode.summary, "score": episode.significance}
270
- )
271
- for emotion in context.emotions:
272
- results.append(
273
- {"id": emotion.id, "memory": emotion.context, "score": emotion.intensity}
274
- )
381
+ results = flatten_context_items(context)
275
382
  append_step(timings, "map_context_results", step)
276
383
 
277
384
  return {
@@ -293,29 +400,28 @@ async def handle_get_memories(params: dict) -> dict:
293
400
  timings: List[dict] = []
294
401
 
295
402
  user_id = params["user_id"]
403
+ customer_id = params.get("customer_id")
404
+ mode = params.get("mode", "fast")
405
+ conversation_id = params.get("conversation_id")
406
+ max_results = params.get("max_results", 100)
407
+ types = params.get("types", ["all"])
296
408
 
297
409
  start = time.perf_counter()
298
410
 
299
411
  step = time.perf_counter()
300
412
  context = await sdk.user.context.fetch(
301
413
  user_id=user_id,
414
+ customer_id=customer_id,
415
+ conversation_id=conversation_id,
302
416
  search_query=[],
303
- max_results=100,
304
- types=["all"],
305
- mode="fast",
417
+ max_results=max_results,
418
+ types=types,
419
+ mode=mode,
306
420
  )
307
421
  append_step(timings, "context_fetch_all", step)
308
422
 
309
423
  step = time.perf_counter()
310
- memories = []
311
- for fact in context.facts:
312
- memories.append({"id": fact.id, "memory": fact.content})
313
- for preference in context.preferences:
314
- memories.append({"id": preference.id, "memory": preference.content})
315
- for episode in context.episodes:
316
- memories.append({"id": episode.id, "memory": episode.summary})
317
- for emotion in context.emotions:
318
- memories.append({"id": emotion.id, "memory": emotion.context})
424
+ memories = flatten_context_items(context)
319
425
  append_step(timings, "map_memories", step)
320
426
 
321
427
  return {
@@ -323,7 +429,113 @@ async def handle_get_memories(params: dict) -> dict:
323
429
  "latencyMs": ms_since(start),
324
430
  "memories": memories,
325
431
  "memoriesCount": len(memories),
432
+ "totalCount": len(memories),
326
433
  "rawResponse": context.raw if hasattr(context, "raw") else {},
434
+ "source": context.metadata.source if context.metadata else "unknown",
435
+ "bridgeTiming": {
436
+ "python_total_ms": ms_since(handler_start),
437
+ "steps": timings,
438
+ },
439
+ }
440
+
441
+
442
+ async def handle_fetch_user_context(params: dict) -> dict:
443
+ handler_start = time.perf_counter()
444
+ timings: List[dict] = []
445
+ start = time.perf_counter()
446
+
447
+ step = time.perf_counter()
448
+ context = await sdk.user.context.fetch(
449
+ user_id=params["user_id"],
450
+ customer_id=params.get("customer_id"),
451
+ conversation_id=params.get("conversation_id"),
452
+ search_query=params.get("search_query"),
453
+ max_results=params.get("max_results", 10),
454
+ types=params.get("types"),
455
+ mode=params.get("mode", "fast"),
456
+ )
457
+ append_step(timings, "context_fetch", step)
458
+
459
+ return {
460
+ "success": True,
461
+ "latencyMs": ms_since(start),
462
+ "context": serialize_context_response(context),
463
+ "bridgeTiming": {
464
+ "python_total_ms": ms_since(handler_start),
465
+ "steps": timings,
466
+ },
467
+ }
468
+
469
+
470
+ async def handle_fetch_customer_context(params: dict) -> dict:
471
+ handler_start = time.perf_counter()
472
+ timings: List[dict] = []
473
+ start = time.perf_counter()
474
+
475
+ step = time.perf_counter()
476
+ context = await sdk.customer.context.fetch(
477
+ customer_id=params["customer_id"],
478
+ conversation_id=params.get("conversation_id"),
479
+ search_query=params.get("search_query"),
480
+ max_results=params.get("max_results", 10),
481
+ types=params.get("types"),
482
+ mode=params.get("mode", "fast"),
483
+ )
484
+ append_step(timings, "context_fetch", step)
485
+
486
+ return {
487
+ "success": True,
488
+ "latencyMs": ms_since(start),
489
+ "context": serialize_context_response(context),
490
+ "bridgeTiming": {
491
+ "python_total_ms": ms_since(handler_start),
492
+ "steps": timings,
493
+ },
494
+ }
495
+
496
+
497
+ async def handle_fetch_client_context(params: dict) -> dict:
498
+ handler_start = time.perf_counter()
499
+ timings: List[dict] = []
500
+ start = time.perf_counter()
501
+
502
+ step = time.perf_counter()
503
+ context = await sdk.client.context.fetch(
504
+ conversation_id=params.get("conversation_id"),
505
+ search_query=params.get("search_query"),
506
+ max_results=params.get("max_results", 10),
507
+ types=params.get("types"),
508
+ mode=params.get("mode", "fast"),
509
+ )
510
+ append_step(timings, "context_fetch", step)
511
+
512
+ return {
513
+ "success": True,
514
+ "latencyMs": ms_since(start),
515
+ "context": serialize_context_response(context),
516
+ "bridgeTiming": {
517
+ "python_total_ms": ms_since(handler_start),
518
+ "steps": timings,
519
+ },
520
+ }
521
+
522
+
523
+ async def handle_get_context_for_prompt(params: dict) -> dict:
524
+ handler_start = time.perf_counter()
525
+ timings: List[dict] = []
526
+ start = time.perf_counter()
527
+
528
+ step = time.perf_counter()
529
+ response = await sdk.conversation.get_context_for_prompt(
530
+ conversation_id=params["conversation_id"],
531
+ style=params.get("style", "structured"),
532
+ )
533
+ append_step(timings, "get_context_for_prompt", step)
534
+
535
+ return {
536
+ "success": True,
537
+ "latencyMs": ms_since(start),
538
+ "context_for_prompt": serialize_context_for_prompt_response(response),
327
539
  "bridgeTiming": {
328
540
  "python_total_ms": ms_since(handler_start),
329
541
  "steps": timings,
@@ -336,6 +548,7 @@ async def handle_delete_memory(params: dict) -> dict:
336
548
  timings: List[dict] = []
337
549
 
338
550
  user_id = params["user_id"]
551
+ customer_id = params.get("customer_id")
339
552
  memory_id = params.get("memory_id")
340
553
 
341
554
  start = time.perf_counter()
@@ -347,6 +560,7 @@ async def handle_delete_memory(params: dict) -> dict:
347
560
  return {
348
561
  "success": True,
349
562
  "latencyMs": ms_since(start),
563
+ "deletedCount": 1,
350
564
  "rawResponse": {"deleted": 1},
351
565
  "bridgeTiming": {
352
566
  "python_total_ms": ms_since(handler_start),
@@ -354,11 +568,15 @@ async def handle_delete_memory(params: dict) -> dict:
354
568
  },
355
569
  }
356
570
 
357
- tracked_ids = user_memory_ids.get(user_id, [])
571
+ if not customer_id:
572
+ raise ValueError("customer_id is required when memory_id is not provided")
573
+
574
+ tracked_ids = user_memory_ids.get(tracking_key(user_id, customer_id), [])
358
575
  if not tracked_ids:
359
576
  return {
360
577
  "success": True,
361
578
  "latencyMs": 0,
579
+ "deletedCount": 0,
362
580
  "rawResponse": None,
363
581
  "note": "No tracked memory IDs for this user",
364
582
  "bridgeTiming": {
@@ -377,7 +595,7 @@ async def handle_delete_memory(params: dict) -> dict:
377
595
  last_error = str(exc)
378
596
  append_step(timings, "delete_tracked_memories", step)
379
597
 
380
- user_memory_ids.pop(user_id, None)
598
+ user_memory_ids.pop(tracking_key(user_id, customer_id), None)
381
599
 
382
600
  if last_error:
383
601
  return {
@@ -393,6 +611,7 @@ async def handle_delete_memory(params: dict) -> dict:
393
611
  return {
394
612
  "success": True,
395
613
  "latencyMs": ms_since(start),
614
+ "deletedCount": len(tracked_ids),
396
615
  "rawResponse": {"deleted": len(tracked_ids)},
397
616
  "note": f"Deleted {len(tracked_ids)} memories",
398
617
  "bridgeTiming": {
@@ -431,6 +650,10 @@ HANDLERS = {
431
650
  "add_memory": handle_add_memory,
432
651
  "search_memory": handle_search_memory,
433
652
  "get_memories": handle_get_memories,
653
+ "fetch_user_context": handle_fetch_user_context,
654
+ "fetch_customer_context": handle_fetch_customer_context,
655
+ "fetch_client_context": handle_fetch_client_context,
656
+ "get_context_for_prompt": handle_get_context_for_prompt,
434
657
  "delete_memory": handle_delete_memory,
435
658
  "shutdown": handle_shutdown,
436
659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maximem/synap-js-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "JavaScript wrapper around the Synap Python SDK",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -64,6 +64,10 @@ function getWrapperTemplate() {
64
64
  type AddMemoryInput,
65
65
  type SearchMemoryInput,
66
66
  type GetMemoriesInput,
67
+ type FetchUserContextInput,
68
+ type FetchCustomerContextInput,
69
+ type FetchClientContextInput,
70
+ type GetContextForPromptInput,
67
71
  type DeleteMemoryInput,
68
72
  } from '@maximem/synap-js-sdk';
69
73
 
@@ -90,6 +94,22 @@ export class SynapTsClient {
90
94
  return this.client.getMemories(input);
91
95
  }
92
96
 
97
+ fetchUserContext(input: FetchUserContextInput) {
98
+ return this.client.fetchUserContext(input);
99
+ }
100
+
101
+ fetchCustomerContext(input: FetchCustomerContextInput) {
102
+ return this.client.fetchCustomerContext(input);
103
+ }
104
+
105
+ fetchClientContext(input?: FetchClientContextInput) {
106
+ return this.client.fetchClientContext(input);
107
+ }
108
+
109
+ getContextForPrompt(input: GetContextForPromptInput) {
110
+ return this.client.getContextForPrompt(input);
111
+ }
112
+
93
113
  deleteMemory(input: DeleteMemoryInput) {
94
114
  return this.client.deleteMemory(input);
95
115
  }
@@ -1,5 +1,200 @@
1
1
  const { BridgeManager } = require('./bridge-manager');
2
2
 
3
+ function pickDefined(...values) {
4
+ for (const value of values) {
5
+ if (value !== undefined) return value;
6
+ }
7
+ return undefined;
8
+ }
9
+
10
+ function normalizeTemporalFields(item = {}) {
11
+ return {
12
+ eventDate: pickDefined(item.eventDate, item.event_date, null),
13
+ validUntil: pickDefined(item.validUntil, item.valid_until, null),
14
+ temporalCategory: pickDefined(item.temporalCategory, item.temporal_category, null),
15
+ temporalConfidence: pickDefined(item.temporalConfidence, item.temporal_confidence, 0),
16
+ };
17
+ }
18
+
19
+ function normalizeMemoryItem(item = {}) {
20
+ return {
21
+ id: item.id || '',
22
+ memory: pickDefined(item.memory, item.content, ''),
23
+ score: item.score,
24
+ source: item.source,
25
+ metadata: item.metadata || {},
26
+ contextType: pickDefined(item.contextType, item.context_type),
27
+ ...normalizeTemporalFields(item),
28
+ };
29
+ }
30
+
31
+ function normalizeContextMetadata(metadata = {}) {
32
+ return {
33
+ correlationId: pickDefined(metadata.correlationId, metadata.correlation_id, ''),
34
+ ttlSeconds: pickDefined(metadata.ttlSeconds, metadata.ttl_seconds, 0),
35
+ source: metadata.source || 'unknown',
36
+ retrievedAt: pickDefined(metadata.retrievedAt, metadata.retrieved_at, null),
37
+ compactionApplied: pickDefined(metadata.compactionApplied, metadata.compaction_applied, null),
38
+ };
39
+ }
40
+
41
+ function normalizeConversationContext(value) {
42
+ if (!value) return null;
43
+ return {
44
+ summary: value.summary || null,
45
+ currentState: pickDefined(value.currentState, value.current_state, {}),
46
+ keyExtractions: pickDefined(value.keyExtractions, value.key_extractions, {}),
47
+ recentTurns: pickDefined(value.recentTurns, value.recent_turns, []),
48
+ compactionId: pickDefined(value.compactionId, value.compaction_id, null),
49
+ compactedAt: pickDefined(value.compactedAt, value.compacted_at, null),
50
+ conversationId: pickDefined(value.conversationId, value.conversation_id, null),
51
+ };
52
+ }
53
+
54
+ function normalizeFact(item = {}) {
55
+ return {
56
+ id: item.id || '',
57
+ content: item.content || '',
58
+ confidence: pickDefined(item.confidence, 0),
59
+ source: item.source || '',
60
+ extractedAt: pickDefined(item.extractedAt, item.extracted_at, null),
61
+ metadata: item.metadata || {},
62
+ ...normalizeTemporalFields(item),
63
+ };
64
+ }
65
+
66
+ function normalizePreference(item = {}) {
67
+ return {
68
+ id: item.id || '',
69
+ category: item.category || '',
70
+ content: item.content || '',
71
+ strength: pickDefined(item.strength, item.confidence, 0),
72
+ source: item.source || '',
73
+ extractedAt: pickDefined(item.extractedAt, item.extracted_at, null),
74
+ metadata: item.metadata || {},
75
+ ...normalizeTemporalFields(item),
76
+ };
77
+ }
78
+
79
+ function normalizeEpisode(item = {}) {
80
+ return {
81
+ id: item.id || '',
82
+ summary: pickDefined(item.summary, item.content, ''),
83
+ occurredAt: pickDefined(item.occurredAt, item.occurred_at, null),
84
+ significance: pickDefined(item.significance, item.confidence, 0),
85
+ participants: item.participants || [],
86
+ metadata: item.metadata || {},
87
+ ...normalizeTemporalFields(item),
88
+ };
89
+ }
90
+
91
+ function normalizeEmotion(item = {}) {
92
+ return {
93
+ id: item.id || '',
94
+ emotionType: pickDefined(item.emotionType, item.emotion_type, ''),
95
+ intensity: pickDefined(item.intensity, item.confidence, 0),
96
+ detectedAt: pickDefined(item.detectedAt, item.detected_at, null),
97
+ context: item.context || '',
98
+ metadata: item.metadata || {},
99
+ ...normalizeTemporalFields(item),
100
+ };
101
+ }
102
+
103
+ function normalizeTemporalEvent(item = {}) {
104
+ return {
105
+ id: item.id || '',
106
+ content: item.content || '',
107
+ eventDate: pickDefined(item.eventDate, item.event_date, null),
108
+ validUntil: pickDefined(item.validUntil, item.valid_until, null),
109
+ temporalCategory: pickDefined(item.temporalCategory, item.temporal_category, ''),
110
+ temporalConfidence: pickDefined(item.temporalConfidence, item.temporal_confidence, 0),
111
+ confidence: pickDefined(item.confidence, 0),
112
+ source: item.source || '',
113
+ extractedAt: pickDefined(item.extractedAt, item.extracted_at, null),
114
+ metadata: item.metadata || {},
115
+ };
116
+ }
117
+
118
+ function normalizeContextResponse(result = {}) {
119
+ const context = result.context || result;
120
+ return {
121
+ facts: (context.facts || []).map(normalizeFact),
122
+ preferences: (context.preferences || []).map(normalizePreference),
123
+ episodes: (context.episodes || []).map(normalizeEpisode),
124
+ emotions: (context.emotions || []).map(normalizeEmotion),
125
+ temporalEvents: (pickDefined(context.temporalEvents, context.temporal_events, []) || []).map(normalizeTemporalEvent),
126
+ conversationContext: normalizeConversationContext(
127
+ pickDefined(context.conversationContext, context.conversation_context, null)
128
+ ),
129
+ metadata: normalizeContextMetadata(context.metadata || {}),
130
+ rawResponse: pickDefined(context.rawResponse, context.raw_response, {}),
131
+ bridgeTiming: result.bridgeTiming,
132
+ };
133
+ }
134
+
135
+ function normalizeRecentMessage(item = {}) {
136
+ return {
137
+ role: item.role || 'user',
138
+ content: item.content || '',
139
+ timestamp: item.timestamp || null,
140
+ messageId: pickDefined(item.messageId, item.message_id, ''),
141
+ };
142
+ }
143
+
144
+ function normalizeContextForPromptResult(result = {}) {
145
+ const payload = pickDefined(result.contextForPrompt, result.context_for_prompt, result);
146
+ return {
147
+ formattedContext: pickDefined(payload.formattedContext, payload.formatted_context, null),
148
+ available: !!payload.available,
149
+ isStale: !!pickDefined(payload.isStale, payload.is_stale, false),
150
+ compressionRatio: pickDefined(payload.compressionRatio, payload.compression_ratio, null),
151
+ validationScore: pickDefined(payload.validationScore, payload.validation_score, null),
152
+ compactionAgeSeconds: pickDefined(payload.compactionAgeSeconds, payload.compaction_age_seconds, null),
153
+ qualityWarning: !!pickDefined(payload.qualityWarning, payload.quality_warning, false),
154
+ recentMessages: (pickDefined(payload.recentMessages, payload.recent_messages, []) || []).map(normalizeRecentMessage),
155
+ recentMessageCount: pickDefined(payload.recentMessageCount, payload.recent_message_count, 0),
156
+ compactedMessageCount: pickDefined(payload.compactedMessageCount, payload.compacted_message_count, 0),
157
+ totalMessageCount: pickDefined(payload.totalMessageCount, payload.total_message_count, 0),
158
+ bridgeTiming: result.bridgeTiming,
159
+ };
160
+ }
161
+
162
+ function normalizeSearchMemoryResult(result = {}) {
163
+ return {
164
+ success: !!result.success,
165
+ latencyMs: result.latencyMs || 0,
166
+ results: (result.results || []).map(normalizeMemoryItem),
167
+ resultsCount: pickDefined(result.resultsCount, result.results?.length, 0),
168
+ rawResponse: result.rawResponse || {},
169
+ source: result.source,
170
+ bridgeTiming: result.bridgeTiming,
171
+ };
172
+ }
173
+
174
+ function normalizeGetMemoriesResult(result = {}) {
175
+ const memories = (result.memories || []).map(normalizeMemoryItem);
176
+ return {
177
+ success: !!result.success,
178
+ latencyMs: result.latencyMs || 0,
179
+ memories,
180
+ totalCount: pickDefined(result.totalCount, result.memoriesCount, memories.length, 0),
181
+ rawResponse: pickDefined(result.rawResponse, null),
182
+ source: result.source,
183
+ bridgeTiming: result.bridgeTiming,
184
+ };
185
+ }
186
+
187
+ function normalizeDeleteMemoryResult(result = {}) {
188
+ return {
189
+ success: !!result.success,
190
+ latencyMs: result.latencyMs || 0,
191
+ deletedCount: pickDefined(result.deletedCount, result.rawResponse?.deleted, 0),
192
+ rawResponse: pickDefined(result.rawResponse, null),
193
+ note: result.note,
194
+ bridgeTiming: result.bridgeTiming,
195
+ };
196
+ }
197
+
3
198
  class SynapClient {
4
199
  constructor(options = {}) {
5
200
  this.bridge = new BridgeManager(options);
@@ -14,41 +209,122 @@ class SynapClient {
14
209
  await this.bridge.ensureStarted();
15
210
  }
16
211
 
17
- async addMemory({ userId, messages }) {
212
+ async addMemory({
213
+ userId,
214
+ customerId,
215
+ conversationId,
216
+ sessionId,
217
+ messages,
218
+ mode,
219
+ documentType,
220
+ documentId,
221
+ documentCreatedAt,
222
+ metadata,
223
+ }) {
18
224
  this.#assert(userId, 'userId is required');
225
+ this.#assert(customerId, 'customerId is required');
19
226
  this.#assert(Array.isArray(messages), 'messages must be an array');
20
227
 
21
- return this.bridge.call(
22
- 'add_memory',
23
- { user_id: userId, messages },
24
- this.options.ingestTimeoutMs
25
- );
228
+ const params = { user_id: userId, messages };
229
+ params.customer_id = customerId;
230
+ if (conversationId !== undefined) params.conversation_id = conversationId;
231
+ if (sessionId !== undefined) params.session_id = sessionId;
232
+ if (mode !== undefined) params.mode = mode;
233
+ if (documentType !== undefined) params.document_type = documentType;
234
+ if (documentId !== undefined) params.document_id = documentId;
235
+ if (documentCreatedAt !== undefined) params.document_created_at = documentCreatedAt;
236
+ if (metadata !== undefined) params.metadata = metadata;
237
+
238
+ return this.bridge.call('add_memory', params, this.options.ingestTimeoutMs);
26
239
  }
27
240
 
28
- async searchMemory({ userId, query, maxResults = 10 }) {
241
+ async searchMemory({ userId, customerId, query, maxResults = 10, mode, conversationId, types }) {
29
242
  this.#assert(userId, 'userId is required');
30
243
  this.#assert(query, 'query is required');
244
+ this.#assertArray(types, 'types must be an array when provided');
31
245
 
32
- return this.bridge.call('search_memory', {
33
- user_id: userId,
34
- query,
35
- max_results: maxResults,
36
- });
246
+ const params = { user_id: userId, query, max_results: maxResults };
247
+ if (customerId !== undefined) params.customer_id = customerId;
248
+ if (mode !== undefined) params.mode = mode;
249
+ if (conversationId !== undefined) params.conversation_id = conversationId;
250
+ if (types !== undefined) params.types = types;
251
+
252
+ return normalizeSearchMemoryResult(await this.bridge.call('search_memory', params));
37
253
  }
38
254
 
39
- async getMemories({ userId }) {
255
+ async getMemories({ userId, customerId, mode, conversationId, maxResults, types }) {
40
256
  this.#assert(userId, 'userId is required');
257
+ this.#assertArray(types, 'types must be an array when provided');
258
+
259
+ const params = { user_id: userId };
260
+ if (customerId !== undefined) params.customer_id = customerId;
261
+ if (mode !== undefined) params.mode = mode;
262
+ if (conversationId !== undefined) params.conversation_id = conversationId;
263
+ if (maxResults !== undefined) params.max_results = maxResults;
264
+ if (types !== undefined) params.types = types;
41
265
 
42
- return this.bridge.call('get_memories', { user_id: userId });
266
+ return normalizeGetMemoriesResult(await this.bridge.call('get_memories', params));
43
267
  }
44
268
 
45
- async deleteMemory({ userId, memoryId = null }) {
269
+ async fetchUserContext({ userId, customerId, conversationId, searchQuery, maxResults = 10, types, mode }) {
46
270
  this.#assert(userId, 'userId is required');
271
+ this.#assertArray(searchQuery, 'searchQuery must be an array when provided');
272
+ this.#assertArray(types, 'types must be an array when provided');
47
273
 
48
- return this.bridge.call('delete_memory', {
49
- user_id: userId,
50
- memory_id: memoryId,
51
- });
274
+ const params = { user_id: userId, max_results: maxResults };
275
+ if (customerId !== undefined) params.customer_id = customerId;
276
+ if (conversationId !== undefined) params.conversation_id = conversationId;
277
+ if (searchQuery !== undefined) params.search_query = searchQuery;
278
+ if (types !== undefined) params.types = types;
279
+ if (mode !== undefined) params.mode = mode;
280
+
281
+ return normalizeContextResponse(await this.bridge.call('fetch_user_context', params));
282
+ }
283
+
284
+ async fetchCustomerContext({ customerId, conversationId, searchQuery, maxResults = 10, types, mode }) {
285
+ this.#assert(customerId, 'customerId is required');
286
+ this.#assertArray(searchQuery, 'searchQuery must be an array when provided');
287
+ this.#assertArray(types, 'types must be an array when provided');
288
+
289
+ const params = { customer_id: customerId, max_results: maxResults };
290
+ if (conversationId !== undefined) params.conversation_id = conversationId;
291
+ if (searchQuery !== undefined) params.search_query = searchQuery;
292
+ if (types !== undefined) params.types = types;
293
+ if (mode !== undefined) params.mode = mode;
294
+
295
+ return normalizeContextResponse(await this.bridge.call('fetch_customer_context', params));
296
+ }
297
+
298
+ async fetchClientContext({ conversationId, searchQuery, maxResults = 10, types, mode } = {}) {
299
+ this.#assertArray(searchQuery, 'searchQuery must be an array when provided');
300
+ this.#assertArray(types, 'types must be an array when provided');
301
+
302
+ const params = { max_results: maxResults };
303
+ if (conversationId !== undefined) params.conversation_id = conversationId;
304
+ if (searchQuery !== undefined) params.search_query = searchQuery;
305
+ if (types !== undefined) params.types = types;
306
+ if (mode !== undefined) params.mode = mode;
307
+
308
+ return normalizeContextResponse(await this.bridge.call('fetch_client_context', params));
309
+ }
310
+
311
+ async getContextForPrompt({ conversationId, style } = {}) {
312
+ this.#assert(conversationId, 'conversationId is required');
313
+
314
+ const params = { conversation_id: conversationId };
315
+ if (style !== undefined) params.style = style;
316
+
317
+ return normalizeContextForPromptResult(await this.bridge.call('get_context_for_prompt', params));
318
+ }
319
+
320
+ async deleteMemory({ userId, customerId, memoryId = null }) {
321
+ this.#assert(userId, 'userId is required');
322
+ if (memoryId == null) this.#assert(customerId, 'customerId is required when memoryId is not provided');
323
+
324
+ const params = { user_id: userId, memory_id: memoryId };
325
+ if (customerId !== undefined) params.customer_id = customerId;
326
+
327
+ return normalizeDeleteMemoryResult(await this.bridge.call('delete_memory', params));
52
328
  }
53
329
 
54
330
  async shutdown() {
@@ -59,6 +335,10 @@ class SynapClient {
59
335
  if (!value) throw new Error(message);
60
336
  }
61
337
 
338
+ #assertArray(value, message) {
339
+ if (value !== undefined && !Array.isArray(value)) throw new Error(message);
340
+ }
341
+
62
342
  #registerShutdownHooks() {
63
343
  const close = async () => {
64
344
  try {
package/types/index.d.ts CHANGED
@@ -1,4 +1,28 @@
1
1
  export type LogLevel = 'debug' | 'error';
2
+ export type RetrievalMode = 'fast' | 'accurate';
3
+ export type IngestMode = 'fast' | 'long-range';
4
+ export type PromptStyle = 'structured' | 'narrative' | 'bullet_points';
5
+ export type ContextType =
6
+ | 'all'
7
+ | 'facts'
8
+ | 'preferences'
9
+ | 'episodes'
10
+ | 'emotions'
11
+ | 'temporal_events';
12
+ export type FlattenedContextType =
13
+ | 'fact'
14
+ | 'preference'
15
+ | 'episode'
16
+ | 'emotion'
17
+ | 'temporal_event';
18
+ export type DocumentType =
19
+ | 'ai-chat-conversation'
20
+ | 'document'
21
+ | 'email'
22
+ | 'pdf'
23
+ | 'image'
24
+ | 'audio'
25
+ | 'meeting-transcript';
2
26
 
3
27
  export interface BridgeLogHandler {
4
28
  (level: LogLevel, message: string): void;
@@ -31,30 +55,81 @@ export interface SynapClientOptions {
31
55
  }
32
56
 
33
57
  export interface ChatMessage {
34
- role?: string;
58
+ role?: 'user' | 'assistant';
35
59
  content: string;
60
+ metadata?: Record<string, string>;
36
61
  }
37
62
 
38
63
  export interface AddMemoryInput {
39
64
  userId: string;
65
+ customerId: string;
66
+ conversationId?: string;
67
+ sessionId?: string;
40
68
  messages: ChatMessage[];
69
+ mode?: IngestMode;
70
+ documentType?: DocumentType;
71
+ documentId?: string;
72
+ documentCreatedAt?: string;
73
+ metadata?: Record<string, unknown>;
41
74
  }
42
75
 
43
76
  export interface SearchMemoryInput {
44
77
  userId: string;
78
+ customerId?: string;
45
79
  query: string;
46
80
  maxResults?: number;
81
+ mode?: RetrievalMode;
82
+ conversationId?: string;
83
+ types?: ContextType[];
47
84
  }
48
85
 
49
86
  export interface GetMemoriesInput {
50
87
  userId: string;
88
+ customerId?: string;
89
+ mode?: RetrievalMode;
90
+ conversationId?: string;
91
+ maxResults?: number;
92
+ types?: ContextType[];
51
93
  }
52
94
 
53
95
  export interface DeleteMemoryInput {
54
96
  userId: string;
97
+ customerId?: string;
55
98
  memoryId?: string | null;
56
99
  }
57
100
 
101
+ export interface FetchUserContextInput {
102
+ userId: string;
103
+ customerId?: string;
104
+ conversationId?: string;
105
+ searchQuery?: string[];
106
+ maxResults?: number;
107
+ types?: ContextType[];
108
+ mode?: RetrievalMode;
109
+ }
110
+
111
+ export interface FetchCustomerContextInput {
112
+ customerId: string;
113
+ conversationId?: string;
114
+ searchQuery?: string[];
115
+ maxResults?: number;
116
+ types?: ContextType[];
117
+ mode?: RetrievalMode;
118
+ }
119
+
120
+ export interface FetchClientContextInput {
121
+ conversationId?: string;
122
+ searchQuery?: string[];
123
+ maxResults?: number;
124
+ types?: ContextType[];
125
+ mode?: RetrievalMode;
126
+ }
127
+
128
+ export interface GetContextForPromptInput {
129
+ conversationId: string;
130
+ style?: PromptStyle;
131
+ }
132
+
58
133
  export interface BridgeStepTiming {
59
134
  step: string;
60
135
  ms: number;
@@ -65,6 +140,115 @@ export interface BridgeTiming {
65
140
  steps: BridgeStepTiming[];
66
141
  }
67
142
 
143
+ export interface TemporalFields {
144
+ eventDate: string | null;
145
+ validUntil: string | null;
146
+ temporalCategory: string | null;
147
+ temporalConfidence: number;
148
+ }
149
+
150
+ export interface Fact extends TemporalFields {
151
+ id: string;
152
+ content: string;
153
+ confidence: number;
154
+ source: string;
155
+ extractedAt: string | null;
156
+ metadata: Record<string, unknown>;
157
+ }
158
+
159
+ export interface Preference extends TemporalFields {
160
+ id: string;
161
+ category: string;
162
+ content: string;
163
+ strength: number;
164
+ source: string;
165
+ extractedAt: string | null;
166
+ metadata: Record<string, unknown>;
167
+ }
168
+
169
+ export interface Episode extends TemporalFields {
170
+ id: string;
171
+ summary: string;
172
+ occurredAt: string | null;
173
+ significance: number;
174
+ participants: string[];
175
+ metadata: Record<string, unknown>;
176
+ }
177
+
178
+ export interface Emotion extends TemporalFields {
179
+ id: string;
180
+ emotionType: string;
181
+ intensity: number;
182
+ detectedAt: string | null;
183
+ context: string;
184
+ metadata: Record<string, unknown>;
185
+ }
186
+
187
+ export interface TemporalEvent {
188
+ id: string;
189
+ content: string;
190
+ eventDate: string | null;
191
+ validUntil: string | null;
192
+ temporalCategory: string;
193
+ temporalConfidence: number;
194
+ confidence: number;
195
+ source: string;
196
+ extractedAt: string | null;
197
+ metadata: Record<string, unknown>;
198
+ }
199
+
200
+ export interface RecentMessage {
201
+ role: string;
202
+ content: string;
203
+ timestamp: string | null;
204
+ messageId: string;
205
+ }
206
+
207
+ export interface ContextResponseMetadata {
208
+ correlationId: string;
209
+ ttlSeconds: number;
210
+ source: string;
211
+ retrievedAt: string | null;
212
+ compactionApplied: string | null;
213
+ }
214
+
215
+ export interface ConversationContext {
216
+ summary: string | null;
217
+ currentState: Record<string, unknown>;
218
+ keyExtractions: Record<string, Array<Record<string, unknown>>>;
219
+ recentTurns: Array<Record<string, unknown>>;
220
+ compactionId: string | null;
221
+ compactedAt: string | null;
222
+ conversationId: string | null;
223
+ }
224
+
225
+ export interface ContextResponse {
226
+ facts: Fact[];
227
+ preferences: Preference[];
228
+ episodes: Episode[];
229
+ emotions: Emotion[];
230
+ temporalEvents: TemporalEvent[];
231
+ conversationContext: ConversationContext | null;
232
+ metadata: ContextResponseMetadata;
233
+ rawResponse: Record<string, unknown>;
234
+ bridgeTiming?: BridgeTiming;
235
+ }
236
+
237
+ export interface ContextForPromptResult {
238
+ formattedContext: string | null;
239
+ available: boolean;
240
+ isStale: boolean;
241
+ compressionRatio: number | null;
242
+ validationScore: number | null;
243
+ compactionAgeSeconds: number | null;
244
+ qualityWarning: boolean;
245
+ recentMessages: RecentMessage[];
246
+ recentMessageCount: number;
247
+ compactedMessageCount: number;
248
+ totalMessageCount: number;
249
+ bridgeTiming?: BridgeTiming;
250
+ }
251
+
68
252
  export interface AddMemoryResult {
69
253
  success: boolean;
70
254
  latencyMs: number;
@@ -73,10 +257,13 @@ export interface AddMemoryResult {
73
257
  bridgeTiming?: BridgeTiming;
74
258
  }
75
259
 
76
- export interface SearchMemoryItem {
260
+ export interface SearchMemoryItem extends TemporalFields {
77
261
  id: string;
78
262
  memory: string;
79
263
  score?: number;
264
+ source?: string;
265
+ metadata: Record<string, unknown>;
266
+ contextType?: FlattenedContextType;
80
267
  }
81
268
 
82
269
  export interface SearchMemoryResult {
@@ -89,22 +276,23 @@ export interface SearchMemoryResult {
89
276
  bridgeTiming?: BridgeTiming;
90
277
  }
91
278
 
92
- export interface MemoryItem {
93
- id: string;
94
- memory: string;
95
- }
279
+ export interface MemoryItem extends SearchMemoryItem {}
96
280
 
97
281
  export interface GetMemoriesResult {
98
282
  success: boolean;
99
283
  latencyMs: number;
100
284
  memories: MemoryItem[];
101
285
  totalCount: number;
286
+ rawResponse: Record<string, unknown> | null;
287
+ source?: string;
102
288
  bridgeTiming?: BridgeTiming;
103
289
  }
104
290
 
105
291
  export interface DeleteMemoryResult {
106
292
  success: boolean;
293
+ latencyMs: number;
107
294
  deletedCount: number;
295
+ rawResponse: Record<string, unknown> | null;
108
296
  note?: string;
109
297
  bridgeTiming?: BridgeTiming;
110
298
  }
@@ -115,6 +303,10 @@ export class SynapClient {
115
303
  addMemory(input: AddMemoryInput): Promise<AddMemoryResult>;
116
304
  searchMemory(input: SearchMemoryInput): Promise<SearchMemoryResult>;
117
305
  getMemories(input: GetMemoriesInput): Promise<GetMemoriesResult>;
306
+ fetchUserContext(input: FetchUserContextInput): Promise<ContextResponse>;
307
+ fetchCustomerContext(input: FetchCustomerContextInput): Promise<ContextResponse>;
308
+ fetchClientContext(input?: FetchClientContextInput): Promise<ContextResponse>;
309
+ getContextForPrompt(input: GetContextForPromptInput): Promise<ContextForPromptResult>;
118
310
  deleteMemory(input: DeleteMemoryInput): Promise<DeleteMemoryResult>;
119
311
  shutdown(): Promise<void>;
120
312
  }