@maximem/synap-js-sdk 0.1.4 → 0.2.1

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)
@@ -169,12 +263,26 @@ async def handle_add_memory(params: dict) -> dict:
169
263
 
170
264
  step = time.perf_counter()
171
265
  mode = params.get("mode", "long-range")
172
- create_result = await sdk.memories.create(
173
- document=transcript,
174
- document_type="ai-chat-conversation",
175
- user_id=user_id,
176
- mode=mode,
177
- )
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)
178
286
  append_step(timings, "memories_create", step)
179
287
 
180
288
  ingestion_id = create_result.ingestion_id
@@ -186,10 +294,16 @@ async def handle_add_memory(params: dict) -> dict:
186
294
  if not content:
187
295
  continue
188
296
  try:
297
+ role = message.get("role", "user")
189
298
  await sdk.instance.send_message(
190
299
  content=content,
191
- role=message.get("role", "user"),
300
+ role=role,
301
+ conversation_id=conversation_id,
192
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"),
193
307
  )
194
308
  except Exception as exc:
195
309
  logger.debug("gRPC send_message failed (non-fatal): %s", exc)
@@ -218,7 +332,7 @@ async def handle_add_memory(params: dict) -> dict:
218
332
 
219
333
  memory_ids = [str(memory_id) for memory_id in (final_status.memory_ids or [])]
220
334
  if memory_ids:
221
- user_memory_ids.setdefault(user_id, []).extend(memory_ids)
335
+ user_memory_ids.setdefault(tracking_key(user_id, customer_id), []).extend(memory_ids)
222
336
 
223
337
  return {
224
338
  "success": final_status.status.value != "failed",
@@ -242,38 +356,29 @@ async def handle_search_memory(params: dict) -> dict:
242
356
  timings: List[dict] = []
243
357
 
244
358
  user_id = params["user_id"]
359
+ customer_id = params.get("customer_id")
245
360
  query = params["query"]
246
361
  max_results = params.get("max_results", 10)
247
362
  mode = params.get("mode", "fast")
363
+ conversation_id = params.get("conversation_id")
364
+ types = params.get("types", ["all"])
248
365
 
249
366
  start = time.perf_counter()
250
367
 
251
368
  step = time.perf_counter()
252
369
  context = await sdk.user.context.fetch(
253
370
  user_id=user_id,
371
+ customer_id=customer_id,
372
+ conversation_id=conversation_id,
254
373
  search_query=[query],
255
374
  max_results=max_results,
256
- types=["all"],
375
+ types=types,
257
376
  mode=mode,
258
377
  )
259
378
  append_step(timings, "context_fetch", step)
260
379
 
261
380
  step = time.perf_counter()
262
- results = []
263
- for fact in context.facts:
264
- results.append({"id": fact.id, "memory": fact.content, "score": fact.confidence})
265
- for preference in context.preferences:
266
- results.append(
267
- {"id": preference.id, "memory": preference.content, "score": preference.strength}
268
- )
269
- for episode in context.episodes:
270
- results.append(
271
- {"id": episode.id, "memory": episode.summary, "score": episode.significance}
272
- )
273
- for emotion in context.emotions:
274
- results.append(
275
- {"id": emotion.id, "memory": emotion.context, "score": emotion.intensity}
276
- )
381
+ results = flatten_context_items(context)
277
382
  append_step(timings, "map_context_results", step)
278
383
 
279
384
  return {
@@ -295,30 +400,28 @@ async def handle_get_memories(params: dict) -> dict:
295
400
  timings: List[dict] = []
296
401
 
297
402
  user_id = params["user_id"]
403
+ customer_id = params.get("customer_id")
298
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"])
299
408
 
300
409
  start = time.perf_counter()
301
410
 
302
411
  step = time.perf_counter()
303
412
  context = await sdk.user.context.fetch(
304
413
  user_id=user_id,
414
+ customer_id=customer_id,
415
+ conversation_id=conversation_id,
305
416
  search_query=[],
306
- max_results=100,
307
- types=["all"],
417
+ max_results=max_results,
418
+ types=types,
308
419
  mode=mode,
309
420
  )
310
421
  append_step(timings, "context_fetch_all", step)
311
422
 
312
423
  step = time.perf_counter()
313
- memories = []
314
- for fact in context.facts:
315
- memories.append({"id": fact.id, "memory": fact.content})
316
- for preference in context.preferences:
317
- memories.append({"id": preference.id, "memory": preference.content})
318
- for episode in context.episodes:
319
- memories.append({"id": episode.id, "memory": episode.summary})
320
- for emotion in context.emotions:
321
- memories.append({"id": emotion.id, "memory": emotion.context})
424
+ memories = flatten_context_items(context)
322
425
  append_step(timings, "map_memories", step)
323
426
 
324
427
  return {
@@ -326,7 +429,113 @@ async def handle_get_memories(params: dict) -> dict:
326
429
  "latencyMs": ms_since(start),
327
430
  "memories": memories,
328
431
  "memoriesCount": len(memories),
432
+ "totalCount": len(memories),
329
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),
330
539
  "bridgeTiming": {
331
540
  "python_total_ms": ms_since(handler_start),
332
541
  "steps": timings,
@@ -339,6 +548,7 @@ async def handle_delete_memory(params: dict) -> dict:
339
548
  timings: List[dict] = []
340
549
 
341
550
  user_id = params["user_id"]
551
+ customer_id = params.get("customer_id")
342
552
  memory_id = params.get("memory_id")
343
553
 
344
554
  start = time.perf_counter()
@@ -350,6 +560,7 @@ async def handle_delete_memory(params: dict) -> dict:
350
560
  return {
351
561
  "success": True,
352
562
  "latencyMs": ms_since(start),
563
+ "deletedCount": 1,
353
564
  "rawResponse": {"deleted": 1},
354
565
  "bridgeTiming": {
355
566
  "python_total_ms": ms_since(handler_start),
@@ -357,11 +568,15 @@ async def handle_delete_memory(params: dict) -> dict:
357
568
  },
358
569
  }
359
570
 
360
- 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), [])
361
575
  if not tracked_ids:
362
576
  return {
363
577
  "success": True,
364
578
  "latencyMs": 0,
579
+ "deletedCount": 0,
365
580
  "rawResponse": None,
366
581
  "note": "No tracked memory IDs for this user",
367
582
  "bridgeTiming": {
@@ -380,7 +595,7 @@ async def handle_delete_memory(params: dict) -> dict:
380
595
  last_error = str(exc)
381
596
  append_step(timings, "delete_tracked_memories", step)
382
597
 
383
- user_memory_ids.pop(user_id, None)
598
+ user_memory_ids.pop(tracking_key(user_id, customer_id), None)
384
599
 
385
600
  if last_error:
386
601
  return {
@@ -396,6 +611,7 @@ async def handle_delete_memory(params: dict) -> dict:
396
611
  return {
397
612
  "success": True,
398
613
  "latencyMs": ms_since(start),
614
+ "deletedCount": len(tracked_ids),
399
615
  "rawResponse": {"deleted": len(tracked_ids)},
400
616
  "note": f"Deleted {len(tracked_ids)} memories",
401
617
  "bridgeTiming": {
@@ -434,6 +650,10 @@ HANDLERS = {
434
650
  "add_memory": handle_add_memory,
435
651
  "search_memory": handle_search_memory,
436
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,
437
657
  "delete_memory": handle_delete_memory,
438
658
  "shutdown": handle_shutdown,
439
659
  }
@@ -489,7 +709,7 @@ async def main() -> None:
489
709
  write_response({"id": req_id, "result": result, "error": None})
490
710
  except Exception as exc:
491
711
  logger.error("Handler error for %s: %s", method, traceback.format_exc())
492
- write_response({"id": req_id, "result": None, "error": str(exc)})
712
+ write_response({"id": req_id, "result": None, "error": str(exc), "error_type": type(exc).__name__})
493
713
 
494
714
 
495
715
  if __name__ == "__main__":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maximem/synap-js-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "JavaScript wrapper around the Synap Python SDK",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -7,6 +7,7 @@ const {
7
7
  resolveInstanceId,
8
8
  setupPythonRuntime,
9
9
  } = require('./runtime');
10
+ const { createSynapError } = require('./errors');
10
11
 
11
12
  class BridgeManager {
12
13
  constructor(options = {}) {
@@ -147,7 +148,7 @@ class BridgeManager {
147
148
  this.pending.delete(payload.id);
148
149
 
149
150
  if (payload.error) {
150
- pending.reject(new Error(payload.error));
151
+ pending.reject(createSynapError(payload.error, payload.error_type));
151
152
  } else {
152
153
  pending.resolve(payload.result);
153
154
  }